10

Lab 10 - Many-to-Many, Middleware și Serilog

Partea 4 din 4

Parte 4 — Cheatsheet

Pagină de referință rapidă pentru tot ce ați văzut în lab.

Many-to-Many — pattern complet

// Models/Tag.cs
public class Tag
{
    public int Id { get; set; }
    [Required, MinLength(2)]
    public string Name { get; set; } = string.Empty;
    public List<Article> Articles { get; set; } = new();
}

// Models/ArticleTag.cs (junction explicit)
public class ArticleTag
{
    public int ArticleId { get; set; }
    public Article Article { get; set; } = null!;
    public int TagId { get; set; }
    public Tag Tag { get; set; } = null!;
}

// Article.cs - navigare
public List<Tag> Tags { get; set; } = new();

// AppDbContext.OnModelCreating
modelBuilder.Entity<Article>()
    .HasMany(a => a.Tags)
    .WithMany(t => t.Articles)
    .UsingEntity<ArticleTag>(
        j => j.HasOne(at => at.Tag).WithMany().HasForeignKey(at => at.TagId),
        j => j.HasOne(at => at.Article).WithMany().HasForeignKey(at => at.ArticleId),
        j => j.HasKey(at => new { at.ArticleId, at.TagId }));

Include / ThenInclude

// Include simplu
.Include(a => a.Tags)

// Include + ThenInclude (pentru navigari nested)
.Include(a => a.Tags).ThenInclude(t => t.Articles)

// Multiple Include - atentie la cartesian explosion
.Include(a => a.Category).Include(a => a.Author).Include(a => a.Tags)

// Pentru volum mare: split query (un round-trip per Include)
.Include(a => a.Tags).AsSplitQuery()

Migrații EF Core

# adaugare migratie
dotnet ef migrations add AddTagsAndManyToMany

# aplicare pe BD
dotnet ef database update

# rollback la o migratie anterioara
dotnet ef database update PreviousMigrationName

# stergerea ultimei migratii (daca nu a fost aplicata)
dotnet ef migrations remove

# script SQL (util pentru deploy in productie)
dotnet ef migrations script

Structura unui middleware

public class FoiMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<FoiMiddleware> _logger;

    public FoiMiddleware(RequestDelegate next, ILogger<FoiMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // PRE - la request
        await _next(context);
        // POST - la response (status code disponibil aici)
    }
}

// Inregistrare in Program.cs
app.UseMiddleware<FoiMiddleware>();

Ordinea pipeline-ului (recomandată)

app.UseMiddleware<ExceptionHandlingMiddleware>();  // primul - prinde tot
app.UseMiddleware<LoggingMiddleware>();             // al doilea - masoara tot ce urmeaza
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(...);

Stopwatch pentru durate

using System.Diagnostics;

var sw = Stopwatch.StartNew();
await DoSomething();
sw.Stop();
_logger.LogInformation("Done in {Duration}ms", sw.ElapsedMilliseconds);

Mapping excepții → status code

var (statusCode, message) = exception switch
{
    KeyNotFoundException        => (404, "Resursa nu a fost gasita."),
    UnauthorizedAccessException => (403, "Acces interzis."),
    ArgumentException           => (400, "Cerere invalida."),
    _                           => (500, "Eroare interna.")
};

Null vs throw - convenția pentru servicii

Situație Service returnează Cine răspunde 404
Căutare cu filtru (poate fi 0 rezultate) null / lista goală Controller cu NotFound()
Lookup după id cunoscut (trebuie să existe) aruncă KeyNotFoundException Middleware-ul (404 JSON automat)
// Variant A - null + controller verifica
public async Task<Article?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
    => await _unitOfWork.ArticleRepository.GetByIdAsync(id, cancellationToken);

// Variant B - throw + middleware mapeaza
public async Task<Article> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
    var article = await _unitOfWork.ArticleRepository.GetByIdAsync(id, cancellationToken);
    return article ?? throw new KeyNotFoundException($"id={id}");
}

În proiectul nostru folosim Variant A (null) pentru MVC. Pentru API pur, Variant B e mai curat - middleware-ul produce JSON consistent fără cod în controller.

Result Pattern ca alternativă (fără excepții): librării ca FluentResults, ErrorOr. Nu îl folosim - boilerplate + incompatibil cu middleware-ul standard.

Serilog — configurare minimă

using Serilog;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .Enrich.FromLogContext()
    .WriteTo.Console(
        outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .WriteTo.File(
        path: "logs/newsportal-.txt",
        rollingInterval: RollingInterval.Day,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
    .CreateLogger();

builder.Host.UseSerilog();

Nivele de logging

Nivel Folosire
LogTrace detalii fine (debug intens) — în general off în producție
LogDebug informații de debug — off în producție
LogInformation flow normal — DEFAULT pentru evenimente importante
LogWarning ceva neașteptat dar non-fatal
LogError erori care eșuează un request, dar aplicația merge mai departe
LogCritical aplicația nu poate continua

Structured logging — regula

// DA - placeholder-e nominale, proprietati pastrate
_logger.LogInformation("User {UserId} created article {ArticleId}", userId, articleId);

// NU - interpolare, proprietati pierdute
_logger.LogInformation($"User {userId} created article {articleId}");

Beneficiul devine vizibil când log-urile ajung într-un sistem care indexează proprietăți (Seq, Elasticsearch, Application Insights).

Output template — placeholdere uzuale

Placeholder Ce afișează
{Timestamp:HH:mm:ss} timpul (poate avea format :yyyy-MM-dd ...)
{Level:u3} nivelul, 3 caractere uppercase (INF, WRN, ERR)
{Message:lj} mesajul + literal JSON pentru obiecte
{Properties:j} toate proprietățile structurate ca JSON
{NewLine} newline platform-aware
{Exception} stack trace dacă există

NullLogger — pentru teste

Dacă în test nu vreți să vă pese de logger-uri:

using Microsoft.Extensions.Logging.Abstractions;

var service = new ArticleService(unitOfWork, NullLogger<ArticleService>.Instance);

Linkuri rapide