Retrieving Services from Dependency Injection in .NET
Introduction
Dependency Injection (DI) is a critical part of .NET since the .NET Core days. A simple, although powerful, DI container is included out of the box, and ASP.NET Core makes heavy use of it.
I already wrote about DI in .NET a few times:
- .NET 8 Dependency Injection Changes: Keyed Services
- ASP.NET Core Inversion of Control and Dependency Injection
- The Evolution of .NET Dependency Resolution
- Dependency Injection Lifetime Validation
- .NET Core Service Provider Gotchas and Less-Known Features
- An Extended Service Provider for .NET
This time I want to highlight something that people may not be aware of, even though I already mentioned it in one of my posts.
Retrieving Services
As you know, the DI is represented by an instance of IServiceProvider. When we want to retrieve a service we call GetService, passing it a Type, for a dynamic call, or GetService<T>, for a strongly-typed version, which just calls the other one with a cast.
Now, if the service represented by the Type or the T generic template parameter is not registered, GetService will return null. If we want to force the service to exist or fail, we call GetRequiredService<T>, which will throw an exception if the service is not registered. If we don't know for sure, GetService or GetService<T> are safer.
As you may know, we can register multiple implementations, even with different lifetimes, for the same service. If we want to retrieve all registrations that possibly exist - even if it's none - we can call GetServices<T>, which is the same as calling GetService<IEnumerable<T>>. This will return an enumeration of all registrations, which can possibly be empty, but won't throw.
Constructor Injection
One thing that you need to be careful is: when injecting a service into a class' constructor, that service needs to be registered, which means, it is treated as if GetRequiredService<T> is used, dependencies must always be registered. For example:
public class ExternalClass(InternalClass obj) { ... }
...
services.AddSingleton<ExternalClass>(); //InternalClass is not registered
...
serviceProvider.GetService<ExternalClass>(); //throws an exception
serviceProvider.GetServices<InternalClass>(); //returns an empty array of InternalClass
In general, I recommend constructor injection instead of retrieving services from the DI - the service locator approach, which some people think is an anti-pattern, not my opinion, though, I think there may be some valid use cases for it.
Scopes
Another thing that is probably obvious is, you cannot inject a service defined as scoped into another registered as singleton, even indirectly, from transient instances. In general, when you need a scoped service outside of a scope, you need first to create a scope and dispose of it at the end of the constructor or method:
using var scope = serviceProvider.CreateScope();
...
scope.ServiceProvider.GetRequiredService<SomeScopedService>();
CreateScope/CreateAsyncScope are actually just extension methods that retrieve an IServiceScopeFactory instance from DI and call its CreateScope method, creating a new IServiceScope. Because there is no way to know if the service provider you are using is already inside a scope, it's better to create one when in doubt. All services registered as scoped will only be instantiated once and cached for the same scope. Don't forget to dispose of the scope when you no longer need it. ASP.NET Core automatically creates a scope for each request and disposes of it at the end. All disposable services instantiated from a scope will too be disposed of with the scope.
Validation
The .NET DI container can validate the lifetime dependencies at construction or upon request, if you pass the appropriate values:
var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true });
ServiceProviderOptions has two properties, ValidateScopes, which does the validation of scope dependencies, and ValidateOnBuild which does it when the service provider (the IServiceProvider instance) is actually built (BuildServiceProvider), rather than when a service is requested. The defaults for these properties are both true if running in development environment, and false otherwise.
Conclusion
As always, happy to hear your thoughts on this topic!
Comments
Post a Comment