I would like to use multiple contexts/schemas in my current project but I am not sufficiently certain about the proper way to wrap write accesses to the contexts in a single transaction.
My understanding is that there a two ways to achieve this: DbContext.Database.BeginTransaction()
and TransactionScope
. What I uncertain about is whether I use them properly and/or whether there are other ways do do this.
Example:
Assume the following Model/Context:
public class A
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class ContextA : DbContext
{
public ContextA() : base( "MultipleContextsTest" ) { }
public DbSet<A> SetA { get; set; }
protected override void OnModelCreating( DbModelBuilder modelBuilder )
{
modelBuilder.HasDefaultSchema( "SchemaA" );
base.OnModelCreating( modelBuilder );
}
}
Then assume a second Model/Context:
public class B
{
[Key]
public int Id { get; set; }
public string OtherName { get; set; }
}
public class ContextB : DbContext
{
public ContextB() : base( "MultipleContextsTest" ) { }
public ContextB( DbConnection conn, bool ownsConnection = false )
: base( conn, ownsConnection ) { }
public DbSet<B> SetB { get; set; }
protected override void OnModelCreating( DbModelBuilder modelBuilder )
{
modelBuilder.HasDefaultSchema( "SchemaB" );
base.OnModelCreating( modelBuilder );
}
}
Then my approach using DbContext.Database.BeginTransaction()
is as follows:
void Using_BeginTransaction()
{
using( ContextA contextA = new ContextA() )
{
using( var transaction = contextA.Database.BeginTransaction() )
{
contextA.SetA.Add( new A { Name = "Name" } );
contextA.SaveChanges();
using( ContextB contextB
= new ContextB( transaction.UnderlyingTransaction.Connection ) )
{
contextB.Database.UseTransaction( transaction.UnderlyingTransaction );
contextB.SetB.Add( new B() { OtherName = "OtherName" } );
contextB.SaveChanges();
}
transaction.Commit();
}
}
}
What concerns me the most is that I could not find an example for re-using the connection of the "outer" context for an "inner" context. In the documentation it says that the transaction is "external" and in the examples I have found so far the transaction was not used to be passed between contexts. Either the transaction was used to run an SQLCommand
or the transaction was from received from an SqlConnection
created externally and explicitely.
With other words: I am asking whether I am misusing a feature which might not be intended to be used this way. This might also be implied by the fact that neither the connection nor the transaction could be directly passed to the "inner" context but that their "underlying" versions have to be used.
My approach to using TransactionScope
looks like this:
void Using_TransactionScope()
{
using( TransactionScope transaction = new TransactionScope() )
{
using( ContextA contextA = new ContextA() )
{
contextA.SetA.Add( new A { Name = "Name" } );
contextA.SaveChanges();
}
using( ContextB contextB = new ContextB() )
{
contextB.SetB.Add( new B() { OtherName = "OtherName" } );
contextB.SaveChanges();
}
transaction.Complete();
}
}
Here my concerns are mostly related to MSDTC (Microsoft Distributed Transaction Coordinator), i.e. avoiding the transaction being elevated to a distributed transaction.
It seems that the transaction will not be elevated if all of the following is true:
- a single database is used
- the contexts use the same connection string
- the contexts use the same connection (not 100% sure about this)
Is it guaranteed, or could it be achieved, that the contexts use the same connection?
However, what concerns me the most are the comments under the accepted answer of this Stack Overflow question: One transaction with multiple dbcontexts.
These comments could imply that the transaction is elevated to a "lightweight" distributed transaction (whatever that is). In any case the commenter regarded this approach as highly dangerous. Is that correct? But then what would be the purpose of TransactionScope
in the first place?
As a sidenote: I did some rough performence measurements (using 5000 calls to each of the two methods above) and found out:
BeginTransaction()
is slightly faster thanTransactionScope
.- Both versions are clearly faster than using no transaction at all. I did not expect that. Perhaps it is because only one connection has to be created?