.NET-Core Series

看到一篇介绍ASP.NET Core DI的文章,讲的挺好,分享分享。

转载至 https://joonasw.net/view/aspnet-core-di-deep-dive

ASP.NET Core Dependency Injection Deep Dive

In this article we take a deep dive to dependency injection in ASP.NET Core and MVC Core. We will walk through almost every conceivable option for injecting dependencies into your components.

Dependency injection is at the core of ASP.NET Core. It allows the components in your app to have improved testability. It also makes your components only dependent on some component that can provide the needed services.

As an example, here we have an interface and its implementing class:

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

If another service depends on DataService, they are dependent on this particular implementation. Testing a service such as that can be quite a lot more difficult. If instead the service depends on IDataService, they only care about the contract provided by the interface. It doesn't matter what implementation is given. It makes it possible for us to pass in a mock implementation for testing the service's behaviour.

Service lifetime

Before we can talk about how injection is done in practice, it is critical to understand what is service lifetime. When a component requests another component through dependency injection, whether the instance it receives is unique to that instance of the component or not depends on the lifetime. Setting the lifetime thus decides how many times a component is instantiated, and if a component is shared.

There are 3 options for this with the built-in DI container in ASP.NET Core:

  1. Singleton
  2. Scoped
  3. Transient

Singleton means only a single instance will ever be created. That instance is shared between all components that require it. The same instance is thus used always.

Scoped means an instance is created once per scope. A scope is created on every request to the application, thus any components registered as Scoped will be created once per request.

Transient components are created every time they are requested and are never shared.

It is important to understand that if you register component A as a singleton, it cannot depend on components registered with Scoped or Transient lifetime. More generally speaking:

A component cannot depend on components with a lifetime smaller than their own.

The consequences of going against this rule should be obvious, the component being depended on might be disposed before the dependent.

Typically you want to register components such as application-wide configuration containers as Singleton. Database access classes like Entity Framework contexts are recommended to be Scoped, so the connection can be re-used. Though if you want to run anything in parallel, keep in mind Entity Framework contexts cannot be shared by two threads. If you need that, it is better to register the context as Transient. Then each component gets their own context instance and can run in parallel.

Service registration

Registering services is done in the ConfigureServices(IServiceCollection) method in your Startup class.

Here is an example of a service registration:

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

That line of code adds DataService to the service collection. The service type is set to IDataService so if an instance of that type is requested, they get an instance of DataService. The lifetime is also set to Transient, so a new instance is created every time.

ASP.NET Core provides various extension methods to make registering services with various lifetimes and other settings easier.

Here is the earlier example using an extension method:

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

Little bit easier right? Under the covers it calls the earlier of course, but this is just easier. There are similar extension methods for the different lifetimes with names you can probably guess.

If you want, you can also register on a single type (implementation type = service type):

  1. services.AddTransient<DataService>();

But then of course the components must depend on the concrete type, which may be unwanted.

Implementation factories

In some special cases, you may want to take over the instantiation of some service. In this case, you can register an implementation factoryon the service descriptor. Here is an example:

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

It instantiates DataService using another component IOtherService. You can get dependencies registered in the service collection with GetService<T>() or GetRequiredService<T>().

The difference is that GetService<T>() returns null if it can't find the service. GetRequiredService<T>() throws an InvalidOperationException if it can't find it.

Singletons registered as constant

If you want to instantiate a singleton yourself, you can do this:

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

It allows for one very interesting scenario. Say DataService implements two interfaces. If we do this:

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

We get two instances. One for both interfaces. If we want to share an instance, this is one way to do it:

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

If the component has dependencies, you can build the service provider from the service collection and get the necessary dependencies from it:

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

Note you should do this at the end of ConfigureServices so you have surely registered all dependencies before this.

Generic services

Services that use generics are a slight special case.

Say we have the following interface:

  1. public interface IDataService<TSomeClass> where TSomeClass : class
  2. {
  3. }

Registering it depends on the way you implement the interface.

If you define implementations for specific types like:

  1. public class SomeClassDataService : IDataService<SomeClass>
  2. {
  3. }

Then you must register each implementation explicitly:

  1. services.AddTransient<IDataService<SomeClass>, SomeClassDataService>();

However, if your implementation is also generic:

  1. public class DataService<TSomeClass> : IDataService<TSomeClass> where TSomeClass : class
  2. {
  3. }

Then you can register it once:

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

Note the usage of overload taking Types. We can't use the generic version with open generic types.

