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.
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;
}
}