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
Kun rakennat .NET-sovelluksia, yksi tärkeimmistä arkkitehtonisista päätöksistäsi on se, miten käsittelet datan saantia ja esinekartoitusta. .NET-ekosysteemi tarjoaa monipuolisen valikoiman lähestymistapoja täyslaidallisista ORM-laitteista paljain jaloin metallisiin SQL-suorituksiin. Jokaisessa lähestymistavassa on omat kompromissinsa suorituskyvyn, kehittäjän tuottavuuden, tyyppiturvallisuuden ja säilyvyyden suhteen.
Tässä kattavassa kaksiosaisessa oppaassa tutustumme .NETin suosituimpiin datankäyttökuvioihin. Samalla kun käytämme esimerkeissämme PostgreSQL:ää Npgsqlin kanssa (koska se on tämän blogin valtti), käsitteet, kuviot ja sovitukset koskevat yhtä lailla SQL-palvelinta, MySQL:tä, SQLite:tä ja muita suhteellisia tietokantoja. Periaatteet pysyvät samoina - vain SQL:n murre ja tietyt erityispiirteet eroavat toisistaan.
Osa 1 (tämä artikla) keskittyy Entity Framework Coreen, SQL-sukupolveen ja yhteisiin sudenkuoppiin. 2 osa Se kattaa Dapperin, raa'an ADO.NETin, esinekartoituskirjastot ja hybridilähestymistavat.
Jos olet kiinnostunut käytännön EF Core -toteutuksista, tutustu muihin artikkeleihini:
.NET-datakenttä voidaan visualisoida taajuudeksi:
Full Abstraction Full Control
↓ ↓
[EF Core] → [EF Core Raw SQL] → [Dapper] → [Npgsql ADO.NET]
Kun siirryt vasemmalta oikealle, saat suorituskykyä ja hallintaa, mutta menetät mukavuutta ja automaattisia ominaisuuksia. Tutkitaan jokainen lähestymistapa yksityiskohtaisesti.
Tässä visuaalinen vertailu siitä, miten kukin lähestymistapa käsittelee tyypillistä kyselyä:
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
"Yhteenliittymän ydin" On Microsoftin lippulaiva ORM, joka tarjoaa täydellisen abstraktion tietokantaan. Se tukee PostgreSQL kautta Npgsql.EntityFrameworkCore.PostgreSQL palveluntarjoaja.
Käytännöllisiä ohjeita EF Coren perustamiseen hankkeessasi, katso artikkelini aiheesta Entiteettikehyksen lisääminen blogikirjoituksiin.
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 tukee myös raakaa SQL-kyselyä, kun tarvitset lisää kontrollia:
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;
}
Käytä EF-ydintä, kun:
Vältä EF-ydintä, kun:
Yksi tärkeimmistä osa-alueista EF Coren tehokkaassa käytössä on sen tuottaman SQL:n ymmärtäminen. EF Core on vuosien varrella parantanut merkittävästi SQL-sukupolvea, mutta on tärkeää varmistaa, että kyselyt lähetetään PostgreSQL:lle.
// 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();
Luotu SQL (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
Huomaa, miten EF Core 8+ tuottaa puhtaan ja tehokkaan SQL:n, jossa on asianmukainen parametrisointi. EF-ydin 10 Tätä suuntausta jatketaan vielä paremmalla parannuksella.
C#-koodi:
var posts = await _context.BlogPosts
.Include(p => p.Category)
.Include(p => p.Comments)
.ToListAsync();
Vanha SQL (EF Core 3.1 – Cartesian Explosion):
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"
Tämä luo Kartesaanituote - jos viestissä on 10 kommenttia, tuo rivi toistetaan 10 kertaa!
C#-koodi jaettuna kyselyllä:
var posts = await _context.BlogPosts
.Include(p => p.Category)
.Include(p => p.Comments)
.AsSplitQuery() // ← This is the key!
.ToListAsync();
Luotu SQL (Multiple Questions):
-- 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"
Tämä poistaa Cartesian tuote ja on usein paljon nopeammin kokoelmiin!
C#-koodi:
var posts = await _context.BlogPosts
.Include(p => p.Comments.Where(c => c.IsApproved))
.ToListAsync();
Luotu 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"
C#-koodi:
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();
Luotu SQL:
SELECT b."Id", b."Title", b."Metadata"
FROM "BlogPosts" AS b
WHERE b."Metadata" ->> 'IsFeatured' = 'true'
EF Core 7+ voi kääntää JSON-kiinteistön pääsyn PostgreSQL JSON-operaattoreihin!
Vanha tapa (tehoton):
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!
New Way (EF Core 7+):
await _context.BlogPosts
.Where(p => p.CategoryId == 5)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.IsArchived, true));
Luotu SQL (Single Query!):
UPDATE "BlogPosts" AS b
SET "IsArchived" = TRUE
WHERE b."CategoryId" = 5
Tämä on massiivinen parannus - yksi SQL-lausunto N:n sijaan!
Vanha tapa:
var oldPosts = await _context.BlogPosts
.Where(p => p.PublishedDate < DateTime.UtcNow.AddYears(-5))
.ToListAsync();
_context.BlogPosts.RemoveRange(oldPosts);
await _context.SaveChangesAsync(); // N DELETE statements
Uusi tapa:
await _context.BlogPosts
.Where(p => p.PublishedDate < DateTime.UtcNow.AddYears(-5))
.ExecuteDeleteAsync();
Luotu SQL:
DELETE FROM "BlogPosts" AS b
WHERE b."PublishedDate" < @__p_0
C#-koodi:
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();
Luotu SQL (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"
Syvempi sukellus kokotekstihakuun, katso artikkelini aiheesta täystekstihaun toteuttaminen EF Corella.
C#-koodi:
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();
Luotu 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
Ongelma:
// ❌ 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!
}
}
Miksi se on ongelma?
DbContextRatkaisu:
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;
}
KRIITTINEN VAROITUS: ÄLÄ KÄYTÄ EF CORE -PROJEKTIA, JOS KOSKET YHTEISÖITÄ
Laiskat kuormauspäätteet + välimuistit = TAKAttu MUISTO LEAK
Jos välimuistissa DbContext-tapaukset tai välimuistin kohteet ladattuna, TAPAHTUMA Vuotomuisti. Valtakirjamekanismissa säilytetään viittaukset DbContextiin, mikä estää jätteiden keräämisen. Tämä on yksi yleisimmistä ja vaarallisimmista virheistä EF Core -sovelluksissa.
Nyrkkisääntö: Sisällytä kokoelmat aina erikseen
.Include(). Käytä valtakirjoja vain, jos ymmärrät täysin vaihtokaupat etkä koskaan, koskaan kätki välityspalvelimia.
Ongelma 1: N+1 Query Nightmare
// ❌ 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!
}
Mitä tapahtuu:
Category käynnistää tietokantakyselynComments laukaisee toisen kyselynOngelma 2: Proxy + Caching = muistivuoto
// ❌ 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;
}
}
Tämä on katastrofaalista:
DbContextDbContext ylläpitää viittausta kaikkiin seurattaviin yhteisöihinRatkaisu: Ole eksplisiittinen
// ✅ 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;
}
}
Kun valtakirjat voivat olla hyväksyttäviä (selkeät kaupat):
Laiskat latauspäätteet saattavat olla hyväksyttäviä VAIN silloin, kun:
Mutta vielä silloinkin, selväsanaisesti Include() on lähes aina parempi valinta, koska:
Lisätietoja DbContextin käyttöiän hallinnasta tuotannossa on artikkelissani aiheesta EF:n muuttoliikkeet oikealla tavalla.
Ongelma:
// ❌ 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);
}
}
Miksi se on väärin?
DbContext is ei kierteiltään turvallinenRatkaisu:
// ✅ 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...
}
}
Ongelma:
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
Ratkaisu:
// ✅ 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();
Ongelma:
// ❌ 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!
}
Ratkaisu:
// ✅ 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);
}
yy) kanssa. EF-ydin 10 .NET 10:n ohessa julkaistaan useita tärkeitä muutoksia, joista kannattaa olla tietoinen päivitettäessä. EF-ytimen muutosten rikkominen 10.
EF Core 10 vaatii .NET 10. Se ei toimi .NET 8, .NET 9, tai .NET Framework. Tämä on merkittävin muutos - varmista projektin tavoitteet net10.0 ennen parannusta.
EF Core 10 muuttaa tapaa, jolla Contains() muistinsisäiset kokoelmat käännetään SQL:lle. Aiemmin käytetty EF Core OpenJson() (SQL-palvelin) tai vastaava. Nyt se on oletuksena parametriryhmät joka tarjoaa paremman kyselysuunnitelman välimuistista.
Vaikutus: Voit nähdä erilaisia SQL luotu kyselyitä, kuten:
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 (Parametrin säteet):
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
Jos koet suoritusregressioita, palaa vanhaan käytökseen:
// SQL Server
optionsBuilder.UseSqlServer(connectionString,
o => o.UseParameterizedCollectionMode(ParameterTranslationMode.Constant));
// PostgreSQL - generally parameter arrays work well, but you can opt out if needed
Erytropoietiini ExecuteUpdateAsync Allekirjoitus on vaihtunut tukemaan ilmaisuttomuutta. Tämä on joustavampaa, mutta murtaa koodia, joka rakensi ilmaisupuita ohjelmallisesti:
Vanha tapa (EF Core 7-9):
// This still works
await _context.BlogPosts
.Where(p => p.CategoryId == 5)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.IsArchived, true));
Uusi EF Core 10:ssä – Ei-ilmaistua lambdat:
// 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 muuttaa, kuinka pesityt monimutkaiset saraketyypit nimetään datan korruption estämiseksi:
EF-ydin 9:
NestedComplex_Property
EF-ydin 10:
OuterComplex_NestedComplex_Property
Maahanmuuttovaikutukset: Jos sinulla on olemassa taulukoita, joissa on monimutkaisia tyyppejä, saatat joutua nimeämään sarakkeet uudelleen tai konfiguroimaan sarakkeen nimenomaiset nimet:
modelBuilder.Entity<Order>()
.ComplexProperty(o => o.ShippingAddress)
.Property(a => a.Street)
.HasColumnName("ShippingAddress_Street"); // Explicit name
Azure SQL Database tai SQL Server 2025 (yhteensopivuustaso 170+), EF Core 10 -oletukset uudelle syntyperäiselle JSON Tietotyyppi, ei NVARCHAR(MAX).
Jättäytyä pois (jos tarvitset takautuvaa yhteensopivuutta):
optionsBuilder.UseAzureSql(connectionString,
o => o.UseCompatibilityLevel(160)); // Use old NVARCHAR behavior
Parannettaessa EF-ytimestä 8/9 EF-ytimeen 10:
net10.0Microsoft.EntityFrameworkCore.* paketit kohteeseen 10.xNpgsql.EntityFrameworkCore.PostgreSQL to 10.xContains() Kokoelmat suoritusmuutoksilleExecuteUpdateAsync ilmauksiaInclude() tai jakaa kyselyt oikeinLIKE kyselyttsvector sarakkeita jossa on GIN-indeksitSeuraavassa artikkelissa tutkimme:
Aiheeseen liittyvät artikkelit tästä blogista:
Osassa 2 sukeltamme Dapperiin, raakaan Npgsqliin, ja tutkimme, miten yhdistellään useita lähestymistapoja optimaaliseen suorituskykyyn ja säilyvyyteen.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.