DI in ASP.NET Core

时间:2023-01-20 17:49:26

.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