【问题标题】:Convert EF-based app to multi-tenant by way of context overrides通过上下文覆盖将基于 EF 的应用程序转换为多租户
【发布时间】:2025-12-29 04:05:11
【问题描述】:

我有一个实体框架,基于代码优先的应用程序,我必须制作多租户,也就是说,现在大约有六个“*”实体需要引用特定的租户 ID。 (当我们有 100 多个用户时,不,我们不会维护单独的模式,所以请不要这样建议。:))

通过像 EF 一样对数据访问进行面向对象的抽象,我试图想象如何到达一个不需要更改 dbcontext 之外的任何底层代码来完成这项工作的地方.本质上,我想将这些作为我的成功标准:

  • 不必更改现有数据访问代码。它有很多,很多都是程序性的和重复的。不幸的是,没有存储库类,尽管我很想到达那里,但我不得不推迟技术债务。
  • 查询过滤租户 ID 上的*对象。例如,现有代码获取 context.Members.Where(x => x.IsAwesome) ,但还神奇地过滤到租户 ID 等于租户 ID 的位置(租户上下文可用于每个请求并且可用于注入)。
  • 添加*实体也会分配租户 ID。换句话说,代码执行了 context.Members.Add(newEntity) 之类的操作,newEntity 神奇地将其 TenantID 属性设置为可通过该注入组件获得的 ID。

似乎可以通过实体类本身来设置租户 ID(没有考虑过注入,某种垫片卡在那里),但我不确定如何最好地添加用于查询的附加过滤器。

【问题讨论】:

  • 如何达到成功标准?另见最后一句话。
  • 所以您想完全保留现有代码(上下文代码除外)?
  • 那是理想的世界,是的。不反对修改实体类型本身。

标签: c# sql-server asp.net-mvc entity-framework


【解决方案1】:

我不确定它是否可以在不更改代码的情况下完全完成,但这是我的处理方法。首先,为您的多租户实体引入一个接口(我假设它们每个都有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(...))。

【讨论】:

  • 太棒了...我要试一试,看看会发生什么。会跟进,但是虽然 DbSet 部分重构起来有点不方便,但我怀疑这是最好的方法。我挂断了保存部分,因为我不太清楚状态跟踪是如何设置的。
  • 另外,您可以修改上下文的 .tt 文件,以便它生成名称如“AllMembers”的 DbSet,而对于成员属性,它会生成那些租户过滤的查询。然后,您现有的所有查询(context.Members ....)都将被租户过滤。但是你所有的 context.Members.Add(...) 语句现在都会产生编译时错误,这可能更容易修复。
  • 只是一个后续......一个有趣的问题是,如果您在上下文模型构建器中声明一个包含父租户实体 ID 的索引,则保存方法似乎将该属性视为“新”并且因此不会坚持下去。这很奇怪。
【解决方案2】:

好吧,从技术上讲,只要在上下文实例化时租户 ID 是已知的,您就可以简单地在上下文中使用该值设置一个字段,并在重载中引用该字段。例如,您可以执行类似从应用程序设置中读取它的操作。右键单击您的项目并选择“属性”。然后,转到“设置”选项卡,然后将其打开。放在那里你将在开发中使用的任何东西。然后,为每个租户为您的项目添加配置,并编辑配置转换以将其切换为适当的值。然后,在您的 DI 初始化中,您可以读取此设置值并将其作为常量注入。

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

【讨论】:

  • 但问题不是如何注入 id(我认为),而是如何在 Where 子句中自动设置,并在将其保存到数据库之前自动将其设置为相关实体。
  • 是的,@Evk 说的。我并不担心注入上下文。如果我必须使用租户上下文 shim 的具体实例,那没什么大不了的。该应用程序已经紧密耦合,所以我有更大的问题。 :) 但是,是的,请参阅问题文本中的成功标准和最后一句话。