模型类(实体)中的依赖注入

时间:2016-03-24 18:17:28

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

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

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

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

{{1}}

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

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

所以我的问题是:是否有另一种方式比构造函数注入注入依赖项,以及如何?我是在想一个属性模式或类似的事情。

7 个答案:

答案 0 :(得分:6)

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

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

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

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

这两个想法都存在修改对象创建方式的问题。有些模型,特别是那些由Entity Framework处理的模型,需要一个空构造函数才能使EF能够创建对象。那么在那时你可能最终会遇到某些情况,其中模型的依赖关系已解决(并且你没有简单的方法)。

一种通常更好的方法,也就是更明确的方法,就是传递你需要的依赖关系,例如:如果您在模型上有一些计算某些东西但需要一些配置的方法,那么让该方法需要该配置。这也使得方法更容易测试。

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

答案 1 :(得分:5)

域驱动设计中常用的模式(特定的富域模型)是将所需的服务传递给您调用的方法。

例如,如果您想计算增值税,您可以将增值服务传递到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;
    }
}

答案 2 :(得分:2)

  

是否存在除构造函数注入之外的其他方式来注入依赖项,以及如何?

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

您可以使用以下代码解析依赖关系,而无需使用构造函数注入或FromServices属性。此外,您可以new按照您认为合适的方式添加该类的实例,并且它仍然有效 - 假设您已在Startup.cs中添加了依赖项。

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

CallContextServiceLocator.Locator.ServiceProvider是全球服务提供商,一切都存在。我们并不建议使用它。但如果你别无选择。建议改为一直使用 DI ,而不要手动实例化一个对象,即;避免new

答案 3 :(得分:1)

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

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

值得注意的是,下面的代码段提供的功能略有不同,希望能够满足您的特定需求。下面的代码预计带有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

答案 4 :(得分:0)

你可以这样做,查看[InjectionMethod]和container.BuildUp(instance);

示例:

  

典型的DI构造函数(如果您使用InjectionMethod则不需要)公开   ClassConstructor(DeviceHead pDeviceHead){       this.DeviceHead = pDeviceHead; }

     

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

答案 5 :(得分:0)

我知道我的回答太迟了,可能不完全是您的要求,但是我想分享我的做法。

首先:如果您希望拥有一个静态类来解析依赖项,则该类为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-最后一步是在应用程序启动时使用ServiceLocator实例初始化IServiceProviderProxy

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));
    }
}

答案 6 :(得分:0)

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

在可接受的答案中提供了

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是我自己项目中的Model类,不像其他数据库类那样从Scaffold-DbContext工具派生而来,它是这些scaffold Model类的逻辑容器,因此我将其放在命名空间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的调用代码以供参考,以便您可以了解如何通过Controller中的构造函数注入从上游传递IServiceProvider:

[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;
    }
    ...