Dependency Injection Mechanism

The Stormancer C++ Client Library uses a dependency injection framework similar to Autofac, which we use on the server side. This mechanism has various advantages over a more ad-hoc approach. Especially, it enables easy extensibility and composability of features; plugins are a good example of this.

Step-by-step Guide

Dependency injection requires two distinct steps:

  • First, you build a container. This is where you specify each of your dependencies.

  • Then, you create a dependency scope from this container, from which the container’s dependencies can be resolved and used.

Building a Container

First, create a ContainerBuilder object.

For every dependency of type T that you want to make available, call registerDependency<T>() on this object.

This method returns a RegistrationHandle<T>, which exposes methods that allow you to tweak the registration’s settings:

  • Contract settings: which types the registration should be registered as.

  • Lifetime settings: how the lifetime of the registration is controlled.

Method calls on the RegistrationHandle<T> can be chained in a “fluent API” fashion.

Here’s a quick example:

#include "stormancer/DependencyInjection.h"

...

ContainerBuilder builder;
// Register a dependency of type Foo. A std::shared_ptr<Foo> will be instantiated every time resolve<Foo>() is called.
builder.registerDependency<Foo>();
// Register a singleton dependency of concrete type Derived and advertised type Base.
// A dependency can only be resolved through its advertised type(s) (it could have more than one).
// singleInstance() means at most one instance of std::shared_ptr<Derived> will be created.
builder.registerDependency<Derived>().as<Base>().singleInstance();

Once you have registered all the dependencies you need, call build() on the ContainerBuilder to build a DependencyScope from which these dependencies can be resolved:

DependencyScope scope = builder.build();

For a complete reference of registration settings, see the API reference for ContainerBuilder and RegistrationHandle.

Using a DependencyScope

From a DependencyScope, you can resolve() dependencies that were registered with the ContainerBuilder.

DependencyScope::resolve<T>() retrieves a single dependency that was registered under the type T:

std::shared_ptr<Foo> foo = scope.resolve<Foo>();

In case there are multiple dependencies registered as T, resolve<T>() always returns the last dependency that was registered as T with the ContainerBuilder.

DependencyScope::resolveAll<T>() retrieves every dependency that was registered under the type T:

std::vector<std::shared_ptr<Foo>> all_foos = scope.resolveAll<Foo>();

For more details, see the DependencyScope API reference.

In order to prevent the possibility of circular dependencies, DependencyScope instances are not copyable, only movable. As a result, a DependencyScope has unique ownership semantics, similar to std::unique_ptr’s.

Child DependencyScopes

A DependencyScope can have any number of child scopes. Child scopes have access to the registrations of their parents, and can also have their own registrations.

When you resolve a certain dependency on a scope, the registrations local to that scope are considered first, then the registrations of its parent scope, and so on, up to the root scope (the initial scope that has no parent). The registrations of children scopes are not available from a parent scope.

Child scopes are created by calling the method DependencyScope::beginLifetimeScope():

// Create a child scope with no additional dependencies
DependencyScope child = scope.beginLifetimeScope();

// Create a child scope with additional dependencies
DependencyScope childWithDependencies = scope.beginLifetimeScope([](ContainerBuilder& builder)
{
        builder.registerDependency<Foo>();
        builder.registerDependency<Bar>().as<Foo>();
});

Scopes can also be tagged. This is intended to be used in tandem with the instancePerMatchingScope registration:

ContainerBuilder builder;
builder.registerDependency<Foo>().instancePerMatchingScope("mytag");
DependencyScope root = builder.build();

try
{
        root.resolve<Foo>();
}
catch (DependencyResolutionException&)
{
        // The root scope does not match Foo's tag, thus the resolve() call will fail.
}

// Create a tagged child scope
DependencyScope child = scope.beginLifetimeScope("mytag");
// child's tag matches the tag for the Foo registration, thus resolve() succeeds.
std::shared_ptr<Foo> foo = child.resolve<Foo>();

Named Dependencies

In addition to being identified by type, dependencies can also have one or more names.

In the following example, a dependency of concrete type B is registered as type A under the name "named_B":

ContainerBuilder builder;
builder.registerDependency<B>.named<A>("named_B");

This dependency can later be resolved using the resolveNamed() method of DependencyScope:

auto scope = builder.build();
std::shared_ptr<A> my_B = scope.resolveNamed<A>("named_B");

Note that this dependency cannot be retrieved through scope.resolve<A>() or scope.resolveAll<A>().

Examples

Different ways to register dependencies:

class A {};

ContainerBuilder builder;

builder.registerDependency<A>();
// This is equivalent to:
builder.registerDependency<A>([](const DependencyScope& scope) { return std::make_shared<A>(); });

class C {
        public:
        C(std::vector<std::shared_ptr<A>> instances) : _allAinstances(instances) {}

        private:
        std::vector<std::shared_ptr<A>> _allAinstances;
};

builder.registerDependency<C, ContainerBuilder::All<A>>();
// This is equivalent to:
builder.registerDependency<C>([](const DependencyScope& scope) { return std::make_shared<C>(scope.resolveAll<A>()); });