Accesso ai dati in .NET: confronto tra ORM e strategie di mappatura (Parte 1 - Ente Quadro Centrale) (Italiano (Italian))

Accesso ai dati in .NET: confronto tra ORM e strategie di mappatura (Parte 1 - Ente Quadro Centrale)

Wednesday, 03 December 2025

//

22 minute read

Quando si costruiscono applicazioni .NET, una delle decisioni architettoniche più importanti che farete è come gestire l'accesso ai dati e la mappatura degli oggetti. L'ecosistema .NET offre una ricca varietà di approcci, dalle ORM complete all'esecuzione SQL bare-metal. Ogni approccio viene fornito con i propri compromessi in termini di prestazioni, produttività degli sviluppatori, sicurezza del tipo e manutenzione.

In questa guida completa in due parti, esploreremo i modelli di accesso ai dati più popolari in .NET. Mentre usiamo PostgreSQL con Npgsql nei nostri esempi (poiché questo è ciò che alimenta questo blog), i concetti, i modelli e i compromessi si applicano ugualmente a SQL Server, MySQL, SQLite e altri database relazionali. I principi rimangono gli stessi - solo il dialetto SQL e alcune caratteristiche specifiche differiscono.

Parte 1 (questo articolo) si concentra su Entity Framework Core, generazione di SQL e insidie comuni. Parte 2 Coprirà Dapper, raw ADO.NET, librerie di mappatura degli oggetti e approcci ibridi.

Articoli correlati

Se siete interessati a pratiche implementazioni EF Core, controllare i miei altri articoli:

Indice

Lo spettro degli approcci all'accesso ai dati

Il panorama di accesso ai dati .NET può essere visualizzato come uno spettro:

Full Abstraction                                      Full Control
     ↓                                                      ↓
[EF Core] → [EF Core Raw SQL] → [Dapper] → [Npgsql ADO.NET]

Passando da sinistra a destra, ottieni prestazioni e controllo, ma perdi comodità e funzionalità automatiche. Esaminiamo ogni approccio in dettaglio.

Confronto dei flussi di accesso ai dati

Ecco un confronto visivo di come ogni approccio gestisce una query tipica:

graph TB
    subgraph "EF Core Flow"
        A1[LINQ Query] -->|Compile| B1[Expression Tree]
        B1 -->|Translate| C1[SQL Query]
        C1 -->|Execute| D1[PostgreSQL]
        D1 -->|Results| E1[DbDataReader]
        E1 -->|Materialize| F1[Tracked Entities]
        F1 -->|Return| G1[Application]
    end

    subgraph "Dapper Flow"
        A2[SQL String] -->|Parameterize| B2[DbCommand]
        B2 -->|Execute| C2[PostgreSQL]
        C2 -->|Results| D2[DbDataReader]
        D2 -->|Map| E2[POCOs]
        E2 -->|Return| F2[Application]
    end

    subgraph "Raw Npgsql Flow"
        A3[SQL + Parameters] -->|Build Command| B3[NpgsqlCommand]
        B3 -->|Execute| C3[PostgreSQL]
        C3 -->|Results| D3[NpgsqlDataReader]
        D3 -->|Manual Mapping| E3[Objects]
        E3 -->|Return| F3[Application]
    end

    style A1 stroke:#2563eb,stroke-width:2px
    style B1 stroke:#2563eb,stroke-width:2px
    style C1 stroke:#2563eb,stroke-width:2px
    style D1 stroke:#2563eb,stroke-width:2px
    style E1 stroke:#2563eb,stroke-width:2px
    style F1 stroke:#2563eb,stroke-width:2px
    style G1 stroke:#2563eb,stroke-width:2px

    style A2 stroke:#059669,stroke-width:2px
    style B2 stroke:#059669,stroke-width:2px
    style C2 stroke:#059669,stroke-width:2px
    style D2 stroke:#059669,stroke-width:2px
    style E2 stroke:#059669,stroke-width:2px
    style F2 stroke:#059669,stroke-width:2px

    style A3 stroke:#dc2626,stroke-width:2px
    style B3 stroke:#dc2626,stroke-width:2px
    style C3 stroke:#dc2626,stroke-width:2px
    style D3 stroke:#dc2626,stroke-width:2px
    style E3 stroke:#dc2626,stroke-width:2px
    style F3 stroke:#dc2626,stroke-width:2px

Performance vs Sviluppatore Produttività Trade-off

