原文链接:ASP.NET Core Dependency Injection Deep Dive - Joonas W's blog

这篇文章我们来深入探讨 ASP.NET Core、MVC Core 中的依赖注入,我们将示范几乎所有可能的操作把依赖项注入到组件中。

依赖注入是 ASP.NET Core 的核心,它能让您应用程序中的组件增强可测试性,还使您的组件只依赖于能够提供所需服务的某些组件。

举个例子,这里我们有一个接口和它的实现类:

  1. public interface IDataService
  2. {
  3. IList<DataClass> GetAll();
  4. }
  5. public class DataService : IDataService
  6. {
  7. public IList<DataClass> GetAll()
  8. {
  9. //Get data...
  10. return data;
  11. }
  12. }

如果另一个服务依赖于DataService,那么它们依赖于特定的实现,测试这样的服务可能会非常困难。如果该服务依赖于IDataService,那么它们只关心接口提供的契约。实现什么并不重要,它使我们能够通过一个模拟实现来测试服务的行为。

服务生命周期

在我们讨论如何在实践中进行注入之前,了解什么是服务生命周期至关重要。当一个组件通过依赖注入请求另一个组件时,它所接收的实例是否对该组件的实例来说是唯一的,这取决于它的生命周期。设置生命周期从而决定组件实例化的次数,以及组件是否共享。

在 ASP.NET Core中,内置的DI容器有三种模式:

  • Singleton
  • Scoped
  • Transient

Singleton意味着只会创建一个实例,该实例在需要它的所有组件之间共享。因此始终使用相同的实例。

Scoped意味着每个作用域创建一个实例。作用域是在对应用程序的每个请求上创建的,因此,任何注册为Scoped的组件每个请求都会创建一次。

Transient每次请求时都会创建瞬态组件,并且永远不会共享。

理解这一点非常重要,如果将组件A注册为单例,则它不能依赖于具有ScopedTransient生命周期的组件。总而言之:

组件不能依赖比自己的生命周期小的组件。

违反这条规则的后果显而易见,依赖的组件可能会在依赖项之前释放。

通常,您希望将组件(如应用程序范围的配置容器)注册为Singleton。数据库访问类(如 Entity Framework 上下文)建议使用Scoped,以便可以重复使用连接。但是如果您想并行运行任何东西,请记住 Entity Framework 上下文不能由两个线程共享。如果您需要这样做,最好将上下文注册为Transient,这样每个组件都有自己的上下文实例而且可以并行运行。

服务注册

注册服务是在Startup类的ConfigureServices(IServiceCollection)方法中完成的。

这是一个服务注册的例子:

  1. services.Add(new ServiceDescriptor(typeof(IDataService), typeof(DataService), ServiceLifetime.Transient));

这行代码将DataService添加到服务集合中。服务类型设置为IDataService,因此如果请求了该类型的实例,则它们将获得DataService的实例。生命周期也设置为Transient,这样每次都会创建一个新实例。

ASP.NET Core 提供了很多扩展方法,使注册各种生命周期的服务和其他设置更加方便。

下面是使用扩展方法的更简单的示例:

  1. services.AddTransient<IDataService, DataService>();

是不是更简单一点?封装后它当然更容易调用,这样做更简单。对于不同的生命周期,也有类似的扩展方法,你也许可以猜到它们的名字。

如果愿意,您也可以在使用单一类型注册(实现类型=服务类型):

  1. services.AddTransient<DataService>();

但是呢,当然组件必须取决于具体的类型,所以这可能是不需要的。

实现工厂

在一些特殊情况下,您可能想要接管某些服务的实例化。在这种情况下,您可以在服务描述符上注册一个实现工厂(Implementation Factory)。这有一个例子:

  1. services.AddTransient<IDataService, DataService>((ctx) =>
  2. {
  3. IOtherService svc = ctx.GetService<IOtherService>();
  4. //IOtherService svc = ctx.GetRequiredService<IOtherService>();
  5. return new DataService(svc);
  6. });

它使用另一个组件IOtherService实例化DataService。您可以使用GetService<T>()GetRequiredService<T>()来获取在服务集合中注册的依赖项。

