Nell’ultimo articolo abbiamo esaminato come realizzare una libreria di componenti e abbiamo visto come sfruttare una libreria anche per condividere codice tra progetti Blazor Server e Blazor WebAssembly. Ci siamo lasciati con un bug da risolvere: la pagina FetchData che non funziona su Blazor Server perchè utilizza il client HTTP, che nel template Blazor Server non ha senso usare dato che abbiamo accesso diretto ai dati.

Il problema da risolvere

In realtà sono tanti i casi reali in cui ci sono delle differenze implementative da gestire per diversi scenari, ed è la ragione per la quale i principali framework di sviluppo forniscono una implementazione di un principio di programmazione chiamato Inversion of Control (IoC). L’idea di base è quella di disaccoppiare due elementi software, nel nostro caso la pagina che mostra dei dati e la sorgente dei dati che mostriamo, affinchè possano collaborare senza vincolarsi.

Per poterlo fare è necessario che non ci siano dipendenze strette tra di loro, cosa che invece succede nel nostro scenario, in cui il template WebAssembly usa direttamente il client HTTP per recuperare i dati, mentre il template Blazor Server usa direttamente la classe WeatherForecastService chiamandone il metodo GetForecastAsync(). Se definiamo un contratto tra la pagina che ha bisogno dei dati e il modo in cui questi dati vengono recuperati, possiamo risolvere il nostro problema.

Definiamo il contratto

Definiamo un’interfaccia che conterrà il nostro contratto, chiamiamola per semplicità IWeatherForecastService, che definisce un metodo GetForecastAsync(), che restituisce un vettore (scusatemi, non ho resistito… non usavo questo termine dai tempi dell’università…) di WeatherForecast, la cui definizione è già presente nel progetto host Blazor Server. Spostiamo questa classe in una cartella Models del progetto LibreriaComponenti e rinominiamo il namespace:

using System;

namespace LibreriaComponenti.Models
{
    public class WeatherForecast
    {
        public DateTime Date { get; set; }
        public int TemperatureC { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
        public string Summary { get; set; }
    }
}

Sempre nel progetto LibreriaComponenti creiamo una cartella Services, in cui definiamo la nostra interfaccia:

using System;
using System.Threading.Tasks;
using LibreriaComponenti.Models;

namespace LibreriaComponenti.Services
{
    public interface IWeatherForecastService
    {
         Task<WeatherForecast[]> GetForecastAsync(DateTime startDate);
    }
}

Non ci resta che implementare questa interfaccia nei due casi che ci servono.

Implementiamo il contratto

Partiamo proprio da Blazor Server, che è l’host che ha rivelato il nostro bug. Andiamo nella cartella Data e modifichiamo la classe WeatherForecastService in modo che implementi la nostra interfaccia. Lavoro semplicissimo perchè l’unico metodo messo a disposizione da questa classe coincide esattamente con quello della nostra interfaccia:

using System;
using System.Linq;
using System.Threading.Tasks;
using LibreriaComponenti.Models;
using LibreriaComponenti.Services;

namespace LibreriaComponenti.Host.BlazorServer.Data
{
    public class WeatherForecastService : IWeatherForecastService
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
        {
            var rng = new Random();
            return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            }).ToArray());
        }
    }
}

Passando invece al progetto Blazor WebAssembly, creiamo una cartella Services in cui creiamo una classe WeatherForecastService che implementa la nostra interfaccia sfruttando il client HTTP per il recupero dei dati:

using System;
using System.Net.Http;
using System.Threading.Tasks;
using LibreriaComponenti.Models;
using LibreriaComponenti.Services;
using System.Net.Http.Json;

namespace LibreriaComponenti.Host.BlazorWASM.Services
{
    public class WeatherForecastService : IWeatherForecastService
    {
        private readonly HttpClient _http;

        public WeatherForecastService(HttpClient http)
        {
            _http = http;
        }

        public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
        {
            return _http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
        }
    }
}

L’implementazione del metodo è la stessa che trovate nella pagina FetchData, e ci mostra una cosa interessante: abbiamo bisogno di una istanza dell’oggetto HttpClient per poter fare la chiamata. Quindi la nostra implementazione ha una dipendenza da HttpClient, che sarà risolta quando l’oggetto sarà creato, dato che l’abbiamo definita sul costruttore.

Iniettiamo la giusta dipendenza