graph LR
    A[High Productivity<br/>Low Performance] --> B[EF Core<br/>Full Tracking]
    B --> C[EF Core<br/>No Tracking]
    C --> D[EF Core<br/>Raw SQL]
    D --> E[Dapper]
    E --> F[Raw Npgsql]
    F --> G[Low Productivity<br/>High Performance]

    style A stroke:#2563eb,stroke-width:2px
    style B stroke:#2563eb,stroke-width:2px
    style C stroke:#3b82f6,stroke-width:2px
    style D stroke:#059669,stroke-width:2px
    style E stroke:#059669,stroke-width:2px
    style F stroke:#dc2626,stroke-width:2px
    style G stroke:#dc2626,stroke-width:2px

Il nucleo del quadro dell'entità: l'ORM completo

Centrale del quadro dell'entità è ammiraglia di Microsoft ORM, fornendo una completa astrazione sul vostro database. Supporta PostgreSQL attraverso il Npgsql.EntityFrameworkCore.PostgreSQL Provider.

Per una guida pratica sulla creazione di EF Core nel vostro progetto, vedere il mio articolo su Aggiunta del quadro dell'entità per i post del blog.

Caratteristiche chiave

  • Cambia tracciatura: Traccia automaticamente i cambiamenti delle entità e genera SQL appropriati
  • Migrazioni: Gestione dello schema codice-primo e controllo della versione (vedi Migrazioni dell'impronta ambientale nel modo giusto)
  • Fornitore LINQ: Type-safe querys using C# language costructs
  • Caricamento Pigro/Eager: Strategie di carico flessibili per entità correlate
  • Caratteristiche avanzate di PostgreSQL: Ricerca full-text (vedi il mio articolo), colonne JSON, array, tipi di campo
  • Intercettori ed eventi: Punti di estensibilità per le preoccupazioni trasversali

Esempio: CRUD di base con nucleo EF

public class BlogDbContext : DbContext
{
    public DbSet<BlogPost> BlogPosts { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql("Host=localhost;Database=blog;Username=postgres;Password=secret");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // PostgreSQL-specific: Full-text search
        modelBuilder.Entity<BlogPost>()
            .HasGeneratedTsVectorColumn(
                p => p.SearchVector,
                "english",
                p => new { p.Title, p.Content })
            .HasIndex(p => p.SearchVector)
            .HasMethod("GIN");

        // PostgreSQL array type
        modelBuilder.Entity<BlogPost>()
            .Property(p => p.Tags)
            .HasPostgresArrayConversion(
                tag => tag.ToLowerInvariant(),
                tag => tag);
    }
}

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public string[] Tags { get; set; }
    public NpgsqlTsVector SearchVector { get; set; }
    public List<Comment> Comments { get; set; }
    public DateTime PublishedDate { get; set; }
}

// Usage
public class BlogService
{
    private readonly BlogDbContext _context;

    public async Task<List<BlogPost>> GetRecentPostsAsync(int count)
    {
        return await _context.BlogPosts
            .Include(p => p.Comments)
            .OrderByDescending(p => p.PublishedDate)
            .Take(count)
            .ToListAsync();
    }

    public async Task<List<BlogPost>> SearchPostsAsync(string searchTerm)
    {
        return await _context.BlogPosts
            .Where(p => p.SearchVector.Matches(EF.Functions.ToTsQuery("english", searchTerm)))
            .ToListAsync();
    }

    public async Task AddPostAsync(BlogPost post)
    {
        _context.BlogPosts.Add(post);
        await _context.SaveChangesAsync();
    }
}

Core EF con SQL grezzo

EF Core supporta anche le query SQL crude quando hai bisogno di più controllo:

