如何使用自定义工厂方法注册开放泛型?

Joh*_*ica 4 c# autofac

TL;DR:我可以使用 Autofac 创建一个通用工厂,以便我可以注入IProduct<TModel>而不是从IFactory我需要的任何地方解析它吗?有没有办法将解决从工厂任务移动到组合根?

所以我使用第三方库,它公开了一些通过工厂创建的通用接口。出于演示目的,我们假设以下代码是库:

第三方库模型

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}");
    }
}
Run Code Online (Sandbox Code Playgroud)

我的代码

而我的TModel

public class Shoe { }
Run Code Online (Sandbox Code Playgroud)

现在,让我们假设我想要一个IProduct<Shoe>in MyService。我需要在那里解决它:

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

但如果我能像这样声明鞋是不是更好:

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

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

[ProductIdentifier("theshoe")]
public class Shoe { }
Run Code Online (Sandbox Code Playgroud)

然后像这样注入它?:

public class MyService
{
    public MyService(IProduct<Shoe> shoeProduct) { }
}
Run Code Online (Sandbox Code Playgroud)

使用 Autofac,我可以使用工厂来创建常规的非泛型类,如下所示:

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

但这不适用于泛型类。我必须使用RegisterGeneric. 不幸的是,您传递给的类型RegisterGeneric是开放的具体类型,而不是开放的接口类型。我想出了两个解决方法。

解决方法 1:反射IFactory提取_privateData(在实际库中这有点复杂,涉及访问其他internal方法和类等),然后将其作为 Autofac 参数提供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();
Run Code Online (Sandbox Code Playgroud)

但很明显,出于多种原因,这是一个糟糕的解决方案,最重要的是它容易受到库内部更改的影响。

解决方法 2:创建一个虚拟类型并将其替换为OnActivating

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

因此,我们将其注册为开放泛型,并在注入之前替换其值:

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();
Run Code Online (Sandbox Code Playgroud)

我们有一个辅助方法,因此我们只依赖于这个 Mongo 模块类中的反射方法:

private IProduct<TModel> CreateProduct<TModel>(IFactory factory, string identifier)
{
    return factory.CreateProduct<TModel>(identifier);
}
Run Code Online (Sandbox Code Playgroud)

现在,显然这比第一种方法更好,并且不依赖太多反射。不幸的是,每次我们想要真正的对象时,它确实涉及创建一个虚拟对象。真糟糕!

问题:有没有其他方法可以使用 Autofac 做到这一点?我可以以某种方式创建 Autofac 可以使用的通用工厂方法吗?我的主要目标是删除创建虚拟类型,并直接跳到调用CreateProduct代码。

注意:我已经删除了一些错误检查等,我通常会这样做以使这个问题尽可能短,同时仍然充分展示问题和我当前的解决方案。

Cyr*_*and 5

如果Create您的工厂中没有非通用方法,您将需要调用MakeGenericMethod.

OnActivating您可以使用IRegistrationSource与解决方法 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;
    }
}
Run Code Online (Sandbox Code Playgroud)