EF Core DbContext Options Explained: A Beginner's Friendly Guide
Learn EF Core DbContext options in simple words: AddDbContext, the options builder, retry on failure, query splitting, logging, lifetimes and pooling, with diagrams and examples.
A tea stall and its recipe card
Picture a small tea stall near a railway station. The owner, Ravi, makes the same chai again and again all day. Before the rush starts in the morning, he writes a small recipe card: how much milk, how much sugar, which stove to use, what to do if the gas runs low, and whether to make one big pot or many small cups.
Once that card is ready, Ravi does not think about these choices for every customer. He just follows the card. The card was set up once, and it shapes every cup he serves.
In Entity Framework Core, your DbContext is the chai maker. The recipe card is the set of options you give it. You write these options one time when your app starts. After that, every database call follows the same card. The object you use to write the card is called the DbContextOptionsBuilder, and the finished card is a DbContextOptions object.
This article explains that recipe card in simple words. We will look at where the options live, the most useful ones, and the mistakes that trip up beginners.
What is a DbContext, really?
A DbContext is your bridge to the database. It knows your tables, tracks the changes you make to objects, and turns your C# code into SQL. But a context cannot do its job until it knows a few things first:
- Which database are we talking to? SQL Server? PostgreSQL? SQLite?
- Where is that database? This is the connection string.
- How should it behave? Should it retry on errors? Should it log queries? Should it split big queries?
All of these answers go into the options. The starting point for every one of them is the DbContextOptionsBuilder.
Where do the options come from?
There are three common places to set options. Beginners get confused because tutorials mix them. Let us keep them clear.
| Place | When you use it | Who creates the context |
|---|---|---|
AddDbContext in Program.cs | ASP.NET Core apps (most common) | Dependency injection |
OnConfiguring inside the context | Small console apps, quick tests | You, with new |
External DbContextOptions passed to the constructor | Unit tests, full control | You, by hand |
For web apps, the first row is the one you will use almost every time. Let us start there.
The AddDbContext way
In an ASP.NET Core app, you register your context in Program.cs. You call AddDbContext and pass a small function that receives the options builder. Inside that function, you chain your settings.
var builder = WebApplication.CreateBuilder(args);
string connectionString = builder.Configuration
.GetConnectionString("AppDb")!;
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
});
var app = builder.Build();The options variable here is your DbContextOptionsBuilder. The UseSqlServer call is the most important line. It tells EF Core which provider to use and where the database lives. Each context must use one and only one provider.
The OnConfiguring way
In a small console app, you may not have dependency injection. There you can override OnConfiguring inside the context itself.
public class AppDbContext : DbContext
{
protected override void OnConfiguring(
DbContextOptionsBuilder options)
{
options.UseSqlite("Data Source=app.db");
}
public DbSet<Student> Students => Set<Student>();
}This is simple, but the connection string is hard-coded. For real apps, prefer AddDbContext so settings come from configuration files and secrets.
The constructor way
The third way passes a ready-made DbContextOptions to the constructor. This is the cleanest for testing because you control everything.
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("TestDb")
.Options;
using var context = new AppDbContext(options);Your context needs a constructor that accepts these options and hands them to the base class.
Three ways to give options to a DbContext
Steps
AddDbContext
DI builds options
OnConfiguring
Context builds its own
Constructor
You pass options in
DbContextOptions
Final recipe card
The provider call and its inner options
The Use... call, like UseSqlServer or UseNpgsql, does two things. First, it picks the provider. Second, it accepts a second function for provider-specific settings. This is where some of the most useful options live, and beginners often miss this inner door.
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString, sql =>
{
sql.EnableRetryOnFailure();
sql.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
sql.CommandTimeout(60);
});
});Notice the two levels. The outer options is the general builder. The inner sql is the SQL Server builder. Retries and query splitting are provider settings, so they belong inside the inner function.
The options you will actually use
Let us go through the settings a beginner meets most often. We will explain what each one does in plain words.
EnableRetryOnFailure (connection resiliency)
Cloud databases sometimes drop a connection for a second. The fix should not be "the user sees an error." EnableRetryOnFailure tells EF Core to try the command again a few times before giving up. Microsoft recommends this when connecting to Azure SQL.
Behind this is an execution strategy. When you turn on retries, EF Core wraps each operation so it can repeat it. One catch: if you start a transaction by hand, you must run it through the strategy, because a retry must replay the whole transaction, not half of it.
EnableSensitiveDataLogging
By default, when EF Core logs a query, it hides the real values. You see @p0 instead of the actual name or password. That keeps logs safe. EnableSensitiveDataLogging removes that shield and shows the real values. This is wonderful while debugging on your laptop, because you can see exactly what was sent. But it can leak private data, so keep it off in production.
EnableDetailedErrors
Normally EF Core gives short error messages to stay fast. EnableDetailedErrors adds more detail, especially when a value fails to read into a property. Like sensitive logging, use it in development.
Query splitting behavior
When you load a parent and several child collections together, a single SQL query can blow up into a huge result with repeated rows. This is called cartesian explosion. Setting UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) makes EF Core load each collection with its own query instead. You can also choose per query with AsSplitQuery() or AsSingleQuery().
Command timeout
CommandTimeout sets how many seconds EF Core waits for a single command before it gives up. Raise it for heavy reports; keep it small for quick web pages.
Here is a quick table that maps each option to where it belongs and when to use it.
| Option | Layer | Use it for |
|---|---|---|
UseSqlServer / UseNpgsql | Outer builder | Picking the provider |
EnableRetryOnFailure | Provider builder | Cloud / flaky networks |
EnableSensitiveDataLogging | Outer builder | Local debugging only |
EnableDetailedErrors | Outer builder | Local debugging only |
UseQuerySplittingBehavior | Provider builder | Many child collections |
LogTo | Outer builder | Seeing the SQL in console |
Seeing the SQL with LogTo
A great way to learn EF Core is to watch the SQL it writes. The LogTo method sends logs to any function you like, such as the console.
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine)
.EnableSensitiveDataLogging(); // dev only
});Now every query prints to the console. Combine it with EnableSensitiveDataLogging during development and you can see the exact values too. Turn both off before you ship.
Lifetimes: how long does a context live?
A DbContext is meant to be short-lived. Create one, do a unit of work, then dispose it. In ASP.NET Core, AddDbContext registers the context with a scoped lifetime by default. That means one context per web request, and it is disposed when the request ends.
This matters because a DbContext is not thread-safe. You should never share one context across parallel tasks. One request, one context, used one step at a time.
Scoped DbContext lifetime in a web request
Steps
Request in
User hits an endpoint
Create context
DI makes a new one
Query and save
Do the work
Dispose context
Cleaned up at end
Pooling: reusing contexts for speed
Creating a context has a small cost. On a very busy site, that cost adds up. AddDbContextPool keeps a small pool of context instances. Instead of building a fresh one each request, EF Core hands out a reset one from the pool, then takes it back when the request ends.
builder.Services.AddDbContextPool<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
});Pooling can speed up high-traffic apps. But there is a rule: do not store per-request data inside your context, like the current user's id in a field. Because the same instance is reused, that data could leak into the next request. Keep the context clean and stateless.
A complete, sensible setup
Let us put the good parts together. This setup reads the connection string from configuration, enables retries for the cloud, and turns on rich logging only when running in development.
var builder = WebApplication.CreateBuilder(args);
string connectionString = builder.Configuration
.GetConnectionString("AppDb")!;
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString, sql =>
{
sql.EnableRetryOnFailure();
sql.CommandTimeout(60);
});
if (builder.Environment.IsDevelopment())
{
options.LogTo(Console.WriteLine)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
});
var app = builder.Build();This is a friendly default for a real app. The risky logging is fenced behind a development check, so production stays safe and quiet.
Common beginner mistakes
A few traps catch almost everyone at the start. Knowing them now saves hours later.
- Putting retry on the outer builder.
EnableRetryOnFailureis a provider option. It goes inside theUseSqlServerfunction, not on the outeroptions. - Leaving sensitive logging on in production. It can leak private values. Guard it with an environment check.
- Sharing one context across threads. A context is not thread-safe. Use one per request, one step at a time.
- Storing per-request state in a pooled context. With pooling, that state can bleed into the next request. Keep the context stateless.
- Configuring the provider twice. If you call
UseSqlServerin bothOnConfiguringandAddDbContext, you can get confusing behavior. Pick one place.
How EF Core reads your options at startup
It helps to picture the flow. When your app starts, dependency injection calls your lambda, which fills the builder. EF Core freezes that into a DbContextOptions. Then, for each request, the context is created using that frozen card.
This is why your options feel "global." You set the card once, and every request follows it. Only per-query helpers like AsSplitQuery() let you override a setting for a single query.
A note on naming and versions
You may see this topic called "Entity Framework extensions options." The settings here are the built-in DbContextOptionsBuilder options that ship with EF Core itself. They are not a separate paid product. As of 2026, .NET 10 is the long-term support release and C# 14 has shipped, but these options have stayed stable across many EF Core versions, so what you learn here keeps working.
If you later add third-party libraries that bring their own Use... extension methods, they plug into the very same builder. The pattern you learned does not change: pick a provider, then chain options.
Quick recap
- The
DbContextis your chai maker; the options are its recipe card, written once at startup. - The
DbContextOptionsBuilderis the object you use to write that card. - The three places to set options are
AddDbContext(web apps),OnConfiguring(small apps), and the constructor (tests). - The provider call like
UseSqlServerhas an inner function for provider settings such as retries and query splitting. EnableRetryOnFailureadds connection resiliency for the cloud; it lives inside the provider function.EnableSensitiveDataLoggingandEnableDetailedErrorsare for development only; never ship them on.LogTo(Console.WriteLine)lets you watch the real SQL while you learn.- A context is scoped and not thread-safe: one per request, used one step at a time.
AddDbContextPoolreuses contexts for speed, but keep them stateless.- Guard risky logging behind an environment check so production stays safe.
References and further reading
- DbContext Lifetime, Configuration, and Initialization — EF Core (Microsoft Learn)
- EnableSensitiveDataLogging — DbContextOptionsBuilder (Microsoft Learn)
- Connection Resiliency — EF Core (Microsoft Learn)
- Single vs. Split Queries — EF Core (Microsoft Learn)
- UseSqlServer Method — SqlServerDbContextOptionsExtensions (Microsoft Learn)
Related Posts
EF Core Query Splitting: Fix Slow Queries and Cartesian Explosion
Learn how EF Core query splitting (AsSplitQuery) fixes the cartesian explosion problem with simple examples, diagrams, and real performance numbers. Know when to split and when not to.
DbContext Is Not Thread-Safe: Parallelizing EF Core Queries the Right Way
Learn why EF Core DbContext is not thread-safe and how to run parallel queries safely using IDbContextFactory in .NET 10. Beginner friendly.
5 EF Core Features You Need to Know (Beginner Friendly)
Learn 5 must-know EF Core features with simple examples: change tracking, AsNoTracking, eager loading, bulk ExecuteUpdate, and query filters.
Using Multiple EF Core DbContext in a Single Application
Learn how to use multiple EF Core DbContext classes in one .NET app. See when to split, how to register, migrate, and coordinate them with simple examples.
How to Manage EF Core DbContext Lifetime: A Beginner's Guide
Learn how to manage EF Core DbContext lifetime safely. Understand scoped, transient, singleton, pooling, and DbContextFactory with simple examples and diagrams.
Soft Delete with EF Core: Delete Data Without Losing It
Learn soft delete in EF Core the right way. Use an interceptor and global query filters to hide deleted rows automatically, with simple examples, diagrams, code, and best practices for .NET 10.