【问题标题】:How to inject collection with open generic types to service with Autofac如何注入具有开放泛型类型的集合以使用 Autofac 提供服务
【发布时间】:2019-04-09 13:30:42
【问题描述】:

我已尽我所能解释我公认的复杂问题。让我知道是否有任何我可以添加澄清的内容。

简要背景

我有一个DbWrapperCollection 用来存储DbWrapper<TInput. TOutput>(因为TInputTOutput 会有所不同,所以集合实际上只是一个包含泛型作为对象的非泛型“容器”列表作为输入和作为 System.Types 输出 - 请参阅下面的实现)

另一方面,我有数量不定的服务,它们都有自己的IDbWrapperCollection,我想在启动时使用 autofac 注入这些服务。

基本上我想做的是:

builder.RegisterType<SaveApplDbWrapper>().As<DbWrapper<AppleDto, SavedAppleDbEntity>>()
    .Named<string>("fruitService");
builder.RegisterType<SaveOrangeDbWrapper>().As<IUcHandler<OrangeDto, OrangeDbEntity>>()
    .Named<string>("fruitService");
builder.RegisterType<SaveMelon>().As<IUcHandler<MelonDto, MelonDbEntity>>()
    .Named<string>("appleService");

builder.Register(c => new FruitService(c.ResolveNamed("appleService")))
    .As<IDbMapperService>();

我的问题

正如您在上面看到的,我在调用ResolveNamed() 时专门省略了预期的类型参数。那是因为我是 autofac 的新手(在某种程度上,泛型),我特别不知道是否有任何策略可以注入一个开放的泛型 DbWrappers 列表并延迟关闭我的泛型类型。

我将尝试在下面解释我为解决这个问题而研究过的策略,以及我到目前为止的实现

我自己的研究

在我看来,我可以为我的包装器创建一个非泛型基类并将它们保存为该基类,将解析原始泛型类型的责任委托给该基类,或者放弃我的包装器集合想法,转而使用我的服务构造函数上的特定参数(无聊 - 并且与我的复合启发实现不兼容)。

随着复合模式的流行,我想我不是第一个想要使用 DI 和 IoC 的具有“通用叶子”的复合模式解决方案的人。

我打算像这样使用我的fruitService:

myFruitService.GetDbMapper<MyFruitDto, DbEntityForThatSaidFruit(myOrange);

服务在它的 DbMapperCollection 中查找,找到具有提供的类型参数的映射器,并调用其 Save() 的实现;

到目前为止的实施

对于那些好奇的人,这是我的实现:

DbWrappers:

class SaveApplebWrapper : DbWrapper<TInput, TOutput>
// and plenty more wrapppers for any fruit imaginable

服务:

public abstract class DbMapperService : IDbMapperService
{
    public IWrapperCollection Wrappers { get; set; }

    protected BestandService(IWrapperCollection wrappers)
    {
        Wrappers = wrappers;
    }

    public DbWrapper<TInput, TResult> GetWrapper<TInput, TResult>()
    {
        return Wrappers.GetWrapper<TInput, TResult>();
    }
}

我的 WrapperCollection 辅助类:

public struct WrapperKey
{
    public static WrapperKey NewWrapperKey <TInput, TResult>()
    {
        return new WrapperKey { InputType = typeof(TInput), ResultType = typeof(TResult) };
    }

    public Type InputType { get; set; }
    public Type ResultType { get; set; }
}

public struct WrapperContainer
{
    public WrapperContainer(object wrapper) : this()
    {
        Wrapper= wrapper;
    }

    public object Wrapper{ get; set; }

    public DbWrapper<TInput, TResult> GetWrapper<TInput, TResult>()
    {
        return Wrapper as DbWrapper<TInput, TResult>;
    }
}

还有我的 WrapperCollection:

public class UcWrapperCollection : Dictionary<WrapperKey, WrapperContainer>,
    IDbWrapperCollection
{
    public void AddWrapper<TInput, TResult>(UcHandler<TInput, TResult> handler)
    {
        Add(WrapperKey.NewWrapperKey<TInput, TResult>(), new WrapperContainer(handler));
    }

    public DbWrapper<TInput, TResult> GetWrapper<TInput, TResult>()
    {
        var key = WrapperKey.NewWrapperKey<TInput, TResult>();
        return this[key].GetWrapper<TInput, TResult>();
    }
}

我在没有运气的情况下看过的问题

我看过的一些问题似乎与我的案例无关(尽管我的问题可能会通过通用委托解决,但我认为这不是解决我的问题的最佳解决方案。

  • 使用 Autofac 注入通用类型参数
  • Autofac。如何在构造函数中注入一个开放的 Generic Delegate
  • 如何使用 Autofac 注入泛型工厂
  • 具有嵌套开放泛型的 Autofac

【问题讨论】:

    标签: c# generics dependency-injection autofac composite


    【解决方案1】:

    我不认为你能做你想做的事。抱歉,可能不是你想要的答案。我会告诉你为什么,也许还有一些解决方法,但是拥有一个在解决之前不会关闭的任意集合并不是真正的事情。

    让我们暂时忽略 DI,只考虑 FruitService,我在问题中没有看到它,但我们在此处的用法中看到了它:

    builder.Register(c => new FruitService(c.ResolveNamed("appleService")))
        .As<IDbMapperService>();
    

    请注意,我们可以看到 FruitService 实现了 IDbMapperService,因为它已注册为该接口。

    此外,我们可以看到FruitService 看起来应该是某种东西的集合,因为在注册示例中有两个名称相同的东西。

    builder.RegisterType<SaveApplDbWrapper>().As<DbWrapper<AppleDto, SavedAppleDbEntity>>()
        .Named<string>("fruitService");
    builder.RegisterType<SaveOrangeDbWrapper>().As<IUcHandler<OrangeDto, OrangeDbEntity>>()
        .Named<string>("fruitService");
    

    我注意到它们都实现了不同的泛型类型。我必须根据问题的其余部分假设它们没有共同的基类。

    为了使其更具体并超越 Autofac 部分,我认为这与更大的问题并不真正相关,让我们这样考虑:

    var wrapper = new[] { CreateWrapper("appleService"), CreateHandler("appleService") };
    var service = new FruitService(wrapper);
    

    让我们假设CreateWrapperCreateHandler 都接受一个字符串,并且神奇地创建了适当的包装器/处理程序类型。不管它是如何发生的。

    这里有两件事需要考虑密切相关:

    • FruitService 构造函数中参数的类型是什么?
    • 您希望CreateWrapper("appleService")CreateHandler("appleService") 会返回什么?

    这里基本上只能看到两个选项。

    选项 1:使用object

    如果没有公共基类,那么一切都必须是object

    public class FruitService : IDBMapperService
    {
      private readonly IEnumerable<object> _wrappers;
      public FruitService(IEnumerable<object>wrapper)
      {
        this._wrapper = wrapper;
      }
    
      public object GetWrapper<TInput, TResult>()
      {
        object foundWrapper = null;
        // Search through the collection using a lot of reflection
        // to find the right wrapper, then
        return foundWrapper;
      }
    }
    

    尚不清楚DbWrapper&lt;TInput, TResult&gt; 是否可以转换为IUcHandler&lt;TInput, TResult&gt;,因此您甚至不能依赖它。没有共同点。

    但是假设有共同的基类。

    选项 2:使用通用基类

    似乎已经有了DbWrapper&lt;TInput, TResult&gt; 的概念。重要的是要注意,即使您定义了该泛型,一旦您关闭它,它们也是两种不同的类型。 DbWrapper&lt;AppleDto, SavedAppleDbEntity&gt; 不能转换为 DbWrapper&lt;OrangeDto, SavedOrangeDbEntity&gt;。泛型更像是“类模板”而不是基类。它们不是一回事。

    例如,你不能这样做:

    var collection = new DbWrapper<,>[]
    {
      new DbWrapper<AppleDto, SavedAppleDbEntity>(),
      new DbWrapper<OrangeDto, SavedOrangeDbEntity>()
    };
    

    但是,如果你有一个通用的接口或基类,你可以这样做......

    var collection = new IDbWrapper[]
    {
      new DbWrapper<AppleDto, SavedAppleDbEntity>(),
      new DbWrapper<OrangeDto, SavedOrangeDbEntity>()
    };
    

    但这意味着您可以切换到该界面,并且表面上可以使用通用界面。

    public class FruitService : IDBMapperService
    {
      private readonly IEnumerable<object> _wrappers;
      public FruitService(IEnumerable<object>wrapper)
      {
        this._wrapper = wrapper;
      }
    
      public IDbWrapper GetWrapper<TInput, TResult>()
      {
        IDbWrapper foundWrapper = null;
        // Search through the collection using a lot of reflection
        // to find the right wrapper, then
        return foundWrapper;
    
        // IDbWrapper could expose those `TInput` and `TResult`
        // types as properties on the interface, so the reflection
        // could be super simple and way more straight LINQ.
      }
    }
    

    您的消费代码可以只使用IDbWrapper 并调用非泛型方法来完成工作。

    将其带回 Autofac...

    记得我提到过,关键在于弄清楚Create 方法应该返回什么;或者FruitService 构造函数期望什么?那。那是黑桃。

    您可以将所有内容注册为键控对象。

    builder.RegisterType<SaveApplDbWrapper>()
           .Named<object>("fruitService");
    builder.RegisterType<SaveOrangeDbWrapper>()
           .Named<object>("fruitService");
    builder.RegisterType<SaveMelon>()
           .Named<object>("appleService");
    
    builder
      .Register(c => new FruitService(c.ResolveNamed<IEnumerable<object>>("appleService")))
      .As<IDbMapperService>();
    

    Autofac 中的Resolve 操作是我的示例中的创建方法。那里没有魔法;它只是创建对象。您仍然需要知道您希望它提供什么类型。

    或者你可以使用一个通用的基类。

    builder.RegisterType<SaveApplDbWrapper>()
           .Named<IDbWrapper>("fruitService");
    builder.RegisterType<SaveOrangeDbWrapper>()
           .Named<IDbWrapper>("fruitService");
    builder.RegisterType<SaveMelon>()
           .Named<IDbWrapper>("appleService");
    
    builder
      .Register(c => new FruitService(c.ResolveNamed<IEnumerable<IDbWrapper>>("appleService")))
      .As<IDbMapperService>();
    

    如果您不介意将 DI 系统混入FruitService,您可以执行以下操作:

    public class FruitService
    {
      private readonly ILifetimeScope _scope;
      public FruitService(ILifetimeScope scope)
      {
        this._scope = scope;
      }
    
      public DbWrapper<TInput, TResult> GetWrapper<TInput, TResult>()
      {
        var type = typeof(DbWrapper<TInput, TResult>);
        var wrapper = this._lifetimeScope.Resolve(type);
        return wrapper;
      }
    }
    

    你必须在没有被命名和AsDbWrapper的情况下注册东西,但如果一切都基于此,它会起作用。

    builder.RegisterType<SaveApplDbWrapper>()
           .As<DbWrapper<AppleDto, SavedAppleDbEntity>>();
    // Must be DbWrapper, can also be other things...
    builder.RegisterType<SaveOrangeDbWrapper>()
           .As<IUcHandler<OrangeDto, OrangeDbEntity>>()
           .As<DbWrapper<OrangeDto, OrangeDbEntity>>();
    builder.RegisterType<SaveMelon>()
           .As<DbWrapper<MelonDto, MelonDbEntity>>()
           .As<IUcHandler<MelonDto, MelonDbEntity>>();
    
    builder.RegisterType<FruitService>()
           .As<IDbMapperService>();
    

    当您解析IDbMapperService 时,FruitService 构造函数将获得对它被解析的生命周期范围的引用。所有包装器都将从同一范围解析。

    人们通常不喜欢像这样将 IoC 引用混合到他们的代码中,但我认为这是唯一能让您避免陷入反射或上下转换的唯一方法。

    祝你好运!

    【讨论】:

    • 感谢广泛的演练!我查看了我的问题,觉得我可能在总结我的问题方面做得更好,因为我最终想要的按定义来说是不可能的,而我真正想要的是一种更优雅的方式来注入我的处理程序.再次感谢您的意见!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-04
    • 1970-01-01
    相关资源
    最近更新 更多