Efficient data access is foundational to every high-performance application. In .NET 8, Entity Framework Core 8 (EF Core) introduces several improvements, making it easier than ever to optimize your queries, reduce overhead, and structure your persistence logic cleanly.
In this guide, we’ll explore the most important data access patterns and best practices using EF Core 8.
🧱 Choosing the Right Pattern: Repository vs. Unit of Work vs. Direct DbContext
Repository Pattern: Provides a cleaner abstraction around data access and query logic.
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task AddAsync(Product product);
}
Unit of Work Pattern: Encapsulates multiple repositories under a single transaction boundary.
public interface IUnitOfWork
{
IProductRepository Products { get; }
Task<int> CommitAsync();
}
Direct DbContext Access: Use this when simplicity matters and you don’t need abstraction layers (e.g., small services).
✅ Recommendation: Use Repository + UoW in complex domains or multi-aggregate architectures. Go direct for microservices or CRUD-heavy apps.
⚡ EF Core 8 Features & Enhancements
1. Interceptors
Allows injecting logic before/after DB commands. Useful for:
- Auditing
- Soft deletes
- Query rewriting
public class AuditInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData, InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
// Custom audit logic
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
Register it:
options.AddInterceptors(new AuditInterceptor());
2. Raw SQL Improvements
Better mapping, async support, and safety with FromSqlRaw:
var products = await db.Products
.FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", 100)
.ToListAsync();
3. Complex Type Mapping
EF Core 8 supports value objects and nested properties more gracefully.
public class Product
{
public int Id { get; set; }
public PriceDetails Price { get; set; }
}
[Owned]
public class PriceDetails
{
public decimal Value { get; set; }
public string Currency { get; set; }
}
🔁 Tracking vs NoTracking
By default, EF Core tracks changes, which adds overhead. Use NoTracking for read-only queries.
var list = await db.Products.AsNoTracking().ToListAsync();
🎯 Compiled Queries
Precompile expensive LINQ queries for faster performance.
public static Func<AppDbContext, int, Task<Product?>> GetById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Products.FirstOrDefault(p => p.Id == id));
Great for high-traffic endpoints or shared query logic.
📦 Batching & Transactions
Group multiple operations into a transaction:
await using var tx = await db.Database.BeginTransactionAsync();
db.Products.Add(new Product { Name = "Test" });
await db.SaveChangesAsync();
await tx.CommitAsync();
EF Core will automatically batch compatible commands where possible to minimize roundtrips.
🔍 Query Diagnostics
Use the ToQueryString() method to inspect SQL translation.
string sql = db.Products.Where(p => p.Price > 100).ToQueryString();
Console.WriteLine(sql);
Use dotnet ef dbcontext optimize for precompilation benefits and trimming analysis.
🚀 Best Practices
- Prefer async APIs for scalability
- Use
AsNoTracking()for queries that don’t modify data - Profile SQL with SQL Profiler or EF logging
- Avoid N+1 by eager loading with
Include() - Use raw SQL only for performance-critical paths
- Break repositories by aggregate (not per-entity)
🔮 When to Consider Dapper
For extremely performance-critical sections (e.g., report generation), consider using Dapper in conjunction with EF Core.
Use EF Core for write-heavy and relational consistency, and Dapper for tight, fast SELECTs.
🧠 Final Thoughts
EF Core 8 gives you all the tools needed to build clean, performant, and maintainable data layers. Whether you use raw SQL, compiled queries, or full repository patterns, the key is understanding when and where to apply each.
By adopting these patterns and practices, your application will scale better, perform faster, and remain flexible as requirements evolve.
Want a full repo example? Stay tuned on DotNetWisdom.co.uk for downloadables, templates, and additional walkthroughs.

Leave a comment