问题
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.
回答1:
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;
}
}
来源:https://stackoverflow.com/questions/57290563/how-to-register-open-generic-with-custom-factory-method