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.
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.
- Parameter-less
- Parameterized
- Resolver parameter
container.Register<ILogger, ConsoleLogger>(options => options
.WithFactory(() => new ConsoleLogger());
// the container uses the factory for instantiation.
IJob job = container.Resolve<ILogger>();
container.Register<IJob, DbBackup>(options => options
.WithFactory<ILogger>(logger => new DbBackup(logger));
// the container uses the factory for instantiation.
IJob job = container.Resolve<IJob>();
container.Register<IJob, DbBackup>(options => options
.WithFactory(resolver => new DbBackup(resolver.Resolve<ILogger>()));
// the container uses the factory for instantiation.
IJob job = container.Resolve<IJob>();
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
.
- Generic API
- Runtime type API
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"]);
container.RegisterFunc<string, IJob>((connectionString, resolver) =>
new DbBackup(connectionString, resolver.Resolve<ILogger>()));
Delegate backupFactory = container.ResolveFactory(typeof(IJob),
parameterTypes: new[] { typeof(string) });
IJob dbBackup = backupFactory.DynamicInvoke(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.
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>();
- ResolveAll
- Array
- IEnumerable
- IList
- ICollection
// jobs contain all three services in registration order.
IEnumerable<IJob> jobs = container.ResolveAll<IJob>();
// jobs contain all three services in registration order.
IJob[] jobs = container.Resolve<IJob[]>();
// jobs contain all three services in registration order.
IEnumerable<IJob> jobs = container.Resolve<IEnumerable<IJob>>();
// jobs contain all three services in registration order.
IList<IJob> jobs = container.Resolve<IList<IJob>>();
// jobs contain all three services in registration order.
ICollection<IJob> jobs = container.Resolve<ICollection<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.
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() { }
}
- To another type
- To all implemented types
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
container.Register<DbBackup>(options => options
.AsImplementedTypes());
IJob job = container.Resolve<IJob>(); // DbBackup
IScheduledJob job = container.Resolve<IScheduledJob>(); // DbBackup
DbBackup job = container.Resolve<DbBackup>(); // DbBackup
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.
Framework types like IDisposable
are excluded from being considered as a service type by default.
You can use the registration configuration API to configure individual registrations.
- Default
- Filters
- Without self
- Registration options
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
In this example, we assume that DbBackup
and StorageCleanup
are implementing IDisposable
besides IJob
and also extending a JobBase
abstract class.
container.RegisterTypes(new[]
{ typeof(DbBackup), typeof(ConsoleLogger), typeof(StorageCleanup) },
// implementation filter, only those implementations that implements IDisposable
impl => typeof(IDisposable).IsAssignableFrom(impl),
// service filter, register them to base classes only
(impl, service) => service.IsAbstract && !service.IsInterface);
IEnumerable<IJob> jobs = container.ResolveAll<IJob>(); // 0 items
IEnumerable<JobBase> jobs = container.ResolveAll<JobBase>(); // 2 items
ILogger logger = container.Resolve<ILogger>(); // error, not found
DbBackup backup = container.Resolve<DbBackup>(); // DbBackup
This example ignores the self registrations completely:
container.RegisterTypes(new[]
{
typeof(DbBackup),
typeof(ConsoleLogger),
typeof(StorageCleanup)
},
registerSelf: false);
IEnumerable<IJob> jobs = container.ResolveAll<IJob>(); // 2 items
ILogger logger = container.Resolve<ILogger>(); // ConsoleLogger
DbBackup backup = container.Resolve<DbBackup>(); // error, not found
ConsoleLogger logger = container.Resolve<ConsoleLogger>(); // error, not found
This example will configure all registrations mapped to ILogger
as Singleton
:
container.RegisterTypes(new[]
{
typeof(DbBackup),
typeof(ConsoleLogger),
typeof(StorageCleanup)
},
configurator: options =>
{
if (options.HasServiceType<ILogger>())
options.WithSingletonLifetime();
});
ILogger logger = container.Resolve<ILogger>(); // ConsoleLogger
ILogger newLogger = container.Resolve<ILogger>(); // the same ConsoleLogger
IEnumerable<IJob> jobs = container.ResolveAll<IJob>(); // 2 items
Another type of service filter is the .RegisterTypesAs<T>()
method, which registers only those types that implements the T
service type.
This method also accepts an implementation filter and a registration configurator action like .RegisterTypes()
.
.RegisterTypesAs<T>()
doesn't create self registrations as it only maps the implementations to the given T
service type.
- Generic API
- Runtime type API
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
container.RegisterTypesAs(typeof(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.
The container also detects and registers open-generic definitions (when applicable) from the supplied type collection. You can read about open-generics here.
- Single assembly
- Multiple assemblies
- Containing type
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
container.RegisterAssemblies(new[]
{
typeof(DbBackup).Assembly,
typeof(JobFromAnotherAssembly).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
container.RegisterAssemblyContaining<DbBackup>(
// 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.
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>();
}
}
- Single
- Assembly
- Override
// compose a single root.
container.ComposeBy<ExampleRoot>();
// compose every root in the given assembly.
container.ComposeAssembly(typeof(IServiceA).Assembly);
// compose a single root with dependency override.
container.ComposeBy<ExampleRoot>(new CustomRootDependency());
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.
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()));