Amb els requisits i la planificació de la lliçó anterior ja tancats, aquesta lliçó ensambla la Iteració 2 completa: l'API de BiblioTech. No s'escriu lògica de domini nova —tot el comportament ja existeix a Biblioteca, a IRepositoriBiblioteca i a les seves implementacions—; la feina consisteix a organitzar aquest codi en una estructura de solució amb diversos projectes, registrar correctament les dependències al contenidor de DI d'ASP.NET Core (recuperant 07-03 i 08-03), i exposar un endpoint per cadascun dels set requisits funcionals definits a la Lliçó 2.

Contingut

  1. Estructura de la solució: diversos projectes .NET
  2. BiblioTech.Domini: el model ja construït, sense canvis
  3. BiblioTech.Persistencia: IRepositoriBiblioteca i les seves quatre implementacions
  4. BiblioTech.Api: registrant serveis al contenidor de DI
  5. Endpoints: catàleg, socis, préstecs, devolucions, metadades
  6. Program.cs complet
  7. Aplicant patrons i estàndards ja vistos

  1. Estructura de la solució: diversos projectes .NET

Fins ara, cada lliçó del curs treballava amb un únic projecte de consola o un únic projecte de la tecnologia de torn. Un projecte real d'aquesta mida s'organitza millor en diversos projectes .NET separats, agrupats en una solució (.sln), cadascun amb una responsabilitat pròpia —el mateix principi de responsabilitat única de 08-01, aplicat ara a nivell de projecte en comptes de a nivell de classe:

dotnet new sln -n BiblioTech

dotnet new classlib -n BiblioTech.Domini
dotnet new classlib -n BiblioTech.Persistencia
dotnet new webapi -n BiblioTech.Api
dotnet new xunit -n BiblioTech.Proves

dotnet sln add BiblioTech.Domini BiblioTech.Persistencia BiblioTech.Api BiblioTech.Proves

dotnet add BiblioTech.Persistencia reference BiblioTech.Domini
dotnet add BiblioTech.Api reference BiblioTech.Domini BiblioTech.Persistencia
dotnet add BiblioTech.Proves reference BiblioTech.Domini BiblioTech.Persistencia
flowchart TD
    Api["BiblioTech.Api<br/>(ASP.NET Core Minimal API)"] --> Persistencia["BiblioTech.Persistencia<br/>(IRepositoriBiblioteca + 4 implementacions)"]
    Api --> Domini["BiblioTech.Domini<br/>(Biblioteca, MaterialBibliotecari, Soci, Prestec...)"]
    Persistencia --> Domini
    Proves["BiblioTech.Proves<br/>(xUnit + Moq, Modul 8)"] --> Domini
    Proves --> Persistencia