After doing the registration either way, your other components can now depend on e.g. IDataService<Employee>.

Injection

Now that we have registered our components, we can move to actually using them.

The typical way in which components are injected in ASP.NET Core is constructor injection. Other options do exist for different scenarios, but constructor injection allows you to define that this component will not work without these other components.

As an example, let's make a basic logging middleware component:

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

There are three different ways of injecting components in middleware:

  1. Constructor
  2. Invoke parameter
  3. HttpContext.RequestServices

Let's inject our component using all three:

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

The middleware is instantiated only once during the app lifecycle, so the component injected through the constructor is the same for all requests that pass through.

The component injected as a parameter for Invoke is absolutely required by the middleware, and it will throw an InvalidOperationException if it can't find an IDataService to inject.

The third one uses the RequestServices property on the HttpContext to get an optional dependency using GetService<T>(). The property is of type IServiceProvider, so it works exactly the same as the provider in implementation factories. If you want to require the component, you can use GetRequiredService<T>().

If IDataService was registered as singleton, we get the same instance in all of them.

If it was registered as scoped, svc2 and svc3 will be the same instance, but different requests get different instances.

In the case of transient, all of them are always different instances.

Use cases for each approach:

  1. Constructor: Singleton components that are needed for all requests
  2. Invoke parameter: Scoped and transient components that are always necessary on requests
  3. RequestServices: Components that may or may not be needed based on runtime information

I would try to avoid using RequestServices if possible, and only use it when the middleware must be able to function without some component as well.

Startup class

In the constructor of the Startup class, you can at least inject IHostingEnvironment and ILoggerFactory. They are the only two interfaces mentioned in the official documentation. There may be others, but I am not aware of them.

In 2.0, IConfiguration is also available here.

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

The IHostingEnvironment is used typically to setup configuration for the application. With the ILoggerFactory you can setup logging.

The Configure method allows you to inject any components that have been registered.

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

So if there are components that you need during the pipeline configuration, you can simply require them there.

If you use app.Run()/app.Use()/app.UseWhen()/app.Map() to register simple middleware on the pipeline, you cannot use constructor injection. Actually the only way to get the components you need is through ApplicationServices/RequestServices.

Here are some examples:

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

So you can request components at configuration time through ApplicationServices on the IApplicationBuilder, and at request time through RequestServices on the HttpContext.

Injection in MVC Core

The most common way for doing dependency injection in MVC is constructor injection.

You can do that pretty much anywhere. In controllers you have a few options:

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

If you wish to get dependencies later based on runtime decisions, you can once again use RequestServices available on the HttpContext property of the Controller base class (well, ControllerBase technically).

You can also inject services required by specific actions by adding them as parameters and decorating them with the FromServicesAttribute. This instructs MVC Core to get it from the service collection instead of trying to do model binding on it.

Razor views

You can also inject components in Razor views with the new @inject keyword:

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

Here we inject a view localizer in _ViewImports.cshtml so we have it available in all views as Localizer.

You should not abuse this mechanism to bring data to views that should come from the controller.

Tag helpers

Constructor injection also works in tag helpers:

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

View components

Same with view components:

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

View components also have the HttpContext available, and thus have access to RequestServices.

Filters

MVC filters also support constructor injection, as well as having access to 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. }

However, we can't add the attribute as usual on a controller since it has to get dependencies at runtime.

We have these two options for adding it on controller- or action level:

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

The key difference is that TypeFilterAttribute will figure out what are the filters dependencies, fetches them through DI, and creates the filter. ServiceFilterAttribute on the other hand attempts to find the filter from the service collection!

To make [ServiceFilter(typeof(TestActionFilter))] work, we need a bit more configuration:

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

Now ServiceFilterAttribute can find the filter.

If you wanted to add the filter globally:

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

There is no need to add the filter to the service collection this time, it works as if you had added a TypeFilterAttribute on every controller.

HttpContext

I've mentioned HttpContext multiple times now. What about if you want to access HttpContext outside of a controller/view/view component? To access the currently signed-in user's claims for example?

You can simply inject IHttpContextAccessor, like here:

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

This allows your service layer access to HttpContext without requiring you to pass it through every method call.

Conclusions

Even though the dependency injection container provided with ASP.NET Core is relatively basic in its features when compared against the bigger, older DI frameworks like Ninject or Autofac, it is still really good for most needs.

You can inject components where ever you might need them, making the components more testable in the process as well.

I wish this article answers most questions you may have about DI in ASP.NET Core. If it doesn't, feel free to shoot a comment below or contact me on Twitter.

