The dependency injection is the mechanism by which a class receives the services it needs without constructing them directly.
In Blazor, DI is the mechanism that allows us to decouple the View (.razor Components) from the Logic (Services). Instead of creating instances of classes with new ServicioUsuario(), we ask the framework to provide them for us.
In Blazor, the rendering model changes the actual scope of Scoped. It’s important to understand this before saving state in a service.
| Lifetime | Behavior | Recommended use |
|---|---|---|
| Transient | Always a new instance. | Lightweight logic, Helpers, independent processors. |
| Scoped | Per request in SSR, per circuit in Interactive Server, and per client app in WebAssembly. | Circuit or client state and services whose scope aligns with those boundaries. |
| Singleton | One global instance for the entire application. | Cache, Configuration, read-only services. |
The service container
It all starts in Program.cs. There we have a collection called Services. Before building the application (app.Build()), we register our classes and tell Blazor how it should instantiate them.
var builder = WebApplication.CreateBuilder(args);
// Here we register our services
builder.Services.AddSingleton<GlobalService>();
builder.Services.AddScoped<ICarrito, CartService>();
builder.Services.AddTransient<TaxCalculator>();
var app = builder.Build();When a component requests a dependency, Blazor looks it up in this container. If a valid instance already exists according to its lifestyle, it delivers it. If not, it creates one.
The three lifetimes
The key to DI is deciding how long the injected object lives.
Transient (AddTransient)
“New every time.” The container creates a new instance every time someone requests it.
- If you have 3 components on a page and all 3 inject this service, 3 different instances will be created.
- It’s ideal for lightweight, stateless services, like unit converters or validators.
Singleton (AddSingleton)
“One for all.” A single instance is created the first time it is requested, and that same instance is reused across the entire application for all users.
- On the server, it can be used for cache, global configuration, or genuinely shared state.
- Caution: In Interactive Server, a Singleton instance is shared among all connected users. In WebAssembly, the container lives in the browser and its Singleton belongs only to that client application.
Scoped (AddScoped)
“One per scope.” The complexity arises when comparing Blazor’s different execution models.
- An instance is created per “scope” and reused within that scope.
- In classic web applications (API, MVC), the scope is the HTTP Request.
- In Blazor, the scope changes depending on the hosting model.
How Scoped changes based on rendering
This is the most critical point of the article. The concept of Scoped works radically differently in Blazor Server and Blazor WebAssembly.
Since the application runs in the client’s browser, there are no constant “HTTP requests”. Here, the Scope is the application’s lifetime.
- A
Scopedservice behaves almost like aSingleton. - It only dies when the user reloads the page (F5) or closes the tab.
Here the application lives on the server, and the browser connects via WebSocket (SignalR). The Scope is the Circuit (the SignalR connection).
- User enters the website -> A Circuit is created -> An instance of the Scoped service is created.
- User navigates the website (Home -> Profile) -> The Circuit is maintained -> The service instance is the same.
- User presses F5 -> The circuit is broken and a new one is created -> A new instance is created.
- Another user enters -> They have their own circuit -> They have their own instance.
During static server rendering, the scope coincides with the HTTP request. Therefore, a Scoped service created for one request is not the same instance that an interactive component will use later.
How to inject services
We have two ways to request these dependencies, depending on where we are writing code.
In .razor files (@inject directive)
We use the directive at the beginning of the file. Behind the scenes, this creates a property in the generated class.
@page "/weather"
@inject IWeatherService WeatherService
@inject NavigationManager NavManager
<h1>Weather</h1>
<p>Temperature: @temperature</p>
@code {
private int temperature;
protected override async Task OnInitializedAsync()
{
// Use the injected instance
temperature = await WeatherService.GetTemperatureAsync();
}
}In C# classes (.cs) (Constructor Injection)
If we are in a code-behind class, a Service, or a ViewModel, we use the standard .NET constructor injection.
public class CartService
{
private readonly IProductRepository _repo;
// The container injects the repository automatically
public CartService(IProductRepository repo)
{
_repo = repo;
}
}If you use Code-Behind (.razor.cs) for your components, you cannot use constructor injection. You must use the [Inject] attribute on public properties.
public partial class MyComponent
{
[Inject]
public IWeatherService WeatherService { get; set; } = default!;
}An Entity Framework DbContext represents a short unit of work and is not safe for concurrent use. In Interactive Server, it’s not advisable to keep one around for the entire circuit; use IDbContextFactory<TContext> to create and release a context per operation.