Les fletxes del diagrama són direccions de dependència (dotnet add reference): Domini no depèn de cap altre projecte —ni tan sols sap que existeix persistència o una API—, mentre que Persistencia i Api depenen d'ell. Aquesta direcció no és casualitat: és exactament el mateix principi que va motivar extreure IRepositoriBiblioteca a 08-03, ara aplicat a l'organització de carpetes i projectes, no només a les classes dins d'un únic projecte. Un projecte opcional BiblioTech.Web (un client Blazor) es tractaria exactament igual: dependria només del que fos estrictament necessari per consumir l'API per HTTP, sense referenciar directament BiblioTech.Domini ni BiblioTech.Persistencia (el client no coneix aquestes classes: només coneix els DTO que l'API exposa en JSON).

  1. BiblioTech.Domini: el model ja construït, sense canvis

Aquest projecte rep, sense modificar-hi ni una línia de comportament, les classes construïdes als Mòduls 2 a 4 i les polítiques de sanció del Mòdul 8:

BiblioTech.Domini/
├── MaterialBibliotecari.cs   // abstract class, Modul 3
├── Llibre.cs                 // Modul 3
├── Revista.cs                 // Modul 3
├── Soci.cs                    // Modul 3, amb SaldoPendent i AplicarSancio (08-05, exercici 2)
├── Prestec.cs                 // Modul 3, amb CalcularSancio(IPoliticaSancio) (08-02)
├── IPrestable.cs              // Modul 4
├── ICercable.cs                // Modul 4
├── IPoliticaSancio.cs         // Modul 8, amb SancioFixa i SancioProgressiva
└── Biblioteca.cs               // Modul 4, amb Cataleg/Socis/Prestecs i PrestarLlibreAsync

Traslladar aquestes classes a un projecte propi és, en si mateix, un exemple d'"extreure classe" a gran escala (08-05, apartat 4): el domini queda físicament separat de qualsevol detall de persistència o d'infraestructura web, quelcom que abans només s'aconseguia amb disciplina dins d'un únic projecte i ara ho imposa la mateixa estructura de la solució.

  1. BiblioTech.Persistencia: IRepositoriBiblioteca i les seves quatre implementacions

Aquest projecte rep, igualment sense canvis de comportament, la interfície i les quatre classes construïdes a 08-03:

BiblioTech.Persistencia/
├── IRepositoriBiblioteca.cs
├── RepositoriText.cs
├── RepositoriJson.cs
├── RepositoriSqlite.cs
├── RepositoriEntityFramework.cs
└── BibliotecaDbContext.cs     // Modul 5, Entity Framework Core

La Lliçó 2 va decidir Entity Framework Core com a mecanisme de persistència de producció; les altres tres implementacions no s'eliminen, simplement no es registren al contenidor de DI de BiblioTech.Api (apartat 4). Segueixen disponibles, per exemple, per a proves manuals ràpides sense base de dades, tal com ja es va veure a 08-03, apartat 7.

  1. BiblioTech.Api: registrant serveis al contenidor de DI

Amb l'estructura de projectes ja al seu lloc, el registre de serveis a Program.cs és pràcticament el mateix que es va construir a 08-03, apartat 7, ara amb la resta d'endpoints de l'apartat 5 afegits:

var builder = WebApplication.CreateBuilder(args);

// Persistencia: Entity Framework Core sobre SQLite, decisio de la Llico 2
builder.Services.AddDbContext<BibliotecaDbContext>(opcions =>
    opcions.UseSqlite(builder.Configuration.GetConnectionString("BiblioTech")));

// IRepositoriBiblioteca resolt com a RepositoriEntityFramework (08-03)
builder.Services.AddScoped<IRepositoriBiblioteca, RepositoriEntityFramework>();

// Biblioteca depen de IRepositoriBiblioteca, mateix cicle de vida que BibliotecaDbContext (08-03)
builder.Services.AddScoped<Biblioteca>();

// Politica de sancio: Strategy del Modul 8, sense estat propi, valida com a Singleton
builder.Services.AddSingleton<IPoliticaSancio, SancioFixa>();

Cal notar la diferència deliberada respecte al builder.Configuration.GetConnectionString(...) de 07-03, on la cadena de connexió estava escrita directament al codi: aquí es llegeix de la configuració (appsettings.json), un avançament de la configuració per entorn que es completarà a la Lliçó 5, i una aplicació directa del requisit no funcional de seguretat bàsica de la Lliçó 2 (no deixar cadenes de connexió de producció escrites al codi font).

  1. Endpoints: catàleg, socis, préstecs, devolucions, metadades

Cada endpoint correspon, de forma directa, a un dels set requisits funcionals (RF1-RF7) de la Lliçó 2:

Endpoint Requisit Què fa
GET /libros RF3 (parcial: llistar) Retorna biblioteca.Cataleg
GET /libros/buscar?q=... RF3 Filtra Cataleg amb LINQ (Mòdul 4)
POST /libros RF1 Afegeix un material amb AfegirMaterial
DELETE /libros/{isbn} RF2 Elimina un material del catàleg
POST /socios RF4 Afegeix un soci amb AfegirSoci
POST /prestamos RF5 Registra un préstec amb PrestarLlibreAsync
POST /prestamos/{id}/devolucion RF6 Crida RegistrarDevolucio() i CalcularSancio(politica)
GET /libros/{isbn}/metadatos RF7 Crida ObtenirMetadadesPerIsbnAsync (Mòdul 5)

  1. Program.cs complet

Unint totes les peces anteriors, així queda el Program.cs complet de BiblioTech.Api:

using BiblioTech.Domini;
using BiblioTech.Persistencia;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// --- Registre de serveis (contenidor de DI, 08-03) ---
builder.Services.AddDbContext<BibliotecaDbContext>(opcions =>
    opcions.UseSqlite(builder.Configuration.GetConnectionString("BiblioTech")));
builder.Services.AddScoped<IRepositoriBiblioteca, RepositoriEntityFramework>();
builder.Services.AddScoped<Biblioteca>();
builder.Services.AddSingleton<IPoliticaSancio, SancioFixa>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); // interficie de prova interactiva, 07-03 apartat 8

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// --- RF3: llistar i cercar al cataleg ---
app.MapGet("/libros", (Biblioteca biblioteca) =>
{
    biblioteca.CarregarCataleg();
    return Results.Ok(biblioteca.Cataleg);
});