public async Task<List<BlogPost>> GetPostsByComplexCriteriaAsync()
{
    var searchTerm = "postgresql";

    return await _context.BlogPosts
        .FromSqlInterpolated($@"
            SELECT * FROM ""BlogPosts""
            WHERE ""SearchVector"" @@ to_tsquery('english', {searchTerm})
            AND array_length(""Tags"", 1) > 3
            ORDER BY ts_rank(""SearchVector"", to_tsquery('english', {searchTerm})) DESC
        ")
        .ToListAsync();
}

// Or with DbDataReader for maximum control
public async Task<List<PostStatistics>> GetPostStatisticsAsync()
{
    using var command = _context.Database.GetDbConnection().CreateCommand();
    command.CommandText = @"
        SELECT
            DATE_TRUNC('month', ""PublishedDate"") as Month,
            COUNT(*) as PostCount,
            AVG(ARRAY_LENGTH(""Tags"", 1)) as AvgTags
        FROM ""BlogPosts""
        GROUP BY DATE_TRUNC('month', ""PublishedDate"")
        ORDER BY Month DESC";

    await _context.Database.OpenConnectionAsync();

    var results = new List<PostStatistics>();
    using var reader = await command.ExecuteReaderAsync();

    while (await reader.ReadAsync())
    {
        results.Add(new PostStatistics
        {
            Month = reader.GetDateTime(0),
            PostCount = reader.GetInt32(1),
            AverageTags = reader.GetDouble(2)
        });
    }

    return results;
}

Quando usare il nucleo EF

Usa EF Core quando:

  • Costruire una nuova applicazione con requisiti di schema in evoluzione
  • È necessario digitare forte e compilare la convalida delle query in tempo
  • Le migrazioni e la versione schematica sono importanti (vedi la mia guida alla migrazione)
  • Il tuo team preferisce lavorare con gli oggetti su SQL
  • Stai usando modelli di dominio complessi con relazioni
  • La velocità di sviluppo è più critica delle prestazioni crude
  • Hai bisogno di portabilità cross-database (anche se le caratteristiche specifiche di PostgreSQL si bloccano in)

Evitare il nucleo dell'impronta ambientale quando:

  • Le massime prestazioni sono critiche (API ad alto rendimento, elaborazione batch)
  • Hai domande SQL complesse e personalizzate a mano
  • Le tue query non mappano bene per obiettare i grafici
  • È necessario un controllo a grana fine su ogni istruzione SQL
  • L'uso della memoria è un vincolo critico (change tracking overhead)
  • Stai lavorando con schemi ereditati che non mappano le convenzioni

Generazione EF Core SQL: capire cosa viene eseguito

Uno degli aspetti più importanti dell'utilizzo efficace di EF Core è la comprensione di ciò che SQL genera. EF Core ha migliorato significativamente la generazione di SQL nel corso degli anni, ma è fondamentale verificare le query inviate a PostgreSQL.

Visualizzazione di SQL generato

// Enable sensitive data logging and detailed errors (development only!)
optionsBuilder
    .UseNpgsql(connectionString)
    .EnableSensitiveDataLogging()
    .EnableDetailedErrors()
    .LogTo(Console.WriteLine, LogLevel.Information);

// Or use logging to see SQL
public class BlogService
{
    private readonly BlogDbContext _context;
    private readonly ILogger<BlogService> _logger;

    public async Task<List<BlogPost>> GetPostsAsync()
    {
        var query = _context.BlogPosts
            .Where(p => p.PublishedDate > DateTime.UtcNow.AddDays(-30))
            .OrderByDescending(p => p.PublishedDate);

        // View the SQL before execution
        var sql = query.ToQueryString();
        _logger.LogInformation("Executing query: {Sql}", sql);

        return await query.ToListAsync();
    }
}

Esempio: interrogazione semplice

C# LINQ:

var recentPosts = await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .OrderByDescending(p => p.PublishedDate)
    .Take(10)
    .ToListAsync();

SQL generato (EF Core 8+):

SELECT b."Id", b."Title", b."Content", b."CategoryId", b."PublishedDate"
FROM "BlogPosts" AS b
WHERE b."CategoryId" = @__categoryId_0
ORDER BY b."PublishedDate" DESC
LIMIT @__p_1

Nota come EF Core 8+ genera SQL pulito ed efficiente con una corretta parametrizzazione. EF Core 10 continua questa tendenza con ulteriori miglioramenti.

Esempio: unirsi con includere (prima del core EF 5)

Codice C#:

var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .ToListAsync();

Vecchio SQL (EF Core 3.1 - Esplosione cartesiana):

SELECT b."Id", b."Title", c."Id", c."Name", cm."Id", cm."Content"
FROM "BlogPosts" AS b
LEFT JOIN "Categories" AS c ON b."CategoryId" = c."Id"
LEFT JOIN "Comments" AS cm ON b."Id" = cm."BlogPostId"
ORDER BY b."Id", c."Id"

Questo crea un Prodotto cartesiano - se un post ha 10 commenti, quella riga viene ripetuta 10 volte!

Esempio: Split Queries (EF Core 5+)

Codice C# con domanda di divisione:

var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .AsSplitQuery()  // ← This is the key!
    .ToListAsync();

Generato SQL (Multiple Queries):

-- Query 1: Get posts and categories
SELECT b."Id", b."Title", b."Content", c."Id", c."Name"
FROM "BlogPosts" AS b
LEFT JOIN "Categories" AS c ON b."CategoryId" = c."Id"

-- Query 2: Get comments for those posts
SELECT cm."Id", cm."Content", cm."BlogPostId"
FROM "Comments" AS cm
INNER JOIN (
    SELECT b."Id"
    FROM "BlogPosts" AS b
) AS t ON cm."BlogPostId" = t."Id"
ORDER BY t."Id"

Questo elimina il prodotto cartesiano ed è spesso molto più veloce per collezioni!

Esempio: include Filtrato (EF Core 5+)

Codice C#:

var posts = await _context.BlogPosts
    .Include(p => p.Comments.Where(c => c.IsApproved))
    .ToListAsync();

SQL generato:

SELECT b."Id", b."Title", b."Content", t."Id", t."Content", t."IsApproved"
FROM "BlogPosts" AS b
LEFT JOIN (
    SELECT c."Id", c."Content", c."IsApproved", c."BlogPostId"
    FROM "Comments" AS c
    WHERE c."IsApproved" = TRUE
) AS t ON b."Id" = t."BlogPostId"
ORDER BY b."Id"

Esempio: richieste di colonne JSON (EF Core 7+)

Codice C#:

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public PostMetadata Metadata { get; set; }  // Stored as JSONB
}

