在EF中跨多个上下文共享连接和事务(UnintentionalCodeFirstException)

9
下面提供了前一个版本和问题作为补充背景。改进后的问题陈述和问题如下:
  • 如何在EF 6.1.0数据库优先和.NET 4.5.2中在多个上下文之间共享事务,而不进行分布式事务?
看起来我需要在多个上下文之间共享连接,但是到目前为止我看到的代码示例和教程并没有那么有成效。问题似乎围绕着如何定义一个连接对象和事务对象类型的功能组合,以便在构建对象上下文时也建立和找到EF数据库优先对象元数据。
也就是说,我想做类似于EF 6.n教程这里所描述的内容。一些示例代码可能如下:
int count1;
int count2;
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) 
{ 
    //How to define this connection so as not to run into UnintentionalCodeFirstException?
    //Creating a dummy context to obtain the connectiong string like so
    //dummyContext.Database.Connection.ConnectionString  and then using the connection will be greeted with the aforementioned exception.      
    using(var conn = new SqlConnection("...")) 
    {
        using(var c1 = new SomeEntities(conn, contextOwnsConnection: false))
        {
            //Use some stored procedures etc.
            count1 = await c1.SomeEntity1.CountAsync();
        }

        using(var c2 = new SomeEntities(conn, contextOwnsConnection: false))
        {
            //Use some stored procedures etc.
            count2 = await c2.SomeEntity21.CountAsync();
        }
    }
}   

int count = count1 + count2;

在示例中,还有其他方法来创建共享连接和事务,但是如上所述,罪魁祸首似乎是如果我在前一个片段中提供连接字符串(“…”部分)作为dummyContext.Database.Connection.ConnectionString,我将只得到异常。
我不确定是否只是阅读了错误的来源,或者在尝试跨多个EF上下文共享事务时是否还有其他问题。该如何完成?
我阅读了许多其他SO帖子(例如,这个),以及一些教程。它们没有帮助。
我遇到了一个奇怪的问题,看起来我没有像其他教程和帖子中定义的构造函数重载。也就是说,根据链接教程的链接,我无法编写new BloggingContext(conn, contextOwnsConnection: false))并使用共享连接和外部事务。
然后,如果我写
public partial class SomeEntities: DbContext
{
    public SomeEntities(DbConnection existingConnection, bool contextOwnsConnection): base(existingConnection, contextOwnsConnection) { }
}

当我像教程中那样使用它时,从以下T4模板生成的代码的下一行中会出现异常。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    throw new UnintentionalCodeFirstException();
}

我正在使用.NET 4.5.2和EF 6.1.0。我已经从现有数据库构建了edmx并从那里生成了代码。在这种特定情况下,我使用Task Parallel线程加载数十个SQL Server 主数据服务暂存表(是的,一个大模型),并调用相关过程(由MDS提供每个表一个)。MDS有自己的补偿逻辑,以防某些表格的暂存失败,但回滚事务也应该可行。只是看起来我的EF出了一些(奇怪的)问题。

<添加: Steve建议使用直接的TransactionScope。没有共享连接就需要分布式事务,这不是我能选择的选项。然后,如果我尝试为上下文提供共享连接(一些选项显示在教程中,其中一个在此处),我会遇到“缺少构造函数”的问题。当我定义一个时,我得到我在代码中提到的异常。总之,这感觉相当奇怪。也许我在如何生成DbContext和相关类方面存在问题。

<注1: 看起来根本原因就像Arthur(EF开发团队的成员)在这篇博客文章中所述。也就是说,在基于数据库的开发中,框架寻找在连接字符串中定义的类关系映射。我的连接字符串中有一些可疑之处..?


关于jgauffin在重复问题上的评论,我的问题不是一个简单的共享事务,而是如何在EF 6.n中执行它,这与4.n或5.n有很大不同,并且与非EF解决方案完全不同。 - Veksi
2个回答

2