app.MapGet("/libros/buscar", (string q, Biblioteca biblioteca) =>
{
    biblioteca.CarregarCataleg();
    List<MaterialBibliotecari> resultats = biblioteca.Cataleg
        .Where(m => m.Titol.Contains(q, StringComparison.OrdinalIgnoreCase)
                 || m.Autor.Contains(q, StringComparison.OrdinalIgnoreCase))
        .ToList(); // LINQ, Modul 4

    return Results.Ok(resultats);
});

// --- RF1: alta de material ---
app.MapPost("/libros", (Llibre llibre, Biblioteca biblioteca) =>
{
    biblioteca.AfegirMaterial(llibre);
    biblioteca.GuardarCataleg();
    return Results.Created($"/libros/{llibre.Isbn}", llibre);
});

// --- RF2: baixa de material ---
app.MapDelete("/libros/{isbn}", (string isbn, Biblioteca biblioteca) =>
{
    biblioteca.CarregarCataleg();
    MaterialBibliotecari? material = biblioteca.Cataleg
        .FirstOrDefault(m => m is Llibre llibre && llibre.Isbn == isbn);

    if (material is null)
    {
        return Results.NotFound($"No existeix cap llibre amb ISBN {isbn}.");
    }

    biblioteca.Cataleg.Remove(material);
    biblioteca.GuardarCataleg();
    return Results.NoContent();
});

// --- RF4: alta de soci ---
app.MapPost("/socios", (Soci soci, Biblioteca biblioteca) =>
{
    biblioteca.AfegirSoci(soci);
    return Results.Created($"/socios/{soci.Id}", soci);
});

// --- RF5: registrar prestec ---
app.MapPost("/prestamos", async (PeticioPrestec peticio, Biblioteca biblioteca) =>
{
    Llibre? llibre = biblioteca.Cataleg.OfType<Llibre>()
        .FirstOrDefault(l => l.Isbn == peticio.Isbn);
    Soci? soci = biblioteca.CercarSociPerId(peticio.IdSoci);

    if (llibre is null || soci is null)
    {
        return Results.BadRequest("ISBN o soci no trobats.");
    }

    try
    {
        await biblioteca.PrestarLlibreAsync(llibre, soci); // valida disponibilitat i persisteix (08-05)
    }
    catch (InvalidOperationException ex)
    {
        return Results.Conflict(ex.Message);
    }

    return Results.Ok(biblioteca.Prestecs.Last());
});

// --- RF6: registrar devolucio, amb calcul de sancio ---
app.MapPost("/prestamos/{id}/devolucion", (int id, Biblioteca biblioteca, IPoliticaSancio politica) =>
{
    Prestec? prestec = biblioteca.Prestecs.ElementAtOrDefault(id);

    if (prestec is null)
    {
        return Results.NotFound($"No existeix el prestec #{id}.");
    }

    prestec.RegistrarDevolucio();
    decimal sancio = prestec.CalcularSancio(politica);

    if (sancio > 0)
    {
        prestec.Soci.AplicarSancio(sancio); // 08-05, exercici 2
    }

    biblioteca.GuardarCataleg();
    return Results.Ok(new { prestec.DataDevolucio, Sancio = sancio });
});

// --- RF7: consulta de metadades externes ---
app.MapGet("/libros/{isbn}/metadatos", async (string isbn, Biblioteca biblioteca) =>
{
    MetadadesLlibreExtern? metadades = await biblioteca.ObtenirMetadadesPerIsbnAsync(isbn);

    return metadades is not null
        ? Results.Ok(metadades)
        : Results.NotFound($"No hi ha metadades externes disponibles per a l'ISBN {isbn}.");
});

app.Run();

// DTO (Modul 3, record): el cos esperat d'un POST /prestamos
record PeticioPrestec(string Isbn, int IdSoci);

Cap endpoint conté lògica de negoci pròpia: cadascun es limita a extreure dades de la petició HTTP (el mateix mecanisme de binding de 07-03), cridar un mètode ja existent de Biblioteca o de les seves dependències, i traduir el resultat a una resposta HTTP amb Results.Ok/Results.NotFound/Results.Conflict. Aquest és, precisament, el senyal que l'arquitectura desacoblada del Mòdul 8 ha funcionat com s'esperava: l'API és una capa fina sobre un domini que ja sabia fer tot això des de molt abans.

  1. Aplicant patrons i estàndards ja vistos

