Dependency Injection in Blazor

Dependency Injection (DI) is a technique that promotes loose coupling of software through separation of concerns. In the context of a Blazor application, DI encourages you to develop discrete services for specific tasks, which are then injected into components and classes that need to use their functionality. These dependent classes are designed to invoke operations against abstractions, rather then specific dependency implementations, ensuring that the consumer classes are not tied to a specific implementation. This results in an application that is easier to maintain and test.

Services in Blazor

Razor components are primarily concerned with UI presentation. Part of the work involved in generating UI often involves communicating with data stores, perhaps via web services. Actions and events within a component might need to be logged. Data access and logging are not among the primary concerns of a Razor component. The code that performs the logging or fetching of data doesn't belong in a UI component. Including this kind of code within a Razor component breaches the Single Responsibility Principal.

The code that calls a web service or logs actions should be written in a separate class (or classes). These classes are generally referred to as Services. Doing this satisfies the Single Responsibility Principal, but you still need some way to make these services available to the Razor component.

You could simply instantiate the service class within a component:

@code{
    DataAccessService service; = new DataAccessService();
    List<Contact> contacts;

    protected void OnInitialized()
    {
        service = new DataAccessService();
        contacts = service.GetContacts();
    }
    ...
}

However, this approach results in tight coupling. The Razor component is tightly coupled to a specific implementation of the data access service. It makes the component difficult to unit test, because of the nature of the relationship between the component and its service: the service implementation is hard-coded into the component. If you wanted to run unit tests on the component, you need to find a way to replace the DataAccessService class with a fake or mock that doesn't actually communicate with a database or web service. Now imagine that problem if it extended across dozens or hundreds of components.

Dependency Injection provides a solution to this problem. First, you use abstractions to represent services. Most commonly, this abstraction takes the form of an interface. Then your component code references the abstraction rather than a specific implementation:

@code{
    IDataAccessService service;
    List<Contact> contacts;

    protected void OnInitialized()
    {
        contacts = service.GetContacts();
    }
    ...
}

The snippet above leaves two questions unanswered:

  1. How does IDataAccessService become DataAccessService?
  2. Where is DataAccessService instantiated?

To answer the first question, we need to look at service registration.

The Blazor Service Collection

Service registration involves mapping a concrete implementation to an abstraction. This is achieved by adding entries to the ServiceCollection, a central registry of ServiceDescriptor objects, which represent a service type, its implementation and a lifetime for the service.

Registration usually takes place in the Main method in the Program class of the application where the application's ServiceCollection is accessible via the Services property of the WebAssemblyHostBuilder:

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("app");
        builder.Services.AddSingleton<IDataAccessService, DataAccessService>();
    }
}

In this instance, the DataAccessService class is registered as the implementation to be used whenever an IDataAccessService type is injected. It is registered as a singleton, which means that one instance is available for the lifetime of the application.

Answering the second of the outstanding questions, the dependency injection system is responsible for providing an instance of the specified type whenever the abstraction is referenced, and for managing its lifetime.

Injection

Services are made available via injection, which is accomplished in different ways, depending on the consumer.

Consumer Injection Method
Razor component @inject directive
ComponentBase class [Inject] attribute
Other class constructor injection

Razor components

The @inject directive is used to make services available to Razor components. It is followed by the type that you want to inject, and an instance of that type:

@inject IDataAccessService service
    ...
@code{
 
    List<Contact> contacts;

    protected void OnInitialized()
    {       
        contacts = service.GetContacts();
    }
    ...
}

ComponentBase classes

Classes that act as code-behind classes for Razor components - those that derive from ComponentBase or that implement IComponent - do not support constructor injection. Services are made available to these classes by adding them as properties and decorating them with the InjectAttribute:

public class MyComponent : ComponentBase
{
    [Inject] IDataAccessService service { get; set; }
     
    protected List<Contact> Contacts { get; set; }

    protected override void OnInitialized()
    {       
        contacts = service.GetContacts();
    }
}

Other classes

Standard constructor injection is supported in non-component related classes:

public class MyService
{
    private readonly IDataAccessService _service;

    public MyService(IDataAccessService service) => _service = service;
 
    public void SomeMethod()
    {
        var x = _service.GetContacts();
    }
}

Default Services

A number of utility services are registered by default:

Service Lifetime Description
HttpClient Singleton Used for making HTTP requests and receiving their responses. The WebAssembly version uses the Fetch API.
NavigationManager Singleton Contains helpers for working with URIs and navigation state.
IJSRuntime Singleton Represents an instance of a JavaScript runtime where JavaScript calls are dispatched.

Service Lifetimes

Services can be registered with one of three lifetime scopes: Singleton, Scoped and Transient.

  • Singleton: Only one instance of the services is created for the the lifetime of the application. All users share the same instance in a Blazor Server application. Each user effectively gets their own version in a WebAssembly application.
  • Scoped: In Blazor Server applications, services registered as scoped are scoped to the current (SignalR) connection (or user). Scoped services are registered as singletons in WebAssembly applications.
  • Transient: A new instance of a services registered with Transient scope is created each time it is needed. This scope is appropriate for services that implement IDisposable, or that maintain state.
Last updated: 15/02/2023 09:00:40

Latest Updates

© 2023 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com