Skip to main content

Advanced registration

This section is about Stashbox's further configuration options, including the registration configuration API, the registration of factory delegates, multiple implementations, batch registrations, the concept of the Composition Root, and many more.

info

This section won't cover all the available options of the registrations API, but you can find them here.

Factory registration

You can bind a factory delegate to a registration that the container will invoke directly to instantiate your service.

You can use parameter-less and custom parameterized delegates as a factory. Here is the list of all available options.

You can also get the current dependency resolver as a delegate parameter to resolve any additional dependencies required for the service construction.

container.Register<ILogger, ConsoleLogger>(options => options
.WithFactory(() => new ConsoleLogger());

// the container uses the factory for instantiation.
IJob job = container.Resolve<ILogger>();

Delegate factories are useful when your service's instantiation is not straight-forward for the container, like when it depends on something that is not available at resolution time. E.g., a connection string.

container.Register<IJob, DbBackup>(options => options
.WithFactory<ILogger>(logger =>
new DbBackup(Configuration["DbConnectionString"], logger));

Factories with parameter overrides

Stashbox can implicitly wrap your service in a Delegate and lets you pass parameters that can override your service's dependencies. Moreover, you can register your own custom delegate that the container will resolve when you request your service wrapped in a Delegate.

container.RegisterFunc<string, IJob>((connectionString, resolver) => 
new DbBackup(connectionString, resolver.Resolve<ILogger>()));

Func<string, IJob> backupFactory = container.Resolve<Func<string, IJob>>();
IJob dbBackup = backupFactory(Configuration["ConnectionString"]);

If a service has multiple constructors, the container visits those first, that has matching parameters passed to the factory, with respecting the additional constructor selection rules.

class Service
{
public Service(int number) { }
public Service(string text) { }
}

container.Register<Service>();

// create the factory with an int input parameter.
var func = constainer.Resolve<Func<int, Service>>();

// the constructor with the int param
// is used for instantiation.
var service = func(2);

Consider this before using the resolver parameter inside a factory

Delegate factories are a black-box for the container. It doesn't have control over what's happening inside a delegate, which means when you resolve additional dependencies with the dependency resolver parameter, they could easily bypass the lifetime and circular dependency validations. Fortunately, you have the option to keep them validated anyway with parameterized factory delegates.

Delegates with dependencies passed as parameters

Rather than using the dependency resolver parameter inside the factory, let the container inject the dependencies into the delegate as parameters. This way, the resolution tree's integrity remains stable because no service resolution happens inside the black-box, and each parameter is validated.

interface IEventProcessor { }

class EventProcessor : IEventProcessor
{
public EventProcessor(ILogger logger, IEventValidator validator)
{ }
}

container.Register<ILogger, ConsoleLogger>();
container.Register<IEventValidator, EventValidator>();

container.Register<IEventProcessor, EventProcessor>(options => options
// Ilogger and IEventValidator instances are injected
// by the container at resolution time, so they will be
// validated against circular and captive dependencies.
.WithFactory<ILogger, IEventValidator>((logger, validator) =>
new EventProcessor(logger, validator));

// the container resolves ILogger and IEventValidator first, then
// it passes them to the factory as delegate parameters.
IEventProcessor processor = container.Resolve<IEventProcessor>();

Accessing the currently resolving type in factories

To access the currently resolving type in factory delegates, you can set the TypeInformation type as an input parameter of the factory. The TypeInformation holds every reflected context information about the currently resolving type.

This can be useful when the resolution is, e.g., in an open generic context, and we want to know which closed generic variant is requested.

interface IService<T> { }

class Service<T> : IService<T> { }

container.Register(typeof(IService<>), typeof(Service<>), options =>
options.WithFactory<TypeInformation>(typeInfo =>
{
// typeInfo.Type here holds the actual type like
// IService<int> based on the resolution request below.
}));

container.Resolve<IService<int>>();

Multiple implementations

As we previously saw in the Named registration topic, Stashbox allows you to have multiple implementations bound to a particular service type. You can use names to distinguish them, but you can also access them by requesting a typed collection using the service type.

note

The returned collection is in the same order as the services were registered. Also, to request a collection, you can use any interface implemented by an array.

container.Register<IJob, DbBackup>();
container.Register<IJob, StorageCleanup>();
container.Register<IJob, ImageProcess>();
// jobs contain all three services in registration order.
IEnumerable<IJob> jobs = container.ResolveAll<IJob>();

When you have multiple implementations registered to a service, a request to the service type without a name will return the last registered implementation.

info

Not only names can be used to distinguish registrations, conditions, named scopes, and metadata can also influence the results.

container.Register<IJob, DbBackup>();
container.Register<IJob, StorageCleanup>();
container.Register<IJob, ImageProcess>();

// job will be the ImageProcess.
IJob job = container.Resolve<IJob>();

Binding to multiple services

When you have a service that implements multiple interfaces, you have the option to bind its registration to all or some of those additional interfaces or base types.

Suppose we have the following class declaration:

class DbBackup : IJob, IScheduledJob
{
public DbBackup() { }
}
container.Register<IJob, DbBackup>(options => options
.AsServiceAlso<IScheduledJob>());

IJob job = container.Resolve<IJob>(); // DbBackup
IScheduledJob job = container.Resolve<IScheduledJob>(); // DbBackup
DbBackup job = container.Resolve<DbBackup>(); // error, not found

Batch registration

You have the option to register multiple services in a single registration operation.

Filters (optional): First, the container will use the implementation filter action to select only those types from the collection we want to register. When we have those, the container will execute the service filter on their implemented interfaces and base classes to select which service type they should be mapped to.

note

Framework types like IDisposable are excluded from being considered as a service type by default.

tip

You can use the registration configuration API to configure individual registrations.

This example will register three types to all their implemented interfaces, extended base classes, and to themselves (self registration) without any filter:

container.RegisterTypes(new[] 
{
typeof(DbBackup),
typeof(ConsoleLogger),
typeof(StorageCleanup)
});

IEnumerable<IJob> jobs = container.ResolveAll<IJob>(); // 2 items
ILogger logger = container.Resolve<ILogger>(); // ConsoleLogger
IJob job = container.Resolve<IJob>(); // StorageCleanup
DbBackup backup = container.Resolve<DbBackup>(); // DbBackup

Another type of service filter is the .RegisterTypesAs<T>() method, which registers only those types that implements the T service type.

note

This method also accepts an implementation filter and a registration configurator action like .RegisterTypes().

caution

.RegisterTypesAs<T>() doesn't create self registrations as it only maps the implementations to the given T service type.

container.RegisterTypesAs<IJob>(new[] 
{
typeof(DbBackup),
typeof(ConsoleLogger),
typeof(StorageCleanup)
});

IEnumerable<IJob> jobs = container.ResolveAll<IJob>(); // 2 items
ILogger logger = container.Resolve<ILogger>(); // error, not found
IJob job = container.Resolve<IJob>(); // StorageCleanup
DbBackup backup = container.Resolve<DbBackup>(); // error, not found

Assembly registration

The batch registration API (filters, registration configuration action, self-registration) is also usable for registering services from given assemblies.

In this example, we assume that the same three services we used in the batch registration section are in the same assembly.

info

The container also detects and registers open-generic definitions (when applicable) from the supplied type collection. You can read about open-generics here.

container.RegisterAssembly(typeof(DbBackup).Assembly,
// service filter, register to interfaces only
serviceTypeSelector: (impl, service) => service.IsInterface,
registerSelf: false,
configurator: options => options.WithoutDisposalTracking()
);

IEnumerable<IJob> jobs = container.ResolveAll<IJob>(); // 2 items
IEnumerable<JobBase> jobs = container.ResolveAll<JobBase>(); // 0 items
ILogger logger = container.Resolve<ILogger>(); // ConsoleLogger
DbBackup backup = container.Resolve<DbBackup>(); // error, not found

Composition root

The Composition Root is an entry point where all services required to make a component functional are wired together.

Stashbox provides an ICompositionRoot interface that can be used to define an entry point for a given component or even for an entire assembly.

You can wire up your composition root implementation with ComposeBy<TRoot>(), or you can let the container find and execute all available composition root implementations within an assembly.

note

Your ICompositionRoot implementation also can have dependencies that the container will resolve.

class ExampleRoot : ICompositionRoot
{
public ExampleRoot(IDependency rootDependency)
{ }

public void Compose(IStashboxContainer container)
{
container.Register<IServiceA, ServiceA>();
container.Register<IServiceB, ServiceB>();
}
}
// compose a single root.
container.ComposeBy<ExampleRoot>();

Injection parameters

If you have pre-evaluated dependencies you'd like to inject at resolution time, you can set them as injection parameters during registration.

note

Injection parameter names are matched to constructor arguments or field/property names.

container.Register<IJob, DbBackup>(options => options
.WithInjectionParameter("logger", new ConsoleLogger())
.WithInjectionParameter("eventBroadcaster", new MessageBus());

// the injection parameters will be passed to DbBackup's constructor.
IJob backup = container.Resolve<IJob>();

Initializer / finalizer

The container provides specific extension points to let you react to lifetime events of an instantiated service.

For this reason, you can specify Initializer and Finalizer delegates. The finalizer is called upon the service's disposal, and the initializer is called upon the service's construction.

container.Register<ILogger, FileLogger>(options => options
// delegate that called right after instantiation.
.WithInitializer((logger, resolver) => logger.OpenFile())
// delegate that called right before the instance's disposal.
.WithFinalizer(logger => logger.CloseFile()));