Following on from my previous post where I implemented a decorator pattern using .NET Core dependency injection I realised that I could use the same method to create a composite pattern in a developer-friendly way.
Composite Pattern
Similar to the decorator pattern, the Composite Pattern let’s you wrap existing implementations of an interface to augment the functionality.
The difference between the two is that the decorator wraps a single instance of the interface; a composite wraps many.
interface IService {
void DoSomething(string value);
}
class Decorator : IService {
public Decorator(IService wrappedService) {
//...
}
}
class Composite : IService {
public Composite(IEnumerable<IService> wrappedServices) {
//...
}
}
This is useful where you have a number of implementations of your service and you don’t want dependent classes to know whether they should call one, some or all of them.
For example, if you have a report generator that wants to send results to multiple sources you might implement several instances of IReporter
:
interface IReporter {
void Send(IReport report);
}
class ConsoleReporter : IReporter {
public void Send(IReport report) {
//write details to console
}
}
class TelemetryReporter : IReporter {
public void Send(IReport report) {
//write stats to a telemetry service
}
}
class EmailReporter : IReporter {
public void Send(IReport report) {
//send a report email to stakeholders
}
}
Your composite reporter would construct on all other implementations of IReporter
and call them in order:
class CompositeReporter : IReporter {
private IEnumerable<IReporter> _reporters;
public CompositeReporter(IEnumerable<IReporter> reporters) {
_reporters = reporters;
}
public void Send(IReport report) {
foreach (var reporter in _reporters)
reporter.Send(report);
}
}
This means that anything that needs to send a report can request a single IReporter
and let the CompositeReporter
worry about routing the report through the correct concrete implementations.
Default DI Behaviour
As discussed in the previous post, the default behaviour of the .NET Core Dependency Injection framework is to provide the last-registered copy of an interface, or all registered copies for an IEnumerable
.
services.AddScoped<IService, ConcreteService1>();
services.AddScoped<IService, ConcreteService2>();
services.AddScoped<IService, ConcreteService3>();
//later
const service = serviceProvider.GetRequiredService<IService>();
// service is instance of ConcreteService3
const allServices = serviceProvider.GetRequiredService<IEnumerable<IService>>();
// allServices contains one instance of all 3 registered implementations
What we need is a way to register a new type to replace the existing registrations and take them in as a constructor dependency.
class Composite : IService {
public class Composite(IEnumerable<IService> services) {
//...
}
}
//BAD - throws StackOverflowException when resolved!
services.AddScoped<IService, Composite>();
Unfortunately the default behaviour of the DI framework is to attempt to fulfil the request for all IService
implementations with…another instance of Composite
! One StackOverflowException
later and we’re back to the drawing board.
How can we make this play nicely with DI?
Borrowing from the Last Post
After some digging through the ASP.NET Core source code in the last post we came up with a couple of useful helpers that we can re-use here: CreateFactory
and CreateInstance
.
ActivatorUtilities.CreateFactory
generates a factory function to create an instance of ConcreteType
from the service provider with some services provided explicitly.
var objectFactory = ActivatorUtilities.CreateFactory(
typeof(ConcreteType),
new[] { typeof(IService) });
CreateInstance
creates an instance of a service from a ServiceDescriptor
.
We can get instances of ServiceDescriptor
from the service collection and use these to create previously-registered types.
public static object CreateInstance(this IServiceProvider services, ServiceDescriptor descriptor)
{
if (descriptor.ImplementationInstance != null)
return descriptor.ImplementationInstance;
if (descriptor.ImplementationFactory != null)
return descriptor.ImplementationFactory(services);
return ActivatorUtilities.GetServiceOrCreateInstance(services, descriptor.ImplementationType);
}
With these tools we can define our desired composite behaviour.
Extract Existing Registrations
When we register a new composite we want to
- Remove all existing registered services for the same interface
- Insert the composite implementation
- Pass instances of all removed implementations into the constructor of the composite
Remove Existing Registrations
IServiceCollection
extends IEnumerable
so we can filter it down to get the services that match the interface of the composite class.
public static void AddComposite<TInterface, TConcrete>(this IServiceCollection services)
where TInterface : class
where TConcrete : class, TInterface
{
//get a list of existing registrations matching the target interface
var wrappedDescriptors = services
.Where(s => s.ServiceType == typeof(TInterface))
.ToList();
//remove each from the service collection
foreach (var descriptor in wrappedDescriptors)
services.Remove(descriptor);
//...
}
We call ToList
to we get a persistent list of the items and then remove them from the original collection.
Add Composite Implementation
Next up we want to insert the definition of our composite class, and we’re going to use the ActivatorUtilites
helper mentioned above.
var objectFactory = ActivatorUtilities.CreateFactory(
typeof(TConcrete),
new[] {
typeof(IEnumerable<TInterface>)
});
Here we create a factory function that can be used with a service provider to resolve an instance of TConcrete
(i.e. our composite class) with any parameters of type IEnumerable{TInterface}
manually specified by us.
The objectFactory
forms the basis of a new ServiceDescriptor
to add to the collection.
var compositeDescriptor = ServiceDescriptor.Describe(
typeof(TInterface),
serviceProvider => (TInterface)objectFactory(serviceProvider, new [] {
/* todo: inject original services here */
},
ServiceLifetime.Scoped);
);
services.Add(compositeDescriptor);
Note: in this example I have hard-coded a lifetime of Scoped
for the service. We can improve on this below but it will do for now.
Inject Original Services
We still need to inject instances of the original services that we removed. We recorded their service descriptors in wrappedDescriptors
and we can now combine those with the CreateInstance
extension method above to populate our constructor parameter.
var compositeDescriptor = ServiceDescriptor.Describe(
typeof(TInterface),
serviceProvider => (TInterface)objectFactory(serviceProvider, new [] {
wrappedDescriptors
.Select(d => serviceProvider.CreateInstance(d))
.Cast<TInterface>()
},
ServiceLifetime.Scoped);
);
Now all of the wrapped services will be created through the service provider and passed to our composite.
This approach may seem long winded but it has the advantage that any other dependencies of either the wrapped services or our composite will also be injected from the service provider with no further input from us!
Calculate Lifetime Scope
The hard-coded lifetime scope isn’t ideal, and whilst we could push the onus onto the caller to specify a scope we can do slightly better and infer it from the existing registrations.
If the composite depends on a Scoped
instance then it can be either Scoped
or Transient
without a problem, but cannot be Singleton
as it would not have access to scoped dependencies.
We can infer the maximum scope of the composite by taking the most specific scope of it’s dependencies. The ServiceLifetime
enum is defined with the least specific scope (Singleton
) as 0
so we can select the maximum value to get the most specific.
Pull it All Together
Combining all of the above we get the following extension method:
public static void AddComposite<TInterface, TConcrete>(this IServiceCollection services)
where TInterface : class
where TConcrete : class, TInterface
{
var wrappedDescriptors = services.Where(s => s.ServiceType == typeof(TInterface)).ToList();
foreach (var descriptor in wrappedDescriptors)
services.Remove(descriptor);
var objectFactory = ActivatorUtilities.CreateFactory(
typeof(TConcrete),
new[] { typeof(IEnumerable<TInterface>) });
services.Add(ServiceDescriptor.Describe(
typeof(TInterface),
s => (TInterface)objectFactory(s, new[] { wrappedDescriptors.Select(d => s.CreateInstance(d)).Cast<TInterface>() }),
wrappedDescriptors.Select(d => d.Lifetime).Max())
);
}
Now we can wrap up as many services as we want in a composite with one line
var services = new ServiceCollection();
services.AddSingleton<IReporter, ConsoleReporter>();
services.AddScoped<IReporter, TelemetryReporter>();
services.AddTransient<IReporter, EmailReporter>();
services.AddComposite<IReporter, CompositeReporter>();