Skip to content

Commit d60d26c

Browse files
authored
Merge pull request #23800 from abpframework/berkan/add-article
Create article: How to Dynamically Set the Connection String in EF Core
2 parents da2ea13 + b5e586e commit d60d26c

File tree

2 files changed

+305
-0
lines changed

2 files changed

+305
-0
lines changed
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# How to Dynamically Set the Connection String in EF Core
2+
3+
In modern web applications, there are scenarios where you need to determine which database to connect to at runtime rather than at compile time. This could be for multi-tenant applications, environment-specific configurations, or modular architectures where different parts of your application connect to different databases.
4+
5+
In this article, I'll walk you through creating a practical solution for dynamic connection string resolution by building a real ASP.NET Core application. We'll start with a standard template and gradually implement our own `IConnectionStringResolver` pattern.
6+
7+
> **Note**: The code examples are simplified for demonstration purposes. Production applications require additional error handling, logging, and caching.
8+
9+
## The Scenario: Building a Multi-Tenant Web Application
10+
11+
Let's imagine we're building a SaaS application where different tenants can have their own databases. Some tenants share a common database, while premium tenants get their own dedicated database for better performance and data isolation.
12+
13+
Our requirements:
14+
- Default behavior: Use the standard connection string
15+
- Multi-tenant support: Route tenants to their specific databases
16+
- Fallback mechanism: If a tenant-specific database isn't available, use the default
17+
- Simple tenant identification: Use query parameters for this example
18+
19+
> **Note**: We're building this from scratch to understand the concepts, but ABP Framework already handles all of this automatically - and does it much better! It supports separate databases for each tenant, different connection strings for different modules, and automatic fallback when connections aren't found. See how comprehensive ABP's approach is in the [Connection Strings documentation](https://abp.io/docs/latest/framework/fundamentals/connection-strings).
20+
21+
### Step 1: Creating the Project
22+
23+
First, create a new ASP.NET Core Web App with Razor Pages and Individual Authentication:
24+
25+
```bash
26+
dotnet new webapp --auth Individual -n DynamicConnectionDemo
27+
cd DynamicConnectionDemo
28+
```
29+
30+
When you open the project, you'll see the default connection in `appsettings.json`:
31+
32+
```json
33+
{
34+
"ConnectionStrings": {
35+
"DefaultConnection": "DataSource=app.db;Cache=Shared"
36+
}
37+
}
38+
```
39+
40+
And in `Program.cs`, you'll find the standard Entity Framework configuration:
41+
42+
```csharp
43+
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
44+
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
45+
46+
builder.Services.AddDbContext<ApplicationDbContext>(options =>
47+
options.UseSqlite(connectionString));
48+
49+
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
50+
.AddEntityFrameworkStores<ApplicationDbContext>();
51+
```
52+
53+
### Step 2: Testing the Base Application
54+
55+
Run the application and test the registration/login functionality to ensure everything works:
56+
57+
```bash
58+
dotnet run
59+
```
60+
61+
Navigate to the registration page, create an account, and verify that the basic authentication flow works correctly.
62+
63+
## Building Our Connection String Resolver
64+
65+
Now let's implement our dynamic connection string resolver. We'll start by defining the interface and implementation.
66+
67+
### Step 3: Creating the Interface
68+
69+
Create a new interface `IConnectionStringResolver` in `Data` folder:
70+
71+
```csharp
72+
public interface IConnectionStringResolver
73+
{
74+
string Resolve(string connectionName = null);
75+
}
76+
```
77+
78+
### Step 4: Implementing the Resolver
79+
80+
Create the `ConnectionStringResolver` class in `Data` folder:
81+
82+
```csharp
83+
public class ConnectionStringResolver : IConnectionStringResolver
84+
{
85+
private readonly IConfiguration _configuration;
86+
private readonly IHttpContextAccessor _httpContextAccessor;
87+
88+
public ConnectionStringResolver(
89+
IConfiguration configuration,
90+
IHttpContextAccessor httpContextAccessor)
91+
{
92+
_configuration = configuration;
93+
_httpContextAccessor = httpContextAccessor;
94+
}
95+
96+
public string Resolve(string connectionName = null)
97+
{
98+
// Add caching logic here if needed
99+
return GetConnectionString(connectionName);
100+
}
101+
102+
private string GetConnectionString(string connectionName)
103+
{
104+
// Try to get given named connection string
105+
if (!string.IsNullOrEmpty(connectionName))
106+
{
107+
var connectionString = _configuration.GetConnectionString(connectionName);
108+
if (!string.IsNullOrEmpty(connectionString))
109+
{
110+
return connectionString;
111+
}
112+
}
113+
114+
// Try to get tenant-specific connection string (for multi-tenant apps)
115+
var tenantId = GetCurrentTenantIdOrNull();
116+
if (!string.IsNullOrEmpty(tenantId))
117+
{
118+
var tenantConnectionString = _configuration.GetConnectionString($"Tenant_{tenantId}");
119+
if (!string.IsNullOrEmpty(tenantConnectionString))
120+
{
121+
return tenantConnectionString;
122+
}
123+
}
124+
125+
// Fallback to default connection string
126+
return _configuration.GetConnectionString("DefaultConnection");
127+
}
128+
129+
private string? GetCurrentTenantIdOrNull()
130+
{
131+
var context = _httpContextAccessor.HttpContext;
132+
if (context == null)
133+
{
134+
return null;
135+
}
136+
137+
// Adds support for subdomain-based, route-based, or header-based tenant identification
138+
139+
// Example: Query string-based tenant identification
140+
if (context.Request.Query.ContainsKey("tenant"))
141+
{
142+
return context.Request.Query["tenant"].ToString();
143+
}
144+
145+
return null;
146+
}
147+
}
148+
```
149+
150+
### Step 5: Registering the Service
151+
152+
Add the service registration to your `Program.cs`:
153+
154+
```csharp
155+
builder.Services.AddScoped<IConnectionStringResolver, ConnectionStringResolver>();
156+
```
157+
158+
### Step 6: Updating the DbContext Configuration
159+
160+
Now we need to modify our `Program.cs` to use the resolver instead of the static connection string.
161+
162+
Replace this code:
163+
164+
```csharp
165+
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
166+
throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
167+
builder.Services.AddDbContext<ApplicationDbContext>(options =>
168+
options.UseSqlite(connectionString));
169+
```
170+
171+
With this simpler version:
172+
173+
```csharp
174+
builder.Services.AddDbContext<ApplicationDbContext>();
175+
```
176+
177+
### Step 7: Modifying ApplicationDbContext
178+
179+
Update your `ApplicationDbContext` in `Data` folder to use the resolver:
180+
181+
```csharp
182+
public class ApplicationDbContext : IdentityDbContext
183+
{
184+
private readonly IConnectionStringResolver _connectionStringResolver;
185+
186+
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IConnectionStringResolver connectionStringResolver)
187+
: base(options)
188+
{
189+
_connectionStringResolver = connectionStringResolver;
190+
}
191+
192+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
193+
{
194+
if (!optionsBuilder.IsConfigured)
195+
{
196+
var connectionString = _connectionStringResolver.Resolve();
197+
optionsBuilder.UseSqlite(connectionString);
198+
}
199+
}
200+
}
201+
```
202+
203+
### Step 8: Testing the Implementation
204+
205+
Let's add a simple way to see our resolver in action. Update your `Pages/Index.cshtml`:
206+
207+
```html
208+
@page
209+
@model IndexModel
210+
@inject IConnectionStringResolver ConnectionStringResolver
211+
212+
@{
213+
ViewData["Title"] = "Home page";
214+
}
215+
216+
<div class="text-center">
217+
<h1 class="display-4">Welcome</h1>
218+
<p>Connection String: @ConnectionStringResolver.Resolve()</p>
219+
</div>
220+
```
221+
222+
### Step 9: Adding Multi-Tenant Configuration
223+
224+
To test the multi-tenant functionality, add some tenant-specific connection strings to your `appsettings.json`:
225+
226+
```json
227+
{
228+
"ConnectionStrings": {
229+
"DefaultConnection": "DataSource=app.db;Cache=Shared",
230+
"Tenant_acme": "DataSource=acme.db;Cache=Shared",
231+
"Tenant_globex": "DataSource=globex.db;Cache=Shared"
232+
}
233+
}
234+
```
235+
236+
### Step 10: Testing Multi-Tenant Functionality
237+
238+
Now run your application and test the multi-tenant functionality:
239+
240+
1. **Default behavior**: Visit `https://localhost:5001/` - you should see the default connection string
241+
2. **Tenant-specific**: Visit `https://localhost:5001/?tenant=acme` - you should see the ACME tenant's connection string
242+
3. **Another tenant**: Visit `https://localhost:5001/?tenant=globex` - you should see the Globex tenant's connection string
243+
4. **Non-existent tenant**: Visit `https://localhost:5001/?tenant=unknown` - you should see the default connection string (fallback behavior)
244+
245+
> **Note**: The port number may be different on your system. Check the console output when you run `dotnet run` to see the actual URL.
246+
247+
> **Important**: This demo shows that our connection string resolver is working correctly, but it only displays which connection string would be used. In a real application, thanks to our `ApplicationDbContext` modifications, the actual database operations would use the resolved connection string automatically. I kept this demo simple for clarity, but if you create actual tenant databases and test with real data operations, you'll see it works as expected.
248+
249+
## Understanding the Implementation
250+
251+
Let's break down what we've accomplished:
252+
253+
Our resolver follows this priority order:
254+
1. **Named Connection**: If a specific connection name is provided, use that
255+
2. **Tenant-Specific**: Check for tenant-specific connection strings
256+
3. **Default Fallback**: Use the default connection string
257+
258+
> **Production Note**: This example uses query string parameters for simplicity. In production, you might use subdomains (`acme.myapp.com`), custom headers (`X-Tenant-ID`), route parameters, or JWT claims for tenant identification. Also consider adding caching, proper error handling and so on.
259+
260+
## How ABP Framework Handles This
261+
262+
The approach we've implemented above is very similar to how ABP Framework handles dynamic connection strings. ABP provides a built-in `IConnectionStringResolver` that works almost identically to our custom implementation, but with additional enterprise features:
263+
264+
### ABP's IConnectionStringResolver
265+
266+
ABP Framework includes a sophisticated connection string resolver that:
267+
268+
- Automatically handles multi-tenancy scenarios
269+
- Supports module-specific connection strings out of the box
270+
- Integrates seamlessly with ABP's configuration system
271+
- Provides advanced caching and performance optimizations
272+
273+
```csharp
274+
// In ABP applications, you can simply inject IConnectionStringResolver
275+
public class ProductService : ITransientDependency
276+
{
277+
private readonly IConnectionStringResolver _connectionStringResolver;
278+
279+
public ProductService(IConnectionStringResolver connectionStringResolver)
280+
{
281+
_connectionStringResolver = connectionStringResolver;
282+
}
283+
284+
public async Task<string> GetConnectionStringAsync()
285+
{
286+
// ABP automatically handles tenant context, module resolution, and fallbacks
287+
return await _connectionStringResolver.ResolveAsync("ProductModule");
288+
}
289+
}
290+
```
291+
292+
ABP's version provides automatic tenant detection, module integration, and enterprise features out of the box.
293+
294+
## Conclusion
295+
296+
The `IConnectionStringResolver` pattern provides a clean way to handle dynamic connection strings in ASP.NET applications. By centralizing connection string logic, you can easily support multi-tenant scenarios, environment-specific configurations, and modular architectures.
297+
298+
This pattern is particularly valuable for applications that need to scale and adapt to different deployment scenarios. Whether you implement your own resolver or use ABP Framework's built-in solution, this approach will make your application more flexible.
299+
300+
## Further Reading
301+
302+
* [Entity Framework Core Documentation](https://docs.microsoft.com/en-us/ef/core/)
303+
* [Multi-tenant Applications with EF Core](https://docs.microsoft.com/en-us/ef/core/miscellaneous/multitenancy)
304+
* [Connection Strings and Configuration in .NET](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings-and-configuration-files)
305+
* [ABP Framework Multi-Tenancy](https://abp.io/docs/latest/framework/architecture/multi-tenancy)
129 KB
Loading

0 commit comments

Comments
 (0)