Dynamic Connection Strings in EF Core 9 Minimal APIs¶
Altering a DbContext
Connection at Runtime¶
In Entity Framework Core (including EF Core 9.x), the database connection is normally configured when the DbContext
is
constructed, and it isn’t intended to be changed afterward. Once a DbContext
is injected (e.g. via DI in a minimal
API), its DbContextOptions
(including the connection string) are essentially fixed. There is no built-in method to
reconfigure the context’s connection string after it’s been created in the DI container. In other words, you cannot
directly “swap out” the connection string of an existing context instance once it’s been configured.
That said, EF Core does provide an advanced feature to support dynamic connections if done before first use. EF
Core’s relational providers (SQL Server, PostgreSQL, etc.) allow you to register a context without initially specifying
a connection string, then supply it at runtime. For example, UseSqlServer
(and other Use*
calls) have an overload
that omits the connection string, in which case you must set it later before using the context. EF Core exposes
the DatabaseFacade.SetConnectionString()
extension method for this purpose. In practice, this means:
- You would configure the
DbContext
with the provider but no connection string at startup. For instance:builder.Services.AddDbContext<MyContext>(opt => opt.UseSqlServer());
(calling the parameterlessUseSqlServer
overload). This registers the context without a concrete connection string. - Then, at runtime (before any database operations), you can set the actual connection string on the context instance. For example:
// Inside your endpoint or service, before using the context:
context.Database.SetConnectionString(actualConnectionString);
// Now you can use context (queries, SaveChanges, etc.)
This approach will dynamically point that context instance to the given connection string. However, caution is
required: you must call SetConnectionString
before the context is used to connect to the database (i.e., before
any query or SaveChanges
call). If the context has already been used (or was configured with a specific connection
string initially), changing it at runtime is unsupported. In summary, EF Core technically allows dynamic assignment of
the connection on a fresh context, but you cannot retroactively change an already-initialized connection string
after the context has been used.
Supported Patterns for Dynamic Connection Strings¶
Given the constraints above, the recommended solution is to provide the correct connection string when the DbContext
is created, rather than trying to alter it afterward. There are several patterns to achieve this in a .NET 9 Minimal
API:
1. Use a DbContext Factory or Manual Context Creation¶
One option is to avoid injecting the DbContext
directly, and instead inject a factory that can create DbContext
instances on the fly with the desired connection string. EF Core provides IDbContextFactory<T>
via
AddDbContextFactory
, or you can implement your own factory. For example, a custom factory interface and implementation
might look like:
public interface ITenantDbContextFactory<TContext> where TContext : DbContext
{
TContext Create(string databaseName);
}
public class Dcms3DbContextFactory : ITenantDbContextFactory<Dcms3DbContext>
{
public Dcms3DbContext Create(string databaseName)
{
// Build new options with the given database name in the connection string
var optionsBuilder = new DbContextOptionsBuilder<Dcms3DbContext>();
optionsBuilder.UseSqlServer($"Server=...;Database={databaseName};TrustServerCertificate=True;");
return new Dcms3DbContext(optionsBuilder.Options);
}
}
This factory constructs a new Dcms3DbContext
with a connection string targeting the specified database (the rest of
the connection details can be fixed). The calling code (e.g. an endpoint handler) would request Dcms3DbContextFactory
from DI and use it to create a context for the current request. This pattern is essentially what one Stack Overflow
answer suggested for multi-database scenarios. For example, in a minimal API endpoint:
app.MapGet("/data/{dbName}", async (string dbName, Dcms3DbContextFactory factory) =>
{
await using var db = factory.Create(dbName);
var results = await db.MyEntities.ToListAsync();
return Results.Ok(results);
});
Here, the context is created at request time with the appropriate connection string. Note: If you use
IDbContextFactory<T>
via AddDbContextFactory
, it’s typically registered as a singleton by default. In a dynamic
scenario, you may still need to call SetConnectionString
on the created context (since AddDbContextFactory
usually
uses a fixed configuration). Alternatively, you can register the factory as scoped and supply the dynamic connection
inside the factory as shown above. In either case, you are responsible for disposing of the context instance (as shown
with await using var db = ...
).
2. Configure DbContext per request via DI (Scoped Configuration)¶
Another approach is to leverage the dependency injection configuration to supply the connection string based on some
scoped context (such as the current HTTP request or tenant). You can use the overload of AddDbContext
that provides
the IServiceProvider
to build options. This allows you to retrieve information from other services (like configuration
or HTTP context) each time a DbContext
is created. For example, in Program.cs:
builder.Services.AddHttpContextAccessor(); // enable accessing HttpContext in DI
builder.Services.AddDbContext<Dcms3DbContext>((serviceProvider, options) =>
{
// Get current HTTP context, route values, etc.
var httpContext = serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext;
var dbName = httpContext?.Request.RouteValues["dbName"] as string;
// Build the connection string dynamically (example assumes a base template)
string baseConn = configuration.GetConnectionString("BaseTemplate"); // e.g. "Server=...;Database={0};...;"
var connectionString = string.Format(baseConn, dbName);
options.UseSqlServer(connectionString);
});
In this example, whenever Dcms3DbContext
is requested, the DI container will execute our factory lambda: it grabs the
current request’s dbName
(perhaps from the URL or headers) and configures the DbContextOptions
with the correct
connection string. This effectively gives each HTTP request a context tied to its specific database. Felipe
Gavilán’s blog illustrates this pattern using an HTTP header to convey a tenant ID, and the Code Maze series provides a
similar example using a custom IDataSourceProvider
service. In either case, the key is that the connection string
comes from a scoped service or request data rather than being hard-coded at startup.
This pattern requires that you have access to the necessary context (like route values, a JWT claim, or a header) by the
time the DbContext
is being constructed. In minimal APIs, route parameters are available in
HttpContext.Request.RouteValues
. Using an IHttpContextAccessor
(registered as singleton) is a straightforward way to
get this information inside the AddDbContext
lambda. Alternatively, you could set up a dedicated scoped service (e.g.
ITenantService
) earlier in the pipeline (via middleware or an endpoint filter) that stores the chosen database name
for the request, and then have the DbContext
configuration read from that service. This approach keeps your
DbContext
registration clean, e.g.:
builder.Services.AddScoped<ITenantService, TenantService>();
builder.Services.AddDbContext<Dcms3DbContext>((sp, options) =>
{
var tenantService = sp.GetRequiredService<ITenantService>();
string connStr = tenantService.GetCurrentTenantConnectionString();
options.UseSqlServer(connStr);
});
In this case, TenantService
would determine the current database (perhaps using the current user info or route data)
and provide the appropriate connection string. The official EF Core documentation for multi-tenancy demonstrates
this pattern: the context can accept an ITenantService
(and perhaps IConfiguration
) via its constructor, and use
that in OnConfiguring
to choose the connection string. The context is then added via AddDbContextFactory
or
AddDbContext
as a scoped or transient service so that each request/tenant evaluation is fresh.
3. Use OnConfiguring with Injected Configuration (Alternative)¶
As mentioned above, you can also implement dynamic connection logic inside your DbContext
class itself by overriding
OnConfiguring
. If your context’s constructor has access to something like IConfiguration
and a tenant identifier,
you can call the appropriate UseSqlServer
(or other provider) within OnConfiguring
. For example:
public class Dcms3DbContext : DbContext
{
private readonly string _connection;
public Dcms3DbContext(DbContextOptions<Dcms3DbContext> opts, IConfiguration config, ITenantService tenantSvc)
: base(opts)
{
// Build the connection string using the current tenant info
var tenantId = tenantSvc.TenantId;
_connection = config.GetConnectionString(tenantId);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!string.IsNullOrEmpty(_connection))
optionsBuilder.UseSqlServer(_connection);
}
}
In this setup, the context is still created per request (e.g. via a context factory or regular DI), and whenever it’s
constructed, it grabs the current tenant’s identifier and finds the matching connection string from configuration. The
OnConfiguring
ensures the context uses that connection. This pattern achieves the same result – each DbContext
instance is configured for the appropriate database – but keeps the logic inside the context class. The downside is that
you must ensure the extra services (IConfiguration
, ITenantService
) are available to the context (which often
means using AddDbContextFactory
with a scoped lifetime or using constructor injection with AddDbContext
and
explicitly passing those services in). The EF Core team’s guidance notes that if a user can switch tenants within the
same session or scope, you might need to register the context as transient to avoid caching the old connection string.
In typical request-based scoping, this isn’t an issue (each request gets a new context anyway).
4. Multiple DbContext
registrations (not typical for this scenario)¶
For completeness, if the set of possible databases is known and small, some applications simply register multiple
contexts (one per connection string) and choose between them. However, in your scenario (a single DbContext
type,
database name only known at request time, no multi-schema), that’s not an ideal solution. It’s better to use one of the
dynamic approaches above rather than duplicating context types or manually choosing between many DI registrations.
Best Practices for Dynamic Connection Scenarios in EF Core 9¶
When implementing dynamic connection strings, keep these best practices in mind:
-
Provide the connection string as early as possible: Ideally, configure the
DbContext
with the correct connection string at creation time (per request). This avoids any need to change it afterward. Use factories or DI patterns to supply the string based on the current context (tenant, user, etc.) before the context is used. -
Use scoped or transient lifetimes appropriately: For web apps, a scoped lifetime (per HTTP request) is usually appropriate for
DbContext
. If there’s a possibility a user will change the target database mid-session and you want the same user session to fetch a new database, consider using a transient context or creating new context instances as needed. Do not reuse the same context instance for different databases. -
Avoid global mutable state: Don’t store the “current connection string” in a static or singleton that is modified per request without proper scoping. For example, the Code Maze example uses a singleton
DataSourceProvider
with aCurrentDataSource
property, but notes this must be carefully managed (and per-user in multi-user scenarios). A safer approach is to keep tenant-specific info in scoped services or theHttpContext
. This ensures threads or concurrent requests don’t interfere with each other’s settings. -
Dispose of contexts properly: When you manually create contexts (via a factory or
new DbContext(options)
), be sure to dispose of them after use (e.g. useusing
/await using
blocks or let the DI scope handle disposal if the context is resolved from the container). Each context/connection should be cleaned up after the request/unit-of-work ends. -
Be mindful of connection pooling and performance: If you use DbContext pooling (
AddDbContextPool
orAddPooledDbContextFactory
), be aware that pooled contexts might retain state (including an open connection or a set connection string). Pooling is generally not recommended for dynamic connection scenarios because a context from the pool might still be tied to a previous connection. Stick to regular scoped contexts or your own factories so that each context starts fresh with the correct connection. If performance becomes a concern, measure it – EF Core is designed to create contexts quickly, and per-request creation is usually fine. -
Secure the tenant selection: Since the database is chosen at runtime, ensure that the mechanism for selecting the database is secure and validated. For example, if you pass a database name via an API route or header, validate that the caller is authorized for that database and that the name is valid. Avoid directly concatenating untrusted input into connection strings without checks (to prevent connection string injection or accidental exposure of other databases).
-
Follow EF Core updates: EF Core (including v9) continues to improve support for multi-tenant scenarios. Keep an eye on official docs and release notes. The official multi-tenancy guidance (for EF Core ⅞+) provides patterns that are still applicable in EF Core 9. While EF Core doesn’t have a built-in multi-tenant manager, the combination of the techniques above (scoped config, context factories,
SetConnectionString
, etc.) is the supported way to go.
By using these patterns, you can handle a scenario where the database name is determined at request processing time. The
preferred approach is to create a new DbContext
(or configure one via DI) with the proper connection string per
request, rather than trying to mutate an existing injected context. This aligns with EF Core’s unit-of-work pattern
and ensures each context instance talks to the correct database.
Sources:
- Microsoft Docs – EF Core Multi-Tenancy (Multiple Databases)
- Microsoft Docs – DbContext Configuration & Lifetime (for using
AddDbContextFactory
and DI) - Code Maze – Dynamically Switching DbContext at Runtime (dynamic connection via DI)
- Stack Overflow – EF Core: dynamic connection string per tenant (custom factory example)