9.0 KiB
sidebar_position | title |
---|---|
2 | DI & IOC |
Definition
When you have understood the fundamentals of object-oriented programming (OOP), you must quickly understand and assimilate several design patterns.
Indeed, this makes it possible to deal with known problems and thus to impose good practices which are based on the experience of software designers.
For me, and surely for many other software designers, one of the primary goals of design is to keep the interface clear, easily maintainable and above all scalable. As any developer knows, technologies evolve very very quickly.
This is why we are going to see dependency injection (DI) and inversion of control (Ioc).
DI (Dependency Injection)
In C# it is simple to instantiate a new object via the “new” operator. Instantiating a new object into a class imposes a coupling (i.e. a tightly bound connection), or even a reference to an assembly.
We could get by if the project was not too complex with simple objects and very little coupling. But, let's say you want to create a service, how are you going to quickly implement it in a new class/assembly? And in the worst case, the day you want to change the type of service in your simple project, which over time has become complex, how are you going to proceed?
As you have understood, this pattern is for any developer who cares about the quality of his software and seeks to make his application as maintainable as possible.
Indeed, the injection makes it possible to establish in a dynamic way the relation between several classes. It consists in dividing the responsibilities between the different modules, the different parts and even facilitates the subsequent modification of the classes.
IOC (Inversion Of Control)
Once we have understood the principle of dependency injection, we can tackle another higher-level concept: inversion of control.
Indeed, if we have few or many classes calling on our service, we will create upstream IMyService
instances as many times as necessary.
So we will end up with multiple instances, which was not meant to happen. Also, the fact of instantiating our interface in several places, we will find ourselves faced with as much code duplication as instantiations, which will make our application difficult to maintain.
Therefore, the first idea that comes to mind when we are unfamiliar with dependency inversion is to make our MyService
class static.
Which is a very bad idea. We no longer have any problem with instances, however, now we will have to make our class thread-safe
and properly manage multi-threading
.
Not to mention the problems we will face when we want to do unit tests.
Others will have thought of the singleton
. This is not the case, there are often too many memory leak problems.
Then, when we look in its fundamentals, we naturally think of one of the design patterns described by the "Gof": the Factory
.
Indeed, a factory with a singleton could be the solution, unfortunately, there would always remain a weak coupling between the class and the call to each factory.
In our example, we would only have one, so we don't see the point of it. But, in an application there are far more factories and there would therefore be as many couplings as there are factories. Without counting on the instantiations which can be more or less long. But under certain conditions we could see it immediately without having to wait for the instantiation time.
To be able to outsource even more code, such as the creation of our object, and not slow down our program if there are long instantiations, we are going to have to map the contracts with their implementations at the start of the application.
The IOC is defined as a container that determines what must be instantiated and returned to the client to prevent the latter from explicitly calling the constructor with the "new" operator.
In a nutshell, it's an object that acts as a cache for the instances we need in various parts of our applications.
Add services to a Blazor Server app
After creating a new app, examine part of the Program.cs
file:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
The builder
variable represents a Microsoft.AspNetCore.Builder.WebApplicationBuilder
with an IServiceCollection
, which is a list of service descriptor objects.
Services are added by providing service descriptors to the service collection.
The following example demonstrates the concept with the IDataAccess
interface and its concrete implementation DataAccess
:
builder.Services.AddSingleton<IDataAccess, DataAccess>();
Service lifetime
Services can be configured with the following lifetimes:
Scoped
Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped
-registered services behave like Singleton
services.
The Blazor Server hosting model supports the Scoped
lifetime across HTTP requests but not across SignalR connection/circuit messages among components that are loaded on the client. The Razor Pages or MVC portion of the app treats scoped services normally and recreates the services on each HTTP request when navigating among pages or views or from a page or view to a component. Scoped services aren't reconstructed when navigating among components on the client, where the communication to the server takes place over the SignalR connection of the user's circuit, not via HTTP requests. In the following component scenarios on the client, scoped services are reconstructed because a new circuit is created for the user:
- The user closes the browser's window. The user opens a new window and navigates back to the app.
- The user closes a tab of the app in a browser window. The user opens a new tab and navigates back to the app.
- The user selects the browser's reload/refresh button.
Singleton
DI creates a single instance of the service. All components requiring a Singleton
service receive the same instance of the service.
Transient
Whenever a component obtains an instance of a Transient
service from the service container, it receives a new instance of the service.
Request a service in a component
After services are added to the service collection, inject the services into the components using the @inject Razor directive, which has two parameters:
- Type: The type of the service to inject.
- Property: The name of the property receiving the injected app service. The property doesn't require manual creation. The compiler creates the property.
Use multiple @inject
statements to inject different services.
The following example shows how to use @inject
.
The service implementing Services.IDataAccess
is injected into the component's property DataRepository
. Note how the code is only using the IDataAccess
abstraction:
@page "/customer-list"
@inject IDataAccess DataRepository
@if (customers != null)
{
<ul>
@foreach (var customer in customers)
{
<li>@customer.FirstName @customer.LastName</li>
}
</ul>
}
@code {
private IReadOnlyList<Customer>? customers;
protected override async Task OnInitializedAsync()
{
customers = await DataRepository.GetAllCustomersAsync();
}
private class Customer
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
private interface IDataAccess
{
public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
}
}
Internally, the generated property (DataRepository
) uses the [Inject]
attribute.
Typically, this attribute isn't used directly.
If a base class is required for components and injected properties are also required for the base class, manually add the [Inject]
attribute:
using Microsoft.AspNetCore.Components;
public class ComponentBase : IComponent
{
[Inject]
protected IDataAccess DataRepository { get; set; }
...
}
Use DI in services
Complex services might require additional services.
In the following example, DataAccess
requires the HttpClient default service. @inject
(or the [Inject]
attribute) isn't available for use in services.
Constructor injection must be used instead.
Required services are added by adding parameters to the service's constructor.
When DI creates the service, it recognizes the services it requires in the constructor and provides them accordingly.
In the following example, the constructor receives an HttpClient
via DI. HttpClient
is a default service.
using System.Net.Http;
public class DataAccess : IDataAccess
{
public DataAccess(HttpClient http)
{
...
}
}
Prerequisites for constructor injection:
- One constructor must exist whose arguments can all be fulfilled by DI. Additional parameters not covered by DI are allowed if they specify default values.
- The applicable constructor must be public.
- One applicable constructor must exist. In case of an ambiguity, DI throws an exception.