Skip to content

IoC Container

bclothier edited this page Nov 18, 2018 · 7 revisions

The Rubberduck (RD) project uses an Inversion of Control (IoC) container to facilitate its dependency injection (DI) needs. In general, we use constructor injection. For specific classes of objects, we use property injection. 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 burden 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 named 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 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 must derive from AutoCompleteHandlerBase and are multi-registered to this class in transient scope.
  • Code metric providers must 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 for 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: Do not implement the ISomeFactory; the implementation is provided by CW automatically. In some cases, we choose to implement the factories ourselves. In those scenarios, those factories are registered as ordinary resolved objects.

Note that such automagic factories should generally only be used with interfaces 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 parameters the user provided. This can cause very surprising behaviour.

In the factory interface, multiple create methods of nearly any name can be mixed; Create is not a special name. CW only considers the method's signature in determining which is a creation method and which is a release method. The only restriction is that the IoC container must be able to resolve the return type using the parameters provided. If the parameters should come from CW itself, it can be omitted as CW will resolve that. But if the parameter itself is not resolvable using CW, you may get a runtime error failing to create the object.

For the create methods, there is one special naming convention: a create method of the form GetSomething 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 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 exception of Dispose.

If the factory interface implements IDisposable, a call to the Dispose method will dispose the factory, which triggers a release of all objects 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 should be 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()
    {
        if(_heavy.IsValueCreated)
        {
            _heavyComponentFactory.Release(_heavy.Value)
        }
    }
}

Property Injection

Our IoC container allows us to inject values into settable properties upon resolution. In general, this causes more problems than it solves, because there are a lot of classes with settable properties not intended to be filled upon construction. Consequently, we limit the scope of property injection. More precisely, we only use it on classes deriving from ViewModelBase class and only for its properties deriving from CommandBase class.

The scope 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

Before providing examples what the actual setup code in CW looks like, there are some general concepts and behaviours that need explaining.

Scopes/Lifestyles

In CW, component registrations can have different lifestyles. Note that in other IoC containers, those are often called scopes. These determine how many instances are generated for a registration. There are quite a few lifestyles, but for RD, only two are really relevant: transient and singleton.

For registrations in transient lifestyle, each request gets its own instance of the concrete class registered to the interface requested. For interfaces resolved by factories, this is usually the right scope. Generally, this lifestyle should be chosen if each user needs a separate set of data on its instance.

For registrations in singleton scope, each request gets the same instance of the concrete class registered to the interface requested. There are two main reasons to use this lifestyle. First, objects that form a global repository or function as a global hub for specific interactions like the RubberduckParserState or the IRewritingManager require this lifestyle to ensure that everybody uses the same data and the same access point to specific functionality. Second, this scope can ease the memory burden for concrete classes without state. However, any dependency of the object will remain in memory until the container gets disposed.

In contrast to most other IoC containers, in CW singleton is the default lifestyle. Since this is not obvious, in RD it is preferred to always state the lifestyle explicitly.

There can Only be One Registration

In CW, registrations belong to the implementing (concrete) type, not the interface. This is important to realize because CW does not support multiple registrations of the same (implementing) type. If you add a second registration, CW will issue an error at runtime. This is one of the reasons you should always test whether RD actually loads after changing registrations.

Generally, CW will not complain if a registration by convention covers an implementing type already registered; it simply ignores it in the convention. However, should a convention pick up an implementing type that gets registered via an individual registration later, the later registration will throw. Consequently, registrations by convention should come after individual registrations.

Multi-Registration and Who Wins

With CW, you can register as many implementing types with an interface as you want. This will result in a multi-registration. With the setup we use in RD, requesting an IEnumerable<IInterface> will yield an enumerable of all the concrete types in the (multi-)registration for IInterface.

In the presence of a multi-registrations, it is still possible to request a single instance of the interface. In this case, the implementing type registered first will be returned. This is different to a number of other IoC containers that return the one registered last.

Individual Registration

A typical individual registration will consist of a call to For<TInterface> to specify the type(s) to be implemented, one to ImplementedBy<TClass> to specify the implementation and one specification of the lifestyle.

container.Register(Component.For<RubberduckParserState, IParseTreeProvider, IDeclarationFinderProvider, IParseManager>()
    .ImplementedBy<RubberduckParserState>()
    .LifestyleSingleton());

The registration above showcases that up to five interfaces can be registered at the same time using the generic syntax.

Providing Specific Dependencies

Sometimes, the concrete class implementing an interface either has a constructor parameter of a concrete type or has to use a different concrete implementation than the default registration for the parameter type. In these cases, one can use DependsOn to specify the dependency.

The dependencies can be specified in different ways using the methods of the Dependency class.

To specify a fixed value, you can use OnValue<T>:

container.Register(Component.For<VBAPredefinedCompilationConstants>()
    .ImplementedBy<VBAPredefinedCompilationConstants>()
    .DependsOn(Dependency.OnValue<double>(double.Parse(_vbe.Version, CultureInfo.InvariantCulture)))
    .LifestyleSingleton());

To specify a dependent type, you can use OnComponent<TInterface, TImplementing>:

container.Register(Component.For<VBAPreprocessorParser>()
    .ImplementedBy<VBAPreprocessorParser>()
    .DependsOn(Dependency.OnComponent<IParsePassErrorListenerFactory, PreprocessingParseErrorListenerFactory>())
    .LifestyleSingleton());

