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

时间:2016-11-05 17:32:40

标签: c# entity-framework ef-code-first multi-tenant azure-sql-database

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

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

TenantId可能会被标头值或原始域标识,除非有更明智的方法。

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

我们需要解决的问题:

  1. 我们如何修改当前架构以支持这一点(我认为简单,只需添加TenantId)
  2. 我们如何检测租户(简单 - 基于原始请求的域或标头值 - 从BaseController拉出)
  3. 我们如何将它传播到服务方法(有点棘手......我们使用DI来通过构造函数进行水合......希望避免使用tenantId对所有方法签名进行处理)
  4. 我们如何修改DbContext以便在我们拥有它之后对此tenantId进行过滤(不知道)
  5. 我们如何优化性能。我们需要哪些索引,我们如何确保查询缓存没有使用tenantId隔离等做任何事情(不知道)
  6. 身份验证 - 使用SimpleMembership,我们如何隔离User,以某种方式将它们与租户联系起来。
  7. 我认为最大的问题是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; }
    }
    

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

     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,是否强制我的控制器将TenantId传递给所有服务方法调用,如何处理我没有原始域(webjob调用等)的情况。

    APPROACH 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的愚蠢版本,但同样令人担忧。

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

3 个答案:

答案 0 :(得分:5)

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

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

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

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

    3. 关于身份验证部分,我建议使用带有owin管道的asp.net identity 2.0。该系统具有可扩展性,可根据需要随时与任何外部身份提供商集成。

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

答案 1 :(得分:4)

  

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

不要修改上下文...

您不必将租户过滤代码与业务代码混合使用。

我认为你需要的只是一个存储库,返回过滤数据
此存储库将根据您从TenantIdProvider获取的ID返回过滤后的数据 然后,您的服务不必了解有关租户的任何信息

using System;
using System.Data.Entity;
using System.Linq;

namespace SqlServerDatabaseBackup
{
    public class Table
    {
        public int TenantId { get; set; }
        public int TableId { get; set; }
    }

    public interface ITentantIdProvider
    {
        int TenantId();
    }

    public class TenantRepository : ITenantRepositoty
    {
        private int tenantId;
        private ITentantIdProvider _tentantIdProvider;
        private TenantContext context = new TenantContext(); //You can abstract this if you want
        private DbSet<Table> filteredTables;

        public IQueryable<Table> Tables
        {
            get
            {
                return filteredTables.Where(t => t.TenantId == tenantId);
            }
        }

        public TenantRepository(ITentantIdProvider tentantIdProvider)
        {
            _tentantIdProvider = tentantIdProvider;
            tenantId = _tentantIdProvider.TenantId();
            filteredTables = context.Tables;
        }

        public Table Find(int id)
        {
            return filteredTables.Find(id);
        }
    }

    public interface ITenantRepositoty
    {
        IQueryable<Table> Tables { get; }
        Table Find(int id);
    }

    public class TenantContext : DbContext
    {
        public DbSet<Table> Tables { get; set; }
    }

    public interface IService
    {
        void DoWork();
    }

    public class Service : IService
    {
        private ITenantRepositoty _tenantRepositoty;

        public Service(ITenantRepositoty tenantRepositoty)
        {
            _tenantRepositoty = tenantRepositoty;
        }

        public void DoWork()
        {
            _tenantRepositoty.Tables.ToList();//These are filtered records
        }
    }  
}

答案 2 :(得分:0)

问题与EF有关,但我认为值得在此提及 EF Core 。在EF Core中,您可以使用Global Query Filters

  

此类过滤器将自动应用于涉及那些实体类型的任何LINQ查询,包括通过使用包含或直接导航属性引用间接引用的实体类型

一个例子:

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