Registering DbContext in ASP.NET Core
Entity Framework Core (EF Core) is the object-relational mapper that lets ASP.NET Core applications talk to a relational database using C# classes instead of raw SQL. Integration starts with defining a class that derives from DbContext, exposing DbSet<T> properties for each entity, and registering it with the dependency injection container via AddDbContext in Program.cs. AddDbContext wires up the context with a scoped lifetime by default, which matches the per-HTTP-request unit-of-work pattern EF Core relies on.
Cricket analogy: Registering DbContext is like a franchise registering its squad list with the league before the season starts — until that registration happens, no match (HTTP request) can use the players (entities) at all.
Connection strings and provider selection happen inside the AddDbContext lambda, typically calling options.UseSqlServer(connectionString) or options.UseNpgsql(connectionString) for PostgreSQL. The connection string itself should live in appsettings.json or, in production, in a secret store like Azure Key Vault, and be pulled in through IConfiguration rather than hardcoded. Because DbContext is registered as scoped, EF Core guarantees that all repositories and services resolved within a single HTTP request share the exact same context instance and therefore the same change-tracker and identity map.
Cricket analogy: Choosing a provider is like a team choosing which pitch conditions to prepare for — a turning pitch (SQL Server) versus a green seamer (PostgreSQL) — the strategy underneath adapts but the scorecard format stays the same.
Migrations and Schema Evolution
Migrations are EF Core's mechanism for evolving the database schema alongside your C# model changes in a version-controlled, repeatable way. Running dotnet ef migrations add <Name> inspects the current model snapshot, diffs it against the previous one, and generates a migration class with Up and Down methods describing the schema delta. Applying dotnet ef database update executes pending migrations in order against the target database, and because migrations are just C# files checked into source control, they travel with the codebase through code review, CI, and deployment pipelines.
Cricket analogy: A migration is like a substitution made mid-innings that gets recorded in the official scorebook — the Up method is the change coming in, the Down method is the record of who it replaced, so the match history stays auditable.
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("Default"),
sql => sql.MigrationsAssembly("MyApp.Infrastructure")));
// AppDbContext.cs
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Course> Courses => Set<Course>();
public DbSet<Enrollment> Enrollments => Set<Enrollment>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Enrollment>()
.HasIndex(e => new { e.StudentId, e.CourseId })
.IsUnique();
}
}
// CLI
// dotnet ef migrations add InitialCreate -o Data/Migrations
// dotnet ef database update
EF Core's change tracker only detects modifications on entities it is currently tracking. Calls like AsNoTracking() on read-only queries skip that overhead entirely and are a cheap, reliable performance win for GET endpoints that never call SaveChanges.
Never run dotnet ef database update directly against a production database as part of application startup code. Apply migrations through a controlled deployment step (or a dedicated migration job) so a bad migration can't take down every replica of your app simultaneously.
Querying and SaveChanges
LINQ queries against a DbSet<T> are translated to SQL only when enumerated — calling ToListAsync, FirstOrDefaultAsync, or iterating in a foreach triggers translation and execution, while the query itself remains an IQueryable expression tree until then. This deferred execution lets you compose filters, includes, and projections across multiple method calls before EF Core generates a single SQL statement. Include() and ThenInclude() eagerly load related entities to avoid N+1 query problems, while SaveChangesAsync() flushes every tracked insert, update, and delete accumulated on the context as a single transaction.
Cricket analogy: Deferred execution is like a captain setting the field before the ball is bowled — the plan (IQueryable expression) is fully composed, but it only 'executes' the instant the bowler releases the delivery (you call ToListAsync).
- AddDbContext registers DbContext with a scoped lifetime, matching EF Core's per-request unit-of-work design.
- Provider selection (UseSqlServer, UseNpgsql, etc.) is configured inside the AddDbContext options lambda.
- Migrations generated via dotnet ef migrations add are versioned C# files with reversible Up/Down methods.
- Apply migrations through a controlled deployment step, never automatically on every app startup in production.
- LINQ queries use deferred execution — SQL is generated only when the query is enumerated.
- Use AsNoTracking() for read-only queries and Include()/ThenInclude() to avoid N+1 query problems.
- SaveChangesAsync() commits all tracked changes on the context as a single database transaction.
Practice what you learned
1. What is the default service lifetime used when you register a DbContext with AddDbContext?
2. When does an EF Core LINQ query actually get translated into SQL and executed?
3. What is the purpose of a migration's Down method?
4. Why should AsNoTracking() be used on read-only queries?
5. What risk does running dotnet ef database update automatically at application startup introduce in production?
Was this page helpful?
You May Also Like
Repository Pattern in ASP.NET Core
How to abstract data access behind repository interfaces in ASP.NET Core, and when that abstraction is worth the added indirection over EF Core directly.
Service Lifetimes: Singleton, Scoped, Transient
How ASP.NET Core's built-in dependency injection container manages object lifetime through Singleton, Scoped, and Transient registrations.
Caching Strategies: In-Memory and Distributed
How to use IMemoryCache and IDistributedCache in ASP.NET Core to reduce database load, and how to choose between them.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics