Articoli

Creare una SPA: integrazione back-end con Blazor WASM

da

Dopo aver visto come sia semplice in Blazor Server integrare un back-end .NET, vediamo come possiamo fare lo stesso con Blazor WebAssembly, creando un API REST per la gestione dei nostri eventi e utilizzando il client HTTP che ci mette a disposizione il framework.

Aggiungiamo il backend per Blazor WebAssembly

A differenza di Blazor Server, quando usiamo WebAssembly ci troviamo nel browser, quindi non possiamo accedere direttamente al database. Possiamo però creare facilmente un API REST per esporre le operazioni CRUD degli eventi, utilizzando un controller MVC.

Andiamo quindi a crearci un nuovo progetto WebAPI, a cui colleghiamo il progetto event-manager-data creato nell’articolo precedente:

dotnet new webapi -o event-manager-backend
cd event-manager-backend
dotnet add reference ../event-manager-data/event-manager-data.csproj 
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
cd ..
dotnet sln add event-manager-backend/event-manager-backend.csproj 
cd event-manager-backend

Nel file Startup.cs andiamo ad aggiungere la configurazione per l’utilizzo di EntityFramework con SqlLite, come già fatto per Blazor Server:

builder.Services.AddControllers();
builder.Services.AddDbContext<EventManagerDbContext>(
        opt => opt.UseSqlite("DataSource=eventmanager.db"));

Non ci resta che applicare la migrazione che abbiamo creato la volta scorsa per generare il database SqlLite con la tabella Eventi:

dotnet ef database update

Passiamo al controller, eliminando dalla cartella Controllers il file WeatherForecastController.cs, che contiene il controller di esempio creato dal template WebAPI, e creare una nuova classe EventiController in cui andiamo a iniettare il nostro EventManagerDbContext:

using event_manager_data;
using Microsoft.AspNetCore.Mvc;

namespace event_manager_backend.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class EventiController : ControllerBase
    {
        private readonly EventManagerDbContext db;
    
        public EventiController(EventManagerDbContext db)
        {
            this.db = db;
        }
    }
}

Non ci resta che aggiungere le classiche operazioni CRUD per l’entità Eventi, creando le action corrispondenti ai verbi HTTP GET, POST, PUT e DELETE.

[HttpGet]
public IActionResult Get()
{
    return Ok(this.db.Eventi.Select(x => new ElementoListaEventi()
    {
        Id = x.Id,
        Nome = x.Nome,
        Localita = x.Localita,
        Data = x.Data
    }).ToList());
}

[HttpGet("{id}")]
public IActionResult Get(int id)
{
    var result = this.db.Eventi
        .Where(x => x.Id == id)
        .Select(x => new Evento()
        {
            Id = x.Id,
            Nome = x.Nome,
            Localita = x.Localita,
            Data = x.Data,
            Descrizione = x.Descrizione,
            Note = x.Note
        }).SingleOrDefault();

    if(result == null) return NotFound();
    return Ok(result);
}

[HttpPost]
public IActionResult Post(Evento item)
{
            if (ModelState.IsValid)
            {
                var entity = new DatiEvento()
                {
                    Id = item.Id,
                    Nome = item.Nome,
                    Localita = item.Localita,
                    Data = item.Data,
                    Descrizione = item.Descrizione,
                    Note = item.Note
                };
                this.db.Add(entity);
                this.db.SaveChanges();
                item.Id = entity.Id;
                return CreatedAtAction(nameof(Get), new { id = entity.Id }, item);
            }
            return BadRequest(ModelState);
 }

 [HttpPut("{id}")]
 public IActionResult Put(int id, Evento item)
 {
            if (ModelState.IsValid)
            {
                var entity = this.db.Eventi.SingleOrDefault(x => x.Id == id);
                if (entity == null) return NotFound();
                entity.Nome = item.Nome;
                entity.Localita = item.Localita;
                entity.Data = item.Data;
                entity.Descrizione = item.Descrizione;
                entity.Note = item.Note;
                this.db.SaveChanges();
                return NoContent();
            }
            return BadRequest(ModelState);
 }

 [HttpDelete("{id}")]
 public IActionResult Delete(int id)
 {
            var entity = this.db.Eventi.SingleOrDefault(x => x.Id == id);
            if (entity == null) return NotFound();
            this.db.Remove(entity);
            this.db.SaveChanges();
            return NoContent();
 }

Come possiamo vedere dal codice, stiamo utilizzando le classi ElementoListaEventi e Evento per proiettare con LINQ le informazioni della tabella che ci servono per il front-end. La buona notizia è che, a differenza dei framework JavaScript, qui le classi di scambio sono scritte in .NET e questo ci permette di creare una libreria condivisa per tali oggetti in modo da non doverli duplicare e soprattuto tenere allineati da entrambi i lati quando ci sono dei cambiamenti.

Creiamoci una libreria .NET condivisa che andremo a chiamare event-manager-shared, aggiungendo la libreria per le Data Annotations utilizzata dalle classi condivise per la validazione:

dotnet new classlib -o event-manager-shared
dotnet sln add event-manager-shared/event-manager-shared.csproj 
cd event-manager-shared
dotnet add package System.ComponentModel.Annotations

Eliminiamo il file Class1.cs creato dal template e spostiamo la cartella Models dal progetto event-manager-wasm alla nostra libreria condivisa, aggiornando i namespace delle due classi. A questo punto non dobbiamo fare altro che aggiungere questa libreria sia al progetto back-end che al progetto front-end WebAssembly, andando fisicamente a condividere gli oggetti di scambio:

cd event-manager-wasm
dotnet add reference ../event-manager-shared/event-manager-shared.csproj 
cd ../event-manager-backend
dotnet add reference ../event-manager-shared/event-manager-shared.csproj 

Aggiorniamo le using di front-end e back-end per farle corrispondere al namespace event_manager_shared.Models e testiamo il back-end con il classico dotnet run:

Per facilitare la fase di sviluppo andiamo a modificare la porta su cui viene lanciato il progetto back-end, utilizzando ad esempio la 5002. Possiamo modificare questa impostazione dal file launchSettings.json, presente nella cartella Properties:

"event_manager_backend": {
    "commandName": "Project",
    "launchBrowser": true,
    "applicationUrl": "http://localhost:5002",
    "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Development"
    }
}

Andiamo inoltre ad abilitare le chiamate CORS , in questo modo possiamo testare in tutta tranquillità le chiamate al back-end. Nel file Program.cs aggiungeremo:

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
    app.UseCors(x => x.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
} else
{
    app.UseHttpsRedirection();
}

Blazor WebAssembly e HTTPClient

Una volta creata l’API, invocarla da Blazor WebAssembly è molto semplice, dato che il framework ha già configurato per noi la dependency injection dell’oggetto HttpClient che mette a disposizione per le chiamate HTTP. Non ci resta che iniettarlo nella pagina eventi:

@inject HttpClient Http
@page "/eventi"

@if(EventoCorrente == null)
{
    <ListaEventi 
        ListaElementi="ListaEventi" OnElimina="EliminaEvento"
        OnCrea="CreaEvento" OnModifica="ModificaEvento" />
}
else
{
    <DettaglioEvento 
        DettaglioElemento="EventoCorrente"
        OnSalva="SalvaEvento" OnAnnulla="AnnullaOperazione" />
}

Creiamoci un metodo privato caricaEventi(), che utilizzeremo per ricaricare la lista degli eventi dopo ogni modifica e dopo l’inizializzazione della pagina:

@code {

    private string apiUrl = @"http://localhost:5002/api/eventi";
    public Evento EventoCorrente { get; set; }
    List<ElementoListaEventi> ListaEventi { get; set; } 
        = new List<ElementoListaEventi>();
    
    protected override Task OnInitializedAsync()
    {
        return caricaEventi();
    }

    private async Task caricaEventi()
    {
        ListaEventi = await Http
            .GetFromJsonAsync<List<ElementoListaEventi>>(apiUrl);
    }

    ...
}

L’oggetto HttpClient ci mette a disposizione tutti i metodi asincroni per l’invocazione del back-end, fornendo in maniera integrata anche la serializzazione/deserializzazione JSON. Dato che i metodi sono asincroni, possiamo utilizzare i costrutti async/await per fare le chiamate, utilizzando OnInitializedAsync per caricare la lista dopo l’inizializzazione della pagina.

Le restanti modifiche diventano a questo punto banali, sostituendo le elaborazioni in memoria con le corrispondenti chiamate HTTP:

public void CreaEvento()
{
    this.EventoCorrente = new Evento();
}

public async Task ModificaEvento(ElementoListaEventi evento)
{
    this.EventoCorrente = await Http
        .GetJsonAsync<Evento>($"{apiUrl}/{evento.Id}");
}

public async Task SalvaEvento(Evento evento)
{
    if(evento.Id == 0)
    {
        await Http.PostJsonAsync<Evento>(apiUrl, evento);
    }
    else
    {
        await Http.PutJsonAsync($"{apiUrl}/{evento.Id}", evento);
    }
    await caricaEventi();
    this.EventoCorrente = null;
}

public void AnnullaOperazione()
{
    this.EventoCorrente = null;
}

public async Task EliminaEvento(ElementoListaEventi evento) 
{
    await Http.DeleteAsync($"{apiUrl}/{evento.Id}");
    await caricaEventi();
}

Avviamo il progetto back-end e quello front-end e ammiriamo il risultato del nostro lavoro:

Potete consultare il codice sorgente qui, nella branch 07-integrazione-backend-wasm.

Conclusioni

Con questo articolo abbiamo ultimato la nostra serie su come creare da zero una Single Page Application basilare, con una semplice CRUD, sia con Blazor Server che con Blazor WebAssembly. Nei prossimi articoli cominceremo a esplorare le funzionalità un po’ più avanzate di Blazor, a partire dall’interoperabilità con JavaScript per coprire tutti gli scenari legacy che ci possono capitare.