区别在于GetService<T>()如果找不到T类型服务,则返回nullGetRequiredService<T>()如果找不到它,则会引发InvalidOperationException异常。

单例作为常量注册

如果您想自己实例化一个单例,你可以这样做:

  1. services.AddSingleton<IDataService>(new DataService());

它允许一个非常有趣的场景,假设DataService实现两个接口。如果我们这样做:

  1. services.AddSingleton<IDataService, DataService>();
  2. services.AddSingleton<ISomeInterface, DataService>();

我们得到两个实例,两个接口都有一个。如果我们打算共享一个实例,这是一种方法:

  1. var dataService = new DataService();
  2. services.AddSingleton<IDataService>(dataService);
  3. services.AddSingleton<ISomeInterface>(dataService);

如果组件具有依赖关系,则可以从服务集合构建服务提供者并从中获取必要的依赖项:

  1. IServiceProvider provider = services.BuildServiceProvider();
  2. IOtherService otherService = provider.GetRequiredService<IOtherService>();
  3. var dataService = new DataService(otherService);
  4. services.AddSingleton<IDataService>(dataService);
  5. services.AddSingleton<ISomeInterface>(dataService);

请注意,您应该在ConfigureServices的末尾执行此操作,以便在此之前确保已经注册了所有依赖项。

注入

我们已经注册了我们的组件,现在我们就可以实际使用它们了。

在 ASP.NET Core 中注入组件的典型方式是构造函数注入,针对不同的场景确实存在其他选项,但构造器注入允许您定义在没有这些其他组件的情况下此组件不起作用。

举个例子,我们来做一个基本的日志记录中间件组件:

  1. public class LoggingMiddleware
  2. {
  3. private readonly RequestDelegate _next;
  4. public LoggingMiddleware(RequestDelegate next)
  5. {
  6. _next = next;
  7. }
  8. public async Task Invoke(HttpContext ctx)
  9. {
  10. Debug.WriteLine("Request starting");
  11. await _next(ctx);
  12. Debug.WriteLine("Request complete");
  13. }
  14. }

在中间件中注入组件有三种不同的方式:

  • 构造函数
  • Invoke方法参数
  • HttpContext.RequestServices

让我们使用三种全部方式注入我们的组件:

  1. public class LoggingMiddleware
  2. {
  3. private readonly RequestDelegate _next;
  4. private readonly IDataService _svc;
  5. public LoggingMiddleware(RequestDelegate next, IDataService svc)
  6. {
  7. _next = next;
  8. _svc = svc;
  9. }
  10. public async Task Invoke(HttpContext ctx, IDataService svc2)
  11. {
  12. IDataService svc3 = ctx.RequestServices.GetService<IDataService>();
  13. Debug.WriteLine("Request starting");
  14. await _next(ctx);
  15. Debug.WriteLine("Request complete");
  16. }
  17. }

中间件在应用的整个生命周期中仅实例化一次,因此通过构造函数注入的组件对于所有通过的请求都是相同的

作为Invoke方法的参数注入的组件是中间件绝对必需的,如果它找不到要注入的IDataService,它将引发InvalidOperationException异常。

第三个通过使用HttpContext请求上下文的RequestServices属性的GetService<T>()方法来获取可选的依赖项。RequestServices属性的类型是IServiceProvider,因此它与实现工厂中的提供者完全相同。如果您打算要求拿到这个组件,可以使用GetRequiredService<T>()

如果IDataService被注册为Singleton,我们会在它们中获得相同的实例。

如果它被注册为Scopedsvc2svc3将会是同一个实例,但不同的请求会得到不同的实例。

Transient的情况下,它们都是不同的实例。

每种方法的用例:

  • 构造函数:所有请求都需要的单例(Singleton)组件
  • Invoke参数:在请求中总是必须的作用域(Scoped)和瞬时(Transient)组件
  • RequestServices:基于运行时信息可能需要或可能不需要的组件

如果可能的话,我会尽量避免使用RequestServices,并且只在中间件必须能够在缺少某些组件一样可以运行的情况下才使用它。

