Blazor服务器应用程序中使用EF Core的多租户

目录

工厂生命周期

一种方法

把事情放在上下文中

依赖的生命周期

transient事件

性能说明


许多业务应用程序旨在与多个客户合作。保护数据安全很重要,这样客户数据就不会被其他客户和潜在竞争对手泄露和看到。这些应用程序被归类为多租户,因为每个客户都被视为应用程序的租户,拥有自己的数据集。

本文按原样提供示例和解决方案。这些不是最佳实践,而是供您考虑的工作实践

有许多方法可以在应用程序中实现多租户。一种常见的方法(有时是必需的)是将每个客户的数据保存在单独的数据库中。架构相同,但数据是特定于客户的。另一种方法是按客户对现有数据库中的数据进行分区。

EF Core支持这两种方法。

对于使用多个数据库的方法,切换到正确的数据库就像提供正确的连接字符串一样简单。如果数据存储在单个数据库中,全局查询过滤器是有意义的,以确保开发人员不会意外编写可以访问其他客户数据的代码。有关如何使用SQL Server行级安全保护单个多租户数据库的精彩文章,请查看只需3个步骤即可保护单个多租户数据库中的数据

工厂生命周期

在Blazor应用程序中使用Entity Framework Core的推荐模式是注册DbContextFactory,然后调用它来创建每个操作的DbContext新实例。默认情况下,工厂是一个单例,因此应用程序的所有用户只存在一个副本。这通常很好,因为虽然工厂是共享的,但单组户DbContext实例不是。但是,对于多租户,每个用户的连接字符串可能会有所不同。由于工厂缓存的配置具有相同的生命周期,这意味着所有用户必须共享相同的配置。

此问题不会在Blazor WebAssembly应用程序中发生,因为单例的范围仅限于用户。另一方面,Blazor Server应用程序提出了独特的挑战。尽管该应用程序是一个Web应用程序,但它通过使用SignalR的实时通信保持活跃。每个用户都会创建一个会话,并持续到初始请求之后。应该为每个用户提供一个新工厂以允许新设置。此特殊工厂的生命周期称为Scoped并为每个用户会话创建一个新实例。

一种方法

为了在Blazor Server应用程序中演示多租户,我构建了:

  JeremyLikness/BlazorEFCoreMultitenant

Blazor服务器应用程序中使用EF Core的多租户_第1张图片

数据库包含用于存储通过反射填充的方法名称和参数的表。参数的数据模型是:

public class DataParameter
{
    public DataParameter() { }

    public DataParameter(ParameterInfo parameter)
    {
        Name = parameter.Name;
        Type = parameter.ParameterType.FullName;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string Type { get; set; }
    public DataMethod Method { get; set; }
}

参数归定义它们的方法所有。方法类如下所示:

public class DataMethod
{
    public DataMethod() { }

    public DataMethod(MethodInfo method)
    {
        Name = method.Name;
        ReturnType = method.ReturnType.FullName;
        ParentType = method.DeclaringType.FullName;
        
        Parameters = method.GetParameters()
            .Select(p => new DataParameter(p))
            .ToList();

        foreach (var parameter in Parameters)
        {
            parameter.Method = this;
        }
    }

    public int Id { get; set; }

    public string Name { get; set; }

    public string ReturnType { get; set; }

    public string ParentType { get; set; }

    public IList Parameters { get; set; } =
        new List();
}

为了这个演示,该ParentType属性是租户。该应用程序附带预加载SQLite数据库。应用程序中有一个选项可以重新生成数据库。然后,您可以导航到各个示例并查看它们的运行情况。

把事情放在上下文中

一个简单的TenantProvider类处理设置用户的当前租户。它提供回调,以便在租户更改时通知代码。实现(为了清楚起见省略了回调)如下所示:

public class TenantProvider
{
    private string tenant = TypeProvider.GetTypes().First().FullName;

    public void SetTenant(string tenant)
    {
        this.tenant = tenant;
        // notify changes
    }

    public string GetTenant() => tenant;

    public string GetTenantShortName() => tenant.Split('.')[^1];
}

然后DbContext可以管理多租户。该方法取决于您的数据库策略。如果您将所有租户存储在单个数据库中,您可能会使用查询过滤器。TenantProvider将被传递到通过依赖注入的构造和用于解析和存储所述租户标识符。

private readonly string tenant = string.Empty;

public SingleDbContext(
    DbContextOptions options,
    TenantProvider tenantProvider)
    : base(options) 
{
    tenant = tenantProvider.GetTenant();
}

OnModelCreating方法被覆盖以指定查询过滤器:

modelBuilder.Entity()
   .HasQueryFilter(dm => dm.ParentType == tenant);

这确保了每个查询都针对每个请求过滤到租户。无需在应用程序代码中进行过滤,因为将自动应用全局过滤器。

依赖的生命周期

租户提供程序和DbContextFactory在应用程序启动时配置如下:

services.AddScoped();

services.AddDbContextFactory(
    opts => opts.UseSqlite("Data Source=alltenants.sqlite"),
    ServiceLifetime.Scoped);

请注意,服务生命周期配置为ServiceLifetime.Scoped 。这使它能够依赖于租户提供者。

依赖项必须始终流向单例。这意味着一个Scoped服务可以依赖另一个Scoped服务或一个Singleton服务,但一个Singleton服务只能依赖其他Singleton服务:Transient => Scoped => Singleton

MultipleDbContext版本是通过为每个租户传递不同的连接字符串来实现的。这可以在启动时通过解析服务提供者并使用它来构建连接字符串来配置:

services.AddDbContextFactory((sp, opts) =>
{
    var tenantProvider = sp.GetRequiredService();
    opts.UseSqlite($"Data Source={tenantProvider.GetTenantShortName()}.sqlite");
}, ServiceLifetime.Scoped);

这适用于大多数场景,但是当用户可以动态更改他们的租户时呢?

transient事件

在之前的多个数据库配置中,选项缓存在Scoped级别。这意味着如果用户更改租户,则不会重新评估选项,因此租户更改不会反映在查询中。

对此的简单解决方案是将生命周期设置为Transient。这确保每次请求DbContext时都会重新评估租户以及连接字符串。用户可以随意切换租户。下表可以帮助您选择最适合您工厂的使用生命周期。

 

单一数据库

多个数据库

用户停留在单个租户中

Scoped

Scoped

用户可以切换租户

Scoped

Transient

如果您的数据库不采用用户范围的依赖项,则默认Singleton仍然有意义。

性能说明

更改范围时的一个有效问题是:这会对性能产生多大影响。” 答案是几乎没有。” 单个用户会话在给定会话期间需要数百或数千个DbContext实例的可能性很小。GitHub存储库包含一个基准项目,该项目显示Transient作用域工厂从请求工厂到创建可用的DbContext 。这意味着每秒超过300,000个请求。

与往常一样,我乐于接受反馈和建议。我希望这个例子提供了可用于帮助解决的多租户需求的模板,Blazor业务应用程序。

提醒:示例应用程序位于:

JeremyLikness/BlazorEFCoreMultitenant

https://www.codeproject.com/Articles/5308540/Multi-tenancy-with-EF-Core-in-Blazor-Server-Apps

你可能感兴趣的:(ASP.NET,CORE,Blazor,EF,Core,多租户)