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.