Skip to content

Commit c22519b

Browse files
authored
Merge pull request #36 from IgniteUI/DNenchev/35-implement-multi-tenant-db-context
Implement Multi-Tenant DbContext for SQLite With Db Expiration
2 parents 2bbeab6 + 7870e41 commit c22519b

File tree

6 files changed

+203
-32
lines changed

6 files changed

+203
-32
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customize-containers-cw.html
2+
3+
files:
4+
"/opt/aws/amazon-cloudwatch-agent/bin/config.json":
5+
mode: "000600"
6+
owner: root
7+
group: root
8+
content: |
9+
{
10+
"agent":{
11+
"metrics_collection_interval":60,
12+
"logfile":"/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log",
13+
"run_as_user":"cwagent"
14+
},
15+
"metrics":{
16+
"namespace":"CWAgent/AppBuilderData",
17+
"append_dimensions":{
18+
"InstanceId":"${aws:InstanceId}",
19+
"InstanceType":"${aws:InstanceType}",
20+
"AutoScalingGroupName":"${aws:AutoScalingGroupName}"
21+
},
22+
"aggregation_dimensions":[
23+
[ "AutoScalingGroupName", "InstanceId" ],
24+
[ ]
25+
],
26+
"metrics_collected":{
27+
"cpu":{
28+
"resources":[
29+
"*"
30+
],
31+
"measurement":[
32+
"time_idle",
33+
"time_iowait",
34+
"time_system",
35+
"time_user",
36+
"usage_steal",
37+
"usage_system",
38+
"usage_user",
39+
"usage_iowait"
40+
]
41+
},
42+
"mem":{
43+
"measurement":[
44+
"used_percent",
45+
"total",
46+
"available_percent"
47+
]
48+
}
49+
}
50+
}
51+
}
52+
53+
container_commands:
54+
start_cloudwatch_agent:
55+
command: /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a append-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace NorthwindCRUD.Middlewares
4+
{
5+
public class TenantHeaderValidationMiddleware
6+
{
7+
private const string TenantHeaderKey = "X-Tenant-ID";
8+
9+
private readonly RequestDelegate next;
10+
11+
public TenantHeaderValidationMiddleware(RequestDelegate next)
12+
{
13+
this.next = next;
14+
}
15+
16+
public async Task InvokeAsync(HttpContext context)
17+
{
18+
var tenantHeader = context.Request.Headers[TenantHeaderKey].FirstOrDefault();
19+
20+
if (tenantHeader != null && !IsTenantValid(tenantHeader))
21+
{
22+
context.Response.StatusCode = StatusCodes.Status400BadRequest;
23+
await context.Response.WriteAsync($"Invalid format for Header {TenantHeaderKey}");
24+
return;
25+
}
26+
27+
await next(context);
28+
}
29+
30+
private bool IsTenantValid(string tenantId)
31+
{
32+
return Regex.IsMatch(tenantId, "^[A-Za-z0-9-_]{0,40}$");
33+
}
34+
}
35+
}

NorthwindCRUD/NorthwindCRUD.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,9 @@
4747
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
4848
</ItemGroup>
4949

50+
<ItemGroup>
51+
<None Include=".ebextensions/**/*">
52+
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
53+
</None>
54+
</ItemGroup>
5055
</Project>

NorthwindCRUD/Program.cs

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using Newtonsoft.Json.Converters;
1212
using NorthwindCRUD.Filters;
1313
using NorthwindCRUD.Helpers;
14+
using NorthwindCRUD.Middlewares;
15+
using NorthwindCRUD.Providers;
1416
using NorthwindCRUD.Services;
1517

1618
namespace NorthwindCRUD
@@ -74,36 +76,10 @@ public static void Main(string[] args)
7476
});
7577
});
7678

77-
var dbProvider = builder.Configuration.GetConnectionString("Provider");
78-
79-
if (dbProvider == "SQLite")
80-
{
81-
// For SQLite in memory to be shared across multiple EF calls, we need to maintain a separate open connection.
82-
// see post https://stackoverflow.com/questions/56319638/entityframeworkcore-sqlite-in-memory-db-tables-are-not-created
83-
var keepAliveConnection = new SqliteConnection(builder.Configuration.GetConnectionString("SQLiteConnectionString"));
84-
keepAliveConnection.Open();
85-
}
86-
87-
builder.Services.AddDbContext<DataContext>(options =>
79+
builder.Services.AddDbContext<DataContext>((serviceProvider, options) =>
8880
{
89-
if (dbProvider == "SqlServer")
90-
{
91-
options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServerConnectionString"));
92-
}
93-
else if (dbProvider == "InMemory")
94-
{
95-
options.ConfigureWarnings(warnOpts =>
96-
{
97-
// InMemory doesn't support transactions and we're ok with it
98-
warnOpts.Ignore(InMemoryEventId.TransactionIgnoredWarning);
99-
});
100-
101-
options.UseInMemoryDatabase(databaseName: builder.Configuration.GetConnectionString("InMemoryDBConnectionString"));
102-
}
103-
else if (dbProvider == "SQLite")
104-
{
105-
options.UseSqlite(builder.Configuration.GetConnectionString("SQLiteConnectionString"));
106-
}
81+
var configurationProvider = serviceProvider.GetRequiredService<DbContextConfigurationProvider>();
82+
configurationProvider.ConfigureOptions(options);
10783
});
10884

10985
var config = new MapperConfiguration(cfg =>
@@ -135,8 +111,10 @@ public static void Main(string[] args)
135111
});
136112

