Search code examples
c#autofacfactory-pattern

Creating a Delegating Factory with Autofac using a property


I'm trying to create a factory to help transform a class based on an interface (IIncomingMessage) into new instances of other classes (AMessage, BMessage) based on a property of the single class, like:

public interface IIncomingMessage
{
    public DeviceTypeEnum DeviceType {get;}
}

public class IncomingMessage : IIncomingMessage
{

    public DeviceTypeEnum DeviceType {get {return DeviceTypeEnum.TypeA;}}

    Public Byte[] RawData {get; set;}
}      

public interface IMessageTransformer<out T> where T:class
{
    T Transform(IIncomingMessage message);
}

public class AMessage 
{
    public int ChoppedUp1 {get; set;}
    public int ChoppedUp2 {get; set;}
    public int ChoppedUp3 {get; set;}       
}

public class BMessage
{
    public string SomeData {get; set;}
    public string SomeMoreData {get; set;}
}

public class AMessageTransformer : IMessageTransformer<AMessage>
{
    public AMessage Transform(IIncomingMessage message)
    {
        var result = new AMessage();

        result.ChoppedUp1 = message.RawData[1];
        result.ChoppedUp2 = message.RawData[2];
        ....
        return result;
    }
}

public class BMessageTransformation : IMessageTransformer<BMessage>
{
    public BMessage Transform(IIncomingMessage message)
    {
        throw new NotImplementedException();
    }
}

public class MessageTransformFactory<T> where T : class
{
    private readonly Func<DeviceTypeEnum, IMessageTransformer<T>> _create;

    public MessageTransformFactory(Func<DeviceTypeEnum, IMessageTransformer<T>> create)
    {
        _create = create;
    }

    public IMessageTransformer<T> GetTransformer(DeviceTypeEnum device)
    {
        return _create(device);
    } 
}

I'm using Autofac and I'm pretty sure there should be a way to create a factory which can give me back the correct transformer based on the property value of DeviceType. Used and seen plenty of examples using the type of object to make the decision but not a value. Named and Keyed Services looked promising but looked like I still end up with a static list somewhere.


