具有基于主机名的多个数据库的 Blazor 服务器端
Blazor server-side with multiple database based on hostname
我开始对此失去理智:
我正在尝试构建一个 Blazor 应用程序,其中最终用户将连接到一个数据库或另一个数据库,具体取决于他们从中访问应用程序的主机名。
例如,subdomain1.application.com 将连接到一个数据库,而 subdomain2.application.com 将连接到另一个数据库。我相信这个原则叫做 Mutlitenancy(?)。
为此,我构建了一个 'master' 数据库,用于存储主机名和不同数据库的连接字符串。然后我有一个 TenantService
class 加载不同的连接和 return 对应于当前基本 URI 的连接字符串使用 IHttpContextAccessor
通过注入。在调试中一切正常。
当我尝试在 Azure 上托管我的应用程序时,我遇到了这个问题。 IHttpContextAccessor.HttpContext
为空,因此我无法访问基本 URI。我在多个线程上读到 HttpContext
不存在于 SignalR 中,也不应与 Blazor 服务器端一起使用。
我尝试过的事情:
- 将
NavigationManager
注入我的 TenantService
但出现异常 InvalidOperationException: 'RemoteNavigationManager' has not been initialized
我见过人们谈论 SignalR 集线器来访问上下文,但我无法理解它是如何工作的。
如果有人构建了类似的东西,我会洗耳恭听更好的方法。也许我需要重新开始,根本不使用基于 url 的多租户。感谢任何人的帮助。
编辑:这是我今天如何实现它的更多细节。
TenantHolder.cs
public class TenantHolder : ITenantHolder
{
private List<Tenants> _tenants;
public TenantHolder(IServiceScopeFactory serviceScopeFactory)
{
using (var scope = serviceScopeFactory.CreateScope())
{
var provider = scope.ServiceProvider;
using (var context = provider.GetRequiredService<MasterContext>())
_tenants = context.Tenants.ToList(); // Retrieve all the existing tenants from the MasterContext
// which is permanantly connected to a master database
}
}
public string GetCurrentTenant(HttpContext context)
{
var hostname = context.Request.Host.Value;
var tenant = _tenants.FirstOrDefault(x => x.Url == hostname);
return tenant.ConnectionString;
}
}
TenantService.cs
public class TenantService : ITenantService
{
private readonly HttpContext _httpContext;
private readonly ITenantHolder _tenantHolder;
public TenantService(IHttpContextAccessor accessor, ITenantHolder tenantHolder)
{
_httpContext = accessor.HttpContext; // Works fine in local but NULL when hosted on Azure
_tenantHolder = tenantHolder;
}
public string GetCurrentTenant()
=> _tenantHolder.GetCurrentTenant(_httpContext);
}
TenantContext.cs
public class TenantContext : IdentityDbContext<ApplicationUser> // Shorten
{
private readonly ITenantService _tenantService;
public TenantContext(DbContextOptions<TenantContext> options, ITenantService tenantService)
: base(options)
{
_tenantService = tenantService;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connectionString = _tenantService.GetCurrentTenant();
if (string.IsNullOrEmpty(connectionString)) ; // TODO: throw an exception or something
optionsBuilder.UseSqlServer(connectionString);
}
}
Startup.cs
services.AddDbContext<TenantContext>(opts => opts.UseSqlServer(masterConnectionString)); // Connected to the master database before the tenant management changes it
services.AddDbContext<MasterContext>(opts => opts.UseSqlServer(masterConnectionString));
services.AddHttpContextAccessor();
services.AddSingleton<ITenantHolder, TenantHolder>();
services.AddScoped<ITenantService, TenantService>();
用户@enet 删除了他们的答案,但它帮助我找到了解决方案。也谢谢@JHBonarius。
这是面向面临相同问题的任何人的新实施。
App.razor.cs
App.razor
的代码隐藏文件,这可以直接在 App.razor
中完成
public partial class App : ComponentBase
{
[Inject] private NavigationManager _navigationManager { get; set; }
[Inject] private IContextFactory _contextFactory { get; set; }
protected override Task OnInitializedAsync()
{
var uri = new Uri(_navigationManager.Uri);
_contextFactory.Hostname = uri.Host; // Or uri.Authority if you need the port
return base.OnInitializedAsync();
}
}
ContextFactory.cs
public class ContextFactory : IContextFactory
{
public string Hostname { get; set; }
}
TenantHolder.cs
public class TenantHolder : ITenantHolder
{
private List<Tenants> _tenants;
public TenantHolder(IServiceScopeFactory serviceScopeFactory)
{
using (var scope = serviceScopeFactory.CreateScope())
{
var provider = scope.ServiceProvider;
using (var context = provider.GetRequiredService<MasterContext>())
_tenants = context.Tenants.ToList(); // Retrieve all the existing tenants from the MasterContext
// which is permanantly connected to a master database
}
}
public string GetCurrentTenant(HttpContext context, string hostname)
{
if (string.IsNullOrEmpty(hostname) && context != null)
hostname = context.Request.Host.Value;
var tenant = _tenants.FirstOrDefault(x => x.Url == hostname);
return tenant.ConnectionString;
}
}
TenantService.cs
public class TenantService : ITenantService
{
private readonly HttpContext _httpContext;
private readonly ITenantHolder _tenantHolder;
private readonly IContextFactory _contextFactory;
public TenantService(IHttpContextAccessor accessor, ITenantHolder tenantHolder, IContextFactory contextFactory)
{
_httpContext = accessor.HttpContext; // HttpContext is still required when DbContext is accessed from a controller
_contextFactory = contextFactory;
_tenantHolder = tenantHolder;
}
public string GetCurrentTenant()
=> _tenantHolder.GetCurrentTenant(_httpContext, _contextFactory.Hostname);
}
TenantContext.cs-不变
Startup.cs
services.AddDbContext<TenantContext>(opts => opts.UseSqlServer(masterConnectionString)); // Connected to the master database before the tenant management changes it
services.AddDbContext<MasterContext>(opts => opts.UseSqlServer(masterConnectionString));
services.AddScoped<IContextFactory, ContextFactory>();
services.AddHttpContextAccessor();
services.AddSingleton<ITenantHolder, TenantHolder>();
services.AddScoped<ITenantService, TenantService>();
导入注释: 这只有效,因为 App.OnInitializedAsync()
在 TenantContext
初始化之前被调用。我不确定这是否会 100% 有效,我很想确认这是安全的,但现在就可以了。感谢所有参与的人!
我的建议是将您的租户 DbContext 放在 TenantService 中,并提供一个 Loader 方法来初始化它。这可以由您放置在应用程序中的组件调用。与使用 App.OnInitializedAsync
基本相同。在我的解决方案中,您然后通过 TenantService 访问 DbContext。
伪代码看起来像这样:
public TenantDbContext MyDbContext { get; private set; }
// Checker to make sure it's loaded
public bool IsLoaded => MyDbContext != null;
// called from the TenantDbLoader UI component Not another service)
public void LoadCurrentTenant(string siteUrl)
{
// figure out the DbContext
var options = new DbContextOptionsBuilder<TenantDbContext>();
options.UseSqlServer("correctconnectionstring");
MyDbContext = new TenantDbContext(options.Options);
}
组件
public class TenantDbLoader : ComponentBase
{
[Inject] private NavigationManager NavManager { get; set; }
[Inject] private TenantService TenService { get; set; }
protected override void OnInitialized()
{
TenService.LoadCurrentTenant(NavManager.Uri);
}
}
我没有测试过,所以不能保证!
关于您的解决方案的时间安排,我认为上下文在使用之前不会构建,所以只要马在车前就可以了。
写得很好。
我开始对此失去理智:
我正在尝试构建一个 Blazor 应用程序,其中最终用户将连接到一个数据库或另一个数据库,具体取决于他们从中访问应用程序的主机名。
例如,subdomain1.application.com 将连接到一个数据库,而 subdomain2.application.com 将连接到另一个数据库。我相信这个原则叫做 Mutlitenancy(?)。
为此,我构建了一个 'master' 数据库,用于存储主机名和不同数据库的连接字符串。然后我有一个 TenantService
class 加载不同的连接和 return 对应于当前基本 URI 的连接字符串使用 IHttpContextAccessor
通过注入。在调试中一切正常。
当我尝试在 Azure 上托管我的应用程序时,我遇到了这个问题。 IHttpContextAccessor.HttpContext
为空,因此我无法访问基本 URI。我在多个线程上读到 HttpContext
不存在于 SignalR 中,也不应与 Blazor 服务器端一起使用。
我尝试过的事情:
- 将
NavigationManager
注入我的TenantService
但出现异常InvalidOperationException: 'RemoteNavigationManager' has not been initialized
我见过人们谈论 SignalR 集线器来访问上下文,但我无法理解它是如何工作的。
如果有人构建了类似的东西,我会洗耳恭听更好的方法。也许我需要重新开始,根本不使用基于 url 的多租户。感谢任何人的帮助。
编辑:这是我今天如何实现它的更多细节。
TenantHolder.cs
public class TenantHolder : ITenantHolder
{
private List<Tenants> _tenants;
public TenantHolder(IServiceScopeFactory serviceScopeFactory)
{
using (var scope = serviceScopeFactory.CreateScope())
{
var provider = scope.ServiceProvider;
using (var context = provider.GetRequiredService<MasterContext>())
_tenants = context.Tenants.ToList(); // Retrieve all the existing tenants from the MasterContext
// which is permanantly connected to a master database
}
}
public string GetCurrentTenant(HttpContext context)
{
var hostname = context.Request.Host.Value;
var tenant = _tenants.FirstOrDefault(x => x.Url == hostname);
return tenant.ConnectionString;
}
}
TenantService.cs
public class TenantService : ITenantService
{
private readonly HttpContext _httpContext;
private readonly ITenantHolder _tenantHolder;
public TenantService(IHttpContextAccessor accessor, ITenantHolder tenantHolder)
{
_httpContext = accessor.HttpContext; // Works fine in local but NULL when hosted on Azure
_tenantHolder = tenantHolder;
}
public string GetCurrentTenant()
=> _tenantHolder.GetCurrentTenant(_httpContext);
}
TenantContext.cs
public class TenantContext : IdentityDbContext<ApplicationUser> // Shorten
{
private readonly ITenantService _tenantService;
public TenantContext(DbContextOptions<TenantContext> options, ITenantService tenantService)
: base(options)
{
_tenantService = tenantService;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connectionString = _tenantService.GetCurrentTenant();
if (string.IsNullOrEmpty(connectionString)) ; // TODO: throw an exception or something
optionsBuilder.UseSqlServer(connectionString);
}
}
Startup.cs
services.AddDbContext<TenantContext>(opts => opts.UseSqlServer(masterConnectionString)); // Connected to the master database before the tenant management changes it
services.AddDbContext<MasterContext>(opts => opts.UseSqlServer(masterConnectionString));
services.AddHttpContextAccessor();
services.AddSingleton<ITenantHolder, TenantHolder>();
services.AddScoped<ITenantService, TenantService>();
用户@enet 删除了他们的答案,但它帮助我找到了解决方案。也谢谢@JHBonarius。
这是面向面临相同问题的任何人的新实施。
App.razor.cs
App.razor
的代码隐藏文件,这可以直接在 App.razor
public partial class App : ComponentBase
{
[Inject] private NavigationManager _navigationManager { get; set; }
[Inject] private IContextFactory _contextFactory { get; set; }
protected override Task OnInitializedAsync()
{
var uri = new Uri(_navigationManager.Uri);
_contextFactory.Hostname = uri.Host; // Or uri.Authority if you need the port
return base.OnInitializedAsync();
}
}
ContextFactory.cs
public class ContextFactory : IContextFactory
{
public string Hostname { get; set; }
}
TenantHolder.cs
public class TenantHolder : ITenantHolder
{
private List<Tenants> _tenants;
public TenantHolder(IServiceScopeFactory serviceScopeFactory)
{
using (var scope = serviceScopeFactory.CreateScope())
{
var provider = scope.ServiceProvider;
using (var context = provider.GetRequiredService<MasterContext>())
_tenants = context.Tenants.ToList(); // Retrieve all the existing tenants from the MasterContext
// which is permanantly connected to a master database
}
}
public string GetCurrentTenant(HttpContext context, string hostname)
{
if (string.IsNullOrEmpty(hostname) && context != null)
hostname = context.Request.Host.Value;
var tenant = _tenants.FirstOrDefault(x => x.Url == hostname);
return tenant.ConnectionString;
}
}
TenantService.cs
public class TenantService : ITenantService
{
private readonly HttpContext _httpContext;
private readonly ITenantHolder _tenantHolder;
private readonly IContextFactory _contextFactory;
public TenantService(IHttpContextAccessor accessor, ITenantHolder tenantHolder, IContextFactory contextFactory)
{
_httpContext = accessor.HttpContext; // HttpContext is still required when DbContext is accessed from a controller
_contextFactory = contextFactory;
_tenantHolder = tenantHolder;
}
public string GetCurrentTenant()
=> _tenantHolder.GetCurrentTenant(_httpContext, _contextFactory.Hostname);
}
TenantContext.cs-不变
Startup.cs
services.AddDbContext<TenantContext>(opts => opts.UseSqlServer(masterConnectionString)); // Connected to the master database before the tenant management changes it
services.AddDbContext<MasterContext>(opts => opts.UseSqlServer(masterConnectionString));
services.AddScoped<IContextFactory, ContextFactory>();
services.AddHttpContextAccessor();
services.AddSingleton<ITenantHolder, TenantHolder>();
services.AddScoped<ITenantService, TenantService>();
导入注释: 这只有效,因为 App.OnInitializedAsync()
在 TenantContext
初始化之前被调用。我不确定这是否会 100% 有效,我很想确认这是安全的,但现在就可以了。感谢所有参与的人!
我的建议是将您的租户 DbContext 放在 TenantService 中,并提供一个 Loader 方法来初始化它。这可以由您放置在应用程序中的组件调用。与使用 App.OnInitializedAsync
基本相同。在我的解决方案中,您然后通过 TenantService 访问 DbContext。
伪代码看起来像这样:
public TenantDbContext MyDbContext { get; private set; }
// Checker to make sure it's loaded
public bool IsLoaded => MyDbContext != null;
// called from the TenantDbLoader UI component Not another service)
public void LoadCurrentTenant(string siteUrl)
{
// figure out the DbContext
var options = new DbContextOptionsBuilder<TenantDbContext>();
options.UseSqlServer("correctconnectionstring");
MyDbContext = new TenantDbContext(options.Options);
}
组件
public class TenantDbLoader : ComponentBase
{
[Inject] private NavigationManager NavManager { get; set; }
[Inject] private TenantService TenService { get; set; }
protected override void OnInitialized()
{
TenService.LoadCurrentTenant(NavManager.Uri);
}
}
我没有测试过,所以不能保证!
关于您的解决方案的时间安排,我认为上下文在使用之前不会构建,所以只要马在车前就可以了。
写得很好。