528 words
3 minutes
Understanding LINQ to SQL (8) Transaction

[LINQ via C# series]

Database data Changing cannot be talked about without transactions.

Implementing TRANSACTION (BEGIN / COMMIT / ROLLBACK)#

The previous post has shown that, when invoking SubmitChanges(), the translated SQL (INSERT / UPDATE / DELETE) are always executed within a TRANSACTION.

Internally, DataContext.SubmitChanges() invokes DataContext.SubmitChanges(ConflictMode.FailOnFirstConflict). The latter is implemented like this:

public class DataContext : IDisposable
{
public virtual void SubmitChanges(ConflictMode failureMode)
{
if (this._isInSubmitChanges) // Concurrency is not allowed.
{
throw new InvalidOperationException(
"The operation cannot be performed during a call to SubmitChanges.");
}
if (!this.ObjectTrackingEnabled) // Tracking must be enabled.
{
throw new InvalidOperationException(
"Object tracking is not enabled for the current data context instance.");
}
this._isInSubmitChanges = true;
try
{
if (Transaction.Current != null ||
this.Transaction != null) // Custom transaction is specified.
{
// Process changes...
return;
}
try
{
try
{
this.Transaction = this.Connection.BeginTransaction(
IsolationLevel.ReadCommitted); // BEGIN TRANSACTION
// Process changes...
this.Transaction.Commit(); // COMMIT TRANSACTION
}
catch
{
this.Transaction.Rollback(); // ROLLBACK TRANSACTION
throw; // Failure is notified to the caller.
}
return; // Successes.
}
finally
{
this.Transaction = null; // Finally block ensures clearing transaction.
}
}
finally
{
this._isInSubmitChanges = false; // Finally block ensures resetting the flag.
}
}
}

It ensures all changes (INSERT / UPDATE / DELETE) are submitted within a TRANSACTION.

Conflict will be explained in the next post.

Default transaction#

If the DataContext.Transaction has never been set, it is null. In such scenarios LINQ to SQL will create a DbTransaction object to implement the TRANSACTION:

try
{
using (NorthwindDataContext database = new NorthwindDataContext())
{
Category[] categories = database.Categories.Take(2).ToArray();
Console.WriteLine("Category[0]: {0}", categories[0].CategoryName); // Beverages
categories[0].CategoryName = "Updated";
// Updating should success.
Console.WriteLine("Category[1]: {0}", categories[1].CategoryName); // Condiments
categories[1].CategoryName = "Aotobots of Transformers";
// Updating should fail in database, because CategoryName is NVARCHAR(15).
database.SubmitChanges();
}
}
catch (Exception exception)
{
Console.WriteLine("{0}: {1}", exception.GetType(), exception.Message);
// Checks whether any change has been submitted.
using (NorthwindDataContext database = new NorthwindDataContext())
{
Category[] categories = database.Categories.Take(2).ToArray();
// All records are not updated.
Console.WriteLine("Category[0]: {0}", categories[0].CategoryName); // Beverages
Console.WriteLine("Category[1]: {0}", categories[1].CategoryName); // Condiments
}
throw;
}

The above code tried to submit two changes, which are translated to two UPDATE statements:

BEGIN TRANSACTION
exec sp_executesql N'UPDATE [dbo].[Categories]
SET [CategoryName] = @p2
WHERE ([CategoryID] = @p0) AND ([CategoryName] = @p1)',N'@p0 int,@p1 nvarchar(4000),@p2 nvarchar(4000)',@p0=1,@p1=N'Beverages',@p2=N'Updated'
-- Successes.
exec sp_executesql N'UPDATE [dbo].[Categories]
SET [CategoryName] = @p2
WHERE ([CategoryID] = @p0) AND ([CategoryName] = @p1)',N'@p0 int,@p1 nvarchar(4000),@p2 nvarchar(4000)',@p0=2,@p1=N'Condiments',@p2=N'Aotobots of Transformers'
-- Falis. SubmitChanges() catches a SqlException.
ROLLBACK TRANSACTION -- this.Transaction.Rollback();
-- SubmitChanges() re-throws the SqlException to caller.

Because the second UPDATE fails, Submit() catches a SqlException, then it invoke DbTransaction.Rollback() and re-throw the SqlException to the code in the upper call stack.

Custom transactions#

If DataContext.Transaction is set with a custom DbTransaction:

using (NorthwindDataContext database = new NorthwindDataContext())
{
database.Transaction = database.Connection.BeginTransaction();
// Now DataContext.Transaction is not null.
}

or current submitting code is bracketed inside a TransactionScope:

using (NorthwindDataContext database = new NorthwindDataContext())
{
using (TransactionScope transactionScope = new TransactionScope())
{
// Transaction.Current is not null here.
}
}

Then it is not LINQ to SQL’s responsibility to implement the logic of transactions.

Because this is a LINQ / functional programming series, not a SQL / ADO.NET series, the further details of transaction will not be explained. Please check MSDN and Wikipedia for more information.

Understanding LINQ to SQL (8) Transaction
https://dixin.github.io/posts/understanding-linq-to-sql-8-transaction/
Author
Dixin
Published at
2010-04-22
License
CC BY-NC-SA 4.0