La lliçó anterior va deixar clar el principal maldecap de l'ADO.NET clàssic: cada consulta exigeix escriure SQL a mà, gestionar paràmetres un a un, i mapejar manualment cada columna cap a cada propietat d'un objecte. Un ORM (Object-Relational Mapper, mapador objecte-relacional) automatitza precisament aquesta feina repetitiva: tradueix operacions sobre objectes i col·leccions de C# al SQL equivalent, i reconstrueix objectes a partir dels resultats sense codi manual de mapeig. Aquesta lliçó presenta Entity Framework Core (EF Core), l'ORM oficial de .NET, i l'aplica al model de domini de BiblioTech: Llibre, Soci i Prestec passen a viure en una base de dades real, consultables amb la mateixa sintaxi LINQ que ja coneixes des del Mòdul 4, i desats amb el mateix async/await de la lliçó de Programació Asíncrona.

Contingut

  1. Què és un ORM i quin problema resol davant l'ADO.NET manual
  2. Instal·lar Entity Framework Core i el proveïdor de SQLite
  3. DbContext i DbSet<T>: mapejant el domini de BiblioTech
  4. Configurar el model sense tocar les classes de domini: Fluent API a OnModelCreating
  5. Migracions: dotnet ef migrations add i dotnet ef database update
  6. Consultes LINQ contra un DbSet<T>
  7. Desar canvis: SaveChanges() i SaveChangesAsync()

  1. Què és un ORM i quin problema resol davant l'ADO.NET manual

Un ORM resol l'anomenat "desajust d'impedància" (impedance mismatch) entre dos mons que s'organitzen de forma diferent: els objectes de C# (amb propietats, herència, col·leccions imbricades) i les taules d'una base de dades relacional (files, columnes, claus foranes). A la lliçó anterior, aquest desajust es resolia a mà, línia a línia:

ADO.NET manual (lliçó anterior) Entity Framework Core (aquesta lliçó)
Escriure el SQL de cada consulta A mà, com a text Generat automàticament a partir de LINQ
Mapejar files a objectes A mà, columna a columna Automàtic, segons el model configurat
Rastrejar què ha canviat per desar-ho El programador decideix quin UPDATE/INSERT executar EF Core detecta els canvis i genera el SQL necessari
Control sobre el SQL exacte Total Alt nivell, amb la possibilitat de baixar a SQL manual si cal

Un ORM no substitueix del tot l'ADO.NET: per sota, EF Core continua fent servir ADO.NET per comunicar-se amb la base de dades; el que aporta és una capa d'abstracció que estalvia escriure aquest SQL repetitiu a mà en el dia a dia.

  1. Instal·lar Entity Framework Core i el proveïdor de SQLite

EF Core s'instal·la com un conjunt de paquets NuGet, més una eina de línia d'ordres per gestionar migracions (apartat 5):

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef

Microsoft.EntityFrameworkCore.Sqlite inclou el motor d'EF Core més el proveïdor específic de SQLite (cada motor de base de dades té el seu propi paquet de proveïdor, seguint la mateixa idea de "proveïdor" ja vista amb ADO.NET). Microsoft.EntityFrameworkCore.Design i l'eina global dotnet-ef són necessàries per generar i aplicar migracions des del terminal.

  1. DbContext i DbSet<T>: mapejant el domini de BiblioTech

El punt d'entrada d'EF Core és una classe que hereta de DbContext, amb una propietat DbSet<T> per cada tipus que es vol persistir. Cada DbSet<T> representa, conceptualment, "la taula de T a la base de dades":

using Microsoft.EntityFrameworkCore;

class BibliotecaDbContext : DbContext
{
    public DbSet<Llibre> Llibres { get; set; } = null!;
    public DbSet<Soci> Socis { get; set; } = null!;
    public DbSet<Prestec> Prestecs { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder opcions)
    {
        opcions.UseSqlite("Data Source=bibliotech_ef.db");
    }
}

