具有DbContext和TenantId的MultiTenancy - 拦截器,过滤器,EF代码优先

SB2*_*055 11 c# entity-framework multi-tenant ef-code-first azure-sql-database

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

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

该TenantId会可能是由一个标头值或原始域来识别,除非有更明智的做法.

我已经看到各种样本利用拦截器,但没有看到TenantId实现的明确示例.


我们需要解决的问题:

  1. 我们如何修改当前架构来支持这个(简单的我认为,只需添加TenantId)
  2. 我们如何检测租户(简单 - 基于原始请求的域或标头值 - 从BaseController拉出)
  3. 我们如何将其传播到服务方法(有点棘手......我们使用DI通过构造函数进行水合...希望避免使用所有方法签名tenantId)
  4. 一旦我们有了它,我们如何修改DbContext来过滤这个tenantId(不知道)
  5. 我们如何优化性能.我们需要什么索引,我们如何确保查询缓存没有与tenantId隔离等做任何事情(不知道)
  6. 身份验证 - 使用SimpleMembership,我们如何隔离Users,以某种方式将它们与租户联系起来.

我认为最大的问题是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; }
}
Run Code Online (Sandbox Code Playgroud)

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

 public class Photo : ITenantEntity
 {

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

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

    [Required]
    public int TenantId { get; set; }
 }
Run Code Online (Sandbox Code Playgroud)

然后我更新我的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);
    }
Run Code Online (Sandbox Code Playgroud)

最后,在我对DbContext的调用中,我使用了这个:

     using (var db = new AppContext())
     {
          db.SetTenantId(someValueDeterminedElsewhere);
     }
Run Code Online (Sandbox Code Playgroud)

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


方法B.

在这里找到:http://howtoprogram.eu/question/na,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);
Run Code Online (Sandbox Code Playgroud)

虽然这看起来像A的愚蠢版本,但同样令人担忧.

我认为,到目前为止,必须有一个成熟的,可取的配置/架构来满足这种需求.我们该怎么做呢?

Sar*_*nan 5

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

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

样品 ctx.TenantFilter().Where(....)

  1. 您可以在所有服务方法中传递租户ID,而不是依赖于http上下文,这样就可以轻松处理Web和Web作业应用程序中的租户联系人.这使得呼叫从联系人中免费,并且更容易测试.多租户实体接口方法看起来很好,我们在我们的应用程序中也有类似的限制,到目前为止工作正常.

  2. 关于添加索引,您需要在具有租户ID的表中添加租户ID列的索引,并且该索引应该处理数据库端查询索引部分.

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

  4. 请查看实体框架的存储库模式,它使您能够以通用方式编写较少的代码.这将有助于我们摆脱代码重复和冗余,并且非常容易从单元测试用例进行测试


Jon*_*gon 5

问题是关于EF的,但我认为这里值得一提的是EF Core。在 EF Core 中,您可以使用全局查询过滤器

此类筛选器会自动应用于涉及这些实体类型的任何 LINQ 查询,包括间接引用的实体类型,例如通过使用 Include 或直接导航属性引用

一个例子:

public class Blog
{
    private string _tenantId;

    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public bool IsDeleted { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("_tenantId");

    // Configure entity filters
    modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == _tenantId);
    modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
}
Run Code Online (Sandbox Code Playgroud)

  • 但有一个问题:如何获取tenantId,它在DbContext中可用。tenantId是根据请求(cookie、登录用户等)确定的,但在DbContext上不可用。 (4认同)