Passa al contenuto principale

Code First — Setup e migration

La filosofia Code First e le motivazioni per adottarla sono descritte in regole/entity-framework. Questa pagina copre la parte pratica: come configurare il progetto, registrare il contesto e gestire le migration.

DbContext

Il DbContext rappresenta la sessione con il database. Eredita da DbContext e dichiara una proprietà DbSet<T> per ogni entità radice del modello.

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

public DbSet<Ordine> Ordini => Set<Ordine>();
public DbSet<Cliente> Clienti => Set<Cliente>();
public DbSet<Prodotto> Prodotti => Set<Prodotto>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Applica tutte le IEntityTypeConfiguration<T> presenti nell'assembly
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}

ApplyConfigurationsFromAssembly rileva automaticamente tutte le classi IEntityTypeConfiguration<T> nell'assembly, evitando di registrarle manualmente una per una.

Registrazione in Program.cs

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

Con AddDbContext il contesto è registrato come scoped: una nuova istanza per ogni richiesta HTTP. Questo garantisce che le modifiche tracciate da una richiesta non interferiscano con quelle di un'altra.

Connection string

// appsettings.json
{
"ConnectionStrings": {
"Default": "Host=localhost;Database=myapp;Username=myapp;Password=secret"
}
}

In produzione la connection string si passa come variabile d'ambiente (ConnectionStrings__Default) o tramite un secret manager. Non si mettono credenziali nei file di configurazione versionati. Vedi regole/configurazione.

IEntityTypeConfiguration<T>

La configurazione EF di ogni entità va in una classe dedicata, non con Data Annotations sull'entità stessa:

public class OrdineConfiguration : IEntityTypeConfiguration<Ordine>
{
public void Configure(EntityTypeBuilder<Ordine> builder)
{
builder.ToTable("ordini");

builder.HasKey(o => o.Id);

builder.Property(o => o.Numero)
.IsRequired()
.HasMaxLength(20);

builder.HasIndex(o => o.Numero)
.IsUnique();

builder.Property(o => o.DataCreazione)
.HasDefaultValueSql("now()");

builder.HasOne(o => o.Cliente)
.WithMany(c => c.Ordini)
.HasForeignKey(o => o.ClienteId)
.OnDelete(DeleteBehavior.Restrict);
}
}

Workflow delle migration

# Aggiungere una migration (dalla root del progetto infrastruttura)
dotnet ef migrations add NomeDescrittivoDelCambiamento \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api

# Applicare le migration al database
dotnet ef database update \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api

# Generare lo script SQL (utile per review o applicazione manuale in produzione)
dotnet ef migrations script \
--idempotent \
--output migration.sql

Il nome della migration è descrittivo: AddOrdineTable, AddIndirizzoACliente, RinominaStatoOrdine. Mai Update1, Fix, Migration20240315.

Applicazione allo startup

Per ambienti semplici o interni, si possono applicare le migration automaticamente all'avvio:

// Program.cs
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync();
}

Per ambienti di produzione con tabelle grandi o migration complesse, si preferisce lo script SQL applicato manualmente prima del deploy. Vedi regole/entity-framework per la policy completa.