我的组织需要一个共享数据库,共享架构多租户数据库。我们将根据TenantId进行查询。我们将只有很少的租户(少于10个),并且所有租户都将共享相同的数据库架构,不支持特定于租户的更改或功能。租户元数据将存储在内存中,而不是存储在DB(静态成员)中。
这意味着所有实体现在都需要TenantId,DbContext
需要知道默认情况下对此进行过滤。
TenantId
可能会被标头值或原始域标识,除非有更明智的方法。
我已经看到各种样本利用拦截器但没有看到TenantId实现的明确示例。
我们需要解决的问题:
tenantId
对所有方法签名进行处理)User
,以某种方式将它们与租户联系起来。我认为最大的问题是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调用等)的情况。
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的愚蠢版本,但同样令人担忧。
我认为,到目前为止,必须有一个成熟的,可取的配置/架构来满足这种需求。我们该怎么做呢?
答案 0 :(得分:5)
我想建议以下方法, 1.为包含核心业务数据的每个表创建一个名称为tenant ID的列,这对任何映射表都不是必需的。
IQueryable
的扩展方法。这个方法可以是dbset的扩展,这样任何编写过滤子句的人都可以调用这个扩展方法,然后调用谓词。这将使开发人员更容易编写代码,而无需担心租户ID过滤器。此特定方法将具有代码,以根据正在执行此查询的租户上下文为租户ID列应用过滤条件。 <强>示例强>
ctx.TenantFilter().Where(....)
您可以在所有服务方法中传递租户ID,而不是依赖于http上下文,以便在Web和Web作业应用程序中轻松处理租户联系人。这使得呼叫从联系人中免费,并且更容易测试。多租户实体接口方法看起来很好,我们的应用程序也有类似的限制,到目前为止工作正常。
关于添加索引,您需要在具有租户ID的表中添加租户ID列的索引,并且应该处理数据库端查询索引部分。
关于身份验证部分,我建议使用带有owin管道的asp.net identity 2.0。该系统具有可扩展性,可根据需要随时与任何外部身份提供商集成。
请查看实体框架的存储库模式,它使您能够以通用方式编写较少的代码。这将有助于我们摆脱代码重复和冗余,并且非常容易从单元测试用例中进行测试
答案 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);
}