diff --git a/NorthwindCRUD/.ebextensions/cloudwatch.config b/NorthwindCRUD/.ebextensions/cloudwatch.config
new file mode 100644
index 0000000..cc8f412
--- /dev/null
+++ b/NorthwindCRUD/.ebextensions/cloudwatch.config
@@ -0,0 +1,55 @@
+# https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customize-containers-cw.html
+
+files:
+ "/opt/aws/amazon-cloudwatch-agent/bin/config.json":
+ mode: "000600"
+ owner: root
+ group: root
+ content: |
+ {
+ "agent":{
+ "metrics_collection_interval":60,
+ "logfile":"/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log",
+ "run_as_user":"cwagent"
+ },
+ "metrics":{
+ "namespace":"CWAgent/AppBuilderData",
+ "append_dimensions":{
+ "InstanceId":"${aws:InstanceId}",
+ "InstanceType":"${aws:InstanceType}",
+ "AutoScalingGroupName":"${aws:AutoScalingGroupName}"
+ },
+ "aggregation_dimensions":[
+ [ "AutoScalingGroupName", "InstanceId" ],
+ [ ]
+ ],
+ "metrics_collected":{
+ "cpu":{
+ "resources":[
+ "*"
+ ],
+ "measurement":[
+ "time_idle",
+ "time_iowait",
+ "time_system",
+ "time_user",
+ "usage_steal",
+ "usage_system",
+ "usage_user",
+ "usage_iowait"
+ ]
+ },
+ "mem":{
+ "measurement":[
+ "used_percent",
+ "total",
+ "available_percent"
+ ]
+ }
+ }
+ }
+ }
+
+container_commands:
+ start_cloudwatch_agent:
+ 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
diff --git a/NorthwindCRUD/Middlewares/TenantHeaderValidationMiddleware.cs b/NorthwindCRUD/Middlewares/TenantHeaderValidationMiddleware.cs
new file mode 100644
index 0000000..b8389a1
--- /dev/null
+++ b/NorthwindCRUD/Middlewares/TenantHeaderValidationMiddleware.cs
@@ -0,0 +1,35 @@
+using System.Text.RegularExpressions;
+
+namespace NorthwindCRUD.Middlewares
+{
+ public class TenantHeaderValidationMiddleware
+ {
+ private const string TenantHeaderKey = "X-Tenant-ID";
+
+ private readonly RequestDelegate next;
+
+ public TenantHeaderValidationMiddleware(RequestDelegate next)
+ {
+ this.next = next;
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ var tenantHeader = context.Request.Headers[TenantHeaderKey].FirstOrDefault();
+
+ if (tenantHeader != null && !IsTenantValid(tenantHeader))
+ {
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
+ await context.Response.WriteAsync($"Invalid format for Header {TenantHeaderKey}");
+ return;
+ }
+
+ await next(context);
+ }
+
+ private bool IsTenantValid(string tenantId)
+ {
+ return Regex.IsMatch(tenantId, "^[A-Za-z0-9-_]{0,40}$");
+ }
+ }
+}
diff --git a/NorthwindCRUD/NorthwindCRUD.csproj b/NorthwindCRUD/NorthwindCRUD.csproj
index faf2e0a..d8276db 100644
--- a/NorthwindCRUD/NorthwindCRUD.csproj
+++ b/NorthwindCRUD/NorthwindCRUD.csproj
@@ -47,4 +47,9 @@
+
+
+ Always
+
+
diff --git a/NorthwindCRUD/Program.cs b/NorthwindCRUD/Program.cs
index 2203523..1b40840 100644
--- a/NorthwindCRUD/Program.cs
+++ b/NorthwindCRUD/Program.cs
@@ -11,6 +11,8 @@
using Newtonsoft.Json.Converters;
using NorthwindCRUD.Filters;
using NorthwindCRUD.Helpers;
+using NorthwindCRUD.Middlewares;
+using NorthwindCRUD.Providers;
using NorthwindCRUD.Services;
namespace NorthwindCRUD
@@ -74,36 +76,10 @@ public static void Main(string[] args)
});
});
- var dbProvider = builder.Configuration.GetConnectionString("Provider");
-
- if (dbProvider == "SQLite")
- {
- // For SQLite in memory to be shared across multiple EF calls, we need to maintain a separate open connection.
- // see post https://stackoverflow.com/questions/56319638/entityframeworkcore-sqlite-in-memory-db-tables-are-not-created
- var keepAliveConnection = new SqliteConnection(builder.Configuration.GetConnectionString("SQLiteConnectionString"));
- keepAliveConnection.Open();
- }
-
- builder.Services.AddDbContext(options =>
+ builder.Services.AddDbContext((serviceProvider, options) =>
{
- if (dbProvider == "SqlServer")
- {
- options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServerConnectionString"));
- }
- else if (dbProvider == "InMemory")
- {
- options.ConfigureWarnings(warnOpts =>
- {
- // InMemory doesn't support transactions and we're ok with it
- warnOpts.Ignore(InMemoryEventId.TransactionIgnoredWarning);
- });
-
- options.UseInMemoryDatabase(databaseName: builder.Configuration.GetConnectionString("InMemoryDBConnectionString"));
- }
- else if (dbProvider == "SQLite")
- {
- options.UseSqlite(builder.Configuration.GetConnectionString("SQLiteConnectionString"));
- }
+ var configurationProvider = serviceProvider.GetRequiredService();
+ configurationProvider.ConfigureOptions(options);
});
var config = new MapperConfiguration(cfg =>
@@ -135,8 +111,10 @@ public static void Main(string[] args)
});
builder.Services.AddAuthorization();
-
+ builder.Services.AddHttpContextAccessor();
+ builder.Services.AddMemoryCache();
builder.Services.AddScoped();
+ builder.Services.AddScoped();
builder.Services.AddTransient();
builder.Services.AddTransient();
builder.Services.AddTransient();
@@ -155,7 +133,7 @@ public static void Main(string[] args)
// Necessary to detect if it's behind a load balancer, for example changing protocol, port or hostname
app.UseForwardedHeaders();
-
+ app.UseMiddleware();
app.UseHttpsRedirection();
app.UseDefaultFiles();
app.UseStaticFiles();
diff --git a/NorthwindCRUD/Providers/DbContextConfigurationProvider.cs b/NorthwindCRUD/Providers/DbContextConfigurationProvider.cs
new file mode 100644
index 0000000..6df8705
--- /dev/null
+++ b/NorthwindCRUD/Providers/DbContextConfigurationProvider.cs
@@ -0,0 +1,97 @@
+using System.Globalization;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
+using NorthwindCRUD.Helpers;
+
+namespace NorthwindCRUD.Providers
+{
+ public class DbContextConfigurationProvider
+ {
+ private const string DefaultTenantId = "default-tenant";
+ private const string TenantHeaderKey = "X-Tenant-ID";
+ private const string DatabaseConnectionCacheKey = "Data-Connection-{0}";
+
+ private readonly IHttpContextAccessor context;
+ private readonly IMemoryCache memoryCache;
+ private readonly IConfiguration configuration;
+
+ public DbContextConfigurationProvider(IHttpContextAccessor context, IMemoryCache memoryCache, IConfiguration configuration)
+ {
+ this.context = context;
+ this.memoryCache = memoryCache;
+ this.configuration = configuration;
+ }
+
+ public void ConfigureOptions(DbContextOptionsBuilder options)
+ {
+ var dbProvider = configuration.GetConnectionString("Provider");
+
+ if (dbProvider == "SqlServer")
+ {
+ options.UseSqlServer(configuration.GetConnectionString("SqlServerConnectionString"));
+ }
+ else if (dbProvider == "SQLite")
+ {
+ var tenantId = GetTenantId();
+
+ var cacheKey = string.Format(CultureInfo.InvariantCulture, DatabaseConnectionCacheKey, tenantId);
+
+ if (!memoryCache.TryGetValue(cacheKey, out SqliteConnection connection))
+ {
+ var connectionString = this.GetSqlLiteConnectionString(tenantId);
+ connection = new SqliteConnection(connectionString);
+ memoryCache.Set(cacheKey, connection, GetCacheConnectionEntryOptions());
+ }
+
+ // For SQLite in memory to be shared across multiple EF calls, we need to maintain a separate open connection.
+ // see post https://stackoverflow.com/questions/56319638/entityframeworkcore-sqlite-in-memory-db-tables-are-not-created
+ connection.Open();
+
+ options.UseSqlite(connection).EnableSensitiveDataLogging();
+
+ SeedDb(options);
+ }
+ }
+
+ private static void SeedDb(DbContextOptionsBuilder optionsBuilder)
+ {
+ using var dataContext = new DataContext(optionsBuilder.Options);
+ DBSeeder.Seed(dataContext);
+ }
+
+ private static void CloseConnection(object key, object value, EvictionReason reason, object state)
+ {
+ //Used to clear datasource from memory.
+ (value as SqliteConnection)?.Close();
+ }
+
+ private MemoryCacheEntryOptions GetCacheConnectionEntryOptions()
+ {
+ var defaultAbsoluteCacheExpirationInHours = this.configuration.GetValue("DefaultAbsoluteCacheExpirationInHours");
+ var cacheEntryOptions = new MemoryCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(defaultAbsoluteCacheExpirationInHours),
+ };
+
+ cacheEntryOptions.RegisterPostEvictionCallback(CloseConnection);
+
+ return cacheEntryOptions;
+ }
+
+ private string GetSqlLiteConnectionString(string tenantId)
+ {
+ var connectionStringTemplate = configuration.GetConnectionString("SQLiteConnectionString");
+ var unsanitizedConntectionString = string.Format(CultureInfo.InvariantCulture, connectionStringTemplate, tenantId);
+ var connectionStringBuilder = new SqliteConnectionStringBuilder(unsanitizedConntectionString);
+ var sanitizedConntectionString = connectionStringBuilder.ToString();
+
+ return sanitizedConntectionString;
+ }
+
+ private string GetTenantId()
+ {
+ return context.HttpContext?.Request.Headers[TenantHeaderKey].FirstOrDefault() ?? DefaultTenantId;
+ }
+ }
+}
diff --git a/NorthwindCRUD/appsettings.json b/NorthwindCRUD/appsettings.json
index 2fe4d3c..4faeb7c 100644
--- a/NorthwindCRUD/appsettings.json
+++ b/NorthwindCRUD/appsettings.json
@@ -3,8 +3,9 @@
"Provider": "SQLite", //SqlServer or InMemory or SQLite
"SqlServerConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Database=NorthwindCRUD;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False;MultipleActiveResultSets=True",
"InMemoryDBConnectionString": "NorthwindCRUD",
- "SQLiteConnectionString": "DataSource=northwind-db;mode=memory;cache=shared"
+ "SQLiteConnectionString": "DataSource=northwind-db-{0};mode=memory;cache=shared;"
},
+ "DefaultAbsoluteCacheExpirationInHours": 24,
"Logging": {
"LogLevel": {
"Default": "Information",