如何在EF7/.NET Core中实现多个数据库的DbContext继承

45

我正在使用ASP.NET Core 1.1构建Web API。

我有许多不同的数据库(针对不同的系统),其中有共同的基本架构,例如Configuration、Users和groups等配置项(总共约25个表)。我试图通过从基类继承来避免复制相当广泛的EF共享模型的配置,如图所示。

My DbContext inheritance tree

然而,这行不通,因为Entity Framework(EF)要求将DbContextOptions<DerivedRepository>作为参数传递给构造函数,其中DerivedRepository必须与调用构造函数的仓储类型匹配。然后必须通过调用:base(param)将参数传递到基本的DbContext

因此,当(例如)使用DbContextOptions<InvestContext>初始化InvestContext时,它会调用base(DbContextOptions<InvestContext>),并且EF会抛出一个错误,因为对ConfigurationContext构造函数的调用正在接收类型为DbContextOptions<InvestContext>而不是所需类型的参数DbContextOptions<ConfigurationContext>。由于DbContext上的选项字段是定义为

    private readonly DbContextOptions _options;

我看不出有什么解决办法。

最好的方法是定义一次共享模型并多次使用它。我猜我可以创建一个帮助函数并从每个派生上下文中调用它,但这远没有继承那么干净或透明。

3个回答

89

我想把OP的GitHub问题中的这篇文章引起大家的注意:

通过提供一个使用DbContextOptions没有任何类型的protected构造函数,我能够解决这个问题,将第二个构造函数设置为protected保证DI不会使用它。

public class MainDbContext : DbContext {
    public MainDbContext(DbContextOptions<MainDbContext> options)
        : base(options) {
    }

    protected MainDbContext(DbContextOptions options)
        : base(options) {
    }
}

public class SubDbContext : MainDbContext {
    public SubDbContext (DbContextOptions<SubDbContext> options)
        : base(options) {
    }
}

8
这应该成为被接受的答案。看起来这将被添加到官方文档中,作为在这种情况下建议使用的模式。https://github.com/aspnet/EntityFramework.Docs/issues/594 - roshan
2
这个东西在我调试了几个小时并尝试了不同的方法之后救了我。非常好用。 - mkieloch352
1
哇,兄弟,我花了好几个小时在这上面。非常感谢你。 - D2TheC
完美运行。 - Yogi
1
这应该是被接受的答案,并且与文档相匹配。当启动应用程序时,仍然可以补充一条注释,解释在服务中需要配置什么。在我看来,每个派生类都应该调用builder.Services.AddDbContext - Jean-Daniel Gasser

25

好的,我已经找到了一种仍然使用继承层次结构的方法来解决这个问题(使用上面的InvestContext作为示例):

如前所述,InvestContext类接收一个构造函数参数,类型为DbContextOptions<InvestContext>,但必须将DbContextOptions<ConfigurationContext>传递给它的基类。

我编写了一个方法,从DbContextOptions变量中获取连接字符串并构建所需类型的DbContextOptions实例。InvestContext在调用base()之前使用此方法将其选项参数转换为正确的类型。

转换方法如下:

    protected static DbContextOptions<T> ChangeOptionsType<T>(DbContextOptions options) where T:DbContext
    {
        var sqlExt = options.Extensions.FirstOrDefault(e => e is SqlServerOptionsExtension);

        if (sqlExt == null)
            throw (new Exception("Failed to retrieve SQL connection string for base Context"));

        return new DbContextOptionsBuilder<T>()
                    .UseSqlServer(((SqlServerOptionsExtension)sqlExt).ConnectionString)
                    .Options;
    }

InvestContext 构造函数的调用方式从这个:

  public InvestContext(DbContextOptions<InvestContext> options):base(options)

转换为:

  public InvestContext(DbContextOptions<InvestContext> options):base(ChangeOptionsType<ConfigurationContext>(options))

到目前为止,InvestContext和ConfigurationContext都适用于简单查询,但似乎有点不太正规,可能不是EF7设计者所考虑的。我仍然担心当我尝试进行复杂查询、更新等操作时,EF会陷入困境。但是看起来这不是问题,详情请见下文。 编辑:我已将此问题记录为EF7团队的一个问题(在此处),并且一位团队成员建议对EF Core核心进行如下更改:

"我们应该更新检查,以允许TContext是从当前上下文类型派生的类型"

这将解决问题。
在与该团队成员进一步互动(您可以在该问题中看到)和对EF Core代码进行一些挖掘后,我提出的方法看起来很安全,并且是在实施建议更改之前最好的方法。

4

根据您的要求,您可以简单地使用非类型特定版本的DbContextOptions。

更改这些内容:

public ConfigurationContext(DbContextOptions<ConfigurationContext> options):base(options)    
public InvestContext(DbContextOptions<InvestContext> options):base(options)

转换为:

public ConfigurationContext(DbContextOptions options):base(options) 
public InvestContext(DbContextOptions options):base(options)

如果您先创建ConfigurationContext,那么继承它的类似乎会获得相同的配置。这可能还取决于初始化不同上下文的顺序。 编辑: 我的示例代码:
public class QueryContext : DbContext
{
    public QueryContext(DbContextOptions options): base(options)
    {
    }
}

public class CommandContext : QueryContext
{
    public CommandContext(DbContextOptions options): base(options)
    {
    }
}

在 Startup.cs 文件中:

services.AddDbContext<CommandContext>(options =>
                 options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddDbContext<QueryContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

另外,在测试类中:

    var connectionString = "Data Source=MyDatabase;Initial Catalog=MyData;Integrated Security=SSPI;";

    var serviceProvider = new ServiceCollection()
        .AddDbContext<QueryContext>(options => options.UseSqlServer(connectionString))
        .BuildServiceProvider();

    _db = serviceProvider.GetService<QueryContext>();

1
你是否真的有能够实现这个功能的代码?如果是这样,你使用的EF7版本是什么?因为当我尝试这样做时,DbContextOptions是抽象的,即使我实现了一个派生类(DbCo),我在调用构造函数时也会得到编译时错误:“DbCo不能分配给参数类型DbContextOptions <DerivedContext>”。查看DbContext构造函数中的EF7代码库,那里有一个检查options.ContextType.GetTypeInfo().IsAssignableFrom(GetType().GetTypeInfo()),所以DbContextOptions必须是一个泛型派生类型,类型为DerivedContext。 - Peter
是的,我已经让它工作了,无论是从我的应用程序还是在测试主机配置中。我正在使用 Microsoft.EntityFrameworkCore 1.1.0。您可能需要从上下文构造函数中的类型中删除 <DerivedContext> - Clicktricity
我的示例已添加。我没有看到太大的区别,但可能会有所帮助。 - Clicktricity
只有在相同继承树中的上下文才能被实例化 - 因此在上面的图表中,您可以实例化 ConfigurationContext 和 InvestContext/CrmContext(前提是它们指向相同的数据库),但不能实例化树外的上下文。由于我需要在一个数据库上创建 ConfigurationContext,在另一个数据库上创建 CrmContext,并且可能需要其他上下文,所以这对我来说行不通。此外,我认为它的工作方式有点难以理解,并且是由于某些奇怪的 DI 行为(在我看来),因此我认为它很可能会在未来造成维护问题。谢谢您的意见! - Peter
同意这是一种hack方法,对于许多用例来说可能不起作用。我已经成功地使2个不同的连接字符串工作 - 一个是真实的,另一个是InMemory。您还可能发现,如果您需要单独的连接字符串,则需要不同的应用程序域,因为所有连接到相同应用程序域的DbContext共享相同的连接。总体而言,它仍然需要修复。 - Clicktricity
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接