草庐IT

c# - 使用 DbContext 和 TenantId 的 Multi-Tenancy - 拦截器、过滤器、EF 代码优先

coder 2024-06-03 原文

我的组织需要共享数据库、共享架构 Multi-Tenancy 数据库。我们将根据 TenantId 进行查询。我们将拥有很少的租户(少于 10 个),并且所有租户都将共享相同的数据库模式,不支持特定于租户的更改或功能。租户元数据将存储在内存中,而不是数据库(静态成员)中。

这意味着所有实体现在都需要一个 TenantId,并且 DbContext 需要知道默认情况下对此进行过滤。

TenantId 可能由 header 值或原始域标识,除非有更可取的方法。

我已经看到各种利用拦截器的示例,但还没有看到关于 TenantId 实现的明确示例。


我们需要解决的问题:

  1. 我们如何修改当前架构以支持此功能(我认为很简单,只需添加 TenantId)
  2. 我们如何检测租户(同样简单 - 基于原始请求的域或 header 值 - 从 BaseController 中提取)
  3. 我们如何将其传播到服务方法(有点棘手...我们使用 DI 通过构造函数进行水合...希望避免使用 tenantId 填充所有方法签名)
  4. 我们如何修改 DbContext 以在我们拥有此 tenantId 后对其进行过滤(不知道)
  5. 我们如何优化性能。我们需要什么索引,我们如何确保查询缓存不会对 tenantId 隔离做任何奇怪的事情,等等(不知道)
  6. 身份验证 - 使用 SimpleMembership,我们如何隔离 User,以某种方式将他们与租户相关联。

我认为最大的问题是 4 - 修改 DbContext。


我喜欢这篇文章如何利用 RLS,但我不确定如何以代码优先的 dbContext 方式处理它:

https://azure.microsoft.com/en-us/documentation/articles/web-sites-dotnet-entity-framework-row-level-security/

我会说我正在寻找的是一种方法 - 考虑到性能 - 使用 DbContext 选择性地查询 tenantId 隔离的资源,而不用 "AND TenantId = 1"


更新 - 我找到了一些选项,但我不确定每个选项的优缺点,或者是否有一些“更好”的方法。我对选项的评估归结为:

  • 易于实现
  • 表现

方法 A

这看起来“很昂贵”,因为每次我们新建一个 dbContext 时,我们都必须重新初始化过滤器:

https://blogs.msdn.microsoft.com/mvpawardprogram/2016/02/09/row-level-security-in-entityframework-6-ef6/

首先,我设置了租户和界面:

public static class Tenant {

    public static int TenantA {
        get { return 1; }
    }
    public static int TenantB
    {
        get { return 2; }
    }

}

public interface ITenantEntity {
    int TenantId { get; set; }
}

我在任何实体上实现该接口(interface):

 public class Photo : ITenantEntity
 {

    public Photo()
    {
        DateProcessed = (DateTime) SqlDateTime.MinValue;
    }

    [Key]
    public int PhotoId { get; set; }

    [Required]
    public int TenantId { get; set; }
 }

然后我更新我的 DbContext 实现:

  public AppContext(): base("name=ProductionConnection")
    {
        Init();
    }

  protected internal virtual void Init()
    {
        this.InitializeDynamicFilters();
    }

    int? _currentTenantId = null;

    public void SetTenantId(int? tenantId)
    {
        _currentTenantId = tenantId;
        this.SetFilterScopedParameterValue("TenantEntity", "tenantId", _currentTenantId);
        this.SetFilterGlobalParameterValue("TenantEntity", "tenantId", _currentTenantId);
        var test = this.GetFilterParameterValue("TenantEntity", "tenantId");
    }

    public override int SaveChanges()
    {
        var createdEntries = GetCreatedEntries().ToList();
        if (createdEntries.Any())
        {
            foreach (var createdEntry in createdEntries)
            {
                var isTenantEntity = createdEntry.Entity as ITenantEntity;
                if (isTenantEntity != null && _currentTenantId != null)
                {
                    isTenantEntity.TenantId = _currentTenantId.Value;
                }
                else
                {
                    throw new InvalidOperationException("Tenant Id Not Specified");
                }
            }

        }
    }

    private IEnumerable<DbEntityEntry> GetCreatedEntries()
    {
        var createdEntries = ChangeTracker.Entries().Where(V => EntityState.Added.HasFlag(V.State));
        return createdEntries;
    }

   protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Filter("TenantEntity", (ITenantEntity tenantEntity, int? tenantId) => tenantEntity.TenantId == tenantId.Value, () => null);

        base.OnModelCreating(modelBuilder);
    }

最后,在调用 DbContext 时,我使用了这个:

     using (var db = new AppContext())
     {
          db.SetTenantId(someValueDeterminedElsewhere);
     }

我对此有疑问,因为我在大约一百万个地方更新了我的 AppContext(一些服务方法需要它,一些不需要)——所以这会使我的代码有点膨胀。还有关于租户确定的问题——我是否传递 HttpContext,我是否强制我的 Controller 将 TenantId 传递到所有服务方法调用中,我如何处理我没有原始域的情况(webjob 调用等)。


方法 B

在这里找到:http://howtoprogram.eu/question/n-a,28158

看起来很相似,但很简单:

 public interface IMultiTenantEntity {
      int TenantID { get; set; }
 }

 public partial class YourEntity : IMultiTenantEntity {}

 public partial class YourContext : DbContext
 {
 private int _tenantId;
 public override int SaveChanges() {
    var addedEntities = this.ChangeTracker.Entries().Where(c => c.State == EntityState.Added)
        .Select(c => c.Entity).OfType<IMultiTenantEntity>();

    foreach (var entity in addedEntities) {
        entity.TenantID = _tenantId;
    }
    return base.SaveChanges();
}

public IQueryable<Code> TenantCodes => this.Codes.Where(c => c.TenantID == _tenantId);
}

public IQueryable<YourEntity> TenantYourEntities => this.YourEntities.Where(c => c.TenantID == _tenantId);

尽管这看起来像是具有相同问题的 A 的愚蠢版本。

我认为到这个时候,必须有一个成熟的、可取的配置/架构来满足这种需求。我们应该怎么做?

最佳答案

我想建议以下方法, 1. 为每个包含核心业务数据的表创建一个名称为租户 ID 的列,这对于任何映射表都不是必需的。

  1. 使用方法 B,创建一个返回 IQueryable 的扩展方法。这个方法可以是 dbset 的扩展,这样任何编写过滤器子句的人都可以调用这个扩展方法,然后调用谓词。这将使开发人员更容易编写代码,而无需担心租户 ID 过滤器。此特定方法将包含根据执行此查询的租户上下文为租户 ID 列应用过滤条件的代码。

样本 ctx.TenantFilter().Where(....)

  1. 您可以在所有服务方法中传递租户 ID,而不是依赖于 http 上下文,这样可以轻松地处理 Web 和 Web 工作应用程序中的租户联系人。这使得通话不受联系人影响,并且更容易测试。 Multi-Tenancy 实体接口(interface)方法看起来不错,但我们的应用程序确实有类似的限制,目前为止运行良好。

  2. 关于添加索引,您需要在具有租户 ID 的表中为租户 ID 列添加索引,这应该负责数据库端查询索引部分。

  3. 关于身份验证部分,我建议使用带有 owin 管道的 asp.net identity 2.0。如果将来需要,该系统的可扩展性和可定制性非常强,并且易于与任何外部身份提供商集成。

  4. 请务必查看 Entity Framework 的存储库模式,它使您能够以通用方式编写更少的代码。这将帮助我们摆脱代码重复和冗余,并且非常容易从单元测试用例中进行测试

关于c# - 使用 DbContext 和 TenantId 的 Multi-Tenancy - 拦截器、过滤器、EF 代码优先,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40441204/

