我的组织需要一个共享数据库,共享架构的多租户数据库。我们将基于TenantId进行查询。我们将有很少的租户(不到10个),并且所有租户将共享相同的数据库架构,没有支持租户特定更改或功能的支持。租户元数据将存储在内存中,而不是数据库中(静态成员)。
这意味着现在所有实体都需要一个TenantId,并且DbContext需要默认知道如何过滤它。
TenantId可能会通过标头值或来源域来识别,除非有更可取的方法。
我已经看到了各种利用拦截器的示例,但还没有看到关于TenantId实现的清晰范例。
我们需要解决的问题:
- 如何修改当前模式以支持此功能(我认为很简单,只需添加TenantId)
- 如何检测租户(也很简单 - 基于发出请求的域或标头值 - 从BaseController中获取)
- 如何将其传播到服务方法(有点棘手...我们使用DI通过构造函数进行水合作用...希望避免在所有方法签名中加入tenantId)
- 如何修改DbContext以过滤此tenantId(没想法)
- 如何针对性能进行优化。我们需要哪些索引,如何确保查询缓存不会在租户Id隔离上做任何奇怪的事情等(没想法)
- 身份验证 - 使用SimpleMembership,如何隔离用户,以某种方式将它们与租户关联。
我认为最大的问题是第4个 - 修改DbContext。
我喜欢这篇文章如何利用RLS,但我不确定如何在代码优先,dbContext的方式处理这个问题:
我认为我正在寻找一种方法 - 考虑性能因素 - 通过使用DbContext有选择地查询TenantId隔离的资源,而不用在所有调用中添加"AND TenantId = 1"之类的内容。
更新 - 我找到了一些选项,但是我不确定每个选项的优缺点,或者是否有更好的方法可供选择。我的选项评估如下:
- 实现的容易程度
- 性能
方案A
这似乎是“昂贵”的,因为每次我们新建一个dbContext时,我们都必须重新初始化过滤器:
首先,我设置了我的租户和接口:
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);
虽然这似乎只是一个带有相同问题的愚蠢版本 A。
我想在现在这个时间点,一定会有成熟、可行的配置/架构来适应这个需求。我们应该怎样做呢?