Startup类

Startup类的构造函数中,您至少可以注入IHostingEnvironmentILoggerFactory。它们是官方文档中提到的仅有两个接口。可能有其他的,但我不知道。

  1. public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory)
  2. {
  3. ...
  4. }

IHostingEnvironment通常用于为应用程序设置配置。您可以使用ILoggerFactory设置日志记录。

Configure方法允许您注入已注册的任何组件。

  1. public void Configure(
  2. IApplicationBuilder app,
  3. IHostingEnvironment env,
  4. ILoggerFactory loggerFactory,
  5. IDataService dataSvc)
  6. {
  7. ...
  8. }

因此,如果在管道配置过程中有需要的组件,您可以在这里简单地要求它们。

如果使用app.Run()/app.Use()/app.UseWhen()/app.Map()在管道上注册简单中间件,则不能使用构造函数注入。事实上,通过ApplicationServices/ RequestServices是获取所需组件的唯一方法。

这里有些例子:

  1. IDataService dataSvc2 = app.ApplicationServices.GetService<IDataService>();
  2. app.Use((ctx, next) =>
  3. {
  4. IDataService svc = ctx.RequestServices.GetService<IDataService>();
  5. return next();
  6. });
  7. app.Map("/test", subApp =>
  8. {
  9. IDataService svc1 = subApp.ApplicationServices.GetService<IDataService>();
  10. subApp.Run((context =>
  11. {
  12. IDataService svc2 = context.RequestServices.GetService<IDataService>();
  13. return context.Response.WriteAsync("Hello!");
  14. }));
  15. });
  16. app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/test2"), subApp =>
  17. {
  18. IDataService svc1 = subApp.ApplicationServices.GetService<IDataService>();
  19. subApp.Run(ctx =>
  20. {
  21. IDataService svc2 = ctx.RequestServices.GetService<IDataService>();
  22. return ctx.Response.WriteAsync("Hello!");
  23. });
  24. });

因此,您可以在配置时通过IApplicationBuilder上的ApplicationServices请求组件,并在请求时通过HttpContext上的RequestServices请求组件。

在MVC Core中注入

在MVC中进行依赖注入的最常见方法是构造函数注入。

您可以在任何地方做到这一点。在控制器中,您有几个选项:

  1. public class HomeController : Controller
  2. {
  3. private readonly IDataService _dataService;
  4. public HomeController(IDataService dataService)
  5. {
  6. _dataService = dataService;
  7. }
  8. [HttpGet]
  9. public IActionResult Index([FromServices] IDataService dataService2)
  10. {
  11. IDataService dataService3 = HttpContext.RequestServices.GetService<IDataService>();
  12. return View();
  13. }
  14. }

如果您希望稍后根据运行时决策获取依赖项,则可以再次使用Controller基类(技术上讲,ControllerBase最好)的HttpContext属性上可用的RequestServices

您也可以通过在特定的 Action 上添加参数,并使用FromServicesAttribute特性对其进行装饰来注入所需的服务,这会指示 MVC Core 从服务集合中获取它,而不是尝试对其进行模型绑定。

Razor 视图

您还可以使用新的关键字@inject在Razor视图中注入组件:

  1. @using Microsoft.AspNetCore.Mvc.Localization
  2. @inject IViewLocalizer Localizer

在这里,我们在_ViewImports.cshtml中注入了一个视图本地化器,因此我们将它作为Localizer在所有视图中提供。

请注意,不应滥用此机制将本应该来自控制器的数据带入视图。

Tag helper

构造函数注入也适用于Tag Helper

  1. [HtmlTargetElement("test")]
  2. public class TestTagHelper : TagHelper
  3. {
  4. private readonly IDataService _dataService;
  5. public TestTagHelper(IDataService dataService)
  6. {
  7. _dataService = dataService;
  8. }
  9. }

视图组件

视图组件也一样:

  1. public class TestViewComponent : ViewComponent
  2. {
  3. private readonly IDataService _dataService;
  4. public TestViewComponent(IDataService dataService)
  5. {
  6. _dataService = dataService;
  7. }
  8. public async Task<IViewComponentResult> InvokeAsync()
  9. {
  10. return View();
  11. }
  12. }

