How to register open generic with custom factory method?

与世无争的帅哥 提交于 2020-05-15 09:53:05

问题


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!