8

The previous version and question are provided as an added context below. The improved problem formulation and question could be as follows:

  • How does one share a transaction between multiple contexts in EF 6.1.0 database first and .NET 4.5.2 without doing a distributed transaction?

For that it looks like I need to share a connection between the multiple contexts, but the code examples and tutorials I've been looking at thus far haven't been that fruitful. The problem looks like is hovering around on how to define a functioning combination of a connection object and transaction object types so that EF database first object metadata is also built and found when constructing the object contexts.

That is, I would like to do akin to what has been described in the EF 6.n tutorials here. Some example code could be

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;

In the examples there are also other methods as to how to create a shared connection and a transaction, but as written, the culprit seem to be that if, say, I provide the connectiong string in (the "..." part) the previous snippet as dummyContext.Database.Connection.ConnectionString I'll get just an exception.


I'm not sure if I'm just reading the wrong sources or if there's something else that's wrong in my code when I try to share a transaction across multiple EF contexts. How could it be done?

I've read quite a few other SO posts regarding this (e.g. this) and some tutorials. They did not help.

I have a strange problem in that it looks I don't have the constructor overloads defined as in other tutorials and posts. That is, taking the linked tutorial link, I can't write new BloggingContext(conn, contextOwnsConnection: false)) and use a shared connection and an external transaction.

Then if I write

public partial class SomeEntities: DbContext
{
    public SomeEntities(DbConnection existingConnection, bool contextOwnsConnection): base(existingConnection, contextOwnsConnection) { }
}

and use it like in the tutorials, I get an exception from the following line from the following T4 template generated code

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

I'm using .NET 4.5.2 and EF 6.1.0. I'ved constructed the edmx from an existing database and generated the code from there. In this particular situation I'm using Task Parallel threads to load dozens of SQL Server Master Data Services staging tables (yes, a big model) and to call the associated procedures (provided by the MDS one per table). MDS has its own compensation logic in case staging to some of the tables fails, but rolling back a transaction should be doable too. It just looks like I have a (strange) problem with my EF.

<Addendum: Steve suggested using straight TransactionScope. Without a shared connection that would require a distributed transaction, which isn't an option I can choose. Then if I try to provide a shared connection for the contexts (some options shown in the tutorials, one here I have the problem of "missing constructors". When I define one, I get the exception I refer in the code. All in all, this feels quite strange. Maybe there's something wrong in how I go about generating the DbContext and related classes.

<Note 1: It looks like the root cause is as in this blog post by Arthur (of EF developer team) Don't use Code First by mistake. That is, in database first development the framework seeks for the class-relational mappings as defined in the connection string. Something fishy in my connection string that is..?

Community
  • 1
  • 1
Veksi
  • 2,868
  • 1
  • 20
  • 52
  • As for a comment to jgauffin's note on duplicate, my problem isn't a plain shared transaction, but how to do it with EF 6.n, which is different than 4.n or 5.n and much different than non-EF solutions. – Veksi May 30 '14 at 08:35

2 Answers2

2

Have you tried wrapping the calls in a transaction scope?

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();
}
Steve Stokes
  • 1,180
  • 18
  • 36
  • Hi! Doing that would require a distributed transaction, which isn't an option in this situation. A succint idea, though. Then if I would like the contexts to share a same connection, well, for some reason, the appropriate constructor calls haven't been generated. When I try to add one (see the code), the thing throws an exception (see my explanation). This is a bit strange, maybe there's something wrong with how I generate the code or something. – Veksi May 30 '14 at 05:53
  • You should have one context per database, and one connection per database. Trying to share the same connection across two databases/contexts is not standard practice. – Steve Stokes Jun 03 '14 at 14:01
2

Because of connection pool, using multiple EF DbContext which are using exactly the same connection string, under same transaction scope will usually not result in DTC escalation (unless you disabled pooling in connection string), because the same connection will be reused for them both (from the pool). Anyway, you can reuse the same connection in your case like this (I assume you already added constructor which accepts DbConnection and flag indicating if context owns connection):

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

This works and does not throw UnintentionalCodeFirstExce‌​ption because you pass EntityConnection. This connection has information about EDMX metadata and that is what database first needs. When you pass plain SqlConnection - EF has no idea where to look for metadata and actually doesn't even know it should look for it - so it immediately assumes you are doing code-first.

Note that I pass EF connection string in code above. If you have some plain SqlConnection which you obtained by some other means, outside EF, this will not work, because connection string is needed. But, it's still possible because EntityConnection has constructor which accepts plain DbConnection. However, then you should pass reference to metadata yourself. If you are interested in this - I can provide code example of how to do that.

To check that you indeed prevent escalation in all cases - disable pooling (Pooling=false in connection string) and stop DTC service, then run this code - it should run fine. Then run another code which does not share the same connection and you should observe error indicating escalation was about to happen but service is not available.

Evk
  • 84,454
  • 8
  • 110
  • 160
  • Hi, thanks for your answer. However the issue lies with DB First. If you override the constructor for `SomeEntities`, you get `System.Data.Entity.Infrastructure.UnintentionalCodeFirstException`. So you are then unable to pass in your own connection – Andrew Nov 17 '17 at 23:21
  • 1
    @Andrew you don't with the code in answer. Just try it and you'll see. Note I'm passing EntityConnection there, not SqlConnection – Evk Nov 17 '17 at 23:21
  • In your answer you said this `(I assume you already added constructor which accepts DbConnection and flag indicating if context owns connection)`. The problem lies here, with DB first you are unable to do this – Andrew Nov 17 '17 at 23:25
  • 1
    @Andrew in what sense not able? Context class is partial so you can add whatever there, including this consructor. Author of this post already doing that, but he passes SqlConnection there and gets exception, I don't do that. This code is tested on EF 6 database first model and it works just fine. – Evk Nov 17 '17 at 23:29
  • Wow bro! You're totally right. Can you explain why passing in an entity connection doesn't trigger the CodeFirst exception please? Thanks a lot! – Andrew Nov 17 '17 at 23:42
  • I will accept your answer on Monday, I have to wait 19 hours to accept and I'm not back into work until Monday – Andrew Nov 18 '17 at 00:11
  • 1
    @Andrew I've expanded answer a bit with explanation. – Evk Nov 19 '17 at 14:20