Doing away with an Assembly Dependency
I'm looking at some oldish code that sits inside of a web control library. This is my main web control library were I keep all of my custom controls that I typically use. It includes all the AJAX controls, a custom databinder, some generic status display controls, a number of stock control extensions, plus a host of web control related helper routines used in control development in general. It's a fairly focused library assembly that's fully self contained without outside dependencies.
The databinder control of the library can run fully self contained, but I also have an optional dependency on my business class library. Basically the automatic databinding validation that takes place can take validation errors from my business objects and automatically shove those validation errors into the databinder so that the databinder's validation code can display these errors - in addition to unbinding errors - in a consistent fashion. It works great, but doing this introduces a dependency into my control project that I always have to remember when I distribute the control assembly when it's not used in relation to the business library (which is common).
So the question is how to deal with this effectively. I have been futzing around with this a bit today and while I have a working solution I'm not really happy with it so I thought I throw this out for some discussion here in case I'm missing something obvious.
So the original code inside of the Web Control library does basically this:
#if USE_WWBUSINESS/// <summary>
/// Takes a collection of ValidationErrors and assigns it to the
/// matching controls. These controls must match in signature as follows:
/// Must have the same name as the field and a 3 letter prefix. For example,
///
/// txtCompany - matches company field
/// cmbCountry - matches the Country field
/// </summary>
/// <returns></returns>
public void AddValidationErrorsToBindingErrors(Westwind.BusinessObjects.ValidationErrorCollection errors)
{foreach (Westwind.BusinessObjects.ValidationError error in errors)
{Control ctl = wwWebUtils.FindControlRecursive(this.Page.Form, error.ControlID);
this.AddBindingError(error.Message, ctl);}
}
#endif
and I then have a project level #define constant that I have to set or unset for compilation. The Westwind.BusinessObjects.ValidationErrorCollection is located in a separate generic business library assembly and so in order to compile the code I need to ensure that the assembly is available.
This works, but it's a hassle for two reasons:
- It's easy to forget the switch and get compile errors
- The assembly is used in several projects some of which require the switch others that don't
- There's no way to tell looking at the assembly whether it has the switch set or not
Regardless though - once compiled - I can deploy the assembly even if the Westwind.BusinessObjects assembly is not present and the assembly works fine as long as the above method is not called.
So what if I want to abstract away the dependency?
The first and probably easiest alternative to do this is to do away with the concrete type altogether and then rely on Reflection to get the values. Something like the following works to remove the dependency:
/// <summary>
/// Takes a collection of ValidationErrors and assigns it to the
/// matching controls. These controls must match in signature as follows:
/// Must have the same name as the field and a 3 letter prefix. For example,
///
/// txtCompany - matches company field
/// cmbCountry - matches the Country field
///
/// The input parameter is a generic IList value, but the type should be
/// specifically Westwind.BusinessObjects.ValidationErrorCollection. The
/// generic parameter is used here to avoid an assembly dependence.
/// </summary>
/// <returns></returns>
/// </summary>
/// <param name="errors">Westwind.BusinessObjects.ValidationErrorCollection list</param>
public void AddValidationErrorsToBindingErrors(IList errors)
{ if (errors == null)
return;foreach (object error in errors)
{string controlID = wwUtils.GetProperty(error, "ControlID") as string;
string message = wwUtils.GetProperty(error, "Message") as string;
Control ctl = wwWebUtils.FindControlRecursive(this.Page.Form, controlID);
this.AddBindingError(message, ctl);}
}
wwUtils.GetProperty() is a wrapper Reflection method that easily retrieves a property using Reflection and there are a bunch of routines on wwUtils that provide similar functionality to use Reflection to call methods, set values and provided 'pathed' Reflection access using . syntax (ie. wwUtils.GetProperty(loObject,"Address.Street")).
Again, this is functional and better since it removes the external dependency but it still doesn't make me happy because the method interface is not very clean to the end user - they have no idea what's expected as an input value at this point other than that it's an IList interface. Yet it won't really work unless the specific ValidationErrorCollection is passed. Hence the rather lengthy comment and explicit type description in the parameter comment which hopefully shows up with the XML documentation file if shipped.
Unfortunately I can't really think of a cleaner way to do this and preserve the type interface as anything that provides the type - Westwind.BusinessObject.ValidationErrorCollection - would introduce the assembly dependence. Using an interface to describe the errors is no better because there too the type would need to be present in both assemblies somehow. I can't see a way to share the type or interface without some sort of external reference.
The only other thing I could possibly see would be to subclass the control and pull it back into the business project or more appropriately into a Web project. The control could then get a new name and provide an override with the explicit type. But that too would suck because it'd have to be done for every web project. Or pull the control subclass into the business project which would work, but be pretty unclean and then require a System.Web in the business library which also is not very clean.
So what do you think? Is there another way to approach this without polluting one or the other assemblies with assembly references that shouldn't be there?
Other Posts you might also like
- Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application
- Resolving Paths To Server Relative Paths in .NET Code
- Map Physical Paths with an HttpContext.MapPath() Extension Method in ASP.NET
- Back to Basics: Rewriting a URL in ASP.NET Core
- Getting the Client IP Address in ASP.NET Core
The Voices of Reason
# re: Doing away with an Assembly Dependency
The list type that is passed in actually pre-exists on another object and that list is strongly typed. So, creating the key/value pairs there is handled on the business object (ie. AddValidationError() or use an explict new ValidationError(Message,ControlID)). It's the inbound interface for the data binder that has the generic IList parameter that really should be something like List<ValidationError> or ValidationErrorCollection but which is defined in the other assembly.
I thought about interface abstraction but it comes back to the same thing - the type needs to exist in both assemblies for it to be useful and that means one or the other needs a reference to the other.
I suspect from a language perspective there's no alternative to what I have here, but maybe some sort of other pattern to represent this data would work a little cleaner might apply and I'm not seeing it.
# re: Doing away with an Assembly Dependency
You may decide to put an Interface into this third library instead of the object, that depends on your architecture.
I don't know the code in ValidationErrorCollection, so not sure how this is implemented but this is the solution I usually implement to avoid this type of dependencies issues.
This way you can compile both libraries without changes.
May this help?
# re: Doing away with an Assembly Dependency
# re: Doing away with an Assembly Dependency
please have a look at the sources of Rob Cnoery's SubSonic (www.subsonicproject.com). I'v been struggled with type not found exceptions for MySql.Data an in another project for the enterprise library (containg MS SQL data access code) until I found out that the required assemblies are neither in the bin folder nor in the GAC, but all of them referenced in the SubSonic assembly. For me it seems that .NET does nothing if it fails to load an assembly, but throws an exception if in should use a type which is not loaded. Maybe you have a look at the SubSonic sources and try to use a (runtime) configuration option which enables/disables your business object validations and only call methods using types from you bo assembly if that option is enabled.
BTW: Some time ago I found some problems in your AppConfiguration class, esp. concerning System.Timespan. Please give me a note to which email address I can send you my changes in that class.
Kind regards,
Marco
# re: Doing away with an Assembly Dependency
I think that you have a classic inter-library (or class) communications problem here.
Conditional compilation, as you've done, is one solution, but generally not the preferred method (as you've pointed out). Of course, your solution exactly mirrors what you're trying to do, you want that class to have functionality contingent on the existence of another class reference.
So you implemented the solution you needed, but as you pointed out, it didn't feel right. My guess is that you've actually solved the wrong problem. You're implementing kind of a top-down library hierarchy, but then you're pulling types from a lower level.
Architecturally, there are two solutions here:
1. Move the base definition of the type you're "pulling".
2. Standardize the communication interface.
You can't do #1, b/c the "other" library is stand-alone. So you have to do #2. However, you want both libraries to be independent.
So either you use an existing type to which both libraries already have access: Exception? Pass in strings b/c that's all that you're using? Truth is, the whole "GetProperty" method is just a method of standardizing the interface, so it's probably as good as any.
Otherwise, this sounds like a textbook case of anonymous types (LINQ?) and Extension methods.
# re: Doing away with an Assembly Dependency
I think you want to achieve two things here: 1) decouple the libraries, and 2) preserve the meaning of the methods and their parameters (to avoid the documentation problem).
The first requirement you already covered by having IList parameter (or IDictionary<string, string>), but that doesn't cover the second requirement.
The original implementation, on the other hand, covers the second requirement, but not the first one. I believe you can have both by employing an adapter-like pattern here.
The way I'd suggest to solve this is:
1. Create a ValidationErrorsCollection (name it as you like) class that resides in your controls library and is specific to the functionality of collecting error messages for the form. It has nothing to do with your business class (covers the first requirement). The new class would be used in the AddValidationErrorsToBindingErrors method. It would essentially have a IDictionary<string, string>-like structure underneath, but would also convey the meaning of what it holds (covers the second requirement).
2. Create a static adapter method on the new Controls.ValidationErrorsCollection class to accept some commonly accessible DTO-like object, which in your case is IDictionary<string, string>. That means create a Controls.ValidationErrorsCollection.FromDictionary(IDictionary<string, string). Set the name this method to whatever satisfies you in terms of the second requirement.
3. In your Business.ValidationErrorsCollection class create an export method, that will push the elements into the generic-purpose DTO object (the IDictionary<string, string>): Business.ValidationErrorsCollection.ToDictionary()
4. In your codebehind/controller class bridge the two collections together like:
page.AddValidationErrorsToBindingErrors(
Controls.ValidationErrorsCollection.FromDictionary(Business.ValidationErrorsCollection.ToDictionary())
);That's a basic quick-n-dirty implementation. You can further play with it to make it more elegant, e.g:
* move the .FromDictionary functionality to a constructor overload;
* have the .ToDictionary be implemented as a implicit cast operator;
* rename the Controls.ValidationErrorsCollection to something different than the Business class (lets say BindingErrorsCollection);
* rename the AddValidationErrorsToBindingErrors to something shorter (like AddBindingErrors);
* optionally move all this into Extension methods and use it on a per-case basis.
Having all this done you would end up with:
page.AddBindingErrors(new BindingErrorsCollection(Business.ValidationErrorsCollection));The binding errors now can come from any place (not just your business layer), as long as the source can be converted to the DTO object (the dictionary). That opens a few more possibilities.
Hope this solves your problem,
Marcin.
# re: Doing away with an Assembly Dependency
Extension methods aren't an option in this scenario because the libraries need to work in plain 2.0, but even with them I still wouldn't solve the clear interface issue. Anonymous types would have to know exactly what to pass ({ { Message="", ControlID="" },{...} }) as well which requires more knowledge than passing the type. But I just realized that even as it is now the code works with any List type that has a member object that includes Message and ControlID properties so the method would actually work with anonymous types which is actually a useful use case.
@Marcin - yeah that would work and is kind of along the way I was thinking but didn't quite figure out the finer details. But in the end this too requires a non-strongly typed value (the list or dictionary) so while it is more explicit it still ends up being 'generic'.
As I mentioned above I think even with the IList parameter the input can come from anywhere as long as Message and ControlID properties are present (actually only Message since the wwUtils.GetProperty calls would result in null if they fail and just the message would add).
But this totally gets me thinking about another way to look at this and how any errors can be added to the binding errors collection. In fact, I'm going to add a more 'generic' overload for the AddBindingError() method that adds a single bindingError.
Thanks all for the discussion - sometimes it's just extremely useful to see how others look at the same problem when you work in - gulp - isolation. <g>
# re: Doing away with an Assembly Dependency
At present the library of the caller of the wwDatabinder.AddValidationErrorsToBindingErrors() (let's call it LibraryA) must already have a reference to both the wwDataBinder and business object library in order to make the call?
(as it will be making a call to MsdnMag.Web.Controls.wwDatabinder.AddValidationErrorsToBindingErrors(errors) where errors is a Westwind.BusinessObjects.ValidationErrorCollection type).
If so, I would say your solution might be to introduce new DataBinderValidationErrorCollection and DataBinderValidationError classes into the project and have a utility method in LibraryA that knows how to convert a BusinessObjects.ValidationError into a DataBinderValidationError (and its collection).
This is duplicating the error container classes but does introduce the strong typing you need - is there a more complicated reason why you can't do this?
e.g. the caller in LibraryA is more generic and does not call wwDatabinder.AddValidationErrorsToBindingErrors() directly?
# re: Doing away with an Assembly Dependency
In the end this is a specialty case, where a generic library adds a non-generic feature <s>. In reality having the dependency on the business library actually works in production - it's at compile time that the compiler complains about the missing assembly reference.
# re: Doing away with an Assembly Dependency
Where can I get the latest source code of wwDatabind?
# re: Doing away with an Assembly Dependency
It still wouldn't eliminate the need for documentation specifying that the key is the controlId and the value is the message, but it gets you closer. The only way I can see to avoid that is a base library with either an interface or a ControlId/Message struct that can be used in an IList