Solution

  • Alright, so after talking with you, it sounds like you're pretty open to some adjustments on how the transformation actually occurs. As a result, I have built out a solution for you that uses Attributes along-side the DeviceTypeEnum to resolve your transformers. The factory loosely follows the abstract factory pattern. The factory is responsible for determining what the actual Type is that needs to be created; it then delegates the instantiation of that Type to a delegate. In this case, Autofac.

    Overview

    In order to get this to work, I had to add an additional interface. IMessageTransformer. I then made IMessageTransformer<T> inherit from it. The base interface is nothing more than a marker interface so that I could make the factory itself non-generic, along with the Func delegate being non-generic. You can now use the same factory instance for multiple IMessageTransformer<T> requests. You may also just instance the factory as needed, rather than injecting it, as it does not have any dependencies.

    To show the factory actually working, I have written a unit test for it.

        [TestMethod]
        public void Shared_factory_instance_resolves_multiple_transformers()
        {
            // Arrange
            var factory = new MessageTransformFactory();
            IMessageTransformer<AMessage> aTransformer = factory.CreateTransformer<AMessage>(DeviceTypeEnum.Foo);
            IMessageTransformer<BMessage> bTransformer = factory.CreateTransformer<BMessage>(DeviceTypeEnum.Bar);
    
            // Act
            AMessage aMessage = aTransformer.Transform(new IncomingFooMessage());
            BMessage bMessage = bTransformer.Transform(new IncomingBarMessage());
    
            // Assert
            Assert.IsNotNull(aMessage, "Transformer failed to convert the IncomingMessage");
            Assert.IsNotNull(bMessage, "Transformer failed to convert the IncomingMessage");
        }
    

    Note that this could have been simplified thanks to the introduction of an attribute, by removing the DeviceTypeEnum all together. I left it in place however since I'm not sure what your total requirements were.

    Implementation

    Now to go over the actual implementation. I took your sample code in the original question and used it to build out a sample project. The sample project containing the implementation is available on GitHub for you to pull down. All of the source code is shown below; the GitHub repository is just so you can download the repo and see the code in action, making sure it does what you were wanting, before implementing it in to your project.

    DeviceTypeEnum

    The first thing I did was create the enum that will be used for linking the transformers with their IIncomingMessage and Message classes.

    public enum DeviceTypeEnum
    {
        Foo,
        Bar,
    }
    

    Message classes

    next I created the two message classes that you want to transform in to.

    public class AMessage
    {
    }
    
    public class BMessage
    {
    }
    

    IIncomingMessage

    Next up is the IIncomingMessage interface. This interface only contains the one property, DeviceType. I then created two implementations of this interface so that I can test the actual transformation.

    public interface IIncomingMessage
    {
        DeviceTypeEnum DeviceType { get; }
    }
    
    public class IncomingFooMessage : IIncomingMessage
    {
        public DeviceTypeEnum DeviceType { get { return DeviceTypeEnum.Foo; } }
    }
    
    public class IncomingBarMessage : IIncomingMessage
    {
        public DeviceTypeEnum DeviceType { get { return DeviceTypeEnum.Bar; } }
    }
    

    IMessageTransformer

    The IMessageTransformer<T> you gave in your example has been modified to inherit from a non-generic variation of the interface.

    public interface IMessageTransformer
    {
    }
    
    public interface IMessageTransformer<T> : IMessageTransformer where T : class
    {
        T Transform(IIncomingMessage message);
    }
    

    This allows you to specify the delegate method used by Autofac during resolution, to not be a generic delegate. Meaning instead of Func<IMessageTransformer<T>>, you now use Func<IMessageTransformer>. This lets you re-use the same factory instance because the instance is not tied to a specific generic type.

    TransformableAttribute

    Now we need to create an attribute. This attribute will be used to tell each transformer, what DeviceTypeEnum they are required to support.

    [AttributeUsage(AttributeTargets.Class)]
    public class TransformableAttribute : Attribute
    {
        public TransformableAttribute(DeviceTypeEnum deviceType)
        {
            this.DeviceType = deviceType;
        }
    
        public DeviceTypeEnum DeviceType { get; private set; }
    }
    

    If the DeviceTypeEnum is only used to facilitate this transformation mapping, then you could really delete the enum all together. You would change the DeviceType property to be public Type TargetType {get; private set;}. The factory (shown below) would then resolve using the TargetType (simplifying the factory I created a bit). Since your example showed this enum being used, I left it in as a requirement.

    Note that removing the enum and using a TargetType property would also let you build out more complex relationships, and not have to update the enum everytime a new mapping had to be created. Instead of doing:

    var factory = new MessageTransformFactory();
    IMessageTransformer<AMessage> aTransformer = factory.CreateTransformer<AMessage>(DeviceTypeEnum.Foo);
    

    It would let you drop the parameter all together. The enum wouldn't be needed because the attribute would let the factory know what the target transformation result needs to be for each transformer.

    var factory = new MessageTransformFactory();
    IMessageTransformer<AMessage> aTransformer = factory.CreateTransformer<AMessage>();
    

    Message Transformers

    We have the attribute and the interface, so we can go ahead and create a couple of transformers that will convert from an IIncomingMessage to either AMessage or BMessage. The TransformableAttribute will tell the transformer which device type to support. The transformer itself does not depend on the attribute; the factory we will use for resolving, will need the attribute to link device types to transformers.

    [Transformable(DeviceTypeEnum.Foo)] // or [Transformable(typeof(AMessage)] if you replace the enum with a Target Type
    public class AMessageTransformer : IMessageTransformer<AMessage>
    {
        public AMessage Transform(IIncomingMessage message)
        {
            if (!(message is IncomingFooMessage))
            {
                throw new InvalidCastException("Message was not an IncomingFooMessage");
            }
    
            return new AMessage();
        }
    }
    
    [Transformable(DeviceTypeEnum.Bar)]
    public class BMessageTransformer : IMessageTransformer<BMessage>
    {
        public BMessage Transform(IIncomingMessage message)
        {
            if (!(message is IncomingBarMessage))
            {
                throw new InvalidCastException("Message was not an IncomingBarMessage");
            }
    
            return new BMessage();
        }
    }
    

    MessageTransformFactory

    Now for the meat and potatoes. The factory has a few things that it is responsible for. It must first scan a collection of assemblies in order to find all of the transformer Types it can create. It also needs to accept a delegate factory that it will use to instantiate the transformer Types it has cached when needed.

    The MessageTransformFactory loosely follows the abstract factory pattern. It delegates the actual creation of the transformation objects to something else, in this case it's Autofac.

    public class MessageTransformFactory
    {
        /// <summary>
        /// The assemblies to cache. Defaults to including the assembly this factory exists in.
        /// if there are additional assemblies that hold transformers, they can be added via the 
        /// MessageTransformFactory.ScanAssembly(Assembly) method.
        /// </summary>
        private static List<Assembly> assembliesToCache
            = new List<Assembly> { typeof(MessageTransformFactory).GetTypeInfo().Assembly };
    
        /// <summary>
        /// The factory method used to instance a transformer
        /// </summary>
        private static Func<Type, IMessageTransformer> factoryMethod;
    
        /// <summary>
        /// The DeviceType to Transformer mapping cache
        /// </summary>
        private static Dictionary<DeviceTypeEnum, Type> deviceTransformerMapCache
            = new Dictionary<DeviceTypeEnum, Type>();
    
        /// <summary>
        /// Initializes the <see cref="CommandFormatterFactory"/> class.
        /// This will build the initial device to transformer mapping when the
        /// Factory is first used.
        /// </summary>
        static MessageTransformFactory()
        {
            BuildCache();
        }
    
        /// <summary>
        /// Sets the transformer factory used to instance transformers.
        /// </summary>
        /// <param name="factory">The factory delegate used to instance new IMessageTransformer objects.</param>
        public static void SetTransformerFactory(Func<Type, IMessageTransformer> factory)
        {
            if (factory == null)
            {
                throw new ArgumentNullException("factory", "Factory delegate can not be null.");
            }
    
            MessageTransformFactory.factoryMethod = factory;
        }
    
        /// <summary>
        /// Scans a given assembly for IMessageTransformer implementations.
        /// </summary>
        /// <param name="assemblyName">Name of the assembly to scan.</param>
        public static void ScanAssembly(AssemblyName assemblyName)
        {
            if (assemblyName == null)
            {
                throw new ArgumentNullException("assemblyName", "A valid assembly name must be provided.");
            }
    
            Assembly assembly = Assembly.Load(assemblyName);
    
            if (assembliesToCache.Any(a => a.FullName == assemblyName.FullName))
            {
                return;
            }
    
            assembliesToCache.Add(assembly);
            MapDeviceTypesFromAssembly(assembly);
        }
    
        /// <summary>
        /// Gets the available transformer types that have been registered to this factory.
        /// </summary>
        public static Type[] GetAvailableTransformerTypes()
        {
            return deviceTransformerMapCache.Values.ToArray();
        }
    
        /// <summary>
        /// Gets an IMessageTransformer implementation for the Device Type given.
        /// </summary>
        /// <param name="deviceType">The DeviceType that the factory must create an IMessageTransformer for.</param>
        public IMessageTransformer<T> CreateTransformer<T>(DeviceTypeEnum deviceType) where T : class
        {
            // If we have a factory method, then we use it.
            if (factoryMethod == null)
            {
                throw new NullReferenceException("The MessageTransformerFactory did not have its factory method set.");
            }
    
            // Cast the non-generic return value to the generic version for the caller.
            Type transformerType = MessageTransformFactory.deviceTransformerMapCache[deviceType];
            return factoryMethod(transformerType) as IMessageTransformer<T>;
        }
    
        /// <summary>
        /// Builds the cache of IMessageTransformer Types that can be used by this factory.
        /// </summary>
        private static void BuildCache()
        {
            foreach (var assembly in assembliesToCache)
            {
                MapDeviceTypesFromAssembly(assembly);
            }
        }
    
        /// <summary>
        /// Creates a DeviceType to IMessageTransformer Type mapping.
        /// </summary>
        /// <param name="assembly"></param>
        private static void MapDeviceTypesFromAssembly(Assembly assembly)
        {
            var transformableTypes = assembly.DefinedTypes
                .Where(type => type
                .ImplementedInterfaces
                .Any(inter => inter == typeof(IMessageTransformer)) && !type.IsAbstract);
    
            foreach (TypeInfo transformer in transformableTypes)
            {
                var commandCode = transformer.GetCustomAttribute<TransformableAttribute>();
                deviceTransformerMapCache.Add(commandCode.DeviceType, transformer.AsType());
            }
        }
    }
    

    Autofac setup

    With this implementation, the responsibility of the mapping has now fallen on the factory. As a result, our Autofac registration becomes really simple.

    var builder = new ContainerBuilder();
    
    // Register all of the available transformers.
    builder
        .RegisterTypes(MessageTransformFactory.GetAvailableTransformerTypes())
        .AsImplementedInterfaces()
        .AsSelf();
    
    // Build the IoC container
    this.container = builder.Build();
    
    // Define our factory method for resolving the transformer based on device type.
    MessageTransformFactory.SetTransformerFactory((type) =>
    {
        if (!type.IsAssignableTo<IMessageTransformer>())
        {
            throw new InvalidOperationException("The type provided to the message transform factory resolver can not be cast to IMessageTransformer");
        }
    
        return container.Resolve(type) as IMessageTransformer;
    });
    

    Once we have completed the Autofac registration, we then set the MessageTransformFactory's delegate factory method to resolve the Type that is given for use by the factory.

    This also works well for testing, as you can now customize the instantiation process that the factory uses during unit testing. You can create mocked transformers and build a simple delegate that returns the mock, by having the test call MessageTransformFactory.SetTransformFactory()

    Lastly, you mentioned that you prefer injecting the factory to stay true with the conventions you've already been using. While this factory does not have any dependencies (therefore can be instanced without needing any IoC) you could abstract the 1 instance method it has, the CreateTransformer<T> method, behind an interface. Then you register the factory with that interface to Autofac and have it injected. This way, nothing has the concrete implementation of the factory. This also prevents someone else from accessing the static methods that the factory must have in order to facilitate the mapping and coupling with Autofac.