Aquest Program.cs, encara que nou com a fitxer, no introdueix cap pràctica que no s'hagi vist ja al curs:

  • Injecció per constructor/paràmetre (08-03): cada endpoint rep Biblioteca, IPoliticaSancio o IRepositoriBiblioteca ja resolts pel contenidor, mai els crea amb new.
  • Strategy (08-02): IPoliticaSancio permet canviar de SancioFixa a SancioProgressiva amb una única línia al registre de serveis, sense tocar l'endpoint de devolució.
  • Gestió d'excepcions com a control de flux esperat (Mòdul 2, 08-05): el try/catch sobre InvalidOperationException a POST /prestamos tradueix un error de domini esperat (llibre no disponible) a un codi HTTP concret (409 Conflict), en comptes de deixar que es propagui com un error 500 genèric.
  • Nomenclatura consistent (08-01): PeticioPrestec, AfegirMaterial, GuardarCataleg segueixen exactament les mateixes convencions ja establertes a la Lliçó 1 del Mòdul 8.

Errors Comuns i Consells

  • Referenciar BiblioTech.Api des de BiblioTech.Domini: invertiria la direcció de dependència del diagrama de l'apartat 1; el domini mai ha de conèixer l'existència de la capa web que el consumeix.
  • Registrar IPoliticaSancio com a AddScoped en comptes de AddSingleton: SancioFixa no desa cap estat propi entre crides (apartat 4 de 08-03), així que es pot compartir de forma segura durant tota la vida de l'aplicació; fer servir un cicle de vida més curt del necessari no és incorrecte, però malbarata el motiu de ser de AddSingleton.
  • Escriure lògica de negoci directament dins d'un endpoint (per exemple, calcular la sanció a mà en comptes de cridar prestec.CalcularSancio(politica)): duplicaria lògica que ja existeix i ja està provada (Mòdul 8, Lliçó 4), trencant l'avantatge principal de reutilitzar el domini existent.
  • Consell: si un endpoint comença a superar 4-5 línies de lògica pròpia (sense comptar l'extracció de paràmetres ni la traducció a Results.*), és un senyal de "mètode llarg" (08-05) a nivell d'endpoint: probablement falta extreure aquest fragment a un mètode de Biblioteca.

Exercicis

  1. Afegeix un endpoint GET /socios/{id} que retorni les dades d'un soci concret fent servir biblioteca.CercarSociPerId(id), retornant 404 Not Found si no existeix.

  2. L'endpoint POST /prestamos/{id}/devolucion de l'apartat 6 fa servir biblioteca.Prestecs.ElementAtOrDefault(id) per localitzar el préstec per posició a la llista. Explica per què això és fràgil en un sistema real, i què canviaries al model de Prestec per resoldre-ho de forma més robusta.

Solucions

app.MapGet("/socios/{id}", (int id, Biblioteca biblioteca) =>
{
    Soci? soci = biblioteca.CercarSociPerId(id);

    return soci is not null
        ? Results.Ok(soci)
        : Results.NotFound($"No existeix cap soci amb Id {id}.");
});

Fer servir la posició dins de biblioteca.Prestecs com si fos un identificador és fràgil perquè aquesta posició canvia si s'elimina algun préstec de la llista, o si l'ordre d'inserció varia (per exemple, en recarregar el catàleg des del repositori); dues peticions diferents podrien referir-se, sense adonar-se'n, a préstecs diferents. La solució robusta és afegir una propietat Id pròpia a Prestec (seguint el mateix patró que ja té Soci.Id, Mòdul 3), assignada de forma única en crear cada préstec, i cercar per aquest Id en comptes de per posició.

Conclusió

Aquesta lliçó ha ensamblat la Iteració 2 completa del projecte: una estructura de solució amb diversos projectes .NET amb responsabilitats separades, el registre d'IRepositoriBiblioteca i Biblioteca al contenidor de DI d'ASP.NET Core recuperant 07-03 i 08-03, i un Program.cs complet amb un endpoint per cadascun dels set requisits funcionals de la Lliçó 2, sense cap lògica de negoci nova que no existís ja al domini de BiblioTech. L'API funciona, però encara no s'ha verificat amb proves més enllà de les unitàries del Mòdul 8, ni s'ha revisat amb cap criteri de qualitat formal. La lliçó següent, Proves i Depuració, completa aquesta feina: amplia la bateria de proves amb proves d'integració sobre aquests mateixos endpoints, i introdueix el checklist de qualitat que ha de superar-se abans de passar a la Lliçó 5, Desplegament.

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