This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Wednesday, 03 December 2025
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.
Se siete interessati a pratiche implementazioni EF Core, controllare i miei altri articoli:
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.
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
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
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.
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();
}
}
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;
}
Usa EF Core quando:
Evitare il nucleo dell'impronta ambientale quando:
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.
// 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();
}
}
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.
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!
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!
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"
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!
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!
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
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"
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
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:
DbContextLa 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;
}
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?
Category attiva una query del databaseComments attiva un'altra queryProblema 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:
DbContextDbContext mantiene un riferimento a tutte le entità rintracciateLa 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:
Ma anche allora Include() è quasi sempre la scelta migliore perché:
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-safeLa 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...
}
}
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();
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);
}
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.
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.
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
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.
});
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
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
Quando si passa da EF Core 8/9 a EF Core 10:
net10.0Microsoft.EntityFrameworkCore.* pacchetti a 10.xNpgsql.EntityFrameworkCore.PostgreSQL a 10.xContains() con collezioni per cambi di performanceExecuteUpdateAsync espressioniInclude() o dividere le query in modo appropriatoLIKE querytsvector colonne con indici GINNel prossimo articolo, esploreremo:
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à.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.