由于连接池的存在,在相同事务范围内使用多个使用完全相同连接字符串的EF DbContext通常不会导致DTC升级(除非您在连接字符串中禁用了连接池),因为它们都将重用相同的连接(从池中获取)。无论如何,您可以按照以下方式在您的情况下重用相同的连接(假设您已经添加了接受DbConnection和标志指示上下文是否拥有连接的构造函数):

using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) {
    // important - use EF connection string here,
    // one that starts with "metadata=res://*/..."
    var efConnectionString = ConfigurationManager.ConnectionStrings["SomeEntities"].ConnectionString;
    // note EntityConnection, not SqlConnection
    using (var conn = new EntityConnection(efConnectionString)) {
        // important to prevent escalation
        await conn.OpenAsync();
        using (var c1 = new SomeEntities(conn, contextOwnsConnection: false)) {
            //Use some stored procedures etc.
            count1 = await c1.SomeEntity1.CountAsync();
        }

        using (var c2 = new SomeEntities(conn, contextOwnsConnection: false)) {
            //Use some stored procedures etc.
            count2 = await c2.SomeEntity21.CountAsync();
        }
    }
    scope.Complete();
}

这段代码有效且不会抛出UnintentionalCodeFirstExce‌​ption,因为你传递的是EntityConnection。该连接包含有关EDMX元数据的信息,这正是数据库优先需要的。当你传递普通的SqlConnection时,EF不知道在哪里查找元数据,实际上甚至不知道它应该寻找它 - 因此它立即假定你正在进行代码优先。
请注意,我在上面的代码中传递了EF连接字符串。如果你有一些通过EF之外其他方式获得的普通SqlConnection,则这将无法工作,因为需要连接字符串。但是,仍然可以使用EntityConnection,因为它具有接受普通DbConnection的构造函数。但是,那时你应该自己传递元数据的引用。如果你对此感兴趣 - 我可以提供如何执行此操作的代码示例。
要检查你确实在所有情况下都防止升级 - 禁用池(连接字符串中的Pooling=false)并停止DTC服务,然后运行此代码 - 它应该正常运行。然后运行另一个不共享同一连接的代码,你应该观察到指示即将发生升级但服务不可用的错误。

1
@Andrew,你不需要回答中的代码。只需尝试一下,你就会明白。请注意,我在那里传递的是EntityConnection,而不是SqlConnection。 - Evk
在你的回答中,你说了这个 (我假设你已经添加了一个接受 DbConnection 和标记表示上下文是否拥有连接的构造函数)。问题就在这里,对于DB First而言,你无法这样做。 - Andrew
1
@Andrew 你说的不能,是指哪方面呢?这个上下文类是部分类,所以你可以在里面添加任何内容,包括这个构造函数。帖子的作者已经这样做了,但他在那里传递了SqlConnection并得到了异常,而我没有这样做。这段代码在EF 6数据库优先模型上进行了测试,运行良好。 - Evk
哇哦兄弟!你完全正确。你能解释一下为什么传入实体连接不会触发CodeFirst异常吗?非常感谢! - Andrew
1
@Andrew 我在回答中加入了一些解释。 - Evk
显示剩余2条评论

2
你尝试将调用包装在事务范围内了吗?
using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted }))
{

    // Do context work here
    context1.Derp();
    context2.Derp();

    // complete the transaction
    scope.Complete();
}

嗨!要做到那个需要分布式事务,但在这种情况下不是一个选项。虽然是一个简洁的想法。如果我想让上下文共享同一个连接,然而出于某种原因,适当的构造函数调用没有被生成。当我尝试添加一个(请参见代码)时,它会抛出异常(请参见我的解释)。这有点奇怪,也许是我生成代码的方式有问题。 - Veksi
每个数据库应该有一个上下文,每个数据库应该有一个连接。尝试在两个数据库/上下文之间共享相同的连接不是标准做法。 - Steve Stokes

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