If you need to use different implementations for different constructor arguments of the same type, you can use OnComponent(string paramName, Type implementingType):

container.Register(Component.For<IModuleParser>()
    .ImplementedBy<ModuleParser>()
    .DependsOn(Dependency.OnComponent("codePaneSourceCodeProvider", typeof(CodePaneSourceCodeHandler)),
        Dependency.OnComponent("attributesSourceCodeProvider", typeof(SourceFileHandlerSourceCodeHandlerAdapter)))
    .LifestyleSingleton());

Registration of Already Existing Instances

The CW IoC container allows to register already existing instances as implementations of a type. These will always act as registered in singleton scope, no matter which lifestyle gets specified. (It is always the concrete instance you provided that gets returned.)

container.Register(Component.For<IVBE>().Instance(_vbe));

We only use this kind of registration to register the top-level COM objects handed to us on startup or immediately acquired from these. These cannot be generated by CW.

Registration by Convention

Registering all types in RD individually would be rather cumbersome, so we use CW's capability to register components by convention.

container.Register(Classes.FromAssembly(assembly)
    .IncludeNonPublicTypes()
    .Where(type => type.Namespace != null
            && !type.Namespace.StartsWith("Rubberduck.VBEditor.SafeComWrappers")
            && !type.Name.Equals(nameof(SelectionChangeService))
            && !type.Name.Equals(nameof(AutoCompleteService))
            && !type.Name.EndsWith("Factory")
            && !type.Name.EndsWith("ConfigProvider")
            && !type.Name.EndsWith("FakesProvider")
            && !type.GetInterfaces().Contains(typeof(IInspection))
            && type.NotDisabledOrExperimental(_initialSettings))
    .WithService.DefaultInterfaces()
    .LifestyleTransient()

Specifying the Implementing Types

A registration by convention always starts with a specification of the implementing types for which to add a registration. Usually we use Classes.FromAssembly(assembly) or Types.FromAssembly(assembly) since we have multiple assemblies to load from. The difference between the version with Classes and with Types is that the former only considers concrete classes whereas the latter also considers interfaces and abstract classes.

We always add IncludeNonPublicTypes to enable registration of internal types in other assemblies. This allows us to make interfaces only used inside one project internal to remove any possible usage from outside the solution.

Next follows a restriction of the implementing types to register. We use two variants, one using a predicate via the Where method and one using BasedOn<TInterface>. The latter filters for implementations of the interface.

It is important to note that applying both Where and BasedOn<T> yields a union of the filter results and not the intersection of the results. In order to specify further a registration using BasedOn<TInterface> one has to use either If or Unless, which have the implied meaning.

container.Register(Classes.FromAssembly(assembly)
    .IncludeNonPublicTypes()
    .BasedOn<IInspection>()
    .If(type => type.NotDisabledOrExperimental(_initialSettings))
    .WithService.Base()
    .LifestyleTransient());

Specifying the Types to be Implemented

The next part of convention is the type to be implemented, the service. There are several possibilities, two of which can be seen in the examples above. The version with Base() registers to the generic type in the call to BasedOn<T>, which has to be present for this to make sense. The version with DefaultInterfaces() registers interfaces to concrete types based on the names -- if the interface is named IFoo, it will connect to any concrete types containing Foo in its name, and that will be returned.

It is also possible to specify the types to be implemented explicitly using Select(Type[] types).

container.Register(Classes.FromAssembly(assembly)
    .IncludeNonPublicTypes()
    .BasedOn<IParseTreeInspection>()
    .If(type => type.NotDisabledOrExperimental(_initialSettings))
    .WithService.Select(new[] { typeof(IInspection) })
    .LifestyleTransient());

Further service specifications are AllInterfaces() and Self(), which do the obvious thing.

Specifying Further Configuration

Usually, the convention ends with the specification of the lifestyle of the registrations. However, there are two further clauses we use to modify the convention, OnCreate and Configure.

The OnCreate method allows to perform actions right after creation of the implementing object. This can be used to perform property injection.

container.Register(Component.For<IParentMenuItem>()
    .ImplementedBy<TMenu>()
    .LifestyleTransient()
    .DependsOn(
        Dependency.OnValue<int>(beforeIndex),
        Dependency.OnComponentCollection<IEnumerable<IMenuItem>>(nonExperimentalMenuItems))
    .OnCreate((kernel, item) => item.Parent = controls));

The Configure method allows to perform specifications like DependsOn on all registered components. We will see an example in the next section.

There are also versions ConfigureFor<T> and ConfigureIf that restrict the configuration to certain types, but we do not use them in RD.

Factory Registration

We register all our factory interfaces for the automagic factories by convention. This is done by simply adding the configuration AsFactory().

container.Register(Types.FromAssembly(assembly)
    .IncludeNonPublicTypes()
    .Where(type => type.IsInterface 
                   && type.Name.EndsWith("Factory") 
                   && !type.Name.Equals("IFakesFactory")
                   && type.NotDisabledOrExperimental(_initialSettings))
    .WithService.Self()
    .Configure(c => c.AsFactory())
    .LifestyleSingleton());

As noted previously; those should be interfaces without a corresponding implementation. CW will provide the implementation. In cases where we use concrete factories, those should be explicitly registered as an ordinary object without the AsFactory().

Clone this wiki locally