Composite Pattern in .NET Core with Dependency Injection

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

  1. Remove all existing registered services for the same interface
  2. Insert the composite implementation
  3. 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>();