Inside CAB Dependency Injection

Table of Contents

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:

  1. Specify dependencies between your objects
  2. Get a hold of an IoC container object.
  3. Call some kind of factory method to create your application objects.

In CAB, this translates to:

  1. Specify dependencies between your objects using special custom attributes
  2. Get a hold of an instance of the WorkItem class or derived class.
  3. Call AddNew() or Add() 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".

WorkItem Anatomy

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:

  1. Public constructor (a.k.a. injection constructor).
  2. Public properties marked with special attributes such as [Dependency] or [ServiceDependency].
  3. 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 undescriptive InvalidAttributeException.

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:

AttributeMeaning
[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:

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.