This is an old revision of the document!
Acest laborator este o continuare a laboratorului precedent si va finaliza structura proiectului. Vom discuta despre cum am structurat partea de Api a proiectului si vom prezenta detalii despre tehnologiile utilizate, atat din acest laborator, cat si din cel precedent.
Va recomandam sa aveti deschis tutorialul oficial de la microsoft pentru .NET 6 minimal API.
In plus, va recomandam sa utilizati canalul de Teams “Discutii Laborator” pentru orice intrebare referitoare la laborator.
Api-ul este al 3-lea proiect creat in cadrul solutiei noastre. Rolul sau este sa expuna rute catre exterior si sa foloseasca elementele de domeniu (core) si infrastructura pentru a realiza actiuni.
In general, cand se urmareste arhitectura Clean, in proiectul de API trebuie scrise configuratiile de server (printre care si dependency injection), middlewares si rute.
Incepand cu .NET 6, Program.cs este punctul de acces si configurare al unui server. Pana in .NET 6, Program.cs era folosit ca punct de acces si Startup.cs servea rol de fisier de configurare.
using BookLibrary.Api.Authorization; using BookLibrary.Api.Infrastructure; using BookLibrary.Api.Web; using MediatR; using System.Reflection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(options => { options.Filters.Add<ApiResponseExceptionFilter>(); }); builder.Services.AddEndpointsApiExplorer(); // Add Swagger with Bearer Configuration builder.Services.AddSwaggerWithBearerConfig(); // Add Authentication configuration builder.AddAuthenticationAndAuthorization(); // Add Database Context builder.AddBookLibraryDbContext(); // Add MediatR builder.Services.AddMediatR(Assembly.GetExecutingAssembly()); // Add Repositories builder.Services.AddBookLibraryAggregateRepositories(); // Add Api Features Handlers builder.Services.AddApiFeaturesHandlers(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } else { app.UseHttpsRedirection(); } app.UseAuthenticationAndAuthorization(); app.UseHttpLogging(); app.MapControllers(); app.Run();
Obiectul builder este folosit pentru configurarea serverului. Configurarea serverului implica injectarea obiectelor necesare la runtime. Un exemplu este:
builder.Services.AddEndpointsApiExplorer();
Secventa de cod de mai sus face posibila descoperirea ruterlor scrise de noi de catre server.
Obiectul app este folosit pentru adaugarea middlewares care sunt executate in momentul in care serverul primeste un request. Imaginati-va middlewares ca o serie de functii care sunt apelate una dupa alta.
app.UseHttpLogging();
Secventa de cod de mai introduce logging de request la fieacare request, sub forma unui middleware.
.NET foloseste foarte mult dependency injection. Si obiectul builder, prin apelarea acelor metode, de fapt introduce obiecte in containerul de depdendente folosind dependency injection.
Dependency Injection, pe scurt, se refera la injectarea obiectelor de care este nevoie, fara sa fie nevoie sa fie construite pe loc.
Propunem urmatorul scenariu: Obiectul A foloseste Obiectul B. Acest lucru se poate face in doua moduri:
Pentru a folosi dependency injection in .NET este nevoie de 2 lucruri:
Alt principiu folosit in .NET, in special pentru configurari, este Metoda extensie.
Metodele extensie reprezinta o metoda care este scrisa in numele altui obiect. Exemplu:
using Microsoft.AspNetCore.Authentication.JwtBearer; namespace BookLibrary.Api.Authorization { public static class AuthorizationExtensions { public static void AddAuthenticationAndAuthorization(this WebApplicationBuilder builder) { builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.Authority = builder.Configuration["Auth:Authority"]; options.Audience = builder.Configuration["Auth:Audience"]; }); // Add Authorization configuration builder.Services.AddAuthorization(configure => { configure.AddPolicy("AdminAccess", policy => policy.RequireClaim("permissions", "book-library:admin")); }); } //... } }
Utilizarea metodelor de extensie ajuta prin imbogatirea functionalitatilor unui obiect la care nu exista acces. In exemplul de mai sus, am adaugat functionalitate obiectului builder, pentru a putea configura aplicatia.
In cadrul proiectului nostru, metodele de extensie sunt grupate in functie de scopul lor:
Folderul Authorization contine metode de extensie pentru configurarea autentificarii si autorizarii la nivel de API, precum si pentru a extrage informatii din utilizatorul autentificat.
Folderul Infrastructure contine metode de extensie pentru dependency injection a repository-urilor definite in Core (sub forma de interfete) si implementate in Infrastructture.
public static void AddBookLibraryAggregateRepositories(this IServiceCollection services) { services.AddTransient<IBooksRepository, BooksRepository>(); services.AddTransient<IUsersRentalsRepository, UsersRentalsRepository>(); }
Pe langa injectarea repository-urilor, am configurat si contextul bazei de date, instruind serverul sa foloseasca Microsoft SQL Server cu configuratia scrisa de noi in appsettings.json.
public static void AddBookLibraryDbContext(this WebApplicationBuilder builder) { builder.Services.AddDbContext<BookLibraryContext>(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("BookLibraryDb"))); }
public class BooksRepository : IBooksRepository { private readonly BookLibraryContext context; public BooksRepository(BookLibraryContext context) { this.context = context; } // ... }
Folderul Web contine metode de extensie pentru Swagger si features scrise de noi, o exceptie custom si un filtru de exceptie pentru exceptia noastra.
using BookLibrary.Api.Features.Books; using BookLibrary.Api.Features.Metrics; using BookLibrary.Api.Features.Profile; using BookLibrary.Api.Features.Rentals; namespace BookLibrary.Api.Web { public static class ApiFeaturesExtensions { public static void AddApiFeaturesHandlers(this IServiceCollection services) { // Add Book Handlers services.AddBooksHandlers(); // Add Profile Handlers services.AddProfilesHandlers(); // Add Rentals Handlers services.AddRentalsHandlers(); // Add Metrics Handlers services.AddMetricsHandlers(); } } }
using BookLibrary.Api.Features.Books.AddBook; using BookLibrary.Api.Features.Books.ViewAllBooks; using BookLibrary.Api.Features.Books.ViewStatusAboutBook; namespace BookLibrary.Api.Features.Books { internal static class BooksModule { internal static void AddBooksHandlers(this IServiceCollection services) { services.AddTransient<IAddBookCommandHandler, AddBookCommandHandler>(); services.AddTransient<IViewAllBooksQueryHandler, ViewAllBooksQueryHandler>(); services.AddTransient<IViewStatusAboutBookQueryHandler, ViewStatusAboutBookQueryHandler>(); } } }
Asa cum am prezentat si la curs, si in laboratoarele precedente, in cadrul arhitecturii de cod am introdus si o versiune simplificata a Vertical Slicing.
Functionalitatile descrise in cadrul User Stories sunt grupate in functie de scopul lor. In cazul nostru, avem 4 grupuri de functionalitati:
Fiecare folder de grupuri de functionalitati are urmatoarele componente:
Aplicand aceasta strategie, ne asiguram ca in cadrul unui folder de functionalitati (ex: AddBook) avem tot codul scris pentru functionalitatea respectiva, iar in cadrul unui folder de grup de functionalitati (ex: Books) avem codul pentru toate functionalitatile respective.
Exemplu de handler care adauga o carte in sistem:
using BookLibrary.Core.Domain.Book; namespace BookLibrary.Api.Features.Books.AddBook { public class AddBookCommandHandler : IAddBookCommandHandler { private readonly IBooksRepository booksRepository; public AddBookCommandHandler(IBooksRepository booksRepository) { this.booksRepository = booksRepository; } public Task HandleAsync(AddBookCommand command, CancellationToken cancellationToken) => booksRepository.AddAsync( new InsertBookInLibraryCommand(command.Name, command.Author, command.Genre, command.MaximumDaysForRental, command.Keywords), cancellationToken); } }