【问题标题】:Dependency Injection in Model classes (entities)模型类(实体)中的依赖注入
【发布时间】:2016-07-12 11:06:22
【问题描述】:

我正在使用 Entity Framework Code-First 构建一个 ASP.NET Core MVC 应用程序。 我选择实现一个简单的存储库模式,为我创建的所有模型类提供基本的 CRUD 操作。 我选择遵循http://docs.asp.net 中提供的所有建议,而 DI 就是其中之一。

在 .NET 5 中,依赖注入非常适用于我们不直接实例化的任何类(例如:控制器、数据存储库……)。

我们只需通过构造函数注入它们,并在应用程序的 Startup 类中注册映射:

// Some repository class
public class MyRepository : IMyRepository
{
    private readonly IMyDependency _myDependency;
    public MyRepository(IMyDependency myDependency)
    {
        _myDependency = myDependency;
    }
}

// In startup.cs :
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyRepository, MyRepository>();

我遇到的问题是,在我的一些模型类中,我想注入一些我已经声明的依赖项。

但我认为我不能使用构造函数注入模式,因为模型类通常是显式实例化的,因此我需要为自己提供依赖项,而我不能。

所以我的问题是:除了构造函数注入之外,还有其他方法可以注入依赖项吗?如何注入?例如,我在考虑属性模式或类似的东西。