在视图组件中也可以获得HttpContext,因此有权访问RequestServices

过滤器

MVC过滤器也支持构造函数注入,以及有权访问RequestServices

  1. public class TestActionFilter : ActionFilterAttribute
  2. {
  3. private readonly IDataService _dataService;
  4. public TestActionFilter(IDataService dataService)
  5. {
  6. _dataService = dataService;
  7. }
  8. public override void OnActionExecuting(ActionExecutingContext context)
  9. {
  10. Debug.WriteLine("OnActionExecuting");
  11. }
  12. public override void OnActionExecuted(ActionExecutedContext context)
  13. {
  14. Debug.WriteLine("OnActionExecuted");
  15. }
  16. }

但是,通过构造函数注入我们不能像往常一样在控制器上添加特性,因为它在运行的时候必须要获得依赖项。

这里我们有两种方式可以将其添加到控制器或 Action 级别:

  1. [TypeFilter(typeof(TestActionFilter))]
  2. public class HomeController : Controller
  3. {
  4. }
  5. // or
  6. [ServiceFilter(typeof(TestActionFilter))]
  7. public class HomeController : Controller
  8. {
  9. }

以上这两种方式关键的区别是TypeFilterAttribute会先找出过滤器的依赖项并通过DI获取它们,然后创建过滤器。另一方面,ServiceFilterAttribute则是直接尝试从服务集合中寻找过滤器!

所以,为了使[ServiceFilter(typeof(TestActionFilter))]正常工作,我们需要多一点配置:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddTransient<TestActionFilter>();
  4. }

现在ServiceFilterAttribute就可以找到过滤器了。

如果您想添加全局过滤器:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddMvc(mvc =>
  4. {
  5. mvc.Filters.Add(typeof(TestActionFilter));
  6. });
  7. }

这样就不需要将过滤器添加到服务集合,它的工作方式就好像您已经在每个控制器上添加了TypeFilterAttribute一样。

HttpContext

我已经多次提到过HttpContext。如果您想访问控制器/视图/视图组件之外的HttpContext,那怎么办?例如,要访问当前登录用户的声明?

您只要简单地注入IHttpContextAccessor,如下所示:

  1. public class DataService : IDataService
  2. {
  3. private readonly HttpContext _httpContext;
  4. public DataService(IOtherService svc, IHttpContextAccessor contextAccessor)
  5. {
  6. _httpContext = contextAccessor.HttpContext;
  7. }
  8. //...
  9. }

这样可以让您的服务层直接访问HttpContext,而不需要通过调用方法来传递它。

结论

相对于 Ninject 或 Autofac 等较大、较老的DI框架来说,ASP.NET Core提供的依赖注入容器在功能上比较基本,但它仍然非常适合大多数需求。

您可以在任何需要的地方注入组件,从而使组件在此过程中更具可测试性。

链接

