Learn best practices for using dependency injection in Blazor, and differences between Blazor Server and Blazor WebAssembly.
Dependency injection is a fundamental design pattern in ASP.NET Core and Blazor web development. It helps developers implement modular and maintainable applications by decoupling implementations.
For example, a Users
page might reference a UserService
by its interface. At runtime, the dependency injection system injects an instance of the UserService
when rendering the Users
page.
In this article, we will focus on the fundamentals of dependency injection in Blazor and learn about best practices and use cases beyond injecting services into Blazor pages. We’ll also learn about a new feature of .NET 9.
There are several options for injecting services into Razor components.
All options have in common that you first need to register the dependency with the dependency system. Usually, you register a dependency in the Program.cs
file of your ASP.NET Core-based application, including Blazor web applications.
builder.Services.AddScoped<IUserService, UserService>();
We use the WebApplicationBuilder
instance to access its Services
property of type IServiceCollection
and register the user service implementation for the IUserService
interface.
Note: The different service lifetimes or scopes will be covered later in this article.
The @inject
directive allows us to inject service implementations into Razor files.
@inject IUserService UserService
This simple line at the top of the Razor component allows us to define a variable of type IUserService
that will receive an instance at runtime.
It makes the variable available in the template and the code sections of the Razor component.
We can also inject services into a Razor component with the Inject
attribute. Instead of using the @inject
, a Razor directive, we use a regular C# attribute.
@code {
[Inject]
private IUserService UserService { get; set; }
}
This approach requires a code block or a code-behind file, while the @inject
direct offers a more straightforward way independent of a code section.
Starting with .NET 9, we can use constructor injection in Razor components similar to other regular C# classes.
@code {
private IUserService _userService;
public Home(IUserService userService)
{
_userService = userService;
}
}
In the code section, we add a constructor and inject an instance of the IUserService
interface that we store in a private field.
The three different service injection options have pros and cons. I almost exclusively use the @inject
directive in my Blazor applications because it is simple and concise.
Constructor injection is a welcomed alternative. It provides full control and the same mechanism as other C# applications, such as WPF desktop applications or ASP.NET Core WebAPI controllers. It helps developers transition into Blazor with a known environment.
However, I don’t see a use case where I would use property injection using the Inject attribute.
Similar to regular ASP.NET Core web applications, we have three different service lifetime scopes—Singleton, Transient and Scoped.
We decide what lifetime we want to use for a specific service when registering it with the dependency injection container, usually in the Program.cs
file.
Let’s learn about the different lifetime scopes and whether they behave differently depending on whether we use Blazor Server or Blazor WebAssembly interactivity (execute server-side or client-side).
Singleton services are instantiated once and shared across the entire application, making them ideal for application-wide state or configuration settings.
Since they are only instantiated once, it doesn’t have a big impact if the initialization takes a little more time.
Transient services create a new instance every time they are injected, making them useful for stateless operations that are quickily initialized.
You don’t want to define services as transient that have a lot of or slow initialization code.
Scoped services are particularly important in Blazor Server, where they are tied to a user’s SignalR circuit (session).
It makes them the preferred choice for services that need to maintain user-specific data or are somehow tied to the (authentiacted) user. In Blazor Server, Scoped can be thought of as “Singleton-per-User.”
For Blazor WebAssembly interaction, the Blazor application/component executes on the client, and there is no SignalR connection between the server and the client.
This means that, for every Singleton and Scoped service, an instance per browser is created.
Registering a service as a singleton does not allow you to share information between different clients/users when using Blazor WebAssembly interactivity, whereas it works when using Blazor Server interactivity.
The transient scope is the same for Blazor Server and Blazor WebAssembly, meaning a new service is created for each injection.
Understanding the nuances of these lifetimes enables optimal resource utilization and helps prevent unintended behavior.
Dependency injection plays a crucial role in managing state, retrieving data and testing in Blazor applications.
For state management, dependency injection allows services to hold and manage shared data. This approach is good enough in many scenarios and avoids the need for complex event-driven patterns.
Dependency injection separates the consumer from the producer and allows for better application testability and reusability of components.
For example, if you encapsulate a behavior in a service, you can inject the service in multiple Razor components and reuse the same functionality.
You don’t want to implement data access logic directly inside your Razor components. Instead, you want to implement data services you can leverage to load and save data. Again, this helps with testability, maintainability and reusability.
By leveraging dependency injection effectively, we can build scalable, maintainable and testable Blazor applications.
The following best practices help with implementing robust, maintainable and reusable Blazor web applications:
Applying those four best practices will help you implement more maintainable and extensible Blazor applications.
Dependency injection is an important pattern for ASP.NET Core web development, and Blazor specifically.
It’s a simple pattern, and you get up to speed quickly. However, understanding details, such as scope lifetimes, and applying best practices are vital to implementing maintainable, robust and flexible Blazor web applications.
If you want to learn more about Blazor development, you can watch my free Blazor Crash Course on YouTube. And stay tuned to the Telerik blog for more Blazor Basics.
Claudio Bernasconi is a passionate software engineer and content creator writing articles and running a .NET developer YouTube channel. He has more than 10 years of experience as a .NET developer and loves sharing his knowledge about Blazor and other .NET topics with the community.