Laboratorul 06: Structura Backend. Api

Scopul Laboratorului

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.

Cuvant inainte

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.

Organizarea Codului API

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.

Modul in care noi am organizat codul nu reflecta o strategie generala, ci doar o parere personala.

Program.cs

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();

In Visual Studio, puneti cursorul peste builder. O sa vedeti ca este un obiect de tipul WebApplicationBuilder.

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.

In Visual Studio, puneti cursorul peste app. O sa vedeti ca este un obiect de tipul WebApplication.

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.

Dependency Injection

.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:

  1. Obiectul A instantiaza Obiectul B
  2. Obiectul A obtine Obiectul B din containerul de dependente, direct instantiat

Scenariul 2 se refera la Dependency Injection. In loc ca obiectele de care se depinde, sa fie instantiate, ele sunt direct preluate din containerul de depenente.

Pentru a folosi dependency injection in .NET este nevoie de 2 lucruri:

  1. Obiectele trebuie injectate in containerul de dependente.
  2. Cand este nevoie de un obiect, acesta trebuie mentionat in constructorul obiectului parinte.

Containerul de dependente implicit din .NET este accesat prin obiectul IServiceCollection, obiect copil al WebApplicationBuilder.

Obiectele care sunt injectate, pot fi injectate in 3 cicluri de viata: transient, scoped si singletone.

Chiar daca acum pare putin ambiguu, o sa vedeti exemple concrete in cod.

Metode Extensie

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"));
            });
        }
 
        //...
    }
}

Observati cum singurul parametru al acestei metode este WebApplicationBuilder builder, care este prefixat cu this. Prefixul cu this marcheaza faptul ca aceasta metoda este una de extensie pentru obiectul de tipulWebApplicationBuilder.

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")));
}

Reminder: Acest context a fost folosit in repository-uri, in cadrul proiectului Infrastructure. Motivul pentru care a putut fi folosit este ca a fost configurat si injectat aici.

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();
        }
    }
}

In metoda de mai sus am instruit obiectul services sa apeleze metodele de extensie pentru fiecare feature scris in parte. Aceste metode de extensie sunt definite in cadrul fiecarui feature.

Exemplu:

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>();
        }
    }
}

Features

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:

  • Functionalitati care vizeaza Carti
  • Functionalitati care vizeaza Metrici
  • Functionalitati care vizeaza Profilul
  • Functionalitati care vizeaza Imprumuturi

Am deci sa separam carti de metrici si imprumuturi ca sa fie separatia cat mai clara.

Fiecare folder de grupuri de functionalitati are urmatoarele componente:

  • Controllerul, care reprezinta rutele ce vor fi expuse catre public (ex: BooksController.cs)
  • Fisierul de Modul, care aplica Dependency Injection pe handlerele scrise in folderele de functionalitati (ex: BooksModule)
  • Folderele de functionalitati care contin unul sau mai multe DTO-uri necesare si handler-ul pentru actiunea respectiva (ex: AddBook)

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);
    }
}

Observati cum IBooksRepository este injectat in handler, fara sa fie nevoie sa fie instantiat direct. Acest lucru este posibil datorita Dependency Injection realizat in fisierul BookLibrary.Api/Infrastructure/DataAccessExtensions.cs, aratat mai sus

Exemplu de utilizare a handler-ului de catre controller:

using BookLibrary.Api.Features.Books.AddBook;
using BookLibrary.Api.Features.Books.ViewAllBooks;
using BookLibrary.Api.Features.Books.ViewStatusAboutBook;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net;
 
namespace BookLibrary.Api.Features.Books
{
    [ApiController]
    [Route("api/v1/[controller]")]
    public class BooksController : ControllerBase
    {
        private readonly IAddBookCommandHandler addBookCommandHandler;
        private readonly IViewAllBooksQueryHandler viewAllBooksQueryHandler;
        private readonly IViewStatusAboutBookQueryHandler viewStatusAboutBookQueryHandler;
 
        public BooksController(
            IAddBookCommandHandler addBookCommandHandler, 
            IViewAllBooksQueryHandler viewAllBooksQueryHandler, 
            IViewStatusAboutBookQueryHandler viewStatusAboutBookQueryHandler)
        {
            this.addBookCommandHandler = addBookCommandHandler;
            this.viewAllBooksQueryHandler = viewAllBooksQueryHandler;
            this.viewStatusAboutBookQueryHandler = viewStatusAboutBookQueryHandler;
        }
 
        [HttpPost("addBook")]
        [Authorize(Policy = "AdminAccess")]
        public async Task<IActionResult> AddBookAsync([FromBody] AddBookCommand command, CancellationToken cancellationToken)
        {
            await addBookCommandHandler.HandleAsync(command, cancellationToken);
 
            return StatusCode((int)HttpStatusCode.Created);
        }
        //...
    }
}

Observati cum am injectat handlerele in constructorul controllerului, fara sa fie nevoie sa fie instantiate. Acest lucru se datoreaza Dependency Injection realizat in fisierul de modul BookModule.cs cat si in fisierul BookLibrary.Api/Web/ApiFeaturesExtensions.cs ilustrat anterior.

Exemplu de inregsitrarea a handlerelor in containerul de dependente, la nivel de fisier de modul:

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>();
        }
    }
}

Investigati modul in care au fost implementate restul functionalitatilor si intrebati pe grupul de Discutii Laborator unde aveti nelamuriri.

pw/laboratoare/06.txt · Last modified: 2022/04/27 13:14 by alexandru.hogea
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0