Note
In Blazor Server applications, multiple threads could be used to handle requests from the same client connection. This can cause problems when using a service that is not thread-safe, such as the DbContext.
IServiceCollection is a container for services.
IServiceProvider is built from the IServiceCollection and is used to inject the services.
There are actually 2 different IServiceProvider instances. One is called the root service provider, which is created when the application starts and is used to resolve singleton services. The other is called the scoped service provider, which is created for each request scope and is used to resolve scoped services. If a child provider is asked for a Singleton or a Transient services, it delegates the request to the root provider.
A scope is created with using var scope = rootServiceProvider.CreateScope(). You can resolve services from the scoped service provider by scope.ServiceProvider.GetRequiredService<T>(). Note that you must use using var to dispose of the scope when you are done with it.
ASP.NET Core API applications behind the scenes create a new scope for each incoming HTTP request.
Quiz
What are the three main service lifetimes in C# and how do they differ?
- Transient: A new instance is created each time the service is requested.
- Scoped: An instance is created once per request scope.
- Singleton: Only one instance of the service is created the first time it is requested, and it is shared throughout the application’s lifetime.
What is the "Scope" of a service for ASP.NET Core API and Blazor Server applications?
- In an ASP.NET Core API application, the scope is typically per HTTP request. This means that a new instance of a scoped service is created for each incoming HTTP request and is shared within that request.
- In a Blazor Server application, the scope is per connection. This means that a new instance of a scoped service is created for each client connection and is shared within that connection.
When you call builder.Services.AddDbContext in ASP.NET Core, what is the default service lifetime for the DbContext?
- The default service lifetime for the DbContext is Scoped. This means that a new instance of the DbContext is created for each request scope (typically per HTTP request in an API application or per client connection in a Blazor Server application).
Why should we use the DbContextFactory instead of directly injecting the DbContext for Blazor Server applications? And what are some things that you should be aware of when using it?
- In Blazor Server applications, the scope is per connection, which means that if you inject the
DbContextdirectly, it will be shared across all requests from that connection. TheDbContextis not thread-safe, and is designed to be short-lived. If you do try to use the sameDbContextinstance, you may encounter anInvalidOperationException. - When using the
DbContextFactory, you should be aware that theDbContextFactoryitself is a singleton service and managed by the DI container, however theDbContextinstances it creates are not managed by the DI container. Therefore, you should useusing varto dispose of them.
What is the golden rule for service scope validation?
- The golden rule for service scope validation is: A service can only depend on services with a lifetime that is equal to or longer than its own lifetime. This means that:
- Transient services can depend on transient, scoped, or singleton services.
- Scoped services can depend on scoped or singleton services, but not transient services.
- Singleton services can only depend on singleton services.
- For example, imagine you have a singleton service that requires
DbContext(which is scoped) in its constructor. The DI container will inject theDbContextinstance into the singleton service when it is created. However, since the singleton service is created only once and shared throughout the application’s lifetime, it will hold onto that sameDbContextinstance. This can lead to critical issues.
Why is injecting IServiceProvider directly not recommended?
- Injecting
IServiceProviderdirectly is not recommended because it hides your true dependency. It also makes unit testing difficult. Lastly, if you forget to register a service, you won’t get an error until you try to resolve it at runtime, which can lead to runtime errors.
When you inject IServiceProvider in a constructor, which kind of IServiceProvider are you getting?
- It dependes on the service that is requesting the
IServiceProvider. If a singleton service is requesting theIServiceProvider, it will get the root service provider. If a scoped service is requesting theIServiceProvider, it will get the scoped service provider.
Are background services singletons? Why should they be?
- Background services should be singletons because they are designed to run for the entire lifetime of the application. Imagine they are
Transient, you have no way to stop them gracefully when the application shuts down. - Note that the
AddHostedService()extension method is just a thin wrapper aroundAddSingleton().
When should you use IServiceScopeFactory? What problem does it solve?
- You cannot use
Scopedservices in aSingletonservice. However, sometimes you may want to useScopedservices in aSingletonservice. For example, you may want to useDbContextin aSingletonservice. In this case, you can useIServiceScopeFactoryto create a new scope and resolve theDbContextfrom that scope. - Use
using var scope = _serviceScopeFactory.CreateScope()andscope.ServiceProvider.GetRequiredService<DbContext>()to resolve theDbContextfrom the new scope. Note that theIServiceScopeFactoryitself is a singleton service and managed by the DI container, however you must dispose of the scope when you are done with the services resolved from it.
Exercise
Run the following code and check if the output is what you expected.