Links

DI in ASP.NET Core的更多相关文章

  1. ASP.NET Core中的依赖注入(2):依赖注入(DI)

    IoC主要体现了这样一种设计思想:通过将一组通用流程的控制从应用转移到框架之中以实现对流程的复用,同时采用"好莱坞原则"是应用程序以被动的方式实现对流程的定制.我们可以采用若干设计 ...

  2. 浅谈ASP.NET Core中的DI

    DI的一些事 传送门马丁大叔的文章 什么是依赖注入(DI: Dependency Injection)?     依赖注入(DI)是一种面向对象的软件设计模式,主要是帮助开发人员开发出松耦合的应用程序 ...

  3. ASP.NET Core中的依赖注入(1):控制反转(IoC)

    ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化&qu ...

  4. ASP.NET Core中的依赖注入(3): 服务的注册与提供

    在采用了依赖注入的应用中,我们总是直接利用DI容器直接获取所需的服务实例,换句话说,DI容器起到了一个服务提供者的角色,它能够根据我们提供的服务描述信息提供一个可用的服务对象.ASP.NET Core ...

  5. ASP.NET Core中的依赖注入(4): 构造函数的选择与服务生命周期管理

    ServiceProvider最终提供的服务实例都是根据对应的ServiceDescriptor创建的,对于一个具体的ServiceDescriptor对象来说,如果它的ImplementationI ...

  6. ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【总体设计 】

    本系列前面的文章我们主要以编程的角度对ASP.NET Core的依赖注入系统进行了详细的介绍,如果读者朋友们对这些内容具有深刻的理解,我相信你们已经可以正确是使用这些与依赖注入相关的API了.如果你还 ...

  7. ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【解读ServiceCallSite 】

    通过上一篇的介绍我们应该对实现在ServiceProvider的总体设计有了一个大致的了解,但是我们刻意回避一个重要的话题,即服务实例最终究竟是采用何种方式提供出来的.ServiceProvider最 ...

  8. ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【补充漏掉的细节】

    到目前为止,我们定义的ServiceProvider已经实现了基本的服务提供和回收功能,但是依然漏掉了一些必需的细节特性.这些特性包括如何针对IServiceProvider接口提供一个Service ...

  9. 全面理解 ASP.NET Core 依赖注入

    DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工作六七年的同事还个东西搞不清楚.另外再介绍一下.NET  Core的DI实现以及对实例 ...

随机推荐

  1. 在Windows的Dos命令中切换盘符

    在Windows的Dos命令中切换盘符... ---------------------------- --------------------------------------- -------- ...

  2. Java6和Java8在Windows上共存

    0x00 需求 最近在做一个Android的项目,一开始安装的是Java8用于项目的开发.但是在项目后期需要用到drozer用于检测项目的安全性,要搭建drozer的测试环境必须要使用Java6,否则 ...

  3. open文件操作之mode模式剖析

    Python可以使用open函数来实现文件的打开,关闭,读写操作: Python3中的open函数定义为: open(file, mode='r', buffering=None, encoding= ...

  4. Hadoop 如何退出安全模式

    在name node 上运行如下命令 hadoop dfsadmin -safemode leave

  5. java-生产者消费者模式

    经常会有公司叫我们手撕代码,比如网易,阿里,那我们是不是该掌握下呢.下面这段代码来自<现代操作系统>进程与线程P49页. public class ProducerConsumer { p ...

  6. 优化关键渲染路径CRP

    什么是关键渲染路径? 从收到 HTML.CSS 和 JavaScript 字节到对其进行必需的处理,从而将它们转变成渲染的像素这一过程中有一些中间步骤 浏览器渲染页面前需要先构建 DOM 和 CSSO ...

  7. 理解 angular 的路由功能

    相信很多人使用angular 都是因为他路由功能而用的 深入理解ANGULARUI路由_UI-ROUTER 最近在用 ionic写个webapp 看到几个demo中路由有好几种,搞的有点晕,查下资料研 ...

  8. Android基础知识大全(精品)

    [1].ProgressBar   <ProgressBar android:id="@+id/progress_bar" android:layout_width=&quo ...

  9. 详解session

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp30 一.术语session 在我的经验里,session这个词被滥用的程度 ...

  10. Spring事务管理(一)

    对于Spring相信很多做web开发的小活动一定不陌生,Spring中我们经常谈到的就是IOC和AOP,但是对于Spring的事务管理,相信大家一定也很感兴趣,今天我们就探讨一下Spring中的事务管 ...