Composite Decorators with StructureMap

04 Oct 2017

While I was developing my Crispin project, I ended up needing to create a bunch of implementations of a single interface, and then use all those implementations at once (for metrics logging).

The interface looks like so:

public interface IStatisticsWriter
{
    Task WriteCount(string format, params object[] parameters);
}

And we have a few implementations already:

  • LoggingStatisticsWriter - writes to an ILogger instance
  • StatsdStatisticsWriter - pushes metrics to StatsD
  • InternalStatisticsWriter - aggregates metrics for exposing via Crispin’s api

To make all of these be used together, I created a fourth implementation, called CompositeStatisticsWriter (a name I made up, but apparently matches the Gang of Four definition of a composite!)

public class CompositeStatisticsWriter : IStatisticsWriter
{
    private readonly IStatisticsWriter[] _writers;

    public CompositeStatisticsWriter(IEnumerable<IStatisticsWriter> writers)
    {
        _writers = writers.ToArray();
    }

    public async Task WriteCount(string format, params object[] parameters)
    {
        await Task.WhenAll(_writers
            .Select(writer => writer.WriteCount(format, parameters))
            .ToArray());
    }
}

The problem with doing this is that StructureMap throws an error about a bi-directional dependency:

StructureMap.Building.StructureMapBuildException : Bi-directional dependency relationship detected!
Check the StructureMap stacktrace below:
1.) Instance of Crispin.Infrastructure.Statistics.IStatisticsWriter (Crispin.Infrastructure.Statistics.CompositeStatisticsWriter)
2.) All registered children for IEnumerable<IStatisticsWriter>
3.) Instance of IEnumerable<IStatisticsWriter>
4.) new CompositeStatisticsWriter(*Default of IEnumerable<IStatisticsWriter>*)
5.) Crispin.Infrastructure.Statistics.CompositeStatisticsWriter
6.) Instance of Crispin.Infrastructure.Statistics.IStatisticsWriter (Crispin.Infrastructure.Statistics.CompositeStatisticsWriter)
7.) Container.GetInstance<Crispin.Infrastructure.Statistics.IStatisticsWriter>()

After attempting to solve this myself in a few different ways (you can even watch the stream of my attempts), I asked in the StructreMap gitter chat room, and received this answer:

This has come up a couple times, and yeah, you’ll either need a custom convention or a policy that adds the other ITest’s to the instance for CompositeTest as inline dependencies so it doesn’t try to make Composite a dependency of itself – Jeremy D. Miller

Finally, Babu Annamalai provided a simple implementation when I got stuck (again).

The result is the creation of a custom convention for registering the composite, which provides all the implementations I want it to wrap:

public class CompositeDecorator<TComposite, TDependents> : IRegistrationConvention
    where TComposite : TDependents
{
    public void ScanTypes(TypeSet types, Registry registry)
    {
        var dependents = types
            .FindTypes(TypeClassification.Concretes)
            .Where(t => t.CanBeCastTo<TDependents>() && t.HasConstructors())
            .Where(t => t != typeof(TComposite))
            .ToList();

        registry
            .For<TDependents>()
            .Use<TComposite>()
            .EnumerableOf<TDependents>()
            .Contains(x => dependents.ForEach(t => x.Type(t)));
    }
}

To use this the StructureMap configuration changes from this:

public CrispinRestRegistry()
{
    Scan(a =>
    {
        a.AssemblyContainingType<Toggle>();
        a.WithDefaultConventions();
        a.AddAllTypesOf<IStatisticsWriter>();
    });

    var store = BuildStorage();

    For<IStorage>().Use(store);
    For<IStatisticsWriter>().Use<CompositeStatisticsWriter>();
}

To this version:

public CrispinRestRegistry()
{
    Scan(a =>
    {
        a.AssemblyContainingType<Toggle>();
        a.WithDefaultConventions();
        a.Convention<CompositeDecorator<CompositeStatisticsWriter, IStatisticsWriter>>();
    });

    var store = BuildStorage();
    For<IStorage>().Use(store);
}

And now everything works successfully, and I have Pull Request open on StructureMap’s repo with an update to the documentation about this.

Hopefully this helps someone else too!

code, structuremap, di, ioc

« Integration Testing with Dotnet Core, Docker and RabbitMQ Testing RabbitMQ Concurrency in MassTransit »