public class PostMetadata
{
    public bool IsFeatured { get; set; }
    public int ViewCount { get; set; }
    public List<string> RelatedTags { get; set; }
}

// Query JSON properties
var featuredPosts = await _context.BlogPosts
    .Where(p => p.Metadata.IsFeatured)
    .ToListAsync();

SQL generato:

SELECT b."Id", b."Title", b."Metadata"
FROM "BlogPosts" AS b
WHERE b."Metadata" ->> 'IsFeatured' = 'true'

EF Core 7+ può tradurre l'accesso alla proprietà di JSON agli operatori di PostgreSQL JSON!

Esempio: Aggiornamento di massa (EF Core 7+ Aggiornamento esecuzione)

Vecchia via (inefficiente):

var posts = await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ToListAsync();

foreach (var post in posts)
{
    post.IsArchived = true;
}

await _context.SaveChangesAsync();  // Generates N UPDATE statements!

Nuovo modo (core EF 7+):

await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsArchived, true));

Generato SQL (Single Query!):

UPDATE "BlogPosts" AS b
SET "IsArchived" = TRUE
WHERE b."CategoryId" = 5

Questo è un massiccio miglioramento - una dichiarazione SQL invece di N!

Esempio: Elimina all'ingrosso (EF Core 7+)

Old Way:

var oldPosts = await _context.BlogPosts
    .Where(p => p.PublishedDate < DateTime.UtcNow.AddYears(-5))
    .ToListAsync();

_context.BlogPosts.RemoveRange(oldPosts);
await _context.SaveChangesAsync();  // N DELETE statements

Nuovo modo:

await _context.BlogPosts
    .Where(p => p.PublishedDate < DateTime.UtcNow.AddYears(-5))
    .ExecuteDeleteAsync();

SQL generato:

DELETE FROM "BlogPosts" AS b
WHERE b."PublishedDate" < @__p_0

Esempio: Aggregazione complessa

Codice C#:

var categoryStats = await _context.Categories
    .Select(c => new CategoryStats
    {
        CategoryName = c.Name,
        PostCount = c.BlogPosts.Count(),
        LatestPostDate = c.BlogPosts.Max(p => p.PublishedDate),
        AverageComments = c.BlogPosts.Average(p => p.Comments.Count)
    })
    .ToListAsync();

SQL generato (EF Core 8+/10):

SELECT c."Name" AS "CategoryName",
       COUNT(*)::int AS "PostCount",
       MAX(b."PublishedDate") AS "LatestPostDate",
       COALESCE(AVG((
           SELECT COUNT(*)::int
           FROM "Comments" AS c0
           WHERE b."Id" = c0."BlogPostId"
       ))::double precision, 0.0) AS "AverageComments"
FROM "Categories" AS c
LEFT JOIN "BlogPosts" AS b ON c."Id" = b."CategoryId"
GROUP BY c."Id", c."Name"

Ricerca di testo completo PostgreSQL

Per un'immersione più profonda nella ricerca full-text, vedere il mio articolo su implementare la ricerca full-text con EF Core.

Codice C#:

var searchResults = await _context.BlogPosts
    .Where(p => p.SearchVector.Matches(EF.Functions.ToTsQuery("english", "postgresql & performance")))
    .OrderByDescending(p => p.SearchVector.Rank(EF.Functions.ToTsQuery("english", "postgresql & performance")))
    .Take(20)
    .ToListAsync();

SQL generato:

