SignalR rappresenta una potente libreria di .NET che consente di stabilire una comunicazione in tempo reale tra il server e il client, gestendo in modo efficace la trasmissione di dati attraverso una connessione persistente. Quando si sviluppa un'applicazione web, è comune dover affrontare scenari in cui il client e il server devono essere sincronizzati continuamente, ad esempio in un'app di chat, una dashboard in tempo reale, o una gestione dinamica di attività.

L'architettura SPA (Single Page Application) è spesso utilizzata in questi casi, in quanto permette di mantenere la stessa connessione con il server durante il caricamento dinamico delle pagine. Ciò che differenzia SignalR da altre soluzioni è la capacità di inviare e ricevere dati in tempo reale, senza dover stabilire una nuova connessione per ogni richiesta. Un aspetto cruciale di SignalR è l'utilizzo di un "Hub", che funge da punto centrale per la gestione degli eventi tra client e server.

Creazione della connessione

La configurazione iniziale di SignalR in un'applicazione Razor Pages avviene attraverso un file JavaScript, dove si definiscono le principali operazioni di connessione al server. Prima di tutto, è necessario creare un oggetto di connessione:

javascript
var connection = new signalR.HubConnectionBuilder()
.withUrl(HUB_URL) .build();

Il withUrl è un metodo che stabilisce l'URL del server con il quale il client dovrà comunicare. In un'applicazione con Razor Pages, l'URL sarà relativo e potrebbe essere qualcosa come /taskmanagerhub, che corrisponde al punto di accesso del server. Dopo aver creato la connessione, si procede con l'associazione degli eventi:

javascript
connection.on(NOTIFY_TASK_MANAGER_EVENT, updateTaskList);

In questo caso, updateTaskList è una funzione che si occupa di aggiornare la lista delle attività completate o incomplete in base alle informazioni ricevute dal server. Il nome dell'evento (NOTIFY_TASK_MANAGER_EVENT) deve essere lo stesso sia sul lato client che sul lato server, e per facilitare la manutenzione, è consigliabile utilizzare costanti.

Invocazione e gestione degli eventi

Una volta configurata la connessione e gli eventi, il passo successivo è avviare la connessione tramite il metodo start(), che restituisce una promessa. Se la connessione ha successo, possiamo abilitare il pulsante "Aggiungi attività":

javascript
connection.start().then(function () {
addTaskButton.disabled = false; }).catch(function (err) { console.error(err.toString()); });

Nel codice di esempio, il pulsante "Aggiungi attività" invoca il metodo server-side CreateTask utilizzando il metodo invoke() di SignalR. Questo metodo invia un oggetto TaskModel al server, che lo elabora e invia i dati aggiornati al client.

javascript
addTaskButton.addEventListener("click", function (event) {
let taskName = document.getElementById(TASK_NAME_ID);
connection.invoke(HUB_ADD_TASK_METHOD, { name: taskName.value })
.
catch(function (err) { console.error(err.toString()); }); taskName.value = ""; taskName.focus(); event.preventDefault(); });

La comunicazione tra client e server

Nel caso della creazione di una nuova attività, il flusso di comunicazione tra client e server segue una sequenza precisa di passi:

  1. L'utente inserisce il nome dell'attività e clicca sul pulsante "Aggiungi attività", il quale invia la richiesta al server attraverso il metodo CreateTask.

  2. Il server elabora la richiesta, aggiungendo la nuova attività a una lista in memoria.

  3. Il server invia un messaggio al client, invocando l'evento NotifyTaskManager e passando il modello TaskModel creato.

  4. Il client esegue una funzione di callback che aggiorna dinamicamente la vista, mostrando le attività in tempo reale.

Questa sequenza di operazioni dimostra come SignalR permetta di gestire la comunicazione tra client e server in tempo reale senza la necessità di ricaricare la pagina.

Streaming con SignalR

Oltre alla gestione degli eventi tradizionali, SignalR supporta anche il streaming, una modalità che consente di inviare dati in continuo, rendendo possibile l'aggiornamento dinamico delle informazioni senza interruzioni. Il flusso continuo di dati è particolarmente utile in scenari come:

  • Feed di notizie in tempo reale.

  • Dashboard che visualizzano dati aggiornati continuamente.

  • App di messaggistica o chat in tempo reale.

A differenza dei modelli tradizionali di richiesta/risposta, in cui i dati vengono inviati in blocchi, lo streaming consente una trasmissione costante e asincrona, garantendo che l'interfaccia utente rimanga reattiva anche quando vengono gestiti grandi volumi di dati.

SignalR supporta anche lo streaming bidirezionale, il che significa che non solo il server può inviare dati al client, ma anche il client può inviare dati al server in modo continuo, senza dover fare richieste esplicite.

L'adozione dello streaming in SignalR apre la strada a scenari avanzati, dove le informazioni devono essere trasmesse in tempo reale e senza ritardi percepibili per l'utente. Questo è particolarmente utile in applicazioni che richiedono aggiornamenti costanti, come nei sistemi di monitoraggio in tempo reale o nelle applicazioni di trading finanziario.

Come Gestire la Connessione al Database e le Migrazioni in ASP.NET Core con Entity Framework

L’utilizzo di tecniche appropriate per la gestione delle connessioni ai database è cruciale per mantenere l'integrità e la sicurezza di un’applicazione. In contesti di sviluppo avanzato, è fondamentale adottare pratiche come l’uso di segreti o server di configurazione, come Azure App Configurator, per la gestione di informazioni sensibili. In un contesto di insegnamento, però, è possibile introdurre la connessione direttamente all'interno del file appsettings.json per semplificare il processo di configurazione.

