Accesso ai dati con Entity Framework Core

Accesso ai dati con Entity Framework Core

Nell'articolo precedente la nostra Web API conservava i dati in memoria, perdendoli a ogni riavvio. Le applicazioni reali hanno bisogno di persistenza, tipicamente tramite un database. Entity Framework Core, abbreviato in EF Core, è l'ORM ufficiale di .NET: un Object-Relational Mapper che permette di lavorare con il database usando oggetti C# anziché scrivere query SQL a mano. In questo articolo vediamo come definire un modello, configurare il contesto, generare lo schema ed eseguire le operazioni fondamentali.

Che cos'è un ORM

Un ORM colma la distanza tra il mondo orientato agli oggetti del codice e quello relazionale del database. Traduce automaticamente le classi in tabelle, le proprietà in colonne e le operazioni sugli oggetti in comandi SQL. Il risultato è che si manipolano collezioni di oggetti tipizzati, mentre EF Core si occupa di generare ed eseguire l'SQL corrispondente, riducendo il codice ripetitivo e gli errori.

Installare EF Core

EF Core è distribuito come pacchetto NuGet, e occorre aggiungere anche il provider specifico per il database scelto. Per un database SQLite, adatto a esempi e sviluppo, si installano i pacchetti relativi.

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design

Definire le entità

Un'entità è una classe che EF Core mappa su una tabella. Le proprietà diventano colonne, e per convenzione una proprietà chiamata Id viene riconosciuta come chiave primaria. Le entità possono anche riferirsi tra loro, modellando le relazioni del database.

class Product
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public decimal Price { get; set; }

    // Chiave esterna e proprieta di navigazione verso la categoria
    public int CategoryId { get; set; }
    public Category? Category { get; set; }
}

class Category
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // Una categoria ha molti prodotti
    public List<Product> Products { get; set; } = new();
}

Il DbContext

Il cuore di EF Core è la classe DbContext, che rappresenta una sessione con il database e funge da punto di accesso ai dati. Si definisce una classe derivata che espone le entità tramite proprietà di tipo DbSet<T>, ognuna corrispondente a una tabella.

using Microsoft.EntityFrameworkCore;

class AppDbContext : DbContext
{
    // Ogni DbSet rappresenta una tabella
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        // Configura la connessione al database SQLite
        options.UseSqlite("Data Source=app.db");
    }
}

Nelle applicazioni ASP.NET Core, la configurazione della connessione avviene di norma registrando il contesto nel contenitore di dependency injection, così che venga iniettato nei controller come qualsiasi altro servizio, tipicamente con ciclo di vita scoped.

Le migrazioni

Le migrazioni tengono sincronizzato lo schema del database con il modello a oggetti. A partire dalle entità, EF Core genera il codice necessario a creare o aggiornare le tabelle. Si usano gli strumenti a riga di comando per creare una migrazione e applicarla.

# Genera una migrazione a partire dal modello corrente
dotnet ef migrations add InitialCreate

# Applica le migrazioni, creando o aggiornando il database
dotnet ef database update

Ogni volta che il modello cambia, si crea una nuova migrazione che descrive la differenza rispetto alla precedente. Questo approccio versiona l'evoluzione dello schema in modo controllato e ripetibile.

Inserire dati

Con il contesto pronto, inserire un record significa aggiungere un oggetto al DbSet corrispondente e salvare le modifiche. EF Core traccia le modifiche e, alla chiamata di salvataggio, genera ed esegue i comandi SQL necessari.

using var context = new AppDbContext();

var category = new Category { Name = "Elettronica" };
var product = new Product
{
    Name = "Cuffie",
    Price = 49.99m,
    Category = category
};

context.Products.Add(product);

// Persiste le modifiche nel database
await context.SaveChangesAsync();

Interrogare i dati

Qui si chiude il cerchio con LINQ: le query su un DbSet si scrivono con gli stessi operatori visti nella serie intermedia. La differenza è che EF Core traduce l'espressione LINQ in SQL ed esegue l'interrogazione sul database, restituendo oggetti tipizzati.

// Query tradotta in SQL ed eseguita sul database
var expensiveProducts = await context.Products
    .Where(p => p.Price > 20m)
    .OrderBy(p => p.Name)
    .ToListAsync();

// Recupero di un singolo elemento per chiave primaria
Product? single = await context.Products.FindAsync(1);

Per includere i dati correlati, come la categoria di un prodotto, si usa il metodo Include, che istruisce EF Core a caricare anche l'entità collegata tramite una join.

// Carica i prodotti insieme alle rispettive categorie
var withCategory = await context.Products
    .Include(p => p.Category)
    .ToListAsync();

Aggiornare ed eliminare

Poiché EF Core traccia gli oggetti recuperati, aggiornare un record significa semplicemente modificarne le proprietà e salvare: il framework rileva le variazioni e genera l'SQL di aggiornamento. L'eliminazione avviene rimuovendo l'oggetto dal DbSet.

// Aggiornamento: modifica e salvataggio
var product = await context.Products.FindAsync(1);
if (product != null)
{
    product.Price = 39.99m;
    await context.SaveChangesAsync(); // genera l'UPDATE
}

// Eliminazione
if (product != null)
{
    context.Products.Remove(product);
    await context.SaveChangesAsync(); // genera il DELETE
}

Tracciamento e prestazioni

Il tracciamento delle modifiche è comodo ma ha un costo. Per le query di sola lettura, in cui non si intende modificare i dati, si può disattivarlo con AsNoTracking, ottenendo un miglioramento delle prestazioni. È una buona abitudine nelle API che espongono dati senza alterarli.

// Query di sola lettura piu efficiente
var readOnly = await context.Products
    .AsNoTracking()
    .ToListAsync();

Conclusione

Abbiamo introdotto Entity Framework Core come ORM che permette di lavorare con il database attraverso oggetti C#. Abbiamo definito le entità e le loro relazioni, configurato il DbContext con i suoi DbSet, gestito lo schema con le migrazioni ed eseguito le operazioni CRUD, interrogando i dati con lo stesso LINQ già conosciuto e ottimizzando le letture con AsNoTracking. Combinando questo con la Web API dell'articolo precedente, si dispone ormai di un'applicazione completa e persistente. Nel prossimo e ultimo articolo della serie garantiremo la qualità del codice con i test automatici usando xUnit.