首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >MultiTenancy与DbContext和TenantId -拦截器,过滤器,EF代码-优先

MultiTenancy与DbContext和TenantId -拦截器,过滤器,EF代码-优先
EN

Stack Overflow用户
提问于 2016-11-05 17:32:40
回答 3查看 8.2K关注 0票数 18

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

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

除非有更明智的方法,否则TenantId可能会由头值或起始域来标识。

我见过各种利用拦截器的示例,但还没有看到TenantId实现上的明确示例。

我们需要解决的问题:

  1. 我们如何修改当前的模式以支持这一点(我认为很简单,只需添加TenantId)
  2. 如何检测承租者(简单的,也是基于原始请求的域或头值-从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隔离的资源,而不需要使用、、

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

  • 易于实施
  • 性能

方法A

这似乎是“昂贵的”,因为每次我们新的dbContext,我们必须重新初始化过滤器:

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

首先,我设置了我的租户和接口:

代码语言:javascript
复制
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; }
}

我在任何实体上实现这一接口:

代码语言:javascript
复制
 public class Photo : ITenantEntity
 {

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

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

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

然后我更新我的DbContext实现:

代码语言:javascript
复制
  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的调用中,我使用以下内容:

代码语言:javascript
复制
     using (var db = new AppContext())
     {
          db.SetTenantId(someValueDeterminedElsewhere);
     }

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

方法B

在此发现:http://howtoprogram.eu/question/n-a,28158

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

代码语言:javascript
复制
 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有同样的关切。

我认为,到目前为止,必须有一个成熟的、明智的配置/体系结构来满足这一需求。我们该怎么做?

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2016-11-07 16:05:00

我想建议以下方法: 1.为包含核心业务数据的每个表创建一个列,该列的名称为承租者ID,这是任何映射表所不需要的。

  1. 使用方法B,通过创建返回IQueryable的扩展方法。该方法可以是dbset的扩展,这样任何人只要编写filter子句,就可以调用这个扩展方法,后面跟着谓词。这将使开发人员更容易编写代码,而不必担心租户ID筛选器。这个特定的方法将根据执行此查询的租户上下文为承租者ID列应用筛选条件的代码。

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

  1. 不必依赖http上下文,您可以在所有服务方法中传递承租者ID,这样就可以轻松地处理web和web作业应用程序中的租户联系人。这使得一个电话没有接触,更容易测试。多租户实体接口方法看起来不错,而且我们在应用程序中也有类似的限制,到目前为止,这个限制还不错。
  2. 关于添加索引,您需要在具有承租者ID的表中为承租者ID列添加一个索引,该索引应该负责DB端查询索引部分。
  3. 关于身份验证部分,我建议在owin管道中使用asp.net identity 2.0。该系统具有很强的可扩展性、可定制性和易于与任何外部身份提供者集成(如果将来需要的话)。
  4. 请看一看实体框架的存储库模式,它使您能够以通用的方式编写较少的代码。这将帮助我们摆脱代码重复和冗余,并且很容易从单元测试用例中进行测试。
票数 5
EN

Stack Overflow用户

发布于 2019-07-12 14:01:15

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

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

举个例子:

代码语言:javascript
复制
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);
}
票数 5
EN

Stack Overflow用户

发布于 2016-11-15 17:39:05

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

不要修改上下文..。

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

我认为您所需要的只是一个存储库,返回过滤后的数据

这个存储库将根据从TenantIdProvider获得的Id返回过滤后的数据。

那么,你的服务就不需要知道任何关于租户的事情

代码语言:javascript
复制
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
        }
    }  
}
票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/40441204

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档