ASP.NET Core Pitfalls – Dependency Injection Lifetime Validation
As you can imagine, it doesn’t make much sense to have a service that is registered as singleton to depend on another service that is registered as scoped, because the singleton instantiation will only happen once. Take this example:
public interface IScopedService
{ } public interface ISingletonService
{
} public class SingletonService : ISingletonService
{ public SingletonService(IScopedService svc)
{
}
}
And this registration:
services.AddSingleton<ISingletonService, SingletonService>();
services.AddScoped<IScopedService, ScopedService>();
The actual implementation does not matter here. What is important is that you will only see this problem when you try to instantiate the ISingletonService, normally through constructor injection.
One way to prevent this problem is, of course, to take care with the services registration. Another one is to have ASP.NET Core validate the registrations for us, when building the service provider. This can be achieved on bootstrap, before the Startup class is instantiated, when the application starts:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
options.ValidateOnBuild = true;
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
Notice the ValidateScopes and ValidateOnBuild properties. The first will prevent a singleton service from taking a scoped service dependency, and the second actually does the validation upon startup. To be clear, one can be used without the other.
When ValidateScopes is enabled, you won’t be able to inject a singleton service that takes as its dependency a scoped one, the application will crash; with ValidateOnBuild you actually specify when it will crash: if set to true, it will crash at startup, otherwise, it will only crash when the injection occurs.
If you really need to instantiate a scoped service from a singleton, you need to use code such as this:
IServiceScopeFactory scopedFactory = … //obtained from dependency injection
using (var scope = scopedFactory.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
//if IScopedService implements IDisposable, it will be disposed at the end of the scope
}
Note that this will not validate open generic types, as stated in the documentation for ValidateOnBuild.
Finally, using the ApplicationServices collection of IApplicationBuilder, in the Configure method, you can only retrieve transient or singleton services, not scoped: the reason for this is that when Configure executes there is not yet a scope. This will fail:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ var scoped = app.ApplicationServices.GetRequiredService<IScopedService>();
//rest goes here
}
Interestingly, you can inject a scoped service as a parameter to Configure, e.g., this will not crash:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IScopedService svs)
{
//rest goes here
}