There are times we need to resolve services during the start-up phase of our application. We’ll take a quick look at many of the possible approaches available to a developer building an ASP.NET Core application.
Why Resolve A Service In ASP.NET Core Startup?
It’s no secret that the latest ASP.NET infrastructure is heavily dependent on dependency injection, with the concept a core pillar of the latest pipeline. When working with dependency injection, we will configure our services once at the start of an application’s lifetime and then call upon the ASP.NET Core framework to instantiate our dependencies. In my opinion, the set-it once and use everywhere is a great advantage to using DI. It’s only natural to want to reuse configuration and setup as much as possible.
Resolving Services In ConfigureServices
The first location we may want to resolve a service is within our ConfigureServices
method. Reasons to do this might be to initialize and register a singleton instance with the application’s services collection.
I do not recommend resolving any services within the ConfigureServices
method. As you’ll see, there are several caveats and expenses in doing so..
In ConfigureServices
, we can build an instance of our service provider so far.
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<Dependency>();
var provider = services.BuildServiceProvider();
var dependency = provider.GetRequiredService<Dependency>();
Console.WriteLine(dependency.Hello);
}
The operative word here is so far. The following code will fail.
public void ConfigureServices(IServiceCollection services)
{
var provider = services.BuildServiceProvider();
// we haven't registered Dependency yet
var dependency = provider.GetRequiredService<Dependency>();
services.AddScoped<Dependency>();
Console.WriteLine(dependency.Hello);
}
The above code fails because we haven’t yet registered our Dependency
class, yet we are attempting to resolve it.
Resolving Services In Configure
If folks want to run some code at startup, it’s best to do that in the Configure
method, as at this point, we’ve registered all of our services. We can perform tasks like calling a remote service, running database migrations, or logging a message. Be aware that any blocking operation may slow your application’s startup time, and any failure will prevent your application from starting at all.
The Best Method
Easily the best approach is to add our dependency to the list of parameters of the Configure
method. ASP.NET Core will resolve our type from the service collection.
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
// our dependency
Dependency dependency
)
{
Console.WriteLine(dependency.Hello);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); });
});
}
The advantage of this approach is that we can even resolve scoped dependencies, as in this case, the scope is the Configure
method.
The “Works But Limited” Way
Another approach, which I do not recommend, is to call the ApplicationServices
property on the IApplicationBuilder
interface. The IApplicationBuilder
should be a parameter by default to all unmodified calls to the Configure
method in Startup
.
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env
)
{
// cannot call Scoped dependencies
var dependency = app
.ApplicationServices
.GetRequiredService<Dependency>();
Console.WriteLine(dependency.Hello);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); });
});
}
This approach only works for services registered as Singleton
and Transient
. Attempting to resolve Scoped
services will result in the following exception. ASP.NET Core will throw this exception in an attempt to avoid potential captured dependencies.
System.InvalidOperationException: Cannot resolve scoped service 'WebApplication7.Dependency' from root provider.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.Microsoft.Extensions.DependencyInjection.ServiceLookup.IServiceProviderEngineCallback.OnResolve(Type serviceType, IServiceScope scope)
While outside the scope of this post, let’s quickly discuss captured dependencies. Captured dependencies are when a Singleton
service is holding onto a Scoped
service. Captured dependencies are especially dangerous as the first instance of a Scoped
service may have information regarding the first user to request our application. It’s imperative you do not use ANY service injected into Configure as part of your pipeline registration code.
Conclusion
There we have it, several ways to resolve services within our Startup
class. As other authors have pointed out, there are many ways to solve this particular problem. Some are more straightforward than others. We must consider the cost of resolving our services in Startup
and its impact on startup performance. Like many things, it all depends.
I hope you found this post enlightening, and please let me know if you have other ways of resolving types in Startup
.