Inside CAB Dependency Injection
Table of Contents
- Introduction
- Work Item: CAB IoC Container
- Build-up process
- Specifying object dependencies in CAB
- Critique of CAB dependency injection
- Conclusion
See also:
Introduction
CAB and SCSF
CAB (Composite UI Application Block) is an example-turned-framework product from the Microsoft Patterns and Practices group. CAB is a framework for developing modular client applications. SCSF (Smart Client Software Factory) is a new, extended version of CAB, but as of May 2007 edition the basic framework remains virtually unchanged.
Audience
This article assumes that you are familiar with the basic structure of the CAB application. Since SCSF is much more complex than the original CAB, CAB quickstarts may be a good starting point. In-depth knowledge of CAB is not required.
CAB and Dependency Injection
Among other features, CAB provides a
dependency injection
mechanism called ObjectBuilder
. ObjectBuilder received some future
development outside of CAB as a
CodePlex
project. However, this article considers the version of ObjectBuilder
shipped with CAB and later with Enterprise Library 3.1, used by SCSF.
As far as I can tell, two versions are virtually identical, the principal
difference being that the Enterprise Library version contains a binary
signed with Microsoft key. The original CAB version shipped
as source code only.
CAB is not the only framework that offers dependency injection capabilities for .NET. Other dependency injection frameworks, also known as Inversion of Control (IoC) Containers are Spring.NET, Castle, StructureMap et al.
Work Item: The CAB IoC Container
To use dependency injection, you general need three things:
- Specify dependencies between your objects
- Get a hold of an IoC container object.
- Call some kind of factory method to create your application objects.
In CAB, this translates to:
- Specify dependencies between your objects using special custom attributes
- Get a hold of an instance of the
WorkItem
class or derived class. - Call
AddNew()
orAdd()
method on one of the work item member collections.
In other words, WorkItem
is your CAB IoC container.
Unlike other IoC containers, WorkItem
has multiple responsibilities
and complicated (and somewhat confusing) public interface. If we focus only
on dependency injection and leave everything else out, it would look like this:
class WorkItem { public ManagedObjectCollection<Command> Commands { get; } public ManagedObjectCollection<EventTopic> EventTopics { get; } public ManagedObjectCollection<object> Items { get; } public ServiceCollection Services { get; } public ManagedObjectCollection<object> SmartParts { get; } public ManagedObjectCollection<WorkItem> WorkItems { get; } public ManagedObjectCollection<IWorkspace> Workspaces { get; } }
Anatomy of a WorkItem
An important (and somewhat shocking) thing to remember about work item collections is that work item collections are not real. They all are facades to a hidden collections called "locator" and "lifetime container".
The "Anatomy of a WorkItem" text describes inner structure of a work item in more detail.
Building Objects with WorkItem
You build new objects by calling Add()
method on one of the colections.
Since the collections are public, this may be done either inside or outside
a work item class:
class MyWorkItem : WorkItem { protected override void OnRunStarted() { base.OnRunStarted(); Items.AddNew<TypeToBuild>(); // using Items collection OutsideClass.SomeMethod(this); } } class OutsideClass { static void SomeMethod(WorkItem workItem) { workItem.Services.AddNew<SomeServiceType>(); // using Services collection } }
Getting a Hold of a Work Item
There is no static method that would give you an instance of "a work item". In theory,
you can create a new work item by simply calling new WorkItem()
, but such a work item is not very useful. A work item must be properly initialized
for the dependency injection to work.
In CAB application, work items make up a tree.
The root work item is created and initialized by the CAB application object.
Each work item may have zero or more child work items stored in its WorkItems
collection.
Root work item type is specified as a tempalte parameter of the CAB application class:
class MyShellApplication : FormShellApplication<ShellWorkItem, ShellForm> { [STAThread] static void Main() { new MyShellApplication().Run(); } }
You may start using your root work item by overriding one or more
protected methods in your root work item type, typically OnRunStarted()
.
This method is automatically called on application startup.
Alternatively, you may use the root work item from within
your application type methods, such as AfterShellCreated()
.
The Build-Up Process
Besides the locator, work item contains another hidden object
called "builder". Normally, all work items in the work item tree share the
same builder instance. The builder is responsible for creating new objects when the user
calls AddNew()
, or injecting dependencies to existing objects when
the user calls Add()
. These two processes are slightly different,
since for new objects the builder gets to choose which constructor to call,
while for existing objects it can only set properties. Building up an existing
object is a proper subset of building up a new object.
The build-up process is divided into a number of steps called "strategies".
Some of these strategies deal with calling constructors, others with setting
properties, etc. There is about a dozen different strategies in a standard
CAB builder. The builder also contains a number of "policies" that influence
how strategies work. One of the strategies, called CreationStrategy
is responsible for calling object's constructor (if making up the new object
and adding the object to the locator.
Specifying Object Dependencies in CAB
In CAB, dependencies between objects are specified using custom attributes. There is no dependency map file, or custom dependency map sources. This means that object dependencies are specified in code and depend only on object type. I.e., two objects of the same type created in the same context will usually receive the same dependencies.
There are three methods to specify dependencies in CAB:
- Public constructor (a.k.a. injection constructor).
- Public properties marked with special attributes such as
[Dependency]
or[ServiceDependency]
. - Public methods marked with
[InjectionMethod]
attribute.
When you create a new object via AddNew()
the builder first
calls the injection constructor, and also sets the injection properties
and calls the injection methods. When you add an existing object using
Add()
, it is not possible to call the constructor, since
the object has already been created. In this case the builder only
sets the injection properties and calls the injection method.
Injection Constructor
When creating a new object, CAB builder needs to pick a constructor, and then try to supply values for its parameters. When picking the constructor, CAB follows the rules below:
- If the object does not define any constructors, CAB will use the default constructor.
- If the object defines exactly one constructor, CAB will use that constructor.
- If the object defines multiple constructors and none of them has
[InjectionConstructor]
attribute, CAB will use the first constructor in declaration order (or, rather, in reflection order). The first constructor will be chosen regardless of whether CAB can actually supply all necessary parameter values for it, or whether other constructors are "better" or "more suitable". - If the object defines multiple constructors and one of them is
marked with
[InjectionConstructor]
attribute, CAB will use that constructor. - If the object defines more than one constructor marked with
[InjectionConstructor]
attribute, initialization will fail with rather undescriptiveInvalidAttributeException
.
Dependency Attribute Types
Dependencies of CAB objects are specified using custom attributes in code. For each injection constructor parameter, injection method parameter, or injection property, CAB user may apply one of the following attributes:
Attribute | Meaning |
---|---|
[Dependency] |
Locate an object by type and/or ID, with ability to create a new object if the dependency is not found. |
[CreateNew]
| Always create a new object for this dependency. |
[ServiceDependency] |
Locate a service by type. Supports advanced features such as service type being different from the parameter type, and add on demand. |
[ComponentDependency] |
Locate an object by type and non-null ID.
A proper subset of features offered by [Dependency] |
At most one dependency attribute can be applied.
Parameters without attribute are treated as if they had a [Dependency]
attribute
with default settings. Properties without attribute do not participate in dependency injection.
In addition, user may specify his/her own custom dependency attribute derived from the abstract
ParameterAttribute
class. This allows to customize dependency injection if
necessary.
NOTE: private/protected constructors, methods and properties are silently ignored by CAB and do not participate in dependency injection.
Dependency attributes influence the way CAB object builder locates object dependencies. Click on attribute type name to see further details about each attribute.
CAB Dependency Injection Critique
CAB dependency injection is definitely a confusing thing. Here is a brief summary of my grievances with it:
- It violates single responsibility principle.
- It violates the principle of least astonishment.
- Its dependency injection capabilities are limited.
- It is hardwired into the heart of CAB
Violation of Single Responsibility Principle
There is no single object that would be responsible for just "IoC" and nothing else.
WorkItem
is the center of the CAB IoC universe but it is responsible for
so many things beyond just IoC. Also, the work item does not have a single
"now give me an object" method. Instead, you have to go through one of the "facade"
collections, all of which will utlimately do the same thing.
Violation of the Least Astonishment Principle
Some of the CAB IoC gotchas are quite
remarkable. Some of them stem from the fact that all work item collections are in fact
"ghosts" that all point to a hidden locator. This idea is not obvious, to say the least,
and I don't think it is clearly documented anywhere. By looking at the WorkItem
external interface no one in their right mind would assume that, say, Items
,
Workspaces
, SmartParts
, and Services
ultimately
point to a single big thing.
Other gotchas are just plain bugs, or unacceptable shortcuts. When your collections
says it ContainsObject(foo)
, while showing object count of zero, this is
not only astonishing, this is an outright deception.
Limited IoC Features
Compared to other IoC containers, CAB offers limited features.
First, object dependencies are specified in code using attributes. This means, that in one context I cannot have two objects of the same type with radically different dependencies. This may not seem like much, but in systems of even moderate complexity this works like a straightjacket.
Second, it is virtually impossible to specify string or other built-in type parameters via IoC. To do that, I must either bypass IoC completely, or resort to tricks and magic IDs:
class StringConsumer { [ComponentDependency("DisplayText")] public string DisplayText { set { Console.WriteLine("Display text is {0}", value); } } } void PassStringAsDependency(WorkItem workItem) { workItem.Items.Add("FooBar", "DisplayText"); workItem.Items.AddNew<StringConsumer>(); }
And third, CAB has very poor support for dependency on abstract types or interfaces. Let me illustrate this with an example. Let's say I have a class that works with a stream. If I have only one stream, I may decide to register it as a service, and thus have something like this:
interface IMyStream {...} class MyStream : IMyStream {...} class MyStreamConsumer { [ServiceDependency] public IMyStream Stream { set {...} } } void InitializeStuff(WorkItem workItem) { workItem.Services.AddNew<MyStream, IMyStream>().Open("file.txt"); workItem.Items.AddNew<MyStreamConsumer>().DoSomething(); }
Now, let's suppose I switch to two streams. I can no longer have IMyStream
as a service, since by definition there may be only one service of particular type.
Probably, having the stream as a service was a bad idea to begin with. So, I convert them
to components:
interface IMyStream {...} class MyStream : IMyStream {...} class MyStreamConsumer { [ComponentDependency("InputStream")] public IMyStream InputStream { set {...} } [ComponentDependency("OutputStream")] public IMyStream OutputStream { set {...} } } void InitializeStuff(WorkItem workItem) { workItem.Items.AddNew<MyStream>("InputStream").Open("file1.txt"); workItem.Items.AddNew<MyStream>("OutputStream").Open("file2.txt"); workItem.Items.AddNew<MyStreamConsumer>().DoSomething(); }
But oops... That would not work! Unlike services, components can be located by
concrete type only. The consumer requests an abstract IMyStream
, but the locator
has only a concrete MyStream
. Services are smart enough to figure it out,
but components are not. So, even though MyStreamConsumer
can work with any
IMyStream
, I am tying it now to concrete MyStream
:
interface IMyStream {...} class MyStream : IMyStream {...} class MyStreamConsumer { [ComponentDependency("InputStream")] public MyStream InputStream { set {...} } [ComponentDependency("OutputStream")] public MyStream OutputStream { set {...} } } void InitializeStuff(WorkItem workItem) { workItem.Items.AddNew<MyStream>("InputStream").Open("file1.txt"); workItem.Items.AddNew<MyStream>("OutputStream").Open("file2.txt"); workItem.Items.AddNew<MyStreamConsumer>().DoSomething(); }
Notice the magic IDs I used. The two streams I give to the consumer must be named "InputStream" and "OutputStream". What if I want two consumers that have different inputs and outputs?
Well, at this point I probably switch to Spring.Net :-)
IoC Is Hardwired Into the Heart of CAB
CAB is a vehicle for building composite UIs. Being an IoC framework is not its main purpose in life. So, it would be reasonable to expect that the IoC susbsystem would be replacable, or at least well isolated. Unfortunately, it is not really so.
CAB is about work items, and work items are CAB IoC. You cannot possibly use
CAB without invoking its IoC. Even if you never AddNew()
anything
and only Add()
things to work item collections, CAB will attempt
to "build-up" your objects,running a dozen or so "strategies" on every addition.
Hopefully, if your objects don't have any CAB dependency attributes, this would be
a no-op, but who knows...
Conversely, you cannot use CAB IoC without using CAB. In order to
use CAB IoC you need a work item, and to get a "real" work item
you need a CAB application. You probably can try to use lower-level
ObjectBuilder
classes, but about half of the CAB
IoC code is actually in CompositeUI
, so you would
lose that part.
In other words, CAB does not really have a separate IoC "subsystem". It is firmly fused into the framework.
As I am writing this, it appears that in theory one may be able to get away with using CAB and creating his or her objects via something like Spring.NET. The only things you cannot create this way are new work items, but it is probably not be a big deal. This, however, does not cancel the fact that CAB and its IoC are very tightly coupled.
Conclusion
The fact that Microsoft recognizes the importance of Inversion of Control and includes IoC capabilities in frameworks like CAB is a Good Thing. The devil, however, is in the details. While the resultion IoC framework is usable, its interface is fuzzy, its features are limited in strange ways, and its implementation is at times flawed.
Would I rather see CAB without an IoC framework at all? I guess not. But I believe that functionally, CAB would be better off integrating an established IoC framework like Spring.NET or at least keeping the user's options open.
In its current form, CAB IoC is something that kinda does the job, but barely and through unnecessary pain.