Data Access with EF Core
|
This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14. This section’s bibliography lists the reference material consulted while preparing these pages. |
Entity Framework Core is the default object-relational mapper for .NET: you model tables as classes, query
with LINQ, and persist changes through a DbContext. This page covers using it from an ASP.NET Core app; for
SQL itself see the SQL Reference.
Registering a context
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
| Registration | Use when |
|---|---|
|
Standard: one scoped context per request. |
|
High-throughput APIs — contexts are reset and reused instead of re-created. |
|
Blazor, background services, or anywhere you need to create contexts on demand outside a request scope. |
{ "ConnectionStrings": { "Default": "Server=localhost;Database=shop;Trusted_Connection=True;Encrypt=False" } }
Provider packages: Microsoft.EntityFrameworkCore.SqlServer, .Sqlite, and Npgsql.EntityFrameworkCore.PostgreSQL.
See Entity Framework Core and
EF Core with ASP.NET Core.
Querying
public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
}
// read-only query: skip change tracking, project to a DTO
var results = await db.Products
.AsNoTracking()
.Where(p => p.Price < 100)
.OrderBy(p => p.Name)
.Select(p => new ProductListItem(p.Id, p.Name, p.Price))
.ToListAsync(ct);
// related data
var order = await db.Orders
.Include(o => o.Lines).ThenInclude(l => l.Product)
.FirstOrDefaultAsync(o => o.Id == id, ct);
Tracked entities (the default) let EF detect changes on SaveChanges; AsNoTracking is faster for reads you
will not modify. Prefer Include (eager) or explicit loading; avoid lazy loading in web apps (it hides N+1
queries).
Saving and concurrency
db.Products.Add(new Product { Name = "Widget", Price = 9.99m });
product.Price = 12.50m; // tracked change
db.Orders.Remove(order);
await db.SaveChangesAsync(ct); // one transaction
The DbContext is the unit of work — all pending changes flush together. Add a concurrency token so a stale
update fails instead of silently overwriting:
public sealed class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
[Timestamp] public byte[]? RowVersion { get; set; } // or .IsRowVersion() in the Fluent API
}
try { await db.SaveChangesAsync(ct); }
catch (DbUpdateConcurrencyException) { /* reload, merge, retry or surface a 409 */ }
Modelling
Conventions cover most mappings; data annotations ([Table], [Column], [MaxLength], [Required]) or the
Fluent API in OnModelCreating handle the rest:
protected override void OnModelCreating(ModelBuilder b)
{
b.Entity<Order>(e =>
{
e.HasMany(o => o.Lines).WithOne(l => l.Order).OnDelete(DeleteBehavior.Cascade);
e.Property(o => o.Status).HasConversion<string>(); // value converter
e.OwnsOne(o => o.ShippingAddress); // owned type
e.HasIndex(o => o.Customer);
});
}
Migrations
dotnet tool install --global dotnet-ef
dotnet ef migrations add AddOrders
dotnet ef database update # apply to the dev database
dotnet ef migrations script -o out.sql # generate SQL for a release pipeline
dotnet ef dbcontext scaffold "Server=...;Database=..." Microsoft.EntityFrameworkCore.SqlServer # database-first
Apply migrations from a pipeline or an idempotent script in production rather than calling
db.Database.Migrate() at startup (which races when several instances start together). See
Migrations overview.
Resiliency, the repository debate, and Dapper
-
Connection resiliency:
o.UseSqlServer(cs, sql ⇒ sql.EnableRetryOnFailure())retries transient failures. -
See the SQL:
o.LogTo(Console.WriteLine, LogLevel.Information)or EF Core’s built-in logging category. -
Repository / unit-of-work:
DbContextalready is both. A repository layer still helps when you want to hide LINQ from the domain, standardise queries, or swap persistence in tests — otherwise it is indirection for its own sake. -
Dapper is a thin micro-ORM for hot paths or complex SQL:
using var conn = new SqlConnection(cs); var rows = await conn.QueryAsync<ProductListItem>( "SELECT Id, Name, Price FROM Products WHERE Price < @max", new { max = 100 });
Cross-link: SQL Reference.