137113
builder.Services.AddAuthorization();
138-
114+
builder.Services.AddHttpContextAccessor();
115+
builder.Services.AddMemoryCache();
139116
builder.Services.AddScoped<DBSeeder>();
117+
builder.Services.AddScoped<DbContextConfigurationProvider>();
140118
builder.Services.AddTransient<CategoryService>();
141119
builder.Services.AddTransient<CustomerService>();
142120
builder.Services.AddTransient<EmployeeTerritoryService>();
@@ -155,7 +133,7 @@ public static void Main(string[] args)
155133

156134
// Necessary to detect if it's behind a load balancer, for example changing protocol, port or hostname
157135
app.UseForwardedHeaders();
158-
136+
app.UseMiddleware<TenantHeaderValidationMiddleware>();
159137
app.UseHttpsRedirection();
160138
app.UseDefaultFiles();
161139
app.UseStaticFiles();
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Globalization;
2+
using Microsoft.Data.Sqlite;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Caching.Memory;
5+
using NorthwindCRUD.Helpers;
6+
7+
namespace NorthwindCRUD.Providers
8+
{
9+
public class DbContextConfigurationProvider
10+
{
11+
private const string DefaultTenantId = "default-tenant";
12+
private const string TenantHeaderKey = "X-Tenant-ID";
13+
private const string DatabaseConnectionCacheKey = "Data-Connection-{0}";
14+
15+
private readonly IHttpContextAccessor context;
16+
private readonly IMemoryCache memoryCache;
17+
private readonly IConfiguration configuration;
18+
19+
public DbContextConfigurationProvider(IHttpContextAccessor context, IMemoryCache memoryCache, IConfiguration configuration)
20+
{
21+
this.context = context;
22+
this.memoryCache = memoryCache;
23+
this.configuration = configuration;
24+
}
25+
26+
public void ConfigureOptions(DbContextOptionsBuilder options)
27+
{
28+
var dbProvider = configuration.GetConnectionString("Provider");
29+
30+
if (dbProvider == "SqlServer")
31+
{
32+
options.UseSqlServer(configuration.GetConnectionString("SqlServerConnectionString"));
33+
}
34+
else if (dbProvider == "SQLite")
35+
{
36+
var tenantId = GetTenantId();
37+
38+
var cacheKey = string.Format(CultureInfo.InvariantCulture, DatabaseConnectionCacheKey, tenantId);
39+
40+
if (!memoryCache.TryGetValue(cacheKey, out SqliteConnection connection))
41+
{
42+
var connectionString = this.GetSqlLiteConnectionString(tenantId);
43+
connection = new SqliteConnection(connectionString);
44+
memoryCache.Set(cacheKey, connection, GetCacheConnectionEntryOptions());
45+
}
46+
47+
// For SQLite in memory to be shared across multiple EF calls, we need to maintain a separate open connection.
48+
// see post https://stackoverflow.com/questions/56319638/entityframeworkcore-sqlite-in-memory-db-tables-are-not-created
49+
connection.Open();
50+
51+
options.UseSqlite(connection).EnableSensitiveDataLogging();
52+
53+
SeedDb(options);
54+
}
55+
}
56+
57+
private static void SeedDb(DbContextOptionsBuilder optionsBuilder)
58+
{
59+
using var dataContext = new DataContext(optionsBuilder.Options);
60+
DBSeeder.Seed(dataContext);
61+
}
62+
63+
private static void CloseConnection(object key, object value, EvictionReason reason, object state)
64+
{
65+
//Used to clear datasource from memory.
66+
(value as SqliteConnection)?.Close();
67+
}
68+
69+
private MemoryCacheEntryOptions GetCacheConnectionEntryOptions()
70+
{
71+
var defaultAbsoluteCacheExpirationInHours = this.configuration.GetValue<int>("DefaultAbsoluteCacheExpirationInHours");
72+
var cacheEntryOptions = new MemoryCacheEntryOptions
73+
{
74+
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(defaultAbsoluteCacheExpirationInHours),
75+
};
76+
77+
cacheEntryOptions.RegisterPostEvictionCallback(CloseConnection);
78+
79+
return cacheEntryOptions;
80+
}
81+
82+
private string GetSqlLiteConnectionString(string tenantId)
83+
{
84+
var connectionStringTemplate = configuration.GetConnectionString("SQLiteConnectionString");
85+
var unsanitizedConntectionString = string.Format(CultureInfo.InvariantCulture, connectionStringTemplate, tenantId);
86+
var connectionStringBuilder = new SqliteConnectionStringBuilder(unsanitizedConntectionString);
87+
var sanitizedConntectionString = connectionStringBuilder.ToString();
88+
89+
return sanitizedConntectionString;
90+
}
91+
92+
private string GetTenantId()
93+
{
94+
return context.HttpContext?.Request.Headers[TenantHeaderKey].FirstOrDefault() ?? DefaultTenantId;
95+
}
96+
}
97+
}

NorthwindCRUD/appsettings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
"Provider": "SQLite", //SqlServer or InMemory or SQLite
44
"SqlServerConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Database=NorthwindCRUD;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False;MultipleActiveResultSets=True",
55
"InMemoryDBConnectionString": "NorthwindCRUD",
6-
"SQLiteConnectionString": "DataSource=northwind-db;mode=memory;cache=shared"
6+
"SQLiteConnectionString": "DataSource=northwind-db-{0};mode=memory;cache=shared;"
77
},
8+
"DefaultAbsoluteCacheExpirationInHours": 24,
89
"Logging": {
910
"LogLevel": {
1011
"Default": "Information",

0 commit comments

Comments
 (0)