通过上下文覆盖将基于ef的应用程序转换为多租户

本文关键字:程序转换 应用 ef 上下文 覆盖 | 更新日期: 2023-09-27 18:11:29

我有一个实体框架,基于代码优先的应用程序,我必须使多租户,也就是说,有大约六个"顶级"实体,现在需要引用特定的租户ID。(当我们达到100个用户时,不,我们不打算维护单个模式,所以请不要建议这样做。:))

使用像EF这样的面向对象的数据访问抽象,我试图想象我如何能够达到一个不需要更改dbcontext之外的任何底层代码来实现此工作的地方。基本上,我想用这些作为我的成功标准:

  • 不需要更改现有的数据访问代码。有很多,很多都是程序性的和重复的。不幸的是,没有存储库类,尽管我很想做到这一点,但我不得不推迟技术债务。
  • 查询根据租户ID过滤顶层对象。例如,现有代码获得context.Members。Where(x => x. isawesome),但也神奇地过滤到租户ID等于租户ID的地方(租户上下文对每个请求都可用,并且可用于注入)。
  • 添加顶级实体也分配租户ID。换句话说,代码做了类似context.Members.Add(newEntity)和newEntity的事情,newEntity神奇地将其TenantID属性设置为通过注入的组件可用的ID。

似乎可以使用实体类本身来设置租户ID(还没有考虑过对其进行注入,有些东西卡在那里),但我不确定如何最好地添加用于查询的额外过滤器。

通过上下文覆盖将基于ef的应用程序转换为多租户

我不确定完全不修改代码就能做到这一点,但以下是我的做法。首先,为您的多租户实体引入一个接口(我假设它们每个都有TenantID属性,映射到数据库列):

public interface IMultiTenantEntity {
    int TenantID { get; set; }
}

然后在所有实体中实现它。它们是自动生成的,但是是局部的,所以只需执行:

public partial class YourEntity : IMultiTenantEntity {}

现在,要在保存时填充此属性,在您的上下文中重写SaveChanges(同样,它是自动生成的,但部分,所以您不必触摸自动生成的代码):

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

以上我假设您已经以某种方式将当前租户id注入_tenantId字段。

然后,对于每个实体集,添加单独的属性,该属性将返回由TenantID过滤的该集(再次在您的上下文的部分类中):

public IQueryable<YourEntity> TenantYourEntities => this.YourEntities.Where(c => c.TenantID == _tenantId);

现在您需要做的就是找到对YourEntities集合的所有引用(用右键单击>找到所有引用),并用对TenantYourEntities的引用替换它们。然后,您的所有查询将被TenantID过滤,而无需太多工作。当然,不要在使用DbSet修改实体(Db.YourEntities.Add(...))的地方替换引用。

从技术上讲,只要在上下文实例化时知道租户ID,您就可以简单地在上下文中设置一个带有该值的字段,并在重载中引用该字段。例如,您可以从应用程序设置中读取它。右键单击项目并选择"属性"。然后,转到"设置"选项卡,打开它。把你在开发中用到的东西都写进去。然后,为每个租户向项目添加配置,并编辑配置转换以将其切换为适当的值。那么,在你的DI初始化中,你可以读取这个设定值并将其作为常量注入。

如果租户是在运行时设置的,比如通过URL的一部分,那么使用DI就会变得有点困难。上下文通常是请求作用域的,所以这不是真正的问题。然而,DI初始化是通常不在请求管道中完成。此时,您可能只需要手动设置该值,或者在代码中创建上下文,其中是请求管道的部分,例如控制器。