.NET-Core Series
- Server in ASP.NET-Core
- DI in ASP.NET-Core
- Routing in ASP.NET-Core
- Error Handling in ASP.NET-Core
- WebSocket in ASP.NET-Core(一)
- WebSocket in ASP.NET-Core(二)
- To be Continue...
看到一篇介绍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:
- Singleton
- Scoped
- 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:
- Constructor
- Invoke parameter
- 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:
- Constructor: Singleton components that are needed for all requests
- Invoke parameter: Scoped and transient components that are always necessary on requests
- 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.