我的组织需要一个共享数据库,共享模式多租户数据库。我们将基于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。该系统具有很强的可扩展性和可定制性,如果将来需要,可以方便地与任何外部身份提供者集成。
请查看实体框架的存储库模式,它使您能够以通用的方式编写较少的代码。这将帮助我们摆脱代码重复和冗余,并且非常容易从单元测试用例中进行测试