4

我有一个标准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; }

有什么方法可以修改这些导航属性以执行特定于租户的查询?还是我应该去掉所有导航属性以支持手写代码?

第二个选项让我害怕,因为很多查询都嵌套了 Includes。这里的任何输入都会很棒。

4

2 回答 2

1

据我所知,除了使用反射或手动查询属性之外别无他法。

因此,在您的IQueryable<T> FilterTenant<T>(IQueryable<T> values)方法中,您必须检查您的类型T以查找实现您的ITenantData接口的属性。

然后你仍然不在那里,因为你的根实体的属性(User在这种情况下)可能是实体本身,或者是实体列表(想想Invoice.InvoiceLines[].Item.Categories[])。

对于通过此操作找到的每个属性,您必须编写一个过滤这些属性Where()的子句。

或者您可以为每个属性手动编码

这些检查至少应该在创建和编辑实体时进行。您需要检查ContactModel.AddressID当前登录的租户是否可以访问由发布到您的存储库(例如从 MVC 站点)的 ID 属性(例如 )引用的导航属性。这是您的批量分配保护,它确保恶意用户无法制作一个请求,否则该请求会将他拥有权限的实体(Contact他正在创建或编辑)链接到Address另一个租户,只需发布​​一个随机或已知的AddressID.

如果您信任此系统,则只需在读取时检查根实体的 TenantID,因为在创建和更新时进行检查,如果根实体可访问,则租户可以访问所有子实体。

由于您的描述,您确实需要过滤子实体。使用此处解释的技术手动编码示例的示例:

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,
                                   };                               
        }
    }
}

但是正如您所看到的,您必须映射每个User类似的属性。

于 2013-11-07T19:02:58.107 回答
1

只是想提供一种实现多租户的替代方法,该方法在当前项目中运行良好,使用 EF5 和 SQL 2012。基本设计是(请耐心等待......):

  1. 数据库中的每个表都有一个列(ClientSid 二进制,默认约束 = SUSER_SID())并且永远不会直接查询,只能通过专用视图查询
  2. 每个视图都是对表的直接选择,WHERE (ClientSid = SUSER_SID())但不选择 ClientSid(有效地暴露表的界面)
  3. EF5 模型映射到 VIEW,而不是 TABLE
  4. 连接字符串根据租户的上下文而变化(用户/客户端,无论多租户分区要求如何)

差不多就是这样 - 尽管分享它可能很有用。我知道这不是您问题的直接答案,但这导致 C# 区域中的自定义代码基本上为零。

于 2013-11-07T22:56:58.747 回答