OnConfiguring indica a quina base de dades connectar-se (aquí, SQLite, amb la mateixa cadena de connexió que a la lliçó anterior); en aplicacions més grans aquesta configuració se sol injectar des de fora mitjançant DbContextOptions, però per a un exemple autocontingut n'hi ha prou amb OnConfiguring. L'operador = null! (el ! és l'operador null-forgiving de C#, recordant la lliçó de Pattern Matching i Característiques Modernes del Mòdul 4) li diu al compilador "confia que aquesta propietat no serà null en temps d'execució", ja que EF Core l'assigna automàticament en construir el DbContext, encara que el compilador no ho pugui veure per si sol.

Llibre i Soci ja són perfectament vàlids per a EF Core tal com estan definits des de Mòduls anteriors: no cal modificar-los. Prestec, amb les seves propietats de només lectura (Llibre, Soci, DataPrestec) i el seu constructor Prestec(Llibre llibre, Soci soci), és una mica més particular; l'apartat següent explica com EF Core el mapeja sense tocar aquesta classe.

  1. Configurar el model sense tocar les classes de domini: Fluent API a OnModelCreating

EF Core ofereix dues maneres d'indicar detalls del mapeig que no pot deduir per convenció (com quina és la clau primària de cada taula): Data Annotations (atributs com [Key] directament sobre les propietats del model) o Fluent API (codi de configuració centralitzat, dins del mateix DbContext, sense tocar les classes de domini). Aquesta lliçó fa servir Fluent API precisament per no haver d'afegir atributs de persistència a Llibre, Soci i Prestec —classes que, fins ara, no sabien res de bases de dades ni d'EF Core, i que continuen sense saber-ho:

class BibliotecaDbContext : DbContext
{
    public DbSet<Llibre> Llibres { get; set; } = null!;
    public DbSet<Soci> Socis { get; set; } = null!;
    public DbSet<Prestec> Prestecs { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder opcions)
    {
        opcions.UseSqlite("Data Source=bibliotech_ef.db");
    }

    protected override void OnModelCreating(ModelBuilder generadorModel)
    {
        generadorModel.Entity<Llibre>().HasKey(llibre => llibre.Isbn);
        generadorModel.Entity<Soci>().HasKey(soci => soci.Id);

        // Prestec no te cap propietat pensada com a identificador propi;
        // se li afegeix una clau "ombra" (shadow property), que existeix a la base
        // de dades pero no com a propietat visible a la classe C#.
        generadorModel.Entity<Prestec>().Property<int>("PrestecId");
        generadorModel.Entity<Prestec>().HasKey("PrestecId");
    }
}

HasKey(llibre => llibre.Isbn) indica que Isbn és la clau primària de la taula Llibres (igual que a la taula SQLite de la lliçó anterior); HasKey(soci => soci.Id) fa el mateix amb Soci.Id, que a més ja era de només lectura per disseny (lliçó d'Encapsulació) —un bon candidat natural a clau primària. Per a Prestec, que no té cap propietat pensada com a identificador únic, es defineix una propietat ombra ("PrestecId"): existeix com a columna a la base de dades i com a clau primària interna d'EF Core, però no apareix com a propietat a la classe Prestec de C#. Aquest és un dels avantatges pràctics d'un ORM madur com EF Core: el model de domini pot romandre net, centrat en les regles de negoci, sense barrejar-se amb detalls de persistència.

EF Core també necessita saber com es relaciona Prestec amb Llibre i Soci; per convenció, en detectar les propietats Llibre i Soci dins de Prestec, genera automàticament columnes de clau forana (dues propietats ombra més, típicament LlibreIsbn i SociId) sense necessitar configuració explícita addicional per a aquest exemple.

  1. Migracions: dotnet ef migrations add i dotnet ef database update

Una migració és un fitxer de codi C#, generat automàticament, que descriu com transformar l'esquema de la base de dades perquè coincideixi amb el model actual (quines taules crear, quines columnes afegir...). Es generen i s'apliquen amb l'eina dotnet ef des del terminal, a la carpeta del projecte:

dotnet ef migrations add InicialBiblioTech
dotnet ef database update
Ordre Què fa
dotnet ef migrations add <Nom> Compara el model actual (DbSet<T> + configuració d'OnModelCreating) amb l'historial de migracions, i genera una nova migració amb els canvis detectats
dotnet ef database update Aplica contra la base de dades real totes les migracions pendents, creant o modificant taules segons calgui

dotnet ef migrations add InicialBiblioTech crea una carpeta Migrations/ amb dos fitxers per migració: un amb el codi que aplica el canvi (Up()) i un altre amb el que el desfà (Down()), més un fitxer d'"instantània" del model complet tal com va quedar després d'aquella migració. Cada vegada que el model canviï (una propietat nova a Llibre, per exemple), es genera una nova migració amb migrations add i s'aplica amb database update; EF Core calcula automàticament només la diferència respecte a la migració anterior, sense necessitat d'escriure l'ALTER TABLE a mà.

  1. Consultes LINQ contra un DbSet<T>

Aquí és on EF Core connecta directament amb una cosa que ja domines: DbSet<T> implementa IQueryable<T> (una extensió d'IEnumerable<T>, la interfície que tanca la lliçó de Col·leccions), així que es consulta amb exactament la mateixa sintaxi LINQ de la lliçó de LINQ —només que, per sota, EF Core tradueix aquestes operacions a SQL i l'executa contra la base de dades, en lloc de recórrer una col·lecció ja carregada a memòria:

using BibliotecaDbContext context = new BibliotecaDbContext();

List<Llibre> llibresDisponibles = context.Llibres
    .Where(llibre => llibre.Disponible)
    .OrderBy(llibre => llibre.Titol)
    .ToList();

foreach (Llibre llibre in llibresDisponibles)
{
    Console.WriteLine(llibre.Titol);
}

Llibre? rayuela = context.Llibres.FirstOrDefault(llibre => llibre.Isbn == "978-84-376-0495-4");

Where, OrderBy, FirstOrDefault... són els mateixos operadors de la lliçó de LINQ, escrits exactament igual; la diferència és que aquí, context.Llibres.Where(...) es tradueix internament a una sentència SELECT ... WHERE ... a SQLite, en lloc de filtrar una llista ja existent a memòria. BibliotecaDbContext, igual que SqliteConnection a la lliçó anterior, implementa IDisposable: es declara amb using pel mateix motiu.

  1. Desar canvis: SaveChanges() i SaveChangesAsync()

Afegir, modificar o eliminar entitats a través d'un DbContext no impacta immediatament la base de dades: EF Core rastreja els canvis a memòria, i només els tradueix a SQL (INSERT, UPDATE, DELETE) quan es crida explícitament SaveChanges() (o el seu equivalent asíncron SaveChangesAsync(), recordant async/await de la lliçó de Programació Asíncrona):

using BibliotecaDbContext context = new BibliotecaDbContext();

Llibre nouLlibre = new Llibre("El Aleph", "Jorge Luis Borges", "978-84-376-0497-8");
context.Llibres.Add(nouLlibre);

await context.SaveChangesAsync(); // aqui, i nomes aqui, s'executa l'INSERT real contra SQLite

Console.WriteLine("Llibre desat correctament.");
using BibliotecaDbContext context = new BibliotecaDbContext();

Llibre? llibre = context.Llibres.FirstOrDefault(l => l.Isbn == "978-84-376-0497-8");
if (llibre is not null)
{
    llibre.Prestar(); // canvia Disponible a false; EF Core detecta aquest canvi automaticament
    await context.SaveChangesAsync(); // genera i executa l'UPDATE necessari
}

En el segon exemple no cal cap context.Llibres.Update(...) explícit: com que llibre es va obtenir a través del propi context, EF Core ja l'està rastrejant (change tracking) i detecta, en cridar SaveChangesAsync(), que la seva propietat Disponible ha canviat, generant l'UPDATE corresponent. Aquesta és una de les diferències més visibles davant l'ADO.NET manual, on cada UPDATE calia escriure'l i executar-lo explícitament.

classDiagram
    class BibliotecaDbContext {
        +DbSet~Llibre~ Llibres
        +DbSet~Soci~ Socis
        +DbSet~Prestec~ Prestecs
        #OnConfiguring(DbContextOptionsBuilder)
        #OnModelCreating(ModelBuilder)
    }
    BibliotecaDbContext --|> DbContext
    BibliotecaDbContext --> "*" Llibre
    BibliotecaDbContext --> "*" Soci
    BibliotecaDbContext --> "*" Prestec

Errors Comuns i Consells

  • Oblidar SaveChanges()/SaveChangesAsync(): afegir una entitat amb context.Llibres.Add(...) no la persisteix per si sola; sense cridar SaveChanges(), el canvi només existeix a memòria i es perd en tancar el DbContext.
  • No generar ni aplicar una migració després de canviar el model: si s'afegeix una propietat nova a Llibre sense executar dotnet ef migrations add i dotnet ef database update, la base de dades real queda desincronitzada respecte al model, i les consultes fallaran en temps d'execució en no trobar la columna esperada.
  • Oblidar HasKey per a un tipus sense una propietat d'identificador natural: EF Core exigeix que tota entitat tingui una clau primària; si cap propietat serveix com a tal (el cas de Prestec), cal definir una propietat ombra explícitament amb Property<T>() + HasKey() a OnModelCreating.
  • Crear un DbContext nou per cada operació petita sense necessitat, o reutilitzar-ne un de sol durant massa temps: la pràctica habitual a EF Core és una vida curta per DbContext (típicament, un per operació o per petició HTTP en una API web, tema del Mòdul 7), ni un de compartit per a tota l'aplicació ni un de diferent per cada línia de codi.
  • Consell: fes servir sempre les consultes LINQ per llegir dades, i dosifica el SQL manual (possible també des d'EF Core, amb FromSqlRaw, fora de l'abast d'aquesta lliçó) per als casos puntuals on LINQ no expressi bé el que necessites; per a la immensa majoria d'operacions del dia a dia, LINQ contra un DbSet<T> és més llegible i menys propens a errors que el SQL escrit a mà.

Exercicis

  1. Defineix BibliotecaDbContext amb DbSet<Llibre> Llibres i DbSet<Soci> Socis, configurant HasKey per a tots dos a OnModelCreating tal com s'ha mostrat en aquesta lliçó. Genera la migració inicial amb dotnet ef migrations add i aplica-la amb dotnet ef database update.

  2. Fent servir el BibliotecaDbContext de l'exercici anterior, afegeix dos Llibre nous amb context.Llibres.Add(...) i desa els canvis amb SaveChangesAsync(). Després, en una nova instància de BibliotecaDbContext, consulta amb LINQ (Where + OrderBy) els llibres disponibles ordenats per títol.

  3. Recupera un Llibre existent amb FirstOrDefault pel seu Isbn, crida Prestar() sobre ell, i desa els canvis amb SaveChangesAsync() sense cridar cap mètode Update explícit. Torna a consultar-lo en una nova instància del context i comprova que Disponible ara és false.

Solucions

class BibliotecaDbContext : DbContext
{
    public DbSet<Llibre> Llibres { get; set; } = null!;
    public DbSet<Soci> Socis { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder opcions)
    {
        opcions.UseSqlite("Data Source=bibliotech_ef.db");
    }

    protected override void OnModelCreating(ModelBuilder generadorModel)
    {
        generadorModel.Entity<Llibre>().HasKey(llibre => llibre.Isbn);
        generadorModel.Entity<Soci>().HasKey(soci => soci.Id);
    }
}
dotnet ef migrations add InicialBiblioTech
dotnet ef database update
using (BibliotecaDbContext context = new BibliotecaDbContext())
{
    context.Llibres.Add(new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
    context.Llibres.Add(new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1"));
    await context.SaveChangesAsync();
}

using (BibliotecaDbContext altreContext = new BibliotecaDbContext())
{
    List<Llibre> disponibles = altreContext.Llibres
        .Where(l => l.Disponible)
        .OrderBy(l => l.Titol)
        .ToList();

    foreach (Llibre llibre in disponibles)
    {
        Console.WriteLine(llibre.Titol);
    }
}
using (BibliotecaDbContext context = new BibliotecaDbContext())
{
    Llibre? llibre = context.Llibres.FirstOrDefault(l => l.Isbn == "978-84-376-0495-4");
    if (llibre is not null)
    {
        llibre.Prestar();
        await context.SaveChangesAsync();
    }
}

using (BibliotecaDbContext altreContext = new BibliotecaDbContext())
{
    Llibre? llibreActualitzat = altreContext.Llibres.FirstOrDefault(l => l.Isbn == "978-84-376-0495-4");
    Console.WriteLine(llibreActualitzat?.Disponible); // False
}

Conclusió

En aquesta lliçó has conegut Entity Framework Core: quin problema resol un ORM davant l'ADO.NET manual, com definir un DbContext amb DbSet<T> per a cada tipus del domini de BiblioTech, com configurar el model amb Fluent API sense embrutar les classes de domini amb atributs de persistència, com generar i aplicar migracions, i com consultar i desar dades reutilitzant LINQ i async/await, dues eines que ja dominaves des del Mòdul 4. Llibre, Soci i Prestec ja viuen en una base de dades relacional real, gestionada per un ORM en lloc de SQL escrit a mà.

Tot el que s'ha vist fins ara al Mòdul 5 assumeix que BiblioTech és l'única aplicació que llegeix i escriu aquestes dades. L'última lliçó del mòdul, Treball amb JSON i Consum d'APIs REST, fa un pas més: aprofundeix en System.Text.Json per a escenaris més complexos, i fa servir HttpClient perquè BiblioTech consulti informació addicional des d'un servei extern —l'escenari habitual de qualsevol aplicació moderna que no viu aïllada, sinó que es comunica amb altres sistemes a través d'una API REST.

Curs de Programació en C#

Mòdul 1: Introducció al C#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats