Серверная часть Blazor с несколькими базами данных на основе имени хоста

#blazor #multi-tenant #blazor-server-side

#blazor #многопользовательский #blazor-на стороне сервера

Вопрос:

Я начинаю сходить с ума по этому поводу:

Я пытаюсь создать приложение Blazor, в котором конечный пользователь будет подключен к той или иной базе данных в зависимости от имени хоста, с которого они присоединяются к приложению.
Например, subdomain1.application.com подключится к одной базе данных и subdomain2.application.com подключится к другой базе данных. Я полагаю, что этот принцип называется Mutlitenancy (?).

Для достижения этой цели я создаю «главную» базу данных, в которой хранятся имена хостов и строки подключения к различным базам данных. Затем у меня есть TenantService класс, который загружает разные соединения и возвращает строку подключения, соответствующую текущему базовому URI, используя IHttpContextAccessor путем инъекции. При отладке все работает нормально.

Проблема, с которой я столкнулся, возникла, когда я попытался разместить свое приложение в Azure. IHttpContextAccessor.HttpContext имеет значение null, поэтому я не могу получить доступ к базовому 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>();
 

Комментарии:

1. Вы можете вводить только NavigationManager в компоненты Razor. Разве вы не можете переслать это при вызове TenantService ?

2. Не уверен, поможет ли это, поскольку я немного не разбираюсь в ваших деталях, но DbContextFactory в Net5 есть концепция a, которая дает вам большую гибкость при подключении к базе данных. Все зависит от того, что вы делаете со своим DbContext. Являются ли базы данных разными структурами и, следовательно, имеют разные модели или точно такую же структуру и, следовательно, используют одну и ту же модель. Измените контекст, и одна и та же модель подойдет всем. Если это так, то вам просто нужно получить правильную строку подключения на основе сайта. Строки подключения могут находиться в настройках приложений. Дайте мне несколько советов, если вам нужна дополнительная помощь.

3. Вот фрагмент, если это поможет: var DbContext = configuration . GetValue<string>(«Configuration:DbContext»); службы. Добавьте dbcontextfactory<WeatherForecastDbContext>(параметры => параметры. UseSqlServer(DbContext), ServiceLifetime. Одноэлементный);

4. @ShaunCurtis Извините, если я не предоставил достаточно подробностей. По сути, мне нужно прочитать базовый uri, с которого конечный пользователь заходит на веб-сайт, и соответственно подключиться к базе данных. Я добавил некоторые подробности в сообщение, чтобы прояснить ситуацию.

5. @JHBonarius Что вы подразумеваете под «пересылкой»?

Ответ №1:

Пользователь @enet удалил свой ответ, но это помогло мне найти решение. Спасибо @JHBonarius тоже.
Вот новая реализация для тех, кто сталкивается с такой же проблемой.

App.razor.cs

Файл CodeBehind для 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) amp;amp; 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 и предоставить метод Loader для его инициализации. Это может быть вызвано компонентом, который вы размещаете в приложении. В основном то же самое, что и using App.OnInitializedAsync . В моем решении вы затем получаете доступ к DbContext через TenantService.

Псевдокод выглядит примерно так:

         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);
        }

    }
 

Я не тестировал его, поэтому никаких гарантий!

Что касается времени для вашего решения, я думаю, что контекст не создается до тех пор, пока он не будет использован, поэтому, пока лошадь находится перед телегой, все в порядке.

Хорошее кодирование.

Комментарии:

1. Я полагаю, это будет работать так же, как и мое решение. Единственным недостатком, который я вижу в этом, является то, что он доступен только из компонента Blazor. Я также работаю с контроллерами, где необходим TenantContext, но это хорошая альтернатива!