Під час створення програм .NET, одним з найважливіших архітектурних рішень, які ви зробите, є спосіб обробки доступу до даних і відображення об' єктів. Екосистема .NET пропонує багате розмаїття підходів, від повнофункціональних ORM до виконання Fole- Methal SQL. Кожен підхід має свій власний торговий підхід з точки зору продуктивності, продуктивності розробки, введіть безпеку і надійність.
У цьому універсальному посібнику з двох частин ми дослідимо найпопулярніші шаблони доступу до даних у. NET. У нашому прикладі ми використовуємо PostgreSQL з Npgsql (оскільки саме це і дає змогу користуватися цим блогом), концепції, шаблони і компроміси, які застосовуються до сервера SQL, MySQL, SQL та інших реляційних баз даних. Ці принципи залишаються однаковими - лише діалект SQL і деякі специфічні можливості відрізняються.
Частина 1 (ця стаття) Зосереджується на створенні Centity Framework Core, створення SQL і типових пастках. Частина 2 буде висвітлено Dapper, raw ADO.NET, об'єкти для відображення об'єктів і гібридні підходи.
Якщо ви цікавитеся практичною реалізацією EC 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
Ядро блокування сутності is Microsoft' s flagshship ORM, надаючи повну абстракцію над вашою базою даних. Вона підтримує PostgreSQL за допомогою Npgsql.EntityFrameworkCore.PostgreSQL провайдер.
За практичними порадами щодо встановлення EC Core у вашому проекті, дивіться мою статтю про Додавання блоку сутності для дописів блогу.
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 core, якщо:
⇩ Вимкнено EF core, коли:
Одним з найважливіших аспектів ефективного використання ECF Core є розуміння того, що генерує SQL. ECF 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 Core 10 продовжує цю тенденцію ще більшим поліпшенням.
C# код:
var posts = await _context.BlogPosts
.Include(p => p.Category)
.Include(p => p.Comments)
.ToListAsync();
Старий SQL (EF Core 3. 1 - Декартовий вибух):
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"
Це створює a Декартовий продукт - якщо допис складається з 10 коментарів, то рядок повторюється 10 разів!
C# Код з розділеним запитом:
var posts = await _context.BlogPosts
.Include(p => p.Category)
.Include(p => p.Comments)
.AsSplitQuery() // ← This is the key!
.ToListAsync();
Створений 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"
Це усуває декартовий продукт, і часто його використовують. набагато швидше на колекції!
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'
ECF Core 7+ може перекладати доступ до власності JSON операторам 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 request!):
UPDATE "BlogPosts" AS b
SET "IsArchived" = TRUE
WHERE b."CategoryId" = 5
Це масивний покращення - одне твердження SQL замість N!
Старий шлях:
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;
}
теперь ЦЕРИТИЧНА ПЕРЕСТОРОГА: НЕ ВЖИВАЙТЕ ПРАВЕДЛИВОСТІ, ЯКЩО ВИ ЗАЦІКАВЛЯЄТЕ ВПЛИВ
Широке завантаження проксій + кеш = ГАРАНТОВАНЕ МЕМОРІЯ ЛЕЕК
Якщо ви кешуєте екземпляри DbContext або елементи кешу, завантажені проксіями, ви можете WOW пам' ять витоків. Механізм проксі- сервера підтримує посилання на DbContext, що запобігає збірці відходів. Це одна з найпоширеніших і небезпечних помилок у програмах EF Core.
Правило великого пальця: Завжди включати колекції явно з
.Include()... використовувати проксі- сервери, лише якщо ви повністю розумієте компроміси і ніколи, ніколи не кешувати проксі- сервери.
Проблема 1. Норма запиту 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!
}
Що відбувається:
Category запускає запит на базу данихComments запускає інший запитПроблема 2: Проксі + 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;
}
}
Коли можуть бути прийнятні прокази (Зрозуміти, що відбувається з торговцями):
Проксі завантаження можуть бути прийнятними протягом ONLY, якщо:
Але навіть тоді, явно Include() майже завжди кращий вибір, тому що:
За детальнішою інформацією про керування життям DBContext у виробництві, дивіться мою статтю про ЕФФ - мігрує на правильний шлях.
Проблема:
// ❌ 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 Core 10 10 - й випуск журналу " Пробудись! ," виданий поруч з сайтом NET, містить декілька важливих змін, про які слід пам'ятати під час оновлення. Breaking changes in EF Core 10.
EF Core 10 потребує NET 10. Його не запущено на . NET 8,. NET 9 або. NET Framework. Це найважливіша зміна - переконайтеся, що ціллю вашого проекту є net10.0 до поновления.
ECF Core 10 змінює спосіб Contains() зі збірками у пам' яті буде перекладено на SQL. Раніше було використано EF- ядро OpenJson() (сервер SQL) або подібний. Тепер типовим значенням є масиви параметрів що забезпечує краще планування запиту.
Зчеплення: Ви можете бачити різні файли 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 Core 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
The ExecuteUpdateAsync Підпис було змінено на підтримку лямбдів без виразу. Це гнучкіше, але розбиває код, що побудований виразом дерева програмно:
Старий шлях (EF Core 7- 9):
// This still works
await _context.BlogPosts
.Where(p => p.CategoryId == 5)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.IsArchived, true));
Створити у EC Core 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.
});
ECF Core 10 змінює спосіб вкладання стовпчиків складеного типу, за допомогою яких можна запобігти пошкодженню даних:
EF Core 9:
NestedComplex_Property
EF Core 10:
OuterComplex_NestedComplex_Property
Ефект міграції: Якщо у вас є таблиці зі складними типами, ймовірно, вам слід буде перейменувати стовпчики або налаштувати явні назви стовпчиків:
modelBuilder.Entity<Order>()
.ComplexProperty(o => o.ShippingAddress)
.Property(a => a.Street)
.HasColumnName("ShippingAddress_Street"); // Explicit name
Для бази даних SQL Azure або SQL Server 2025 (рівень несумісності 170+), EF Core 10 типово для нової рідної JSON Тип даних замість NVARCHAR(MAX).
Щоб вибрати (якщо вам потрібна зворотна сумісність):
optionsBuilder.UseAzureSql(connectionString,
o => o.UseCompatibilityLevel(160)); // Use old NVARCHAR behavior
Під час оновлення з EF Core 8/9 до EF Core 10:
net10.0Microsoft.EntityFrameworkCore.* пакунки до 10. xNpgsql.EntityFrameworkCore.PostgreSQL до 10.xContains() зі збірками для змін швидкодіїExecuteUpdateAsync виразиInclude() або достатньо розділити запитиLIKE Запитиtsvector стовпчиків з індексами GINУ наступній статті ми розглянемо:
Супутні статті про цей блог:
У другій частині ми зануримося в Dapper, сирий Npgsql, і досліджуємо, як комбінувати кілька підходів для оптимальної продуктивності та стійкості.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.