Possiamo adesso registrare le nostre dipendenze nel motore di Dependency Injection fornito da .NET Core: sarà questo motore che creerà per noi gli oggetti giusti, quando questi saranno richiesti. Andiamo nel metodo ConfigureServices() della classe Startup del progetto Blazor Server, e istruiamo il motore su come vogliamo che venga risolta la dipendenza dal servizio IWeatherForecastService. Troverete già la seguente registrazione:

services.AddSingleton<WeatherForecastService>();

Questo perchè il template già sfruttava il motore di Dependency Injection di .NET Core per fornire l’istanza del servizio alla pagina, ma non aveva contrattualizzato il suo utilizzo, cosa che invece noi facciamo modificando la registrazione in questa:

services.AddSingleton<IWeatherForecastService, WeatherForecastService>();

In questo modo diciamo a .NET Core “quando qualcuno ti chiede una istanza di un oggetto che implementa IWeatherForecastService, tu costruisci e restituisci un oggetto WeatherForecastService”. Utilizzando il metodo AddSingleton() gli stiamo anche dando una importante informazione su come vogliamo che venga gestito il ciclo di vita dell’oggetto creato: vogliamo che sia un Singleton, cioè sempre la stessa istanza, chiunque sia il richiedente. Questa scelta ha delle implicazioni in termini di performance e occupazione di memoria di cui ci occuperemo in un apposito articolo.

Andiamo adesso nel file Program.cs del progetto WebAssembly per fare una operazione molto simile, modificando il metodo Main()in questo modo:

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("app");

    builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

    builder.Services.AddTransient<IWeatherForecastService, WeatherForecastService>();

    await builder.Build().RunAsync();
}

Registriamo anche qui il servizio, ma in questo caso l’oggetto da creare sarà l’implementazione che utilizza il client HTTP per recuperare i dati. Notate che subito prima della nostra registrazione c’è quella del client HTTP, che come la nostra, utilizza il metodo AddTransient() per definire il ciclo di vita dell’oggetto. In questo caso infatti non vogliamo un Singleton ma una nuova istanza ad ogni nuova richiesta HTTP. Il motore di Dependency Injection risolve le dipendenze a cascata, questo significa che quando creerà l’istanza di WeatherForecastService si accorgerà di aver bisogno di una istanza di HttpClient da passare nel costruttore, e, dato che anche HttpClient è registrato tra le dipendenze, può creare l’oggetto per noi.

C’è una buona ragione per la quale il client HTTP viene registrato come oggetto transiente e non come Singleton, dovuto al modo in cui è internamente implementato e risolve gli indirizzi in scenari di Load Balancing. Se registrassimo il nostro servizio come Singleton, il motore di Dependency Injection non avrebbe occasione di passarci un nuovo HttpClient ad ogni richiesta, dato che non ricostruirebbe il nostro oggetto.

Modifichiamo la pagina incriminata

Non ci resta che modificare la pagina FetchData per utilizzare il nostro contratto. Andiamo quindi nel progetto LibreriaComponentie aggiungiamo al file _Imports.razor le using a Models e Services:

@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.JSInterop
@using LibreriaComponenti.Shared
@using LibreriaComponenti.Services
@using LibreriaComponenti.Models

Notate che abbiamo anche eliminato quelle relative al client HTTP, che non è più una nostra dipendenza! Possiamo quindi eliminare dal progetto il riferimento a System.Net.Http.Json. La pagina FetchData diventa la seguente:

@page "/fetchdata"
@inject IWeatherForecastService WeatherForecastService

<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}
@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await WeatherForecastService.GetForecastAsync(DateTime.Now);
    }
}

Come potete notare, nessun riferimento a come è implementato il recupero dati, ma solo una iniezione attraverso il contratto: @inject IWeatherForecastService WeatherForecastService. Trovate il codice illustrato sull’account GitHub della community.

Conclusioni

In questo articolo abbiamo visto come disaccoppiare le nostre pagine da specifiche dipendenze sfruttando la tecnica dell’iniezione di dipendenze, implementata nativamente da .NET Core con il suo motore di Dependency Injection. Questo meccanismo apre tantissimi scenari implementativi e ci permette di rendere il nostro codice bassamente accoppiato e quindi facilmente manutenibile. Possiamo ad esempio facilmente testare la nostra pagina in maniera automatica, o possiamo, in base a una configurazione, sostituire la fonte dati da cui arrivano i dati che visualizziamo. Nei prossimi articoli vedremo altri scenari di utilizzo di questa tecnica, che ci permette di risolvere agevolmente problemi come la gestione dello stato applicativo e la comunicazione tra componenti non relazionati tra loro.