【问题标题】:Blazor server-side with multiple database based on hostname具有基于主机名的多个数据库的 Blazor 服务器端
【发布时间】:2021-02-15 16:04:32
【问题描述】:

我开始失去理智了:

我正在尝试构建一个 Blazor 应用程序,其中最终用户将连接到一个或另一个数据库,具体取决于他们访问应用程序的主机名。
例如,subdomain1.application.com 将连接到一个数据库,而 subdomain2.application.com 将连接到另一个数据库。我相信这个原则叫做 Mutlitenancy(?)。

为了实现这一点,我构建了一个“主”数据库,用于存储不同数据库的主机名和连接字符串。然后我有一个TenantService 类,它加载不同的连接并使用IHttpContextAccessor 通过注入返回与当前基本URI 对应的连接字符串。在调试中一切正常。

当我尝试在 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>();

【问题讨论】:

  • 您只能将 NavigationManager 注入 Razor 组件。当你打电话给TenantService时,你不能转发吗?
  • 不确定这是否有帮助,因为我对您的详细信息有点模糊,但是,Net5 中有一个DbContextFactory 的概念,它为您的数据库连接提供了很大的灵活性。这完全取决于您使用 DbContext 做什么。数据库是不同的结构,因此具有不同的模型,还是完全相同的结构,因此共享相同的模型。更改上下文,相同的模型适合所有人。如果是这样,那么您只需要根据站点获取正确的连接字符串。连接字符串可以存在于 appsettings 中。如果您需要更多帮助,请给我加分。
  • 如果有帮助,这里有一个 sn-p: var dbContext = configuration.GetValue("Configuration:DBContext"); services.AddDbContextFactory(options => options.UseSqlServer(dbContext), ServiceLifetime.Singleton);
  • @ShaunCurtis 抱歉,如果我没有提供足够的详细信息。基本上我需要读取最终用户访问网站的基本 uri 并相应地连接到数据库。我在帖子中添加了一些细节以澄清问题。
  • @JHBonarius 你说的“前锋”是什么意思?

标签: blazor multi-tenant blazor-server-side


【解决方案1】:

用户@enet 删除了他们的答案,但它帮助我找到了解决方案。也谢谢@JHBonarius。
这是针对面临相同问题的任何人的新实现。

App.razor.cs

App.razor 的 CodeBehind 文件,这可以直接在 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% 的时间内有效,我很想确认这是安全的,但现在就可以了。感谢参与的人!

【讨论】:

    【解决方案2】:

    我的建议是将您的租户 DbContext 放在 TenantService 中并提供一个加载器方法来初始化它。这可以由您放置在 App 中的组件调用。与使用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 组件访问。我也在使用需要 TenantContext 的控制器,但这是一个不错的选择!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-21
    • 2018-01-02
    • 2011-01-22
    • 2023-04-01
    • 2018-10-12
    • 1970-01-01
    相关资源
    最近更新 更多