DbContext in C sharp

DbContext in C#: Guida Completa ed Approfondita

Introduzione

In Entity Framework Core, il DbContext in C# è il cuore del data access:

  • mantiene il Change Tracker,
  • trasforma le query LINQ in SQL,
  • gestisce transazioni e unità di lavoro.

È un concetto semplice ma potente: un errore nella gestione del DbContext può rallentare l’applicazione o introdurre bug complessi. In questo articolo vediamo come sfruttarlo al meglio, ottimizzando performance, testabilità e architettura.


Cos’è il DbContext in C#

Il DbContext in C# è l’implementazione del pattern Unit of Work: ogni istanza rappresenta una sessione verso il database.
Carica entità, ne traccia le modifiche e al momento opportuno esegue un SaveChanges() traducendo tutto in SQL (documentazione ufficiale Microsoft).

public class AppDbContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>(b =>
        {
            b.HasKey(x => x.Id);
            b.Property(x => x.Total).HasPrecision(18, 2);
            b.HasIndex(x => new { x.CustomerId, x.Status });
        });
    }
}

Gestione del ciclo di vita del DbContext

  • Web API → registra il DbContext come Scoped (una istanza per request).
  • Worker / BackgroundService → crea scope manuali con IServiceScopeFactory.
  • Mai come Singleton → non è thread-safe.
builder.Services.AddDbContextPool<AppDbContext>(opt =>
    opt.UseSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        sql => sql.EnableRetryOnFailure())
);

👉 Usa AddDbContextPool per attivare il pooling ed evitare overhead di creazione.


Tracking vs No-Tracking

  • Tracking → EF mantiene uno snapshot delle entità, utile per Update/Delete.
  • No-Tracking → nessun tracciamento, perfetto per letture ad alte prestazioni.
var orders = await db.Orders
    .AsNoTracking()
    .Where(o => o.CustomerId == id)
    .ToListAsync();

Per grafi complessi:

db.Orders.AsSplitQuery().Include(o => o.Items);

Query ottimizzate con DbContext in C#

  • Proiezioni DTO → carica solo ciò che serve.
var list = await db.Orders
    .Where(o => o.CustomerId == id)
    .Select(o => new OrderDto(o.Id, o.Total, o.Status))
    .ToListAsync();
  • Compiled Queries → ottimizzano scenari con query ripetitive.
static readonly Func<AppDbContext, string, Task<decimal>> _compiled =
    EF.CompileAsyncQuery((AppDbContext db, string customerId) =>
        db.Orders.Where(o => o.CustomerId == customerId && o.Status == "Completed")
                 .Sum(o => o.Total));

Concorrenza e Transazioni

Concorrenza Ottimistica

builder.Entity<Order>().Property<byte[]>("RowVersion").IsRowVersion();

Gestione:

try { await db.SaveChangesAsync(); }
catch (DbUpdateConcurrencyException ex) { /* merge strategy */ }

Transaction Scope

var strategy = db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
    await using var tx = await db.Database.BeginTransactionAsync();
    await db.SaveChangesAsync();
    // altre operazioni
    await tx.CommitAsync();
});

Interceptors e filtri globali

Il DbContext in C# supporta interceptors e global query filters, utili per:

  • Audit logging
  • Soft delete
  • Multi-tenancy
public class AuditInterceptor : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData data, InterceptionResult<int> result, CancellationToken ct = default)
    {
        var now = DateTime.UtcNow;
        var db = (AppDbContext)data.Context!;
        foreach (var e in db.ChangeTracker.Entries<IAuditable>())
        {
            if (e.State == EntityState.Added) e.Entity.CreatedAt = now;
            if (e.State == EntityState.Modified) e.Entity.UpdatedAt = now;
        }
        return base.SavingChangesAsync(data, result, ct);
    }
}

Testing con DbContext

  • InMemory Provider → rapido, ma non realistico (ignora vincoli).
  • SQLite in-memory → più realistico, simula SQL e vincoli.
var connection = new SqliteConnection("DataSource=:memory:");
await connection.OpenAsync();

var opts = new DbContextOptionsBuilder<AppDbContext>()
    .UseSqlite(connection)
    .Options;

using var db = new AppDbContext(opts);
await db.Database.EnsureCreatedAsync();

Errori comuni da evitare

❌ Creare DbContext con new invece di usare DI.
❌ Usarlo come Singleton.
❌ Tenere un’istanza viva troppo a lungo → memory leak.
❌ Mischiare logica di dominio e query EF nello stesso servizio.
❌ Usare InMemory e credere di aver testato il database reale.


Conclusioni

Il DbContext in C# è uno strumento potente:

  • trattalo come unit of work a vita breve,
  • usa tracking/no-tracking con criterio,
  • sfrutta compiled queries e interceptors,
  • separa la logica di dominio da EF (Clean Architecture).

Così avrai codice più veloce, stabile e manutenibile.

Per approfondimenti → Guida Microsoft a DbContext.


FAQ su DbContext in C#

Cos’è il DbContext in EF Core?

È l’oggetto principale per accedere e tracciare entità nel database. Implementa il pattern Unit of Work.

Posso usare un DbContext come Singleton?

No. Non è thread-safe e va usato solo per unità di lavoro brevi (per request o scope).

Quando usare AsNoTracking()?

Per query di sola lettura, migliora performance e riduce l’uso di memoria.

Cosa sono le Compiled Queries?

Query precompilate da EF Core che evitano il costo di traduzione LINQ→SQL in scenari ripetitivi.

Qual è il miglior provider per test?

SQLite in-memory: più realistico dell’InMemory provider perché applica vincoli e SQL reale.

Qui puoi leggere un altro articolo su EF in .net Core


Pubblicato

in

da

Tag: