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

<!-- category -- .NET, EF Core, PostgreSQL, Performance, Database -->
<datetime class="hidden">2025-12-03T14:00</datetime>

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:

- [Aggiunta del quadro dell'entità per i post del blog (parte 1)](/blog/addingentityframeworkforblogpostspt1) - Configurazione del nucleo EF da zero
- [Migrazioni dell'impronta ambientale nel modo giusto](/blog/efmigrationstherightway) - Come gestire correttamente le migrazioni in produzione
- [Ricerca completa del testo (Pt 1)](/blog/textsearchingpt1) - Implementazione della ricerca postgreSQL full-text con EF Core
- [Moderno CQRS ed Event Sourcing](/blog/moderncqrsandeventsourcing) - Modelli architettonici avanzati con EF Core

## 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:

```mermaid
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

```mermaid
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à](https://learn.microsoft.com/en-us/ef/core/) è ammiraglia di Microsoft ORM, fornendo una completa astrazione sul vostro database. Supporta PostgreSQL attraverso il [Npgsql.EntityFrameworkCore.PostgreSQL](https://www.npgsql.org/efcore/) 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](/blog/addingentityframeworkforblogpostspt1).

### 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](/blog/efmigrationstherightway))
- **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](/blog/textsearchingpt1)), colonne JSON, array, tipi di campo
- **Intercettori ed eventi**: Punti di estensibilità per le preoccupazioni trasversali

### Esempio: CRUD di base con nucleo EF

```csharp
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:

```csharp
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](/blog/efmigrationstherightway))
- 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

```csharp
// 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:**

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

**SQL generato ([EF Core 8+](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew)):**

```sql
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](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) continua questa tendenza con ulteriori miglioramenti.

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

**Codice C#:**

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

**Vecchio SQL (EF Core 3.1 - Esplosione cartesiana):**

```sql
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:**

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

**Generato SQL (Multiple Queries):**

```sql
-- 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#:**

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

**SQL generato:**

```sql
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#:**

```csharp
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:**

```sql
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):**

```csharp
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+):**

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

**Generato SQL (Single Query!):**

```sql
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:**

```csharp
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:**

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

**SQL generato:**

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

### Esempio: Aggregazione complessa

**Codice C#:**

```csharp
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):**

```sql
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](/blog/textsearchingpt1).

**Codice C#:**

```csharp
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:**

```sql
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:**

```csharp
// ❌ 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:**

```csharp
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**

```csharp
// ❌ 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**

```csharp
// ❌ 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**

```csharp
// ✅ 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)
2. - Tu **mai** enti cache
3. Sei d'accordo con le prestazioni della query N+1
4. Sei prototipazione e si ottimizzerà in seguito
5. 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](/blog/efmigrationstherightway).

**Il problema:**

```csharp
// ❌ 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:**

```csharp
// ✅ 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:**

```csharp
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:**

```csharp
// ✅ 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:**

```csharp
// ❌ 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:**

```csharp
// ✅ 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](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) 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](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes).

### 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:

```csharp
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):**

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

**EF Core 10 (Parameter Arrays):**

```sql
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:

```csharp
// 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):**

```csharp
// 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:**

```csharp
// 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:

```csharp
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):

```csharp
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](https://github.com/borisdj/EFCore.BulkExtensions) 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](/blog/textsearchingpt1)) 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

- [Documentazione del quadro di riferimento dell'entità](https://learn.microsoft.com/en-us/ef/core/)
- [Npgsql Entity Framework Core Provider](https://www.npgsql.org/efcore/)
- [Prestazioni del nucleo EF](https://learn.microsoft.com/en-us/ef/core/performance/)
- [Cosa c'è di nuovo in EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew)
- [Breaking changes in EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes)
- [Cosa c'è di nuovo in EF Core 8](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew)
- [Documentazione PostgreSQL](https://www.postgresql.org/docs/)

**Articoli correlati su questo blog:**

- [Aggiunta del quadro dell'entità per i post del blog](/blog/addingentityframeworkforblogpostspt1)
- [Migrazioni dell'impronta ambientale nel modo giusto](/blog/efmigrationstherightway)
- [Ricerca del testo completo con il nucleo EF](/blog/textsearchingpt1)
- [Moderno CQRS ed Event Sourcing](/blog/moderncqrsandeventsourcing)

---


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