[译]ASP.NET Core依赖注入深入讨论的更多相关文章

  1. ASP.NET Core依赖注入——依赖注入最佳实践

    在这篇文章中,我们将深入研究.NET Core和ASP.NET Core MVC中的依赖注入,将介绍几乎所有可能的选项,依赖注入是ASP.Net Core的核心,我将分享在ASP.Net Core应用 ...

  2. ASP.NET Core 依赖注入最佳实践——提示与技巧

    在这篇文章,我将分享一些在ASP.NET Core程序中使用依赖注入的个人经验和建议.这些原则背后的动机如下: 高效地设计服务和它们的依赖. 预防多线程问题. 预防内存泄漏. 预防潜在的BUG. 这篇 ...

  3. ASP.NET Core 依赖注入(构造函数注入,属性注入等)

    原文:ASP.NET Core 依赖注入(构造函数注入,属性注入等) 如果你不熟悉ASP.NET Core依赖注入,先阅读文章: 在ASP.NET Core中使用依赖注入   构造函数注入 构造函数注 ...

  4. # ASP.NET Core依赖注入解读&使用Autofac替代实现

    标签: 依赖注入 Autofac ASPNETCore ASP.NET Core依赖注入解读&使用Autofac替代实现 1. 前言 2. ASP.NET Core 中的DI方式 3. Aut ...

  5. 实现BUG自动检测 - ASP.NET Core依赖注入

    我个人比较懒,能自动做的事绝不手动做,最近在用ASP.NET Core写一个项目,过程中会积累一些方便的工具类或框架,分享出来欢迎大家点评. 如果以后有时间的话,我打算写一个系列的[实现BUG自动检测 ...

  6. asp.net core 依赖注入几种常见情况

    先读一篇注入入门 全面理解 ASP.NET Core 依赖注入, 学习一下基本使用 然后学习一招, 不使用接口规范, 直接写功能类, 一般情况下可以用来做单例. 参考https://www.cnblo ...

  7. 自动化CodeReview - ASP.NET Core依赖注入

    自动化CodeReview系列目录 自动化CodeReview - ASP.NET Core依赖注入 自动化CodeReview - ASP.NET Core请求参数验证 我个人比较懒,能自动做的事绝 ...

  8. ASP.NET Core依赖注入最佳实践,提示&技巧

    分享翻译一篇Abp框架作者(Halil İbrahim Kalkan)关于ASP.NET Core依赖注入的博文. 在本文中,我将分享我在ASP.NET Core应用程序中使用依赖注入的经验和建议. ...

  9. ASP.NET Core依赖注入解读&使用Autofac替代实现【转载】

    ASP.NET Core依赖注入解读&使用Autofac替代实现 1. 前言 2. ASP.NET Core 中的DI方式 3. Autofac实现和自定义实现扩展方法 3.1 安装Autof ...

随机推荐

  1. ACdream 1068

    我没有用二分法,直接构造最小数,既然题目保证答案一定存在那么与上界无关. 如给定S=16,它能构成的最小数为79,尽量用9补位,最高位为S%9.如果构造的数大于下界A,那么直接输出,因为这是S能构成的 ...

  2. 03 Spring的父子容器

    1.概念理解和知识铺垫 在Spring整体框架的核心概念中,容器是核心思想,就是用来管理Bean的整个生命周期的,而在一个项目中,容器不一定只有一个,Spring中可以包括多个容器,而且容器有上下层关 ...

  3. Hive命令及操作

    1.hive表操作 复制表结构 create table denserank_amt like otheravgrank_amt;修改表名 alter table tmp rename to cred ...

  4. Swift 之属性setter、getter方法

    Swift 之属性setter.getter方法 Swift中的属性分为两种属性,一种就是计算型属性 一种就是存储型属性,开始我虽然知道这两种属性,但是了解并不深对于他的setter和getter方法 ...

  5. armlinux下的网路传输(tcp/ip)

    /*ex2serv.c*/ #include <time.h> #include<stddef.h> #include <stdio.h> #include < ...

  6. mysql数据库字符集编码查看以及设置

      show variables like %char% character_set_client     | gb2312                           character_s ...

  7. 列出JDK中常用的Java包

    列出JDK中常用的Java包 1.java.lang 2.java.sql 3.java.io 4.java.math 5.java.text 6.java.net 7.java.util 8.jav ...

  8. python实现简单排序算法

    算法 递归两个特点: 调用自身 有穷调用 计算规模越来越小,直至最后结束 用装饰器修饰一个递归函数时会出现问题,这个问题产生的原因是递归的函数也不停的使用装饰器.解决方法是,只让装饰器调用一次即可,那 ...

  9. google浏览器插件推荐

    http://www.tuicool.com/articles/eQ32Ur http://blog.jobbole.com/1386/ https://www.oschina.net/news/46 ...

  10. 最新的 iOS 申请证书与发布流程

    申请流程. 1. 申请钥匙串文件 进入  (Launchpad),找到   (我的是在其他里面找到的),运行后再左上角 存储在桌面就好了,然后就完成退出钥匙串工具就可以了. 2.申请开发证书,发布证书 ...