In this post I’m going to talk about a few gotchas with the .NET Core’s built-in inversion of control (IoC) / service provider (SP)/dependency injection (DI) library. It is made available as the Microsoft.Extensions.DependencyInjection NuGet package.
I wrote another post some time ago, but this one supersedes it, in many ways.
The single method exposed by the IServiceProvider interface, GetService, is not strongly typed. If you add a using statement for Microsoft.Extensions.DependencyInjection, you’ll get a few ones that are:
- GetRequiredService<T>: tries to retrieve a service that is registered under the type of the generic template parameter and throws an exception if one cannot be found; if it is, it is cast to the template parameter;
- GetService<T>: retrieves a service and casts it to the template parameter; if no service is found, null is returned;
- GetServices<T>: returns all services registered as the template parameter type, cast appropriately.
Using a Different Service Provider
You are not forced to use the built-in service provider; you can use anyone you like, as long as it exposes an IServiceProvider implementation. You just need to return this implementation from the ConfigureServices method, which normally does not return anything:
Why would you want to do that, you may ask? Well, there are service providers out there that offer much more interesting features than Microsoft’s (for example, more lifetimes), and this has a reason: Microsoft kept his simple on purpose.
You may not have realized that you can register any number of implementations for a given service, even with different lifetimes:
So, what happens when you ask for an implementation for IService? Well, you get the last one registered, in this case, ServiceB. However, you can ask for all the implementations, if you call GetServices<T>.
You can specify how a service implementation is constructed when you register a service, and it can depend upon other services that are also registered in the service provider:
Don’t worry about registration order: IOtherService will only be required once IService is retrieved.
You can create nested scopes at any time and retrieve services from them. If you are using the extension methods in the Microsoft.Extensions.DependencyInjection namespace, it’s as easy as this:
Why is this needed? Because of lifetime dependencies: using this approach you can instantiate a service marked as a singleton that takes as a parameter a scoped one, inside a scope.
All services instantiated using the Scoped or Transient lifetimes that implement the IDisposable interface will have their Dispose methods called at the end of the request – or the nested scope (when it is disposed). The root service provider is only disposed with the app itself.
The built-in service provider validates the registrations so that a singleton does not depend on a scoped registration. This has the effect of preventing retrieving services in the Configure method, through IApplicationBuilder.ApplicationServices, that are not transient or singletons.
If, however, you think you know what you’re doing, you can bypass this validation:
As I said before, the other alternative is creating a scope and instantiating your singleton service inside the scope. This will always work.
ASP.NET Core only supports constructor:
inheritance, but not property, in controllers and Razor Pages. You can achieve that through actions or conventions. Another option is to use the Service Locator pattern.
You can retrieve any registered services from HttpContext.RequestServices, so whenever you have a reference to an HttpContext, you’re good. From the Configure method, you can also retrieve services from IApplicationBuilder.ApplicationServices, but not scoped ones (read the previous topics). However, it is generally accepted that you should prefer constructor or parameter injection over the Service Locator approach.
Although the service provider that comes with .NET Core is OK for most scenarios, it is clearly insufficient for a number of others. These include:
- Other lifetimes, such as, per resolve-context, per thread, etc;
- Property injection;
- Lazy<T> support;
- Named registrations;
- Automatic discovery and configuration of services;
- Child containers.
You should consider a more featured DI library, and there are many out there, if you need any of these.