From 43da45462bb312426e5d1569354ca0537425e332 Mon Sep 17 00:00:00 2001 From: rajibmahata Date: Thu, 9 Jan 2025 11:24:17 +0530 Subject: [PATCH 1/2] We created a service that checks the X-Client-ID header in the HTTP request. If a valid client ID (e.g., ClientA or ClientB) is found, it retrieves the corresponding connection string from the application configuration. If the client ID is missing or invalid, the request is rejected. --- .../ConnectionStringMiddleware.cs | 41 +++++++++++++++++++ Controllers/TodoController.cs | 7 ++-- Models/DynamicDbContextFactory.cs | 32 +++++++++++++++ Services/TenantService.cs | 40 ++++++++++++++++++ Startup.cs | 11 +++++ appsettings.Development.json | 6 +++ wwwroot/app/scripts/todoListSvc.js | 2 + 7 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 ContextMiddleware/ConnectionStringMiddleware.cs create mode 100644 Models/DynamicDbContextFactory.cs create mode 100644 Services/TenantService.cs diff --git a/ContextMiddleware/ConnectionStringMiddleware.cs b/ContextMiddleware/ConnectionStringMiddleware.cs new file mode 100644 index 00000000..d5eb5909 --- /dev/null +++ b/ContextMiddleware/ConnectionStringMiddleware.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using System.Threading.Tasks; + +namespace TodoApi.ContextMiddleware +{ + public class ConnectionStringMiddleware + { + private readonly RequestDelegate _next; + private readonly IConfiguration _configuration; + + public ConnectionStringMiddleware(RequestDelegate next, IConfiguration configuration) + { + _next = next; + _configuration = configuration; + } + + public async Task InvokeAsync(HttpContext context) + { + // Extract the client identifier + var clientId = context.Request.Headers["X-Client-ID"].ToString(); + + if (string.IsNullOrEmpty(clientId) || + !_configuration.GetSection("ConnectionStrings:ClientDatabases").Exists()) + { + context.Response.StatusCode = 400; // Bad Request + await context.Response.WriteAsync("Invalid or missing client ID."); + return; + } + + // Retrieve the connection string + var connectionString = _configuration.GetSection($"ConnectionStrings:ClientDatabases:{clientId}").Value; + + // Store the connection string in the HttpContext for downstream services + context.Items["ConnectionString"] = connectionString; + + await _next(context); + } + } + +} diff --git a/Controllers/TodoController.cs b/Controllers/TodoController.cs index aad8137b..e7b821ad 100644 --- a/Controllers/TodoController.cs +++ b/Controllers/TodoController.cs @@ -16,11 +16,11 @@ namespace TodoApi.Controllers public class TodoController : Controller { private readonly TodoContext _context; - - public TodoController(TodoContext context) + private readonly DynamicDbContextFactory _dbContextFactory; + public TodoController(TodoContext context, DynamicDbContextFactory dbContextFactory) { _context = context; - + _dbContextFactory = dbContextFactory; if (_context.TodoItems.Count() == 0) { _context.TodoItems.Add(new TodoItem { Name = "Item1" }); @@ -32,6 +32,7 @@ public TodoController(TodoContext context) [HttpGet] public async Task>> GetTodoItem() { + using var dbContext = _dbContextFactory.CreateDbContext(); return await _context.TodoItems.ToListAsync(); } diff --git a/Models/DynamicDbContextFactory.cs b/Models/DynamicDbContextFactory.cs new file mode 100644 index 00000000..c0b92bea --- /dev/null +++ b/Models/DynamicDbContextFactory.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using System.Data.Entity.Infrastructure; +using System.Data.Entity; +using TodoApi.Services; + +namespace TodoApi.Models +{ + + public class DynamicDbContextFactory + { + private readonly ITenantService _tenantService; + + public DynamicDbContextFactory(ITenantService tenantService) + { + _tenantService = tenantService; + } + + + public TodoContext CreateDbContext() + { + var connectionString = _tenantService.GetConnectionString(); + + var optionsBuilder = new DbContextOptionsBuilder(); + + //TODO: here need to set the connection string for the database + //optionsBuilder.UseSqlServer(connectionString); + //optionsBuilder.UseSqlite(connectionString); + + return new TodoContext(optionsBuilder.Options); + } + } +} diff --git a/Services/TenantService.cs b/Services/TenantService.cs new file mode 100644 index 00000000..95eef256 --- /dev/null +++ b/Services/TenantService.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using System; + +namespace TodoApi.Services +{ + public interface ITenantService + { + string GetConnectionString(); + } + public class TenantService : ITenantService + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IConfiguration _configuration; + + public TenantService(IHttpContextAccessor httpContextAccessor, IConfiguration configuration) + { + _httpContextAccessor = httpContextAccessor; + _configuration = configuration; + } + + public string GetConnectionString() + { + // Extract client ID from header + var clientId = _httpContextAccessor.HttpContext?.Request.Headers["X-Client-ID"].ToString(); + + if (string.IsNullOrEmpty(clientId)) + throw new UnauthorizedAccessException("Client ID is missing."); + + // Retrieve the connection string from configuration + var connectionString = _configuration.GetSection($"ConnectionStrings:ClientDatabases:{clientId}").Value; + + if (string.IsNullOrEmpty(connectionString)) + throw new UnauthorizedAccessException("Invalid client ID or connection string not found."); + + return connectionString; + } + + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index aeb859af..b5c5c1dd 100644 --- a/Startup.cs +++ b/Startup.cs @@ -6,6 +6,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; using TodoApi.Models; +using TodoApi.ContextMiddleware; +using System.Data.Entity.Infrastructure; +using System.Data.Entity; +using TodoApi.Services; namespace TodoApi { @@ -30,6 +34,11 @@ public void ConfigureServices(IServiceCollection services) }); services.AddDbContext(options => options.UseInMemoryDatabase("TodoList")); + + services.AddHttpContextAccessor(); + // Add the tenant service + services.AddScoped(); + services.AddScoped(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -50,6 +59,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } + // app.UseMiddleware(); //app.UseHttpsRedirection(); app.UseDefaultFiles(); @@ -60,6 +70,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthorization(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/appsettings.Development.json b/appsettings.Development.json index c9294ca4..5f59a4a1 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -5,5 +5,11 @@ "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } + }, + "ConnectionStrings": { + "ClientDatabases": { + "ClientA": "Server=serverA;Database=dbA;User Id=userA;Password=****;", + "ClientB": "Server=serverB;Database=dbB;User Id=userB;Password=****;" + } } } diff --git a/wwwroot/app/scripts/todoListSvc.js b/wwwroot/app/scripts/todoListSvc.js index 55a18d60..0ebffc29 100644 --- a/wwwroot/app/scripts/todoListSvc.js +++ b/wwwroot/app/scripts/todoListSvc.js @@ -4,6 +4,8 @@ angular.module('todoApp') $http.defaults.useXDomain = true; delete $http.defaults.headers.common['X-Requested-With']; + // Add X-Client-ID header to all requests + $http.defaults.headers.common['X-Client-ID'] = 'ClientA'; return { getItems : function(){ From 479b00f410a0b5c7e98fc124fb8e4d065824f591 Mon Sep 17 00:00:00 2001 From: rajibmahata Date: Thu, 9 Jan 2025 11:46:09 +0530 Subject: [PATCH 2/2] only user for GetTodoItem --- Controllers/TodoController.cs | 15 ++++++++++++--- Models/DynamicDbContextFactory.cs | 3 ++- appsettings.Development.json | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Controllers/TodoController.cs b/Controllers/TodoController.cs index e7b821ad..a310b5a8 100644 --- a/Controllers/TodoController.cs +++ b/Controllers/TodoController.cs @@ -17,23 +17,32 @@ public class TodoController : Controller { private readonly TodoContext _context; private readonly DynamicDbContextFactory _dbContextFactory; + private readonly TodoContext _contextDynamic; public TodoController(TodoContext context, DynamicDbContextFactory dbContextFactory) { _context = context; - _dbContextFactory = dbContextFactory; + if (_context.TodoItems.Count() == 0) { _context.TodoItems.Add(new TodoItem { Name = "Item1" }); _context.SaveChanges(); } + + _dbContextFactory = dbContextFactory; + _contextDynamic = _dbContextFactory.CreateDbContext(); + if (_contextDynamic.TodoItems.Count() == 0) + { + _contextDynamic.TodoItems.Add(new TodoItem { Name = "Item1" }); + _contextDynamic.SaveChanges(); + } } // GET: api/Todo [HttpGet] public async Task>> GetTodoItem() { - using var dbContext = _dbContextFactory.CreateDbContext(); - return await _context.TodoItems.ToListAsync(); + + return await _contextDynamic.TodoItems.ToListAsync(); } // GET: api/Todo/5 diff --git a/Models/DynamicDbContextFactory.cs b/Models/DynamicDbContextFactory.cs index c0b92bea..26178553 100644 --- a/Models/DynamicDbContextFactory.cs +++ b/Models/DynamicDbContextFactory.cs @@ -21,11 +21,12 @@ public TodoContext CreateDbContext() var connectionString = _tenantService.GetConnectionString(); var optionsBuilder = new DbContextOptionsBuilder(); - + //TODO: here need to set the connection string for the database //optionsBuilder.UseSqlServer(connectionString); //optionsBuilder.UseSqlite(connectionString); + optionsBuilder.UseInMemoryDatabase(connectionString); return new TodoContext(optionsBuilder.Options); } } diff --git a/appsettings.Development.json b/appsettings.Development.json index 5f59a4a1..142f0578 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -8,8 +8,8 @@ }, "ConnectionStrings": { "ClientDatabases": { - "ClientA": "Server=serverA;Database=dbA;User Id=userA;Password=****;", - "ClientB": "Server=serverB;Database=dbB;User Id=userB;Password=****;" + "ClientA": "DatabaseforClientA", + "ClientB": "DatabaseforClientB" } } }