What is different in the first place?
The difference lies in how ASP.NET Core and Blazor use the dependency injection (DI) systems concept of “scopes” in the container. Scopes are used to control the lifetime of objects retrieved from the DI container. The default DI system from Microsoft in ASP.NET Core knows three different lifetime configurations. These are singleton, transient, and scoped.
Singleton
The first one is the singleton, which is pretty straightforward: When a type is requested from the container for the very first time, it creates a new instance and returns it. After that, whenever something else requests this exact type, it will receive the very same instance of this type. So whatever is registered as a singleton needs to make sure that accessing it is thread-safe, and the developer needs to know that data kept on this instance is the same for all other parts of the application. So for multi-user applications – as ASP.NET Core server-side applications usually are – you should not keep any information in a singleton that belongs to a certain user or session.
Transient
The second lifetime that the DI system supports is transient. Whenever something requests a type registered as transient, it will receive a new instance of that type. This is especially important for types that are intended to be only used for a very short time and discarded afterward. A good example would be a class that provides short-lived access to an external resource like a file provider (read or write a small file or a chunk, and that’s it).
Scoped
The DI system knows a third lifetime, which is a so-called scope. An application by itself or an application framework like ASP.NET Core, and, of course, you as a developer, can create a new scope whenever it seems appropriate. A type registered as scoped will behave like a singleton – but only limited to the current scope: The first access will create a new instance and bind it to the scope. When the same type is requested from a different scope, again, a new instance will be created for that other scope and bound to that. Any subsequent request for that type within a given scope then will return the specific instance bound to the corresponding scope. As the scope gets disposed of, it will also dispose of all instances bound to this scope.
This is intended to be used for stateful types that are still relatively short-lived. A very good example would be the Entity Frameworks DbContext
. The context instance gets hold of a database connection from the connection pool and keeps a reference to it until it is disposed of. After disposal, it then returns the connection to the pool for re-use. On the other hand, while it is alive, it also keeps track of all entities that were loaded by it, in order to capture all changes on these entities and to be able to save the changes later on. This is usually user-specific data, so the DbContext
instance should not be shared with other users.
Scope behavior in ASP.NET Core MVC, Razor Pages and Web APIs
This is where we leave the responsibility of the DI system itself and shift our attention to how the application, more specifically our application framework, uses the DI system to control the creation and cleanup of scopes. In ASP.NET Core, this means two things:
The root
First of all, we have a DI container root, which is available at the very start of our application. This is technically not a scope, so you can only request singleton and transient types from the DI root, but no scoped types. If you try to resolve a scoped type out of the root container, it will throw an exception. The application host (i.e., the WebHost
) works on the DI container root. This especially affects types we register as IHostedService
(including all implementations that derive from BackgroundService
), as these are created early from the root and are kept alive until the application host is stopped. That means, if a hosted service needs something registered as scoped, you can’t inject this into your hosted service directly, as this comes from the root.
When you need a scoped instance but only have access to the root, it is your responsibility to create a new scope as need be. This could be either a long-lived scope for the lifetime of the hosted service or a separate scope for each consecutive run. The latter is recommended, especially when the service executes a job over and over again. You, as the developer, then request the service types from this scope and dispose of the scope after each run is finished. Disposing of the scope will also automatically take care of cleaning up and disposing all instances bound to this scope.
The scopes
Secondly, ASP.NET Core itself creates a new scope for each and every incoming HTTP request that needs to be handled by our web application. The Controller
(in MVC and Web API workloads) and a Razor page itself lives in this per-request scope. This makes it convenient to inject a DbContext
into your service classes and keep track of all changes to the entities during processing this request. It also makes sure that everything from this scope that captures user-specific state is cleaned up correctly and disposed of after the request has been processed, no matter if successful or not.
Side note: This is, by the way, also true for each invocation on a SignalR Core hub: Each message sent to the server through SignalR will create a new DI scope, create a new instance of the Hub
type, and then gets all subsequent dependencies from a scope dedicated to this single message. In other words, ASP.NET Core treats each SignalR message just like a normal HTTP request.
Scope behavior in Blazor
For Blazor, we have to think a little bit differently about the concept of scopes: The programming model of an ASP.NET Core Blazor application does not resemble the request/response-based approach like the rest of ASP.NET Core. Blazor is a Single Page Application (SPA) framework, and a SPA has a very different concept: A Blazor application’s lifetime is tied to the browser window it is displayed in. That might be a normal browser tab or an embedded web view in case of a Blazor Hybrid / MAUI application. As such, the lifetime can be compared more to that of a desktop application, which is started once and lives until the user choses to close the application’s main window. So how do scopes fit into this model?
Scope behavior in Blazor WASM & Blazor Hybrid
In Blazor WebAssembly and also in Blazor Hybrid / MAUI applications, there is only a single scope. It is created when the Blazor application starts, and it is cleaned up when the whole application in the Browser (WASM) or the corresponding Blazor WebView (Hybrid) is closed. As such, the lifetime of everything that is registered as scoped is tied to the instance of the current application. This is what I like to call a “user session”. In the WASM and Hybrid hosting model, this is conceptually identical to that of singletons. Blazor WASM and Blazor Hybrid do not provide means to provide shorter-lived scopes at all.
Scope behavior in Blazor Server
In Blazor Server, the hosting model again is extremely different to that of Blazor WASM and Blazor Hybrid. There is a single instance of the server process, but this host process needs to be able to handle multiple concurrent Blazor Server sessions for different users at once, as well as both normal ASP.NET Core MVC and Razor Page requests. And of course it must be possible to use hosted services and additional SignalR Hubs within the same application, too.
So, in order to fit into the common ASP.NET Core request/response scope handling and at the same time to be compatible with the way that scopes are handled in Blazor WASM and Blazor Hybrid – pretty much like singletons but not exactly like that – Blazor Server has a very distinct way of managing scopes: It creates a scope for each so-called “circuit”. A circuit is created when a new browser tab connects to the Blazor server via the underlying SignalR transport, and it is terminated when the browser tab is closed again (or if the connection is lost and fails to reconnect within a specified time frame). This also, conveniently, matches to the “user session” concept in Blazor WASM and Hybrid.
Again, this comes with the same restrictions as in Blazor WASM and Hybrid: If you need something that should be shorter-lived than a whole user session, this needs to be managed manually.
How to use shorter lifetimes when required
For us as developers, this mainly comes into play when working with the HttpClient
and, of course, the DbContext
. The usage of the IHttpClientFactory
in ASP.NET Core is generally recommended, so this also applies here. In Blazor, it is also not recommended to inject the DbContext
directly into your classes. Instead, you should use a DbContextFactory
to create a new instance of a DbContext
to process a single operation (unit of work), and then dispose of the context manually.
Note: Talking about a DbContext
in terms of Blazor WebAssembly: Of course, it isn’t possible to create a connection to a database server directly from the Browser. There is, however, an EF Core database provider to connect to an Azure Cosmos DB via its HTTP (aka gateway) bindings (see this article by Jeremy Likness), and you can ship a full SQlite database engine compiled to WebAssembly into the browser, directly packaged with your Blazor WASM applications, as shown by my colleague Patrick Jahr in his article Blazor WebAssembly: The Power Of EF Core And SQLite In The Browser, but please be very careful when doing this and learn about all possible limitations beforehand.
For other types than the HttpClient
and the DbContext
, where there are no factories provided by the framework, you need to control the lifetime of your services by yourself. For that, you can inject an IServiceProvider
and use something like this:
// create a scope manually
using var scope = _serviceProvider.CreateScope();
// retrieve a type from that scope
var myService = scope.ServiceProvider.GetRequiredService();
// do something with your service
myService.DoSomething();
// In your ConfigureServices
// do NOT do this anymore
services.AddDbContext(o => o.UseSQlite("filename.db"));
// but instead do this:
services.AddDbContextFactory(o => o.UseSQlite("filename.db"));
// and then the usage:
public class MyRepo
{
private readonly IDbContextFactorey _contextFactory;
public MyRepo(IDbContextFactory contextFactory)
{
_contextFactory = contextFactory;
}
// create a new context for each operation / unit of work
public async Task DoSomethingAsync()
{
using (var ctx = _contextFactory.CreateDbContext())
{
// this using clause contains your unit of work
}
}
}
When the code then disposes the scope through the using statement, all instances bound to the scope are disposed too. That said, a scope is relatively heavyweight, so try to use this approach only if absolutely necessary.
Differences of registrations in different Blazor editions
It is important to note that because of the differences in the lifetime of a scope in Blazor Server vs. Blazor WASM/Hybrid, this also leads to differences in the registration of a few of the Blazor default services.
The following services are explicitly registered as Singleton in Blazor WASM and Hybrid and as Scoped in Blazor Server:
IJSRuntime
and theNavigationManager
Both make sense, as in WASM and Hybrid, this connects to the actual Browser instance hosting our application, so there is only one JS runtime and one navigation state (the current URL) to interact with, and this could also be used globally to modify these states of the current application. In Blazor Server, for each concurrently connected user session, there is a different Browser and as such different JS runtimes and also a different navigation state to manage. So these need to be scoped here. See also the official documentation for details.
Summary
In Blazor, instances of types that are registered as scoped
in the dependency injection system will live for as long as the current user session is active. This usually is way longer than a developer would expect. Especially when they are familiar with the extremely short-lived scoped lifetime in ASP.NET Core MVC, Razor Pages and SignalR.
In order to avoid problems with this “desktop application”-like lifetime, especially with types that are designed for short-lived units of work like a HttpClient
or an EF Core DbContext
, you should use the corresponding factory classes provided by the framework to manually control the lifetime of your instances. If there are no supporting types available for your specific use case and you can’t avoid it, you can still resort to using an IServiceProvider
and its .CreateScope()
method to create a distinct sub-scope and use that for your unit of work.