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
- Estructura de la solució: diversos projectes .NET
BiblioTech.Domini: el model ja construït, sense canvisBiblioTech.Persistencia:IRepositoriBibliotecai les seves quatre implementacionsBiblioTech.Api: registrant serveis al contenidor de DI- Endpoints: catàleg, socis, préstecs, devolucions, metadades
Program.cscomplet- Aplicant patrons i estàndards ja vistos
- 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.Persistenciaflowchart 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).
BiblioTech.Domini: el model ja construït, sense canvis
BiblioTech.Domini: el model ja construït, sense canvisAquest 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ó.
BiblioTech.Persistencia: IRepositoriBiblioteca i les seves quatre implementacions
BiblioTech.Persistencia: IRepositoriBiblioteca i les seves quatre implementacionsAquest 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.
BiblioTech.Api: registrant serveis al contenidor de DI
BiblioTech.Api: registrant serveis al contenidor de DIAmb 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).
- 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) |
Program.cs complet
Program.cs completUnint 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.
- 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,IPoliticaSanciooIRepositoriBibliotecaja resolts pel contenidor, mai els crea ambnew. - Strategy (08-02):
IPoliticaSanciopermet canviar deSancioFixaaSancioProgressivaamb 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/catchsobreInvalidOperationExceptionaPOST /prestamostradueix 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,GuardarCatalegsegueixen exactament les mateixes convencions ja establertes a la Lliçó 1 del Mòdul 8.
Errors Comuns i Consells
- Referenciar
BiblioTech.Apides deBiblioTech.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
IPoliticaSanciocom aAddScopeden comptes deAddSingleton:SancioFixano 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 deAddSingleton. - 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 deBiblioteca.
Exercicis
-
Afegeix un endpoint
GET /socios/{id}que retorni les dades d'un soci concret fent servirbiblioteca.CercarSociPerId(id), retornant404 Not Foundsi no existeix. -
L'endpoint
POST /prestamos/{id}/devolucionde l'apartat 6 fa servirbiblioteca.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 dePrestecper 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#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
