我的组织需要一个共享数据库,共享模式多租户数据库。我们将基于tenantid进行查询。我们将只有很少的租户(少于10个),并且所有租户都将共享同一数据库模式,而不支持特定于租户的更改或功能。租户元数据将存储在内存中,而不是数据库(静态成员)中。
这意味着所有实体现在都需要一个tenantid,默认情况下DbContext需要知道如何对此进行过滤。
TenantId可能由头值或原始域标识,除非有更可取的方法。
我已经看到了各种利用拦截器的示例,但是还没有看到tenantid实现上的clearcut示例。
我们需要解决的问题:
我们如何修改当前的模式来支持它(我认为很简单,只需添加tenantid)
我们如何检测租户(也很简单-基于原始请求的域或头值-从BaseController提取)
我们如何将其传播到服务方法(稍微复杂一点……我们用去离子水通过构造器…希望避免在所有方法签名中添加tenantId
一旦有了dbcontext,我们如何修改它来过滤这个tenantid(不知道)
如何优化性能。我们需要什么索引,我们如何确保查询缓存在TunaDID隔离等方面没有做任何事情?(不知道)
身份验证-使用SimeMeMeBrAbess,我们如何隔离User s,以某种方式将它们与租户关联起来。
我认为最大的问题是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调用等)。
方法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);

尽管这看起来像是一个愚蠢的版本,有着同样的担心。
我认为,到目前为止,必须有一个成熟的、可取的配置/架构来满足这一需求。我们该怎么办?

最佳答案

我想建议以下方法,
1。为包含核心业务数据的每个表创建一个名为tenant id的列这对于任何映射表都是不需要的。
通过创建一个返回IQueryable的扩展方法,使用方法b。此方法可以是DBSET的扩展,以便任何编写过滤器子句的人都可以调用这个扩展方法,然后使用谓词。这将使开发人员更容易编写代码,而不必担心租户id过滤器。此特定方法将具有基于执行此查询的租户上下文为租户ID列应用筛选条件的代码。
样品
ctx.TenantFilter().Where(....)
您可以在所有的服务方法中传递租户ID,而不是依赖HTTP上下文,这样就可以轻松地处理Web和Web作业应用程序中的租户联系人。这使得通话不需要联系人,而且更易于测试。多租户实体接口方法看起来不错,而且我们的应用程序中也有类似的限制,到目前为止运行良好。
关于添加索引,您需要在具有租户ID的表中为租户ID列添加索引,该索引应负责数据库端查询索引部分。
关于身份验证部分,我建议对owin管道使用asp.net identity 2.0。该系统具有很强的可扩展性和可定制性,如果将来需要,可以方便地与任何外部身份提供者集成。
请查看实体框架的存储库模式,它使您能够以通用的方式编写较少的代码。这将帮助我们摆脱代码重复和冗余,并且非常容易从单元测试用例中进行测试

10-06 13:26