有关c# - 使用 DbContext 和 TenantId 的 Multi-Tenancy - 拦截器、过滤器、EF 代码优先的更多相关文章

  1. c# - 如何在 ruby​​ 中调用 C# dll? - 2

    如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

  2. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  3. ruby-on-rails - 事件管理员日期过滤器日期格式自定义 - 2

    是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s

  4. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  5. ruby-on-rails - 在 Controller 中干净地处理多个过滤器(参数) - 2

    我有一个名为Post的类,我需要能够适应以下场景:如果用户选择了一个类别,则只显示该类别的帖子如果用户选择了一种类型,则只显示该类型的帖子如果用户选择了一个类别和类型,则只显示该类别中该类型的帖子如果用户没有选择任何内容,则显示所有帖子我想知道我的Controller是否不可避免地会因大量条件语句而显得粗糙...这是我解决此问题的错误方法-有谁知道我如何才能做到这一点?classPostsController 最佳答案 您最好遵循“胖模型,瘦Controller”的惯例,这意味着您应该将这种逻辑放在模型本身中。Post类应该能够报告

  6. ruby-on-rails - 如何处理 Grape 中特定操作的过滤器之前? - 2

    我正在我的Rails项目中安装Grape以构建RESTfulAPI。现在一些端点的操作需要身份验证,而另一些则不需要身份验证。例如,我有users端点,看起来像这样:moduleBackendmoduleV1classUsers现在如您所见,除了password/forget之外的所有操作都需要用户登录/验证。创建一个新的端点也没有意义,比如passwords并且只是删除password/forget从逻辑上讲,这个端点应该与用户资源。问题是Grapebefore过滤器没有像except,only这样的选项,我可以在其中说对某些操作应用过滤器。您通常如何干净利落地处理这种情况?

  7. ruby-on-rails - Rails 3 - 过滤器链暂停为 :authentication rendered or redirected - 2

    我仍然收到标题中的“错误”消息,但不知道如何解决。在ApplicationController中,classApplicationController在routes.rb#match'set_activity_account/:id/:value'=>'users#account_activity',:as=>:set_activity_account--thisdoesn'tworkaswell..resources:usersdomemberdoget:action_a,:action_bendcollectiondoget'account_activity'endend和User

  8. ruby-on-rails - ActiveAdmin 自定义选择过滤器下拉名称 - 2

    对于用户模型,我有一个过滤器来检查用户的预订状态,该状态由整数值(0、1或2)表示。UserActiveAdmin索引页上的过滤器是通过以下代码实现的:filter:booking_status,as::select然而,这会导致下拉选项为0、1或2。当管理员用户从下拉列表中选择它们时,我更愿意自己将它们命名为“未完成”、“待定”和“已确认”之类的名称。有没有办法在不改变booking_status在模型中的表示方式的情况下做到这一点? 最佳答案 假设booking_status是模型中的枚举字段,您可以使用:过滤器:booking

  9. ruby - 使用 ruby​​ gem net-ssh-multi 同时在多个服务器上执行 sudo 命令 - 2

    在previousquestion中我想出了如何在多个服务器上启动经过密码验证的sshsession来运行单个命令。现在我需要能够执行“sudo”命令。问题是,net-ssh-multi没有分配sudo需要运行的伪终端(pty),导致以下错误:[127.0.0.1:stderr]sudo:sorry,youmusthaveattytorunsudo根据documentation,可以通过调用channel对象的方法来分配伪终端,但是,以下代码不起作用:它会生成上面的“notty”错误:require'net/ssh'require'net/ssh/multi'Net::SSH::Mul

  10. c# - C# 中的 Flatten Ruby 方法 - 2

    我如何做Ruby方法"Flatten"RubyMethod在C#中。此方法将锯齿状数组展平为一维数组。例如:s=[1,2,3]#=>[1,2,3]t=[4,5,6,[7,8]]#=>[4,5,6,[7,8]]a=[s,t,9,10]#=>[[1,2,3],[4,5,6,[7,8]],9,10]a.flatten#=>[1,2,3,4,5,6,7,8,9,10 最佳答案 递归解决方案:IEnumerableFlatten(IEnumerablearray){foreach(variteminarray){if(itemisIEnume

随机推荐