Nel caso specifico di un'applicazione ASP.NET Core, una stringa di connessione potrebbe essere inserita nel file JSON in questo formato:

json
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": {
"BankingDbContext": "Server=localhost;Database=dbBanking;User Id=sa;Password=Password123;TrustServerCertificate=True"
} }

Questa stringa di connessione viene utilizzata da un server di database in esecuzione in un ambiente Docker, con un database chiamato dbBanking. Il sistema di Entity Framework (EF) Core si occupa di gestire automaticamente la connessione, mappando gli oggetti C# alle entità del database e generando comandi SQL appropriati.

Un componente essenziale di EF Core è il DbContext, che agisce come intermediario tra l'applicazione e il database. Questo oggetto implementa il pattern di progettazione "Unit of Work", gestendo gli stati degli oggetti in memoria e applicando modifiche quando necessario. Il DbContext è anche la chiave per realizzare una separazione delle responsabilità all'interno dell'architettura, delegando la logica di persistenza dei dati alla gestione delle entità e lasciando le regole di business al livello superiore dell'applicazione.

Il pattern "Unit of Work" trova applicazione in numerosi contesti, favorendo la separazione tra la logica di business e la gestione dei dati. Permette di gestire le operazioni di persistenza in modo centralizzato, riducendo la complessità e aumentando la coesione del codice. Ad esempio, con l’utilizzo di EF Core, la gestione del ciclo di vita dei dati diventa trasparente all'applicazione, che interagisce con oggetti di dominio anziché con dettagli di implementazione legati al database.

Nel contesto di questa applicazione, si crea una classe chiamata BankingDbContext.cs che estende il DbContext di EF Core e definisce le proprietà corrispondenti alle tabelle del database. Ecco un esempio della sua implementazione:

csharp
namespace WorkingWithOrm.Context
{ using Microsoft.EntityFrameworkCore; using WorkingWithOrm.Model; public class BankingDbContext : DbContext {
public BankingDbContext(DbContextOptions options) : base(options) { }
public DbSet<Account> Accounts { get; set; } public DbSet<Customer> Customers { get; set; } public DbSet<Movement> Movements { get; set; } } }

In questo caso, le proprietà Accounts, Customers e Movements rappresentano rispettivamente le tabelle del database. EF Core inferisce automaticamente la struttura delle tabelle e le colonne a partire dai nomi delle classi e delle loro proprietà. Ad esempio, la classe Account avrà una tabella nel database con le colonne Id, Name, Balance, e CustomerId. Inoltre, grazie al legame tra Account e Customer, EF Core riconosce automaticamente la relazione tra le due entità, stabilendo una chiave esterna.

Tuttavia, nel caso in cui sia necessario seguire convenzioni di denominazione diverse o gestire scenari complessi, è possibile intervenire direttamente sul modello utilizzando la Fluent API o le annotazioni dei dati. Ad esempio, per personalizzare la mappatura della classe Customer alla tabella tbl_customer, si può sovrascrivere il metodo OnModelCreating all'interno del DbContext:

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Customer>(entity => { entity.ToTable("tbl_customer"); entity.HasKey(c => c.Id).HasName("pk_customer_id"); entity.Property(c => c.Name).HasColumnName("customer_name").HasMaxLength(100).IsRequired(); entity.HasMany(c => c.Accounts).WithOne(a => a.Customer).HasForeignKey(a => a.CustomerId); }); }

Questa mappatura personalizzata permette di definire in modo preciso la struttura del database, applicando anche altre configurazioni come vincoli di lunghezza, chiavi primarie, e relazioni tra le entità.

Una volta completato il DbContext, è necessario configurarlo nel contenitore di Iniezione delle Dipendenze (DI) dell'applicazione, associando la stringa di connessione configurata nel file appsettings.json. L'aggiunta al Program.cs avviene attraverso il metodo AddDbContext, che configura EF Core per utilizzare il database definito:

csharp
builder.Services.AddDbContext<BankingDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("BankingDbContext")));

Questo codice assicura che il contesto venga configurato correttamente e consente all'applicazione di comunicare con il database in modo trasparente. Tuttavia, affinché il database rifletta la struttura del modello, è necessario eseguire le migrazioni. Le migrazioni sono uno strumento potente che permette di sincronizzare l'applicazione con il database, gestendo le modifiche strutturali nel tempo, come l’aggiunta o la rimozione di tabelle o colonne.

Per aggiungere una migrazione, si utilizza il comando CLI di EF Core:

bash
dotnet ef migrations add InitialDatabase

Questo comando crea una "fotografia" del modello di dominio e genera uno script SQL che verrà applicato al database per mantenerlo aggiornato con le modifiche del modello.

Le migrazioni sono fondamentali per garantire che le modifiche al modello siano sempre riflettute nel database, evitando conflitti e incongruenze. Senza migrazioni, l’applicazione e il database potrebbero diventare desincronizzati, portando a errori difficili da diagnosticare.

In sintesi, la configurazione di una connessione al database e la gestione delle migrazioni sono passaggi cruciali per il successo di qualsiasi applicazione basata su Entity Framework. L'adozione delle best practice, come l'uso di pattern come "Unit of Work" e l'integrazione di strumenti di migrazione, consente di creare applicazioni robuste e facilmente manutenibili, minimizzando al contempo i rischi legati alla gestione dei dati.