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