Search code examples
c#autofac

How to register open generic with custom factory method?


TL;DR: Can I create a generic factory with Autofac, so that I can inject IProduct<TModel> rather than resolving it from IFactory anywhere I need it? Is there a way to move the resolving-from-factory task to the composition root?

So I'm using a third party library, which exposes some generic interfaces which are created through a factory. For demonstration purposes, we'll assume that the following code is the library:

Third party library mock-up:

public interface IFactory
{
    IProduct<TModel> CreateProduct<TModel>(string identifier);
}

internal class Factory : IFactory
{
    private readonly string _privateData = "somevalues";

    public IProduct<TModel> CreateProduct<TModel>(string identifier)
    {
        return new Product<TModel>(_privateData, identifier);
    }
}

public interface IProduct<TModel>
{
    void DoSomething();
}

internal sealed class Product<TModel>: IProduct<TModel>
{
    private readonly string _privateData;
    private readonly string _identifier;

    public Product(string privateData, string identifier)
    {
        _privateData = privateData;
        _identifier = identifier;
    }

    public void DoSomething()
    {
        System.Diagnostics.Debug.WriteLine($"{_privateData} + {_identifier}");
    }
}

My code:

And my TModel:

public class Shoe { }

Now, let's assume that I want an IProduct<Shoe> in MyService. I need to resolve it there:

public class MyService
{
    public MyService(IFactory factory)
    {
        IProduct<Shoe> shoeProduct = factory.CreateProduct<Shoe>("theshoe");
    }
}

But wouldn't it be nicer if I could declare shoe like this:

public class ProductIdentifierAttribute : System.Attribute
{
    public string Identifier { get; }

    public ProductIdentifierAttribute(string identifier)
    {
        this.Identifier = identifier;
    }
}

[ProductIdentifier("theshoe")]
public class Shoe { }

and then inject it like this?:

public class MyService
{
    public MyService(IProduct<Shoe> shoeProduct) { }
}

With Autofac I can use a factory to create regular non-generic classes like so:

builder
    .Register<INonGenericProduct>(context =>
    {
        var factory = context.Resolve<INonGenericFactory>();
        return factory.CreateProduct("bob");
    })
    .AsImplementedInterfaces();

But this doesn't work for generic classes. I have to use RegisterGeneric. Unfortunately, the type you pass to RegisterGeneric is the open concrete type, rather than the open interface type. I've come up with two workarounds.

Workaround 1: Reflect IFactory to extract _privateData (in the real library this is somewhat more complicated, and involves accessing other internal methods and classes, etc.) and then supply that as Autofac parameters in OnPreparing:

Type factoryType = typeof(Factory);
Type factoryField = factoryType.GetField("_privateData", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Getfield);
Type productType = typeof(Product); // this is `internal` in the third party library, so I have to look it up from the assembly in reality

builder
.RegisterGeneric(productType)
.OnPreparing(preparing =>
{
    var factory = preparing.Context.Resolve<IFactory>();
    var privateFieldValue = factoryField.GetValue(factory);
    var closedProductType = preparing.Component.Activator.LimitType;
    var productModel = closedProductType.GetGenericArguments().Single();
    var productIdentifier = productModel.GetGenericArgument<ProductIdentifierAttribute>().Identifier;

    preparing.Parameters = new List<Parameter>()
    {
        new PositionalParameter(0, privateFieldValue),
        new PositionalParameter(0, productIdentifier)
    };
})
.AsImplementedInterfaces();

But clearly this is a terrible solution for numerous reasons, the most significant being that it's vulnerable to internal changes within the library.

Workaround 2: Create a dummy type and substitute it in OnActivating:

public class DummyProduct<TModel> : IProduct<TModel>
{
    public void DoSomething() => throw new NotImplementedException("");
}

So, we register that as the open generic, and substitute its value before injecting it:

MethodInfo openProductBuilder = this.GetType().GetMethod(nameof(CreateProduct), BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod);
builder
    .RegisterGeneric(typeof(DummyProduct<>))
    .OnActivating(activating => 
    {
        var productModel = activating.Instance.GetType().GetGenericArguments().First();
        var productIdentifier = productModel.GetGenericArgument<ProductIdentifierAttribute>().Identifier;
        var factory = activating.Context.Resolve<IFactory>();
        var closedProductBuilder = openProductBuilder.MakeGenericMethod(productModel);
        object productObject = closedProductBuilder.Invoke(this, new object[] { factory, productIdentifier });
        handler.ReplaceInstance(productObject);
    })
    .AsImplementedInterfaces();

and we have a helper method so that we're only reliant on reflecting methods in this Mongo module class:

private IProduct<TModel> CreateProduct<TModel>(IFactory factory, string identifier)
{
    return factory.CreateProduct<TModel>(identifier);
}

Now, clearly this is better than the first method, and doesn't rely on too much reflection. Unfortunately, it does involve creating a dummy object each time we want the real one. That sucks!

Question: Is there another way to do this using Autofac? Can I somehow create a generic factory method that Autofac can use? My main goal is to cut out the creating the dummy type, and skip straight to calling the CreateProduct code.

Notes: I've cut out a fair bit of error checking, etc. that I would normally do to make this question as short as possible whilst still adequately demonstrating the problem and my current solutions.


Solution

  • If there is no non generic Create method in your factory you will need a call to the MakeGenericMethod.

    Instead of OnActivating event you can use a IRegistrationSource component that will do the same as in your workaround 2

    internal class FactoryRegistrationSource : IRegistrationSource
    {
        private static MethodInfo openProductBuilder = typeof(Factory).GetMethod(nameof(Factory.CreateProduct));
    
        public Boolean IsAdapterForIndividualComponents => false;
    
        public IEnumerable<IComponentRegistration> RegistrationsFor(Service service, Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor)
        {
            IServiceWithType typedService = service as IServiceWithType;
    
            if (typedService != null && typedService.ServiceType.IsClosedTypeOf(typeof(IProduct<>)))
            {
                IComponentRegistration registration = RegistrationBuilder.ForDelegate(typedService.ServiceType, (c, p) =>
                 {
                     IFactory factory = c.Resolve<IFactory>();
    
                     Type productModel = typedService.ServiceType.GetGenericArguments().First();
                     String productIdentifier = productModel.GetCustomAttribute<ProductIdentifierAttribute>()?.Identifier;
    
                     MethodInfo closedProductBuilder = openProductBuilder.MakeGenericMethod(productModel);
                     Object productObject = closedProductBuilder.Invoke(factory, new object[] { productIdentifier });
    
                     return productObject;
                 }).As(service).CreateRegistration();
                yield return registration;
            }
            yield break;
        }
    }