Composites
The composite pattern allows a collection of objects to be treated in the same way as a single instance of that same type of object.
In Autofac 6.0, we added support for expressing the composite pattern for services in your container that have multiple implementations, but should be exposed through a single wrapper class.
You can register a composite wrapper registration for a service, that will be returned when a consuming class resolves that service, and can take a collection of the same service as a constructor parameter.
This functionality is particularly useful if you find yourself needing to add a provider for a service, but you don’t want the consuming code of the original provider to a) have to change, or b) know that there are multiple providers.
// This is my service with multiple registrations.
public interface ILogSink
{
void WriteLog(string log);
}
// A composite wrapper is just a regular class that injects a collection of the same service.
public class CompositeLogSink : ILogSink
{
private readonly IEnumerable<ILogSink> _implementations;
public CompositeLogSink(IEnumerable<ILogSink> implementations)
{
_implementations = implementations;
}
public void WriteLog(string log)
{
foreach (var sink in _implementations)
{
sink.WriteLog(log);
}
}
}
// ....
var builder = new ContainerBuilder();
builder.RegisterType<FileLogSink>().As<ILogSink>();
builder.RegisterType<DbLogSink>().As<ILogSink>();
// Register a composite wrapper for ILogSink.
builder.RegisterComposite<CompositeLogSink, ILogSink>();
var container = builder.Build();
// This will return the composite wrapper, with the two registrations injected.
var logSink = container.Resolve<ILogSink>();
logSink.WriteLog("log message");
If you don’t know the type up front, you can manually specify instead of using the generic:
builder.RegisterComposite(typeof(CompositeLogSink), typeof(ILogSink));
For more complex composite creation, you can also specify a lambda for your composite registration:
builder.RegisterComposite<ILogSink>((context, parameters, implementations) => new CompositeLogSink(implementations));
In the lambda, context
is the IComponentContext
in which the resolution is happening (so you could resolve other things if needed),
parameters
is an IEnumerable<Parameter>
with all the parameters passed in,
and implementations
is an IEnumerable
of all the implementations of the service.
It is also possible to register open generic composites:
// Generic providers...
builder.RegisterGeneric(typeof(FileLogSink<>)).As(typeof(ILogSink<>));
builder.RegisterGeneric(typeof(DbLogSink<>)).As(typeof(ILogSink<>));
// ...with a generic composite.
builder.RegisterGenericComposite(typeof(CompositeLogSink<>), typeof(ILogSink<>));
var container = builder.Build();
// Will return a composite of FileLogSink<HttpClient> and DbLogSink<HttpClient>.
var sink = container.Resolve<ILogSink<HttpClient>>();
Composite wrappers can have their own additional dependencies, as well as use any combination of the implicit relationships on the set of implementations passed in:
public class CompositeWrapper : ILogSink
{
public CompositeWrapper(IEnumerable<ILogSink> implementations, IAnotherService service)
{
}
}
public class LazyCompositeWrapper : ILogSink
{
// Lazy loading for the set of composites.
public LazyCompositeWrapper(Lazy<IEnumerable<ILogSink>> implementations)
{
}
}
public class MetaCompositeWrapper : ILogSink
{
// Access the metadata of each implementation.
public LazyCompositeWrapper(IEnumerable<Meta<ILogSink>> implementations)
{
}
}
Metadata
Composite registrations can have their own metadata, much like a normal registration; however they do not expose any metadata of the individual registrations they wrap:
// Register a composite wrapper for ILogSink:
builder.RegisterComposite<CompositeLogSink, ILogSink>()
.WithMetadata("key", "value");
var container = builder.Build();
// This will return the composite wrapper and expose the metadata.
var logSink = container.Resolve<Meta<ILogSink>>();
Lifetime
Composite wrappers can have their own lifetime, much like any other registration. However, you should consider the
implications of making composite registrations long-living; a SingleInstance
composite would ignore any additional registrations
for the wrapped service made in nested lifetime scopes (for example).
Decorators
When using the composite pattern, decorators are only applied to the individual implementations, and not to the composite itself.
So, if you register a decorator for ILogSink
, and have a composite registration with implementations FileLogSink
and DbLogSink
, when you resolve ILogSink
FileLogSink
and DbLogSink
will be decorated, but your composite wrapper will not be decorated.
Composites and Collections
Composite registrations are never returned when resolving a collection of implementations, even outside of a composite wrapper:
var builder = new ContainerBuilder();
builder.RegisterType<FileLogSink>().As<ILogSink>();
builder.RegisterType<DbLogSink>().As<ILogSink>();
// Register a composite wrapper for ILogSink:
builder.RegisterComposite<CompositeLogSink, ILogSink>();
var container = builder.Build();
// This will return 2 items only (the actual implementations).
var logSinks = container.Resolve<IEnumerable<ILogSink>>();