SELECT b."Id", b."Title", b."Content", b."SearchVector"
FROM "BlogPosts" AS b
WHERE b."SearchVector" @@ to_tsquery('english', @__searchTerm_0)
ORDER BY ts_rank(b."SearchVector", to_tsquery('english', @__searchTerm_0)) DESC
LIMIT 20

Allertazioni e cadute critiche del nucleo dell'impronta ambientale

1. Cambiare perdite di memoria di monitoraggio

Il problema:

// ❌ DANGER: This can cause memory leaks!
public class PostCache
{
    private readonly BlogDbContext _context;
    private List<BlogPost> _cachedPosts;

    public PostCache(BlogDbContext context)
    {
        _context = context;
    }

    public async Task LoadCacheAsync()
    {
        // These entities are now tracked by the context
        _cachedPosts = await _context.BlogPosts.ToListAsync();

        // The DbContext holds references to these entities FOREVER
        // They can never be garbage collected while the context lives!
    }
}

Perche' e' un problema:

  • Le entità rintracciate rimangono in memoria per tutta la vita del DbContext
  • L'inseguitore di cambiamento mantiene i riferimenti, impedendo la raccolta dei rifiuti
  • Contesti di lunga durata (ad es. singoli tonnellate) = perdita di memoria
  • In ASP.NET Core, il contesto è definito per impostazione predefinita (buono!)
  • Ma se hai rintracciato delle entità, sei nei guai.

La soluzione:

public async Task LoadCacheAsync()
{
    // ✅ Use AsNoTracking() for read-only queries
    _cachedPosts = await _context.BlogPosts
        .AsNoTracking()
        .ToListAsync();

    // Or detach entities after loading
    var posts = await _context.BlogPosts.ToListAsync();
    foreach (var post in posts)
    {
        _context.Entry(post).State = EntityState.Detached;
    }
    _cachedPosts = posts;
}

2. Generazione proxy e pericoli di caricamento pigri

ATTENZIONE CRITICA: NON USARE PROXIES EF CORE SE CACHE ENTITÀ

Proxies di caricamento pigro + cache = MEMORIA GARANTITA

Se si cache DbContext istanze o entità cache caricate con proxy abilitati, si Lo faro'. memoria di perdita. Il meccanismo proxy mantiene riferimenti al DbContext, prevenendo la raccolta dei rifiuti. Questo è uno degli errori più comuni e pericolosi nelle applicazioni EF Core.

Regola del pollice: Includere sempre le collezioni esplicitamente con .Include(). Utilizzare proxy solo se si capisce pienamente i compromessi e mai, mai, mai cache proxy entità.

Problema 1: L'incubo N+1

// ❌ Enable lazy loading
optionsBuilder
    .UseNpgsql(connectionString)
    .UseLazyLoadingProxies();  // Convenient but dangerous!

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public virtual Category Category { get; set; }  // Virtual = proxy
    public virtual List<Comment> Comments { get; set; }
}

// Somewhere in your code
var posts = await _context.BlogPosts.ToListAsync();

foreach (var post in posts)
{
    Console.WriteLine(post.Category.Name);  // N+1 query here!
    Console.WriteLine(post.Comments.Count);  // Another N+1 query!
}

Cosa succede?

  1. La prima query carica tutti i post
  2. Per ogni posto, accesso Category attiva una query del database
  3. Per ogni posto, accesso Comments attiva un'altra query
  4. Se hai 100 post, hai appena eseguito 201 interrogazioni!

Problema 2: Proxy + Caching = Memory Leak

// ❌ CATASTROPHIC: Lazy loading proxies + caching
public class BlogPostCache
{
    private static List<BlogPost> _cachedPosts;
    private readonly BlogDbContext _context;

    public BlogPostCache()
    {
        var optionsBuilder = new DbContextOptionsBuilder<BlogDbContext>();
        optionsBuilder
            .UseNpgsql(connectionString)
            .UseLazyLoadingProxies();  // ⚠️ DANGER!

        _context = new BlogDbContext(optionsBuilder.Options);
    }

    public async Task<List<BlogPost>> GetCachedPostsAsync()
    {
        if (_cachedPosts == null)
        {
            // ❌ These proxy entities hold references to _context
            _cachedPosts = await _context.BlogPosts.ToListAsync();
        }
        return _cachedPosts;
    }
}

Perché questo è catastrofico:

  • Le entità proxy mantengono un riferimento alla loro DbContext
  • La DbContext mantiene un riferimento a tutte le entità rintracciate
  • La tua cache impedisce ora che l'intero grafico dell'oggetto venga raccolto dalla spazzatura
  • Ogni volta che si accede a una proprietà di navigazione, si può attivare query utilizzando il vecchio, contesto cache
  • La memoria cresce senza limiti man mano che si caricano più dati
  • Alla fine esaurirai la memoria o i pool di connessione di scarico

