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

public interface IDataService
{
IList<DataClass> GetAll();
} public class DataService : IDataService
{
public IList<DataClass> GetAll()
{
//Get data...
return data;
}
}

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:

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:

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

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:

services.AddTransient<IDataService, DataService>((ctx) =>
{
IOtherService svc = ctx.GetService<IOtherService>();
//IOtherService svc = ctx.GetRequiredService<IOtherService>();
return new DataService(svc);
});

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:

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

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

services.AddSingleton<IDataService, DataService>();
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:

var dataService = new DataService();
services.AddSingleton<IDataService>(dataService);
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:

IServiceProvider provider = services.BuildServiceProvider();

IOtherService otherService = provider.GetRequiredService<IOtherService>();

var dataService = new DataService(otherService);
services.AddSingleton<IDataService>(dataService);
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:

public interface IDataService<TSomeClass> where TSomeClass : class
{
}

Registering it depends on the way you implement the interface.

If you define implementations for specific types like:

public class SomeClassDataService : IDataService<SomeClass>
{
}

Then you must register each implementation explicitly:

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

However, if your implementation is also generic:

public class DataService<TSomeClass> : IDataService<TSomeClass> where TSomeClass : class
{
}

Then you can register it once:

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:

public class LoggingMiddleware
{
private readonly RequestDelegate _next; public LoggingMiddleware(RequestDelegate next)
{
_next = next;
} public async Task Invoke(HttpContext ctx)
{
Debug.WriteLine("Request starting");
await _next(ctx);
Debug.WriteLine("Request complete");
}
}

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:

public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly IDataService _svc; public LoggingMiddleware(RequestDelegate next, IDataService svc)
{
_next = next;
_svc = svc;
} public async Task Invoke(HttpContext ctx, IDataService svc2)
{
IDataService svc3 = ctx.RequestServices.GetService<IDataService>();
Debug.WriteLine("Request starting");
await _next(ctx);
Debug.WriteLine("Request complete");
}
}

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.

public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory)
{
}

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.

public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IDataService dataSvc)
{
}

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:

IDataService dataSvc2 = app.ApplicationServices.GetService<IDataService>();
app.Use((ctx, next) =>
{
IDataService svc = ctx.RequestServices.GetService<IDataService>();
return next();
});
app.Map("/test", subApp =>
{
IDataService svc1 = subApp.ApplicationServices.GetService<IDataService>();
subApp.Run((context =>
{
IDataService svc2 = context.RequestServices.GetService<IDataService>();
return context.Response.WriteAsync("Hello!");
}));
});
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/test2"), subApp =>
{
IDataService svc1 = subApp.ApplicationServices.GetService<IDataService>();
subApp.Run(ctx =>
{
IDataService svc2 = ctx.RequestServices.GetService<IDataService>();
return ctx.Response.WriteAsync("Hello!");
});
});

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:

public class HomeController : Controller
{
private readonly IDataService _dataService; public HomeController(IDataService dataService)
{
_dataService = dataService;
} [HttpGet]
public IActionResult Index([FromServices] IDataService dataService2)
{
IDataService dataService3 = HttpContext.RequestServices.GetService<IDataService>();
return View();
}
}

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:

@using Microsoft.AspNetCore.Mvc.Localization
@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:

[HtmlTargetElement("test")]
public class TestTagHelper : TagHelper
{
private readonly IDataService _dataService; public TestTagHelper(IDataService dataService)
{
_dataService = dataService;
}
}

View components

Same with view components:

public class TestViewComponent : ViewComponent
{
private readonly IDataService _dataService; public TestViewComponent(IDataService dataService)
{
_dataService = dataService;
} public async Task<IViewComponentResult> InvokeAsync()
{
return View();
}
}

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:

public class TestActionFilter : ActionFilterAttribute
{
private readonly IDataService _dataService; public TestActionFilter(IDataService dataService)
{
_dataService = dataService;
} public override void OnActionExecuting(ActionExecutingContext context)
{
Debug.WriteLine("OnActionExecuting");
} public override void OnActionExecuted(ActionExecutedContext context)
{
Debug.WriteLine("OnActionExecuted");
}
}

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:

[TypeFilter(typeof(TestActionFilter))]
public class HomeController : Controller
{
}
// or
[ServiceFilter(typeof(TestActionFilter))]
public class HomeController : Controller
{
}

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:

public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<TestActionFilter>();
}

Now ServiceFilterAttribute can find the filter.

If you wanted to add the filter globally:

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(mvc =>
{
mvc.Filters.Add(typeof(TestActionFilter));
});
}

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:

public class DataService : IDataService
{
private readonly HttpContext _httpContext; public DataService(IOtherService svc, IHttpContextAccessor contextAccessor)
{
_httpContext = contextAccessor.HttpContext;
}
//...
}

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. Notification弹出实现

    Notification的几种基本使用方法,大家肯定都已经烂熟于心,我也不必多说.给一个链接:https://zhuanlan.zhihu.com/p/25841482 接下来我想说的是android ...

  2. vue-cli脚手架npm相关文件解读(5)vue-loader.conf.js

    系列文章传送门: 1.build/webpack.base.conf.js 2.build/webpack.prod.conf.js 3.build/webpack.dev.conf.js 4.bui ...

  3. python的引用计数分析(二)

    python所有对象引用计数被减少1的情况: 一.对象的别名被赋予新的对象; a = 23345455 # 增加了一个引用 b = a # 增加了一个引用 print(sys.getrefcount( ...

  4. [读书笔记] 四、SpringBoot中使用JPA 进行快速CRUD操作

    通过Spring提供的JPA Hibernate实现,进行快速CRUD操作的一个栗子~. 视图用到了SpringBoot推荐的thymeleaf来解析,数据库使用的Mysql,代码详细我会贴在下面文章 ...

  5. 大手册(书籍)排版利器-XML自动排版生成工具

    --支持全球化/多语言/符合W3C标准的XML自动排版工具 Boxth XML/XSL Formatter是专为XML数据或其他结构化数据源自动输出排版文件(如: PDF等)而设计的集数据格式化.版式 ...

  6. 【详细资料】ICN6211:MIPI DSI转RGB芯片简介

    ICN6211功能MIPI DSI转RGB,分辨率1920*1200,封装QFN48

  7. C++ Concept 和Java 接口

    C++ Concept 和Java 接口 Concept及接口 我会用Java写个case来解释什么是C++的Concept.Concept可以理解为接口,它是一种广义的接口.不同于Java的Inte ...

  8. 重写equals就必须重写hashCode的原理分析

    因为最近在整理Java集合的源码, 所以今天再来谈谈这个古老的话题,因为后面讲HashMap会用到这个知识点, 所以重新梳理下. 如果不被重写(原生Object)的hashCode和equals是什么 ...

  9. html5 canvas元素使用(一)

    html5新增了一个canvas元素,用于在网页上生成一块矩形区域,用于绘制图像,功能非常强大,下面我来简单介绍一下 在页面中添加canvas <canvas id="canvasDe ...

  10. VMware bridge 桥接方式连接internet

    经过反复测试,关于VMware内虚拟机(包括ubuntu linux和windows)连接internet 目前的结论是 使用bridge方式时,VMware相当于一个交换机(switch),虚拟机和 ...