【问题讨论】:

  • @kall2sollies 我认为您问题中的存储库示例并不能真正帮助传达您有关创建 model 对象并在其中注入依赖项的问题。您可能想要更改您的问题,以便您的实际意图变得更加清晰。
  • @kall2sollies:如果您的模型类需要强制依赖项,那么您的应用程序设计就会出现根本性错误。尝试详细说明为什么您的模型中需要此依赖项
  • 一般你可以使用方法注入将某个服务(即IVatCalculator通过order.CalculateVat(vatCalculator)注入你的模型类。IVatCalculator可以有一个`public Currency Calculate(Currency currency, Country country ) 方法从数据库中获取税率并根据增值税率进行计算,然后返回计算值并将其分配到您的模型中。我可以发布一个示例作为答案,说明您将如何在以 DDD 为中心的情况下进行操作使用一项或多项服务的方式,如果这是您试图通过将逻辑移动到模型来实现的目标,称为富域模型
  • @JohnErnest 只要我可以自己新建模型类,我认为您的模型就可以工作。但是 Entity Framework 自己创建新类,调用它们的默认无参数构造函数。在这种情况下,应该实现一个逻辑以避免在不满足依赖关系时出现错误。
  • @JohnErnest 你认为你可以发布你的简化解决方案作为问题的答案吗?

标签: c# dependency-injection asp.net-core


【解决方案1】:

正如我已经在评论中解释的那样,当使用new 创建对象时,依赖注入框架中没有涉及该过程的任何内容。因此,DI 框架不可能神奇地将东西注入该对象,它根本不知道。

由于让 DI 框架创建您的模型实例(模型不是 依赖项)没有任何意义,因此您必须显式传递您的依赖项如果您希望模型拥有它们。你如何做到这一点取决于你的模型用于什么,以及这些依赖项是什么。

简单明了的情况是让您的模型期望依赖于构造函数。这样,如果您不提供它们,则会出现编译时错误,并且模型可以立即访问它们。因此,无论上述什么,创建模型都需要具有模型类型所需的依赖项。但在那个级别上,这很可能是一个服务或控制器,它可以访问 DI 并且可以请求依赖本身。

当然,根据依赖的数量,这可能会变得有点复杂,因为您需要将它们全部传递给构造函数。因此,另一种选择是拥有一些“模型工厂”来负责创建模型对象。另一种选择是使用service locator pattern,将IServiceCollection 传递给模型,然后模型可以请求它需要的任何依赖项。请注意,这通常是一种不好的做法,不再是真正的控制反转。

这两个想法都存在修改对象创建方式的问题。并且某些模型,尤其是那些由实体框架处理的模型,需要一个空的构造函数,以便 EF 能够创建对象。因此,到那时您可能会遇到某些情况,在这些情况下,您的模型的依赖关系没有解决(而且您没有简单的方法来判断)。

通常更好的方法,也更明确,是在你需要的地方传递依赖,例如如果您在模型上有一些方法可以计算一些东西但需要一些配置,那么让该方法需要该配置。这也使这些方法更易于测试。

另一种解决方案是将逻辑移出模型。例如ASP.NET Identity models 真的很愚蠢。他们什么都不做。所有逻辑都在UserStore 中完成,这是一个服务,因此可以具有服务依赖项。

【讨论】:

  • 这实际上是可能的,正如我在回答中所详述的那样。您可以手动使用CallContextServiceLocator.Locator.ServiceProvider 来解决依赖关系,使用您手动实例化的类。
  • @DavidPine 不过这不是依赖注入。这是一种非常丑陋的服务定位器模式,它对静态(全局)对象有很强的依赖性。另外,我不知道您在 “这实际上是可能的” 中使用 “this” 指的是什么。你让它看起来好像我的答案是两行长。
  • 两者都通过在Startup.cs 中添加IInterfaceImplementation 来控制。对于所有与帮助 OP 获得他们正在寻找的东西有关的密集目的,这是他们所需要的,因为您呼吁注意“这是不可能的”。
  • @DavidPine 这不是关于注册的地方,而是关于如何解决依赖关系的问题。引用我自己的话,“DI 框架不可能神奇地将东西注入到该对象中”。 DI 根本无法做到这一点。如果你选择一个完全不同的解决方案,不是依赖注入和控制反转,当然你可以通过某种方式使它工作,但这并不能与 DI 一起工作——这是一种完全不同的方法。
  • 在我看来,这个答案很好地总结了我的问题是什么,以及为什么它不能通过设计来解决。模型应该是完全没有依赖关系的简单 POCOS。与它们相关的任何业务逻辑都应该在它们相应的存储库中,或者在 ModelController 类中,就像 MS Identity 对 UserStore 所做的那样。我无法将此答案标记为解决方案,因为没有解决方案,所以我会 +1 你的答案。
【解决方案2】:

领域驱动设计(具体来说是富领域模型)中经常使用的模式是将所需的服务传递给您正在调用的方法。

例如,如果您想计算增值税,您可以将增值税服务传递给CalculateVat 方法。

在你的模型中

    public void CalculateVat(IVatCalculator vatCalc) 
    {
        if(vatCalc == null)
            throw new ArgumentNullException(nameof(vatCalc));

        decimal vatAmount = vatcalc.Calculate(this.TotalNetPrice, this.Country);
        this.VatAmount = new Currency(vatAmount, this.CurrencySymbol);
    }

您的服务等级

    // where vatCalculator is an implementation IVatCalculator 
    order.CalculateVat(vatCalculator);

最后,您的服务可以注入其他服务,例如获取某个国家/地区税率的存储库

public class VatCalculator : IVatCalculator
{
    private readonly IVatRepository vatRepository;

    public VatCalculator(IVatRepository vatRepository)
    {
        if(vatRepository == null)
            throw new ArgumentNullException(nameof(vatRepository));

        this.vatRepository = vatRepository;
    }

    public decimal Calculate(decimal value, Country country) 
    {
        decimal vatRate = vatRepository.GetVatRateForCountry(country);

        return vatAmount = value * vatRate;
    }
}

【讨论】:

  • 如果 Entity Framework Code First 知道如何处理 IVatCalculator 的注入,这实际上会起作用。它需要无参数的构造函数。
  • @MilivojMilani:EF Core 不参与其中。实体不应该有构造函数依赖。正如您从第一个示例中看到的那样,计算器被注入到模型的方法中,而不是构造函数。因此,在您的订单服务中,您显式调用order.CalculateVat(vatCalculator);order 是您的域模型)并传递计算器的实例。所以 EF/EFCore 根本不参与这个过程
【解决方案3】:

我知道我的回答迟了,可能不完全符合您的要求,但我想分享一下我是如何做到的。

首先:如果你想要一个静态类来解决你的依赖关系,这是一个ServiceLocator,它是Antipattern,所以尽量不要使用它。 就我而言,我需要它在我的 DomainModel 中调用 MediatR 来实现 DomainEvents 逻辑。

无论如何,我必须找到一种方法来调用我的 DomainModel 中的静态类,以从 DI 获取某些已注册服务的实例。

所以我决定使用HttpContext 来访问IServiceProvider,但我需要从静态方法访问它,而在我的域模型中没有提及它。

让我们开始吧:

1- 我创建了一个接口来包装 IServiceProvider

public interface IServiceProviderProxy
{
    T GetService<T>();
    IEnumerable<T> GetServices<T>();
    object GetService(Type type);
    IEnumerable<object> GetServices(Type type);
}

2- 然后我创建了一个静态类作为我的 ServiceLocator 访问点

public static class ServiceLocator
{
    private static IServiceProviderProxy diProxy;

    public static IServiceProviderProxy ServiceProvider => diProxy ?? throw new Exception("You should Initialize the ServiceProvider before using it.");

    public static void Initialize(IServiceProviderProxy proxy)
    {
        diProxy = proxy;
    }
}

3- 我为IServiceProviderProxy 创建了一个实现,它在内部使用IHttpContextAccessor

public class HttpContextServiceProviderProxy : IServiceProviderProxy
{
    private readonly IHttpContextAccessor contextAccessor;

    public HttpContextServiceProviderProxy(IHttpContextAccessor contextAccessor)
    {
        this.contextAccessor = contextAccessor;
    }

    public T GetService<T>()
    {
        return contextAccessor.HttpContext.RequestServices.GetService<T>();
    }

    public IEnumerable<T> GetServices<T>()
    {
        return contextAccessor.HttpContext.RequestServices.GetServices<T>();
    }

    public object GetService(Type type)
    {
        return contextAccessor.HttpContext.RequestServices.GetService(type);
    }

    public IEnumerable<object> GetServices(Type type)
    {
        return contextAccessor.HttpContext.RequestServices.GetServices(type);
    }
}

4- 我应该像这样在 DI 中注册 IServiceProviderProxy

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpContextAccessor();
    services.AddSingleton<IServiceProviderProxy, HttpContextServiceProviderProxy>();
    .......
}

5- 最后一步是在应用程序启动时使用 IServiceProviderProxy 的实例初始化 ServiceLocator

public void Configure(IApplicationBuilder app, IHostingEnvironment env,IServiceProvider sp)
{
    ServiceLocator.Initialize(sp.GetService<IServiceProviderProxy>());
}

因此,您现在可以在 DomainModel 类中调用 ServiceLocator“或者和需要的地方”并解决您需要的依赖关系。

public class FakeModel
{
    public FakeModel(Guid id, string value)
    {
        Id = id;
        Value = value;
    }

    public Guid Id { get; }
    public string Value { get; private set; }

    public async Task UpdateAsync(string value)
    {
        Value = value;
        var mediator = ServiceLocator.ServiceProvider.GetService<IMediator>();
        await mediator.Send(new FakeModelUpdated(this));
    }
}

【讨论】:

  • 抱歉也迟到了!从那以后,我也意识到ServiceLocator是邪恶的,但也许是实现这一点的唯一方法!
【解决方案4】:

除了构造函数注入之外,还有其他方法可以注入依赖项吗?

答案是“不”,这不能通过“依赖注入”来完成。但是,“是的”,您可以使用“服务定位器模式”来实现您的最终目标。

您可以使用下面的代码来解决依赖关系,而无需使用构造函数注入或FromServices 属性。此外,您可以new 为您认为合适的类的一个实例,它仍然可以工作——假设您已经在Startup.cs 中添加了依赖项。

public class MyRepository : IMyRepository
{
    public IMyDependency { get; } =
        CallContextServiceLocator.Locator
                                 .ServiceProvider
                                 .GetRequiredService<IMyDependency>();
}

CallContextServiceLocator.Locator.ServiceProvider 是全球服务提供商,一切都在其中。真的不建议使用它。但如果你别无选择,你可以。建议一直使用 DI 并且永远不要手动实例化对象,即;避免new

【讨论】:

  • 这实际上与本示例中的 OP 代码没有任何区别,因为存储库也是通过 DI 注入的。 OP 实际上是在询问他们想要在其中注入依赖项的 model 类。
  • 如之前的评论中所说,[FromServices] 是构造函数注入的替代方法,该类本身由 DI 解析。但是在问题 cmets 中,@poke 指出模型类是通过设计明确实例化的,这是我问题的根源。
  • 这不是依赖注入,这是service locator pattern,它与DI非常不同,并且确实很难将类耦​​合到静态类型,因此很难测试并击败控制反转的整个目的。
  • 这个答案实际上提供了一个解决方案,但该解决方案打破了设计规则(POCO 最终应该只是 POCO)并注入了与应该避免的模式的强耦合。感谢您抽出宝贵时间,我无法将此答案标记为已接受,但我已经 +1 编辑了它。
  • @kall2sollies:即使它很旧,我现在也注意到了[FromService] 评论。它不是那样工作的。 [FromService] 只能在控制器动作中使用,并且从 rc2 开始,它仅限于方法参数,因为人们混淆了它并试图将它用于通用方法/属性注入
【解决方案5】:

内置模型绑定器抱怨找不到默认 ctor。因此,您需要一个自定义的。

您可能会找到类似问题here 的解决方案,它会检查已注册的服务以创建模型。

请务必注意,下面的 sn-ps 提供的功能略有不同,希望能满足您的特定需求。下面的代码需要带有 ctor 注入的模型。当然,这些模型具有您可能已经定义的常用属性。这些属性完全按照预期填充,因此使用 ctor 注入绑定模型时的正确行为

    public class DiModelBinder : ComplexTypeModelBinder
    {
        public DiModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders) : base(propertyBinders)
        {
        }

        /// <summary>
        /// Creates the model with one (or more) injected service(s).
        /// </summary>
        /// <param name="bindingContext"></param>
        /// <returns></returns>
        protected override object CreateModel(ModelBindingContext bindingContext)
        {
            var services = bindingContext.HttpContext.RequestServices;
            var modelType = bindingContext.ModelType;
            var ctors = modelType.GetConstructors();
            foreach (var ctor in ctors)
            {
                var paramTypes = ctor.GetParameters().Select(p => p.ParameterType).ToList();
                var parameters = paramTypes.Select(p => services.GetService(p)).ToArray();
                if (parameters.All(p => p != null))
                {
                    var model = ctor.Invoke(parameters);
                    return model;
                }
            }

            return null;
        }
    }

此活页夹将由以下人员提供:

public class DiModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) { throw new ArgumentNullException(nameof(context)); }

        if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
        {
            var propertyBinders = context.Metadata.Properties.ToDictionary(property => property, context.CreateBinder);
            return new DiModelBinder(propertyBinders);
        }

        return null;
    }
}

以下是活页夹的注册方式:

services.AddMvc().AddMvcOptions(options =>
{
    // replace ComplexTypeModelBinderProvider with its descendent - IoCModelBinderProvider
    var provider = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(ComplexTypeModelBinderProvider));
    var binderIndex = options.ModelBinderProviders.IndexOf(provider);
    options.ModelBinderProviders.Remove(provider);
    options.ModelBinderProviders.Insert(binderIndex, new DiModelBinderProvider());
});

我不太确定新的活页夹是否必须在同一索引处完全注册,您可以尝试一下。

最后,你可以这样使用它:

public class MyModel 
{
    private readonly IMyRepository repo;

    public MyModel(IMyRepository repo) 
    {
        this.repo = repo;
    }

    ... do whatever you want with your repo

    public string AProperty { get; set; }

    ... other properties here
}

模型类由提供(已注册的)服务的绑定器创建,其余模型绑定器提供来自其常用来源的属性值。

HTH

【讨论】:

    【解决方案6】:

    我只是在此处为提供的答案添加一些补充信息,以提供帮助。

    在接受的答案中提供了 IServiceProvider,但不是重要的 IServiceProvider.CreateScope() 方法。您可以根据需要使用它创建通过 ConfigureServices 添加的范围。

    我不确定 IServiceProvider 是否实际上是幕后的服务定位器模式,但据我所知,这是您创建范围的方式。至少在它是一种服务定位器模式的情况下,它是今天在 .NET 中的官方模式,因此不会因编写自己的服务定位器的问题而复杂化,我也同意这是反模式。

    示例,Startup.cs/ConfigureServices 和配置:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<SomeDbContext>(options =>
            {
                options.UseSqlServer(Configuration.GetSection("Databases").GetSection("SomeDb")["ConnectionString"]);
                options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
            }, ServiceLifetime.Scoped);
    
            services.AddMvcCore().AddNewtonsoftJson();
            services.AddControllersWithViews();
        }
    
        public async void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider provider)
        {
            ...
            IServiceScope scope = provider.CreateScope();
            SomeDbContext context = scope.ServiceProvider.GetRequiredService<SomeDbContext>();
            SomeModelProxyClass example = new SomeModelProxyClass(context);
            await example.BuildDefaults(
                Configuration.GetSection("ProfileDefaults").GetSection("Something"),
                Configuration.GetSection("ProfileDefaults").GetSection("SomethingSomething"));
            scope.Dispose();
        }
    

    以上是为了在启动时做一些默认交互,也许如果您需要在第一次使用时在数据库中构建一些默认记录,仅作为示例。

    好的,让我们进入您的存储库和依赖项,它们会起作用吗?

    是的!

    这是我自己的 CRUD 项目中的一个测试,我像这样对您的 IMyDependency 和 IMyRepository 做了一个简单的极简实现,然后将它们添加到 Startup/ConfigureServices 的范围内:

    public interface IMyRepository
    {
        string WriteMessage(string input);
    }
    
    public interface IMyDependency
    {
        string GetTimeStamp();
    }
    
    public class MyDependency : IMyDependency
    {
        public MyDependency()
        {
    
        }
    
        public string GetTimeStamp()
        {
            return DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString();
        }
    }
    
    public class MyRepository : IMyRepository
    {
        private readonly IMyDependency _myDependency;
        public MyRepository(IMyDependency myDependency)
        {
            _myDependency = myDependency;
        }
    
        public string WriteMessage(string input)
        {
            return input + " - " + _myDependency.GetTimeStamp();
        }
    }
    

    这里的 ContextCRUD 是我自己的项目中的一个模型类,它不像我的其他数据库类那样从 Scaffold-DbContext 工具派生,它是那些脚手架模型类的逻辑容器,所以我把它放在命名空间 Models.ProxyModels 中来保存它自己的业务逻辑来执行 CRUD 操作,这样控制器就不会被模型中应该存在的逻辑所困扰:

        public ContextCRUD(DbContext context, IServiceProvider provider)
        {
            Context = context;
            Provider = provider;
    
            var scope = provider.CreateScope();
            var dep1 = scope.ServiceProvider.GetService<IMyRepository>();
            string msg = dep1.WriteMessage("Current Time:");
            scope.Dispose();
        }
    

    调试我在 msg 中得到了预期的结果,所以一切都检查出来了。

    Controller中的调用代码供参考,方便大家看一下IServiceProvider是如何通过Controller中的构造函数注入从上游传递过来的:

    [Route("api/[controller]")]
    public class GenericController<T> : Controller where T: DbContext
    {
        T Context { get; set; }
        ContextCRUD CRUD { get; set; }
        IConfiguration Configuration { get; set; }
    
        public GenericController(T context, IConfiguration configuration, IServiceProvider provider)
        {
            Context = context;
            CRUD = new ContextCRUD(context, provider);
            Configuration = configuration;
        }
        ...
    

    【讨论】:

      【解决方案7】:

      可以的,看看[InjectionMethod]和container.BuildUp(instance);

      例子:

      典型的 DI 构造函数(如果使用 InjectionMethod,则不需要) public 类构造器(设备头 pDeviceHead){ this.DeviceHead = pDeviceHead; }

      此属性导致调用此方法来设置 DI。 [注入方法] public void Initialize(DeviceHead pDeviceHead) { this.DeviceHead = pDeviceHead; }

      【讨论】:

      • 能否提供一个简单的代码sn-p,让本帖的答案都一致?
      • public ClassConstructor(DeviceHead pDeviceHead) { this.DeviceHead = pDeviceHead; } [InjectionMethod] public void Initialize(DeviceHead pDeviceHead) { this.DeviceHead = pDeviceHead; }
      猜你喜欢
      • 2018-12-07
      • 1970-01-01
      • 1970-01-01
      • 2020-11-20
      • 2015-05-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-30
      相关资源
      最近更新 更多