La soluzione: essere esplicito

// ✅ NEVER use lazy loading proxies - always be explicit
optionsBuilder
    .UseNpgsql(connectionString);
    // NO .UseLazyLoadingProxies()!

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public Category Category { get; set; }  // NOT virtual
    public List<Comment> Comments { get; set; }  // NOT virtual
}

// ✅ Explicit eager loading - you control what's loaded
var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .ToListAsync();

// ✅ Or use split queries for better performance
var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .AsSplitQuery()
    .ToListAsync();

// ✅ Or use projection to DTOs (best for caching)
var posts = await _context.BlogPosts
    .Select(p => new PostDto
    {
        Title = p.Title,
        CategoryName = p.Category.Name,
        CommentCount = p.Comments.Count
    })
    .ToListAsync();

// ✅ If you MUST cache, use AsNoTracking and no proxies
public class SafeBlogPostCache
{
    private static List<BlogPost> _cachedPosts;
    private readonly IDbContextFactory<BlogDbContext> _contextFactory;

    public async Task<List<BlogPost>> GetCachedPostsAsync()
    {
        if (_cachedPosts == null)
        {
            using var context = await _contextFactory.CreateDbContextAsync();

            _cachedPosts = await context.BlogPosts
                .Include(p => p.Category)
                .Include(p => p.Comments)
                .AsNoTracking()  // Critical for caching!
                .ToListAsync();
        }
        return _cachedPosts;
    }
}

Quando i proxy potrebbero essere accettabili (capire i compromessi):

I proxy di carico pigri potrebbero essere accettabili SOLO quando:

  1. Hai di breve durata Contesti oggetto (ad esempio, per richiesta HTTP)
    • Tu mai enti cache
  2. Sei d'accordo con le prestazioni della query N+1
  3. Sei prototipazione e si ottimizzerà in seguito
  4. Il tuo team comprende appieno le implicazioni

Ma anche allora Include() è quasi sempre la scelta migliore perché:

  • Fa il caricamento dei dati esplicito e ovvio
  • E' più facile da ottimizzare (si può vedere cosa viene caricato)
  • Esso previene le query N+1 accidentali
  • Funziona correttamente con i contesti di cache e long-lived
  • E' il Approccio raccomandato dal gruppo centrale dell'EF

3. Problemi a vita di DbContext

Per maggiori dettagli sulla gestione della durata in produzione di DbContext, vedere il mio articolo su Migrazioni dell'impronta ambientale nel modo giusto.

Il problema:

// ❌ NEVER do this - singleton DbContext
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<BlogDbContext>();  // WRONG!
}

// ❌ Also wrong - storing context in static field
public static class DataAccess
{
    private static BlogDbContext _context = new BlogDbContext();

    public static async Task<BlogPost> GetPostAsync(int id)
    {
        return await _context.BlogPosts.FindAsync(id);
    }
}

Perche' e' sbagliato?

  • DbContext è non thread-safe
  • Le richieste concomitanti causeranno corruzione dei dati
  • Cambia tracciatore cresce indefinitamente
  • Esaurimento della piscina di collegamento
  • Stai dati dalla cache

La soluzione:

// ✅ Use scoped lifetime (default in ASP.NET Core)
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<BlogDbContext>(options =>
        options.UseNpgsql(connectionString));
}

// ✅ Or use DbContext factory for background services
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContextFactory<BlogDbContext>(options =>
        options.UseNpgsql(connectionString));
}

public class BlogBackgroundService
{
    private readonly IDbContextFactory<BlogDbContext> _contextFactory;

    public async Task ProcessPostsAsync()
    {
        // Create a new context for this operation
        using var context = await _contextFactory.CreateDbContextAsync();

        var posts = await context.BlogPosts.ToListAsync();
        // Process posts...
    }
}

4. Incomprensibile Include nelle Proprietà di Navigazione

Il problema:

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public List<Comment> Comments { get; set; }
}

// You query one post...
var post = await _context.BlogPosts.FirstAsync();

// Add a new comment
var newComment = new Comment { Content = "Great post!" };
post.Comments.Add(newComment);

await _context.SaveChangesAsync();

// ❌ EF Core saves the comment, BUT...
// If Comments wasn't loaded, you just lost all existing comments!
// The collection is empty, so EF thinks there are no other comments

La soluzione:

// ✅ Always load navigation properties before modifying
var post = await _context.BlogPosts
    .Include(p => p.Comments)
    .FirstAsync(p => p.Id == postId);

post.Comments.Add(newComment);
await _context.SaveChangesAsync();

// Or add directly to the DbSet
_context.Comments.Add(new Comment
{
    BlogPostId = postId,
    Content = "Great post!"
});
await _context.SaveChangesAsync();

5. Async vs Sync Mixing

Il problema:

// ❌ Mixing sync and async - deadlock risk!
public async Task<BlogPost> GetPostAsync(int id)
{
    var post = _context.BlogPosts
        .Where(p => p.Id == id)
        .FirstOrDefault();  // Sync method in async context!

    return post;
}

// ❌ Even worse - blocking async code
public BlogPost GetPost(int id)
{
    return _context.BlogPosts
        .FirstOrDefaultAsync(p => p.Id == id)
        .Result;  // DEADLOCK RISK!
}

La soluzione:

// ✅ Use async all the way
public async Task<BlogPost> GetPostAsync(int id)
{
    return await _context.BlogPosts
        .FirstOrDefaultAsync(p => p.Id == id);
}

// ✅ Or use sync all the way (not recommended for ASP.NET Core)
public BlogPost GetPost(int id)
{
    return _context.BlogPosts
        .FirstOrDefault(p => p.Id == id);
}

EF Core 10: Novità e cambiamenti in corso

Con EF Core 10 rilasciato insieme a .NET 10, ci sono diverse modifiche importanti di cui essere a conoscenza durante l'aggiornamento. Per l'elenco completo, vedere Breaking changes in EF Core 10.

Requisiti di runtime

EF Core 10 richiede .NET 10. Non verrà eseguito su .NET 8, .NET 9, o .NET Framework. Questo è il cambiamento più significativo - garantire i vostri obiettivi di progetto net10.0 prima dell'aggiornamento.

Modifiche di interruzione correlate all'interrogazione

1. Traduzione della raccolta parametrizzata (cambiata di default)

EF Core 10 cambia il modo in cui Contains() con le collezioni in-memory è tradotto in SQL. Precedentemente, EF Core utilizzato OpenJson() (SQL Server) o simile. Ora è predefinito per array di parametri che forniscono un migliore piano di query cacheing.

Impatto: Si può vedere diversi SQL generati per query come:

var ids = new List<int> { 1, 2, 3, 4, 5 };
var posts = await _context.BlogPosts
    .Where(p => ids.Contains(p.Id))
    .ToListAsync();

EF Core 8/9 (OpenJson):

SELECT b."Id", b."Title"
FROM "BlogPosts" AS b
WHERE b."Id" IN (SELECT value FROM OPENJSON(@__ids_0))

EF Core 10 (Parameter Arrays):

SELECT b."Id", b."Title"
FROM "BlogPosts" AS b
WHERE b."Id" = ANY(@__ids_0)  -- PostgreSQL
-- Or: WHERE b."Id" IN (@__ids_0_0, @__ids_0_1, @__ids_0_2, ...) -- SQL Server

Se si verificano regressioni delle prestazioni, tornare al vecchio comportamento:

// SQL Server
optionsBuilder.UseSqlServer(connectionString,
    o => o.UseParameterizedCollectionMode(ParameterTranslationMode.Constant));

// PostgreSQL - generally parameter arrays work well, but you can opt out if needed

2. EseguireUpdateAsync Signature Change

La ExecuteUpdateAsync firma è cambiata per supportare lambdas non-espressione. Questo è più flessibile, ma rompe il codice che ha costruito gli alberi di espressione programmaticamente:

Vecchia via (EF core 7-9):

// This still works
await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsArchived, true));

Novità in EF Core 10 - Lambdas non-espressione:

// Now you can include custom logic!
await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ExecuteUpdateAsync(setters =>
    {
        setters.SetProperty(p => p.IsArchived, true);
        setters.SetProperty(p => p.UpdatedAt, DateTime.UtcNow);
        // Can now include conditional logic, loops, etc.
    });

3. Nome della colonna di tipo complesso

EF Core 10 cambia come le colonne di tipo complesso annidato sono nominati per prevenire la corruzione dei dati:

EF Core 9:

NestedComplex_Property

EF Core 10:

OuterComplex_NestedComplex_Property

Impatto della migrazione: Se hai tabelle esistenti con tipi complessi, potrebbe essere necessario rinominare le colonne o configurare i nomi espliciti delle colonne:

modelBuilder.Entity<Order>()
    .ComplexProperty(o => o.ShippingAddress)
    .Property(a => a.Street)
    .HasColumnName("ShippingAddress_Street"); // Explicit name

SQL Server / Azure SQL Modifiche specifiche

Tipo di dati JSON predefinito

Per Azure SQL Database o SQL Server 2025 (livello di compatibilità 170+), EF Core 10 di default per il nuovo nativo JSON tipo di dati invece di NVARCHAR(MAX).

Opt-out (se hai bisogno di compatibilità all'indietro):

optionsBuilder.UseAzureSql(connectionString,
    o => o.UseCompatibilityLevel(160)); // Use old NVARCHAR behavior

Aggiornamento lista di controllo

Quando si passa da EF Core 8/9 a EF Core 10:

  1. Aggiornare il quadro obiettivo a net10.0
  2. Aggiorna tutti Microsoft.EntityFrameworkCore.* pacchetti a 10.x
  3. Aggiornamento Npgsql.EntityFrameworkCore.PostgreSQL a 10.x
  4. Rivedere le query usando Contains() con collezioni per cambi di performance
  5. Provare qualsiasi codice che costruisce programmaticamente ExecuteUpdateAsync espressioni
  6. Controllare i nomi delle colonne di tipo complesso se si utilizzano tipi complessi annidati
  7. Recensione dell'utilizzo della colonna SQL Server JSON se si utilizza Azure SQL/SQL Server 2025

Cosa c'è di nuovo (evidenziazioni)

  • Migliorata traduzione LINQ: Migliore generazione di SQL per query complesse
  • Lambda non-espressione in esecuzioneAggiornamentoAsync: Più flessibilità negli aggiornamenti alla rinfusa
  • Migliore gestione dei parametri: Migliorato il caching del piano di query
  • Supporto JSON migliorato: Native JSON type on SQL Server 2025
  • Miglioramenti delle prestazioni: Materializzazione e monitoraggio dei cambiamenti più rapidi

Caratteristiche di prestazione

  • Prestazioni di query: 20-50% rispetto a Dapper per richieste semplici
  • Uso della memoria: Più alto a causa del cambiamento di tracciamento e generazione proxy
  • Prima interrogazione: Lento (compilazione di query e cache)
  • Queries successive: Più veloce a causa della cache di query compilata
  • Inserisci/Aggiornamenti: Automatic change tracking aggiunge overhead
  • Operazioni all'ingrosso: Scarse prestazioni con i metodi predefiniti (considerare EFCore.BulkEstensioni o EseguiAggiornamento/EseguiElimina in EF Core 7+)

Migliori pratiche per il nucleo dell'impronta ambientale

Orientamenti generali

  1. Utilizzare AsNoTracking() per le query in sola lettura per ridurre la memoria in overhead
  2. Evita le interrogazioni N+1 - uso Include() o dividere le query in modo appropriato
  3. Usa interrogazioni compilate per i modelli di query ripetuti
  4. Considera AsSplitQuery() per complessi include per evitare prodotti cartesiani
  5. Utilizzare il batching per inserti/aggiornamenti multipli
  6. Progetto a DTO presto per ridurre il trasferimento dei dati e l'utilizzo della memoria
  7. Sfrutta EseguiAggiorna/EseguiElimina (EF core 7+) per le operazioni all'ingrosso
  8. Registra e recensisci sempre SQL generato nello sviluppo

Quando si lavora con PostgreSQL

  1. Usa la ricerca full-text caratteristiche (la mia guida) invece di LIKE query
  2. Sfrutta colonne JSONB per dati flessibili senza schema
  3. Usa i tipi di array per le raccolte all'interno delle entità
  4. Configura messa in comune delle connessioni correttamente per il vostro carico di lavoro
  5. Index your tsvector colonne con indici GIN
  6. Usa i tipi di intervallo per intervalli di tempo/data

In arrivo nella parte 2

Nel prossimo articolo, esploreremo:

  • DapperCity name (optional, probably does not need a translation): Il micro-ORM Sweet spot
  • Raw ADO.NET/Npgsql: Massime prestazioni e controllo
  • Librerie di mappatura degli oggetti: Mapster vs AutoMapper
  • Approcci ibridi: Combining EF Core and Dapper (CQRS pattern)
  • Performance Benchmarks: Confronti tra mondo reale e mondo reale
  • Matrice della decisione: Scegliere lo strumento giusto per il tuo scenario

Riferimenti e ulteriori letture

Articoli correlati su questo blog:


Nella parte 2, ci immergeremo in Dapper, raw Npgsql, ed esploreremo come combinare più approcci per prestazioni ottimali e manutenbilità.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.