Creating CAB-module service dependency hierarchies
The Problem
Microsoft’s Composite UI Application Block (CAB) attempts to instantiate and inject services on your behalf, but it often fails when you have services depending upon other services.
The Context
The CAB provides attributes for us to (a) declare that a class is a service that should be instantiated and placed into a Services collection for others to use, and (b) declare within any class a dependency upon that service and let the CAB framework deal with creating the service instance and supplying it to your client instance. Troubles arise, though, when your service itself depends upon another service. The ObjectBuilder has support for solving dependency graphs, but if you assumed they leverage that for services creation, brace yourself for disappointment. Worse, in our project we have frequently seen the same executable with this hierarchy of services work on certain clients and not work on others. What follows below is just enough spelunking into the CAB module initialization code to describe why the problem occurs, and offer one reliable way to avoid it.
First, some boundaries for this discussion:
- Assume all the services we want created exist in the same module;
- All the services are not required explicitly by any code executed in the startup-assembly. That is a special case that is module-loaded first, before other modules and it does not participate in any dependency-graph-based loading of modules. If it needs instances of types from outside modules, it has to explicitly create them itself.
Before modules are loaded (in the CAB sense, at least), for each module listed in the ProfileCatalog.xml file, the CAB loads the assembly (in the .NET Assembly.LoadFrom sense) and reflects over all the public types found on the assembly, creating simple lists of types it cares about: types that implement ModuleInit, types with ServiceAttribute tags, types that extend WorkItem, and other things. Armed with this metadata (including assembly-level attributes declaring CAB module dependencies), it builds a dependency-ordered list of modules to load and loads them one by one. For each module, it tries to fill the Services collection with all its services before initializing the module classes — but it does this by simply iterating over all the types with a [Service] attribute, it doesn’t solve any dependency graph of those services first. So if the first class in the list depends on another service, the CAB will throw.
A Solution
(Or is it just a workaround?) One way to avoid this failure is to make all your services ‘AddOnDemand’ by changing your service tags to:
[Service(typeof(IMyService), AddOnDemand=true)]
By doing this we get significantly different behavior in the CAB module load. Instead of trying to instantiate that type and add the instance to the Services collection, the CAB puts a placeholder for it into the Services collection. It doesn’t have to worry about any further service dependencies of this service because it’s not actually creating it; so the CAB can merrily cycle through this list creating placeholder after placeholder, fully populating its Services collection. When your code actually wants the service, the CAB can succesfully generate the dependencies ‘on demand’ because it has all the necessary info to do so in the Services collection. I don’t think the library designers were trying to solve this dependency-injection problem when they implemented the AddOnDemand concept, but so what: it works.



December 5th, 2006 at 1:22 pm
Hi,
Thanks for this and your post on gotdotnet. Very clear and very helpful. The nuggets of info that get revealed on gotdotnet and other blogs about CAB are so useful ad I look forward to your next one!!
John