Skip to content

IoC Container

Max Dörner edited this page Nov 8, 2018 · 7 revisions

The Rubberduck (RD) project uses an Inversion of Control (IoC) container to facilitate its dependency injection (DI) needs, in particular constructor injection and property injection for specific classes of objects. This means that IoC container is used to resolve the constructor parameters for most objects used in RD.

An explicit request to resolve an object only appears in one place in RD only, in the so called composition root. There, the App object gets resolved. The remaining resolution either happens during recursively resolving this object or is deferred to automatically generated factories supplied by the IoC container.

The currently used IoC container in RD is Castle Windsor (CW).

General Setup

Our IoC container has several useful registration features we use to ease the burdon of registering components in the IoC container. This includes conventions, which register entire categories of classes, and automagic factories, which allow to defer (repeated) resolution of objects to a later point in time than the resolution in the composition root. We also use the property injection capabilities to inject the commands into our view models.

Registrations by Convention

With a registration by convention, concrete classes get registered to interfaces based on general conditions. We currently use the following conventions:

  • We register almost all concrete types to their default interface, in particular a class Something will automatically register to the interface ISomething if it exists. The resolution happens in transient scope, i.e. each user gets its own object.
  • Code inspections must be implement IInspection or IParseInspection depending on the nature of the inspection. In both cases they automatically multi-register to IInspection, in transient scope. This means that resolving an IEnumerable<IInspection> will yield an enumerable containing all inspections.
  • Quickfixes have to implement IQuickFix and are automatically multi-registered to this interface, in singleton scope, i.e. each requestor will get the same object.
  • Auto complete handlers have to derive from AutoCompleteHandlerBase and are multi-registered to this class, in transient scope.
  • Code metric providers have to derive from 'CodeMetric` and are multi-registered to this class, in singleton scope.
  • All interfaces whose names end in Factory get registered as automatic factories implemented by the IoC container (see below), in singleton scope.

Automagic Factories

Our IoC container allows a special kind of registration factory interfaces that makes the container provide the implementation. More precisely, when registering an interface like

public interface ISomeFactory : IDisposable
{
    ISomething Create(IFoo foo);
    void Release(ISomething something);
}

as a factory interface, CW will resolve the interface ISomething when Create is called. To construct the concrete type registered to ISomething, it will use the argument for the parameter foo for a constructor parameter of the same name of the concrete type's constructor. All other constructor arguments will be resolved by CW.

Note that such automagic factories should generally only be used with interfaced for which the registration is in transient scope, i.e. where every caller gets a new object. If the registration is in singleton scope, the second caller will get the same object as the first without any regard to the paramters he provided. This can cause very surprising behaviour.

In the factory interface, multiple create methods of nearly any name can be mixed; Create is no special name. The only restriction is that the IoC container must be able to resolve the return type using the parameters provided.

For the create methods there is one special naming convention: a create method of the form GetSoemthing uses a named registration Something to resolve the return type. So, be cautious not to start the methods with Get unless you know what you are doing.

Note that a each automagic factory will keep a reference to all objects it has resolved in order to be able to dispose them when the IoC container gets disposed. This means that without further action the lifetime of the resolved objects is at least as long as that of the factory. To tackle this problem, each void method on the interface gets implemented as a release method, i.e. resolved components passed to these methods get released from the container and disposed if they implement IDisposable. Here, the names of the methods are irrelevant, with the excpetions of Dispose.

If the factory interface implements IDisposable, a call to the Dispose method will dispose the factory, which triggers a release of all objectes resolved by the factory, including disposal if they implement IDisposable themselves.

For more information refer to the documentation.

Deferred Resolution via Factories

The automagic factories combined with the type Lazy<T> allow to defer the resolution of heavy-weight components to the time when they are used the first time. To do this the factory gets constructor injected instead of the component in order to use its create method as the construction delegate for the Lazy<T>.

Suppose we have the following class using an IHeavyComponent.

public class Foo
{
    public Foo(IHeavyComponent heavyComponent)
    {
        Heavy = heavyComponent;
    }

    public IHeavyComponent Heavy { get;}
}

To make it load the heavy dependency lazily, we first define the following factory interface.

public interface IHeavyComponentFactory : IDisposable
{
   IHeavyComponent Create();
   void Release(IHeavyComponent heavyComponent);
}

How we add this to the class Foo depends on whether Foo will live for the lifetime of the container anyway or not. In the first case, thigs are simple.

public class Foo
{
    private readonly Lazy<IHeavyComponent> _heavy;

    public Foo(IHeavyComponentFactory heavyComponentFactory)
    {
        _heavy = new Lazy<IHeavyComponent>(heavyComponentFactory.Create);
    }

    public IHeavyComponent Heavy => _heavy.Value;
}

In the second case, we have to ensure that the factory releases the IHeavyComponent.

public class Foo : IDisposable
{
    private readonly Lazy<IHeavyComponent> _heavy;
    private readonly IHeavyComponentFactory _heavyComponentFactory;

    public Foo(IHeavyComponentFactory heavyComponentFactory)
    {
        _heavyComponentFactory = heavyComponentFactory;
        _heavy = new Lazy<IHeavyComponent>(heavyComponentFactory.Create);
    }

    public IHeavyComponent Heavy => _heavy.Value;

    public void Dispose()
    {
        _heavyComponentFactory.Release(_heavy.Value)
    }
}

Property Injection

Our IoC container allows to inject values into settable properties upon reolution. In genreal, this causes more problems than it solves, because there are a lot of classes with settable properties not intended to be filled upon consttuction. Consquently, we limit the scope of property injection. More precisely, properties only get injected into classes deriving from ViewModelBase and only properties for commands, i.e. classes deriving from CommandBase, get injected.

The extend of property injection is governed by the RubberduckPropertiesInspector.

Component Registration

All components have to be registered to the CW IoC container via an IWindsorInstaller. The RubberduckIoCInstaller in Rubberduck.Main.Root is our implementation of this interface. Here, you can find all component registrations for RD.

To understand the registration it is helpful to consult CW's documentation, in particular the part concerning the registration API. Nonetheless, some guidance regarding constructs used in the RubberduckIoCInstaller is provided below.

General Behaviour and Restrictions

Scopes/Lifestyles

There can Only be One Registration

Multi-Registration and Who Wins

Individual Registration

Providing Specific Dependencies

Registration by Convention

Restricting Conventions

Factory Registration

Clone this wiki locally