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

AddDbContext<T>

Standard: one scoped context per request.

AddDbContextPool<T>

High-throughput APIs — contexts are reset and reused instead of re-created.

AddDbContextFactory<T>

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: DbContext already 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.