我有一个标准DbContext
,代码如下:
public DbSet<Interest> Interests { get; set; }
public DbSet<User> Users { get; set; }
我最近通过创建包含以下内容的TenantContext
实现了多租户:
private readonly DbContext _dbContext;
private readonly Tenant _tenant;
public TenantContext(Tenant tenant)
: base("name=DefaultConnection") {
this._tenant = tenant;
this._dbContext = new DbContext();
}
public IQueryable<User> Users { get { return FilterTenant(_dbContext.Users); } }
public IQueryable<Interest> Interests { get { return FilterTenant(_dbContext.Interests); } }
private IQueryable<T> FilterTenant<T>(IQueryable<T> values) where T : class, ITenantData
{
return values.Where(x => x.TenantId == _tenant.TenantId);
}
到目前为止,这一直很有效。每当我的任何服务创建一个新的TenantContext时,所有直接来自该上下文的查询都将通过此FilterTenant
方法进行过滤,以保证我只返回与租户相关的实体。
我遇到的问题是我使用的导航属性没有考虑到这一点:
using (var db = CreateContext()) // new TenantContext
{
return db.Users.
Include(u => u.Interests).FirstOrDefault(s => s.UserId == userId);
}
此查询会提取特定于租户的Users
,但Include()
语句仅为该用户提取Interests
,但会在所有租户中提取。因此,如果用户在多个租户中拥有兴趣,我会通过上述查询获得所有用户的兴趣。
我的用户模型具有以下内容:
public int UserId { get; set; }
public int TenantId { get; set; }
public virtual ICollection<Interest> Interests { get; set; }
有什么方法可以以某种方式修改这些导航属性来执行特定于租户的查询?或者我应该去掉所有导航属性,转而使用手写代码?
第二个选项让我感到害怕,因为很多查询都嵌套了包含。这里的任何输入都会很棒。
答案 0 :(得分:1)
据我所知,除了使用反射或手动查询属性之外别无他法。
因此,在IQueryable<T> FilterTenant<T>(IQueryable<T> values)
方法中,您必须检查类型T
以查找实现ITenantData
接口的属性。
然后你仍然不在,因为你的根实体(在这种情况下是User
)的属性可能是实体本身,或实体列表(想象Invoice.InvoiceLines[].Item.Categories[]
)。
对于您通过执行此操作找到的每个属性,您必须编写Where()
条filters those properties。
或者你可以hand-code it per property。
这些检查至少应该在创建和编辑实体时发生。您需要检查当前登录的租户是否可以访问发布到您的存储库(例如来自MVC站点)的ID属性(例如ContactModel.AddressID
)引用的导航属性。这是您的mass assignment保护,可确保恶意用户无法制作请求,否则会将他拥有权限的实体(正在创建或编辑的Contact
)链接到{{1}另一个租户,只需发布随机或已知的Address
。
如果您信任此系统,则只需在读取时检查根实体的TenantID,因为在创建和更新时进行检查,如果可以访问根实体,则所有子实体都可供租户访问。
由于您的描述, 需要过滤子实体。使用解释的技术手动编码您的示例的示例here:
AddressID
但是如你所见,你必须像public class UserRepository
{
// ctor injects _dbContext and _tenantId
public IQueryable<User> GetUsers()
{
var user = _dbContext.Users.Where(u => u.TenantId == _tenantId)
.Select(u => new User
{
Interests = u.Interests.Where(u =>
u.TenantId == _tenantId),
Other = u.Other,
};
}
}
}
一样映射每个属性。
答案 1 :(得分:1)
只是想提供一种实现多租户的替代方法,这种方法在当前项目中运行得非常好,使用EF5和SQL 2012.基本设计是(请耐心等待......):
WHERE (ClientSid = SUSER_SID())
直接选择表格,但不选择ClientSid(有效地公开表格的界面)这就是它 - 尽管分享可能有用。我知道这不是你问题的直接答案,但这导致C#领域基本上没有自定义代码。