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
Όταν χτίζετε .NET εφαρμογές, μια από τις σημαντικότερες αρχιτεκτονικές αποφάσεις που θα κάνετε είναι πώς να χειριστείτε την πρόσβαση των δεδομένων και τη χαρτογράφηση των αντικειμένων. Το οικοσύστημα .NET προσφέρει μια πλούσια ποικιλία προσεγγίσεων, από πλήρως εξοπλισμένο ORMs έως γυμνό μέταλλο εκτέλεση SQL. Κάθε προσέγγιση έρχεται με τις δικές του συναλλαγές όσον αφορά την απόδοση, την παραγωγικότητα του προγραμματιστή, την ασφάλεια του τύπου και τη διατήρηση.
Σε αυτό το περιεκτικό οδηγό δύο μερών, θα εξερευνήσουμε τα πιο δημοφιλή πρότυπα πρόσβασης δεδομένων σε .NET. Ενώ χρησιμοποιούμε PostgreSQL με Npgsql στα παραδείγματα μας (αφού αυτό είναι ό, τι εξουσία αυτό το blog), οι έννοιες, τα πρότυπα, και τις συναλλαγές ισχύουν εξίσου για SQL Server, MySQL, SQLite, και άλλες σχετικές βάσεις δεδομένων. Οι αρχές παραμένουν οι ίδιες - μόνο η διάλεκτος SQL και ορισμένα συγκεκριμένα χαρακτηριστικά διαφέρουν.
Μέρος 1 (το παρόν άρθρο) εστιάζει σε πυρήνα πλαισίου οντοτήτων, γενιά SQL και κοινές παγίδες. Μέρος 2 θα καλύψει Dapper, ωμό ADO.NET, βιβλιοθήκες χαρτογράφησης αντικειμένων, και υβριδικές προσεγγίσεις.
Αν ενδιαφέρεστε για πρακτικές εφαρμογές EF Core, ελέγξτε τα άλλα άρθρα μου:
Το τοπίο πρόσβασης δεδομένων NET μπορεί να απεικονιστεί ως ένα φάσμα:
Full Abstraction Full Control
↓ ↓
[EF Core] → [EF Core Raw SQL] → [Dapper] → [Npgsql ADO.NET]
Καθώς κινείστε από αριστερά προς τα δεξιά, αποκτάτε απόδοση και έλεγχο αλλά χάνετε την ευκολία και τα αυτόματα χαρακτηριστικά.
Εδώ είναι μια οπτική σύγκριση του πώς κάθε προσέγγιση χειρίζεται ένα τυπικό ερώτημα:
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
Κεντρικό πλαίσιο οντότητας είναι η ναυαρχίδα ORM της Microsoft, παρέχοντας μια πλήρη αφαίρεση πάνω από τη βάση δεδομένων σας. Npgsql.Entity frameworkCore.PostgreSQL Προμηθευτής.
Για πρακτική καθοδήγηση σχετικά με τη δημιουργία πυρήνα EF στο έργο σας, δείτε το άρθρο μου σχετικά με Προσθήκη πλαισίου οντοτήτων για δημοσιεύσεις 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 υποστηρίζει επίσης ακατέργαστα ερωτήματα SQL όταν χρειάζεστε περισσότερο έλεγχο:
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;
}
Χρησιμοποιήστε τον πυρήνα EF όταν:
Αποφύγετε τον πυρήνα EF όταν:
Μια από τις πιο σημαντικές πτυχές της χρήσης EF Core αποτελεσματικά είναι η κατανόηση τι SQL που παράγει. EF Core έχει βελτιώσει σημαντικά την γενιά SQL όλα αυτά τα χρόνια, αλλά είναι κρίσιμης σημασίας για την επαλήθευση των ερωτημάτων που αποστέλλονται στο 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 (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
Παρατηρήστε πώς το EF Core 8+ παράγει καθαρό, αποδοτικό SQL με σωστή παραμετροποίηση. EF πυρήνας 10 συνεχίζει αυτή την τάση με ακόμη περισσότερες βελτιώσεις.
Κωδικός C#:
var posts = await _context.BlogPosts
.Include(p => p.Category)
.Include(p => p.Comments)
.ToListAsync();
Old 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"
Αυτό δημιουργεί ένα Καρτεσιανό προϊόν - εάν μια δημοσίευση έχει 10 σχόλια, αυτή η σειρά επαναλαμβάνεται 10 φορές!
Κωδικός C# με το Split Query:
var posts = await _context.BlogPosts
.Include(p => p.Category)
.Include(p => p.Comments)
.AsSplitQuery() // ← This is the key!
.ToListAsync();
Δημιουργία 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"
Αυτό εξαλείφει το Καρτεσιανό προϊόν και είναι συχνά Πολύ πιο γρήγορα. για τις συλλογές!
Κωδικός C#:
var posts = await _context.BlogPosts
.Include(p => p.Comments.Where(c => c.IsApproved))
.ToListAsync();
Δημιουργία 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#:
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:
SELECT b."Id", b."Title", b."Metadata"
FROM "BlogPosts" AS b
WHERE b."Metadata" ->> 'IsFeatured' = 'true'
EF Core 7+ μπορεί να μεταφράσει JSON πρόσβαση σε ιδιοκτησία στους φορείς PostgreSQL JSON!
Παλαιός Τρόπος (Δεν είναι αποτελεσματικός):
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!
Νέος τρόπος (EF Core 7+):
await _context.BlogPosts
.Where(p => p.CategoryId == 5)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.IsArchived, true));
Δημιουργία SQL (Single Query!):
UPDATE "BlogPosts" AS b
SET "IsArchived" = TRUE
WHERE b."CategoryId" = 5
Αυτό είναι συμπαγές βελτίωση - μια δήλωση SQL αντί για 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
Νέος τρόπος:
await _context.BlogPosts
.Where(p => p.PublishedDate < DateTime.UtcNow.AddYears(-5))
.ExecuteDeleteAsync();
Δημιουργία SQL:
DELETE FROM "BlogPosts" AS b
WHERE b."PublishedDate" < @__p_0
Κωδικός 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 (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"
Για μια βαθύτερη κατάδυση σε αναζήτηση πλήρους κειμένου, δείτε το άρθρο μου σχετικά με εφαρμογή αναζήτησης πλήρους κειμένου με πυρήνα EF.
Κωδικός 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:
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
Το πρόβλημα:
// ❌ 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!
}
}
Γιατί είναι πρόβλημα:
DbContextΗ Λύση:
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;
}
ΚΡΙΤΙΚΗ ΠΡΟΕΙ∆ΟΠΟΙΗΣΗ: ΜΗ ΧΡΗΣΙΜΟΠΟΙΕΙΤΕ EF CORE PROXIES if you cache agricultures
Τεμπέλης φόρτωσης proxies + caching = ΕΓΓΥΗΣΗ ΜΝΗΜΙΚΟΥ ΛΙΘΟΥ
Εάν cache DbContext περιπτώσεις ή cache οντότητες φορτωμένες με proxies ενεργοποιημένη, σας ΓΟΥΙΛ Ο μηχανισμός μεσολάβησης διατηρεί αναφορές στο DbContext, εμποδίζοντας τη συλλογή σκουπιδιών. Αυτό είναι ένα από τα πιο κοινά και επικίνδυνα λάθη στις εφαρμογές EF Core.
Κανόνας του αντίχειρα: Πάντα να περιλαμβάνει συλλογές ρητά με
.Include()Χρησιμοποιήστε πληρεξούσια μόνο αν κατανοήσετε πλήρως τις συναλλαγές και ποτέ, ποτέ, ποτέ πληρεξουσίους φορείς.
Πρόβλημα 1: Ο Εφιάλτης Ν+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!
}
Τι συμβαίνει;
Category ενεργοποιεί ένα ερώτημα βάσης δεδομένωνComments πυροδοτεί άλλο ένα ερώτημαΠρόβλημα 2: Proxy + Caching = Διαρροή μνήμης
// ❌ 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;
}
}
Γιατί αυτό είναι καταστροφικό:
DbContextDbContext διατηρεί αναφορά σε όλες τις ιχνηλατούμενες οντότητεςΗ Λύση: Να Είστε Ακριβής
// ✅ 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;
}
}
Όταν Proxies μπορεί να είναι αποδεκτή (κατανόηση των συναλλαγών):
Η τεμπέλα φόρτωσης μπορεί να είναι αποδεκτή ΜΟΝΟ όταν:
Αλλά ακόμα και τότε, ρητή Include() είναι σχεδόν πάντα η καλύτερη επιλογή επειδή:
Για περισσότερες λεπτομέρειες σχετικά με τη διαχείριση της διάρκειας ζωής DbContext στην παραγωγή, δείτε το άρθρο μου σχετικά με Μεταναστεύσεις EF Ο Σωστός Τρόπος.
Το πρόβλημα:
// ❌ 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);
}
}
Γιατί είναι λάθος:
DbContext ί ας όχι από νήμα-ασφαλέςΗ Λύση:
// ✅ 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...
}
}
Το πρόβλημα:
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
Η Λύση:
// ✅ 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();
Το πρόβλημα:
// ❌ 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!
}
Η Λύση:
// ✅ 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 πυρήνας 10 NET 10, υπάρχουν πολλές σημαντικές αλλαγές που πρέπει να γνωρίζουμε κατά την αναβάθμιση. Για την πλήρη λίστα, δείτε Σπάζοντας τις αλλαγές στο πυρήνα EF 10.
Το EF Core 10 απαιτεί NET 10. NET 8, .NET 9, ή .NET πλαίσιο. Αυτή είναι η πιο σημαντική αλλαγή - εξασφαλίζουν τους στόχους του έργου σας net10.0 πριν από την αναβάθμιση.
Ο πυρήνας EF 10 αλλάζει τον τρόπο Contains() με συλλογές in-memory μεταφράζεται σε SQL. Προηγουμένως, EF Core χρησιμοποιείται OpenJson() (SQL Server) ή παρόμοια. Συστοιχίες παραμέτρων Τα οποία παρέχουν καλύτερο σχέδιο έρευνας.
Αντίκτυπος: Μπορείτε να δείτε διαφορετικά SQL που παράγονται για ερωτήματα όπως:
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 10 (Βελτιώσεις Παραμέτρου):
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
Εάν αισθανθείτε οπισθοδρόμηση απόδοσης, να επανέλθει στην παλιά συμπεριφορά:
// SQL Server
optionsBuilder.UseSqlServer(connectionString,
o => o.UseParameterizedCollectionMode(ParameterTranslationMode.Constant));
// PostgreSQL - generally parameter arrays work well, but you can opt out if needed
Η ExecuteUpdateAsync υπογραφή έχει αλλάξει για να υποστηρίξει τη μη-έκφραση lambdas. Αυτό είναι πιο ευέλικτο, αλλά σπάει τον κώδικα που έχτισε δέντρα έκφρασης προγραμματικά:
Παλαιός τρόπος (EF Core 7-9):
// This still works
await _context.BlogPosts
.Where(p => p.CategoryId == 5)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.IsArchived, true));
Νέα σε πυρήνα EF 10 - Λάμδα μη-έκφρασης:
// 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 10 αλλάζει τον τρόπο με τον οποίο ονομάζονται οι στήλες του σύνθετου τύπου για την πρόληψη της διαφθοράς των δεδομένων:
EF Core 9:
NestedComplex_Property
EF πυρήνας 10:
OuterComplex_NestedComplex_Property
Επιπτώσεις στη μετανάστευση: Εάν έχετε υφιστάμενους πίνακες με πολύπλοκους τύπους, μπορεί να χρειαστεί να μετονομάσετε στήλες ή να ρυθμίσετε ρητά ονόματα στήλης:
modelBuilder.Entity<Order>()
.ComplexProperty(o => o.ShippingAddress)
.Property(a => a.Street)
.HasColumnName("ShippingAddress_Street"); // Explicit name
Για Azure SQL βάση δεδομένων ή SQL Server 2025 (επίπεδο συμβατότητας 170+), προεπιλογές EF Core 10 για τη νέα μητρική JSON Τύπος δεδομένων αντί του NVARCHAR(MAX).
Για να αποσυνδεθείς. (αν χρειάζεστε πίσω συμβατότητα):
optionsBuilder.UseAzureSql(connectionString,
o => o.UseCompatibilityLevel(160)); // Use old NVARCHAR behavior
Κατά την αναβάθμιση από τον πυρήνα EF 8/9 σε πυρήνα EF 10:
net10.0Microsoft.EntityFrameworkCore.* συσκευασίες έως 10.xNpgsql.EntityFrameworkCore.PostgreSQL έως 10 xContains() με συλλογές για αλλαγές απόδοσηςExecuteUpdateAsync εκφράσειςInclude() ή ερωτήσεις διάσπασης κατάλληλαLIKE Ερωτήσειςtsvector στήλες με δείκτες GINΣτο επόμενο άρθρο, θα εξερευνήσουμε:
Σχετικά άρθρα σε αυτό το blog:
Στο Μέρος 2, θα βουτήξουμε στο Ντάπερ, ωμό Npgsql, και θα εξερευνήσουμε πώς να συνδυάσουμε πολλαπλές προσεγγίσεις για βέλτιστη απόδοση και συντηρησιμότητα.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.