Skip to content

Entity Framework Core

Databases & CachingDatabase/ORMC#

What it is

Entity Framework Core is Microsoft's official ORM for .NET, translating LINQ queries into SQL with change tracking, migrations and relationship management.

Define a DbContext with DbSet properties. Queries are LINQ expressions translated to SQL; SaveChanges computes the minimal set of statements from tracked changes.

Installation

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Getting started

The smallest useful thing you can do with it, and what each part means.

Context, query and save
public class AppDb : DbContext
{
    public DbSet<Book> Books => Set<Book>();
    public DbSet<Author> Authors => Set<Author>();

    protected override void OnModelCreating(ModelBuilder b)
    {
        b.Entity<Book>().HasIndex(x => x.Year);
        b.Entity<Book>().Property(x => x.Title).HasMaxLength(200).IsRequired();
    }
}

var recent = await db.Books
    .Where(b => b.Year >= 1990)
    .Include(b => b.Author)          // one join instead of N queries
    .OrderByDescending(b => b.Year)
    .Take(10)
    .ToListAsync();

db.Books.Add(new Book { Title = "Dune", Year = 1965 });
await db.SaveChangesAsync();
Include is what prevents the N+1 problem. Without it, touching b.Author inside a loop issues a separate query per book — or throws, if lazy loading is disabled.
Read-only queries and projections
// No change tracking needed for reads — measurably faster and less memory.
var titles = await db.Books
    .AsNoTracking()
    .Where(b => b.Year > 2000)
    .Select(b => new BookSummary(b.Id, b.Title, b.Author.Name))
    .ToListAsync();
Projecting with Select generates a SQL query for exactly those columns, so no join is needed for the whole entity. AsNoTracking should be the default for anything you are not going to modify.

Advanced usage

Where the library earns its place over a simpler alternative.

Transactions and bulk operations
await using var tx = await db.Database.BeginTransactionAsync();
try
{
    // ExecuteUpdate issues one UPDATE — it does not load the entities.
    await db.Stock
        .Where(s => s.Sku == sku)
        .ExecuteUpdateAsync(s => s.SetProperty(x => x.Quantity, x => x.Quantity - qty));

    db.Orders.Add(new Order { Sku = sku, Quantity = qty });
    await db.SaveChangesAsync();

    await tx.CommitAsync();
}
catch
{
    await tx.RollbackAsync();
    throw;
}
ExecuteUpdate and ExecuteDelete run set-based SQL directly. The old pattern of loading every row into memory to modify it does not scale, and this is the fix.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

A second operation was started on this context instance
Concurrent use of one DbContext. Await every call, and do not share a context across parallel tasks.
The LINQ expression could not be translated
The query uses a C# method EF cannot express in SQL. Move that part after an explicit AsEnumerable, accepting that it then runs in memory.

Best practices

  • Use AsNoTracking for read-only queries; change tracking is pure overhead there.
  • Always Include or project relations — the alternative is N+1 queries or a lazy-loading exception.
  • Use ExecuteUpdate and ExecuteDelete for bulk changes rather than loading entities.
  • Register DbContext as scoped; it is not thread-safe and must not be a singleton.

Background

Why it exists, and what it was reacting to.

EF Core is a ground-up rewrite of the older Entity Framework, built to be cross-platform, lighter and provider-agnostic. Its defining feature is that LINQ — the same syntax you use on in-memory collections — becomes SQL.