问题描述
public DbSet< Interest>兴趣{get;组; }
public DbSet< User>用户{get;组;
我最近通过创建一个 TenantContext
包含以下内容:
private readonly DbContext _dbContext;
私人只读租户_tenant;
public TenantContext(租户)
:base(name = DefaultConnection){
this._tenant = tenant;
this._dbContext = new DbContext();
}
public IQueryable< User>用户{get {return FilterTenant(_dbContext.Users); }}
public IQueryable< Interest>兴趣{get {return FilterTenant(_dbContext.Interests); }}
private IQueryable< T> FilterTenant< T(IQueryable T value)其中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);
}
此查询提取租户特定的 / code>,但是那个
Include()
语句只为该用户提供兴趣
所有租户。因此,如果用户对多个租户有兴趣,我可以通过上述查询获取所有用户的兴趣。
我的用户模型有以下内容:
public int UserId {get;组; }
public int TenantId {get;组; }
public virtual ICollection< Interest>兴趣{get;组;
有什么办法可以修改这些导航属性来执行特定于租户的查询?还是应该去掉所有的导航属性以支持手写代码?
第二个选项吓倒我,因为很多查询都嵌套了Includes。
据我所知,没有其他方法可以使用反射或查询
所以在你的 IQueryable< T> FilterTenant< T>(IQueryable< T>值)方法,您必须检查您的类型
T
,以实现您的 ITenantData
界面。
然后你还没有在那里,因为根实体的属性(在这种情况下为 User
)是实体本身,或实体列表(认为 Invoice.InvoiceLines []。Item.Categories []
)。
对于通过这样做找到的每个属性,您必须编写一个 Where()
clause 。
这些检查至少应在创建和编辑实体时发生。您将需要检查通过ID属性引用的导航属性(例如, ContactModel.AddressID
),可以将其发布到您的存储库(例如从MVC站点)当前登录的租户。这是您的保护,这确保恶意用户无法制定一个请求,否则将其具有权限的实体(一个联系人
他正在创建或编辑)链接到另一个租户的一个地址
只需发布随机或已知的 AddressID
。
如果您信任该系统,您只需检查阅读时根实体的TenantID,因为在创建和更新时给予检查,所有子实体都可以访问,如果根实体可访问。
由于您的描述你 do 需要过滤子实体。使用解释的技术手动编写示例的示例发现,:
public class UserRepository
{
// ctor注入_dbContext和_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),
其他= u.Other,
};
}
}
}
但是如你所见,你必须映射每一个用户
的属性。
I have a standard DbContext
with code like the following:
public DbSet<Interest> Interests { get; set; }
public DbSet<User> Users { get; set; }
I've recently implemented multi-tenancy by creating a TenantContext
that contains the following:
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);
}
So far, this has been working great. Whenever any of my services creates a new TenantContext, all queries directly off of that context are filtered through this FilterTenant
method that guarantees I'm only returning tenant-relevant entities.
The problem that I'm encountering is my usage of navigation properties that do not take this into account:
using (var db = CreateContext()) // new TenantContext
{
return db.Users.
Include(u => u.Interests).FirstOrDefault(s => s.UserId == userId);
}
This query pulls up the tenant-specific Users
, but then the Include()
statement pulls in Interests
for that user only - but across all tenants. So if a user has Interests across multiple Tenants, I get all of the user's Interests with the above query.
My User model has the following:
public int UserId { get; set; }
public int TenantId { get; set; }
public virtual ICollection<Interest> Interests { get; set; }
Is there any way that I can somehow modify these navigation properties to perform tenant-specific queries? Or should I go and tear out all navigation properties in favor of handwritten code?
The second option scares me because a lot of queries have nested Includes. Any input here would be fantastic.
As far as I know, there's no other way than to either use reflection or query the properties by hand.
So in your IQueryable<T> FilterTenant<T>(IQueryable<T> values)
method, you'll have to inspect your type T
for properties that implement your ITenantData
interface.
Then you're still not there, as the properties of your root entity (User
in this case) may be entities themselves, or lists of entities (think Invoice.InvoiceLines[].Item.Categories[]
).
For each of the properties you found by doing this, you'll have to write a Where()
clause that filters those properties.
Or you can hand-code it per property.
These checks should at least happen when creating and editing entities. You'll want to check that navigation properties referenced by an ID property (e.g. ContactModel.AddressID
) that get posted to your repository (for example from an MVC site) are accessible for the currently logged on tenant. This is your mass assignment protection, which ensures a malicious user can't craft a request that would otherwise link an entity to which he has permissions (a Contact
he is creating or editing) to one Address
of another tenant, simply by posting a random or known AddressID
.
If you trust this system, you only have to check the TenantID of the root entity when reading, because given the checks when creating and updating, all child entities are accessible for the tenant if the root entity is accessible.
Because of your description you do need to filter child entities. An example for hand-coding your example, using the technique explained found here:
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,
};
}
}
}
But as you see, you'll have to map every property of User
like that.
这篇关于虚拟导航属性和多租户的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!