WCF uses the [TransactionFlow] attribute for transaction propagation. This article shows a possible solution to obtain the same behavior with WebAPI in order to enlist operations performed in different application domains, permitting participation of different processes in the same transaction.
Building the Sample
In order to enlist transactions belonging to different application domains and event different servers, we can rely on Distributed Transaction Coordinators (DTC).
When the application is hosted on different servers, in order to use the DTC, those servers must be on the same Network Domain. DTC needs bidirectional communication, in order to coordinate the principal transaction with other transactions. Violation this precondition end up with the following error message:
The MSDTC transaction manager was unable to push the transaction to the destination transaction manager due to communication problems. Possible causes are: a firewall is present and it doesn't have an exception for the MSDTC process, the two machines cannot find each other by their NetBIOS names, or the support for network transactions is not enabled for one of the two transaction managers. (Exception from HRESULT: 0x8004D02A).
To check this precondition you can simply ping one server from another and vice-versa.
The DTC must be enabled for network access. The settings used for this example are shown in the following image:
In the end, the DTC service must be running:
Description
With this example, I created a client WebAPI application working as a server. This endpoint exposes an action to save data posted from a client. The client, a console application, in addition, to call the WebAPI endpoint, perform an INSERT operation on its local database. Both those operations must be done in the same transaction, in order to commit or rollback all together.
Client application
The client application must initiate the transaction and then forward the transaction token to the WebAPI action method. To simplify this operation, I created an extension method on the HttpRequestMessage class that retrieves the transaction token from the ambient transaction and sets it as an HTTP header.
public static class HttpRequestMessageExtension
{
public static void AddTransactionPropagationToken(this HttpRequestMessage request)
{
if (Transaction.Current != null)
{
var token = TransactionInterop.GetTransmitterPropagationToken(Transaction.Current);
request.Headers.Add("TransactionToken", Convert.ToBase64String(token));
}
}
}
Of course, this method must be invoked before calling the WebAPI endpoint and within a transaction context. For this reason, the client application must initiate the main transaction,
using (var scope = new TransactionScope())
{
// database operation done in an external app domain
using (var client = new HttpClient())
{
using (var request = new HttpRequestMessage(HttpMethod.Post, String.Format(ConfigurationManager.AppSettings["urlPost"], id)))
{
// forward transaction token
request.AddTransactionPropagationToken();
var response = client.SendAsync(request).Result;
response.EnsureSuccessStatusCode();
}
}
// database operation done in the client app domain
using (var connection = new SqlConnection(ConfigurationManager.ConnectionStrings["connectionStringClient"].ConnectionString))
{
connection.Open();
using (var command = new SqlCommand(String.Format("INSERT INTO [Table_A] ([Name], [CreatedOn]) VALUES ('{0}', GETDATE())", id), connection))
{
command.ExecuteNonQuery();
}
}
// Commit local and cross domain operations
scope.Complete();
}
WebAPI application
Server-side, I need to retrieve the transaction identifier and enroll the action method in the client transaction. I resolved it creating an action filter.
public class EnlistToDistributedTransactionActionFilter : ActionFilterAttribute
{
private const string TransactionId = "TransactionToken";
/// <summary>
/// Retrieve a transaction propagation token, create a transaction scope and promote
/// the current transaction to a distributed transaction.
/// </summary>
/// <param name="actionContext">The action context.</param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.Request.Headers.Contains(TransactionId))
{
var values = actionContext.Request.Headers.GetValues(TransactionId);
if (values != null && values.Any())
{
byte[] transactionToken = Convert.FromBase64String(values.FirstOrDefault());
var transaction = TransactionInterop.GetTransactionFromTransmitterPropagationToken(transactionToken);
var transactionScope = new TransactionScope(transaction);
actionContext.Request.Properties.Add(TransactionId, transactionScope);
}
}
}
/// <summary>
/// Rollback or commit transaction.
/// </summary>
/// <param name="actionExecutedContext">The action executed context.</param>
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Request.Properties.Keys.Contains(TransactionId))
{
var transactionScope = actionExecutedContext.Request.Properties[TransactionId] as TransactionScope;
if (transactionScope != null)
{
if (actionExecutedContext.Exception != null)
{
Transaction.Current.Rollback();
}
else
{
transactionScope.Complete();
}
transactionScope.Dispose();
actionExecutedContext.Request.Properties[TransactionId] = null;
}
}
}
}
Now we can apply this filter on our action endpoint in order to participate in the caller transaction.
[HttpPost]
[EnlistToDistributedTransactionActionFilter]
public HttpResponseMessage Post(string id)
{
using (var connection = new SqlConnection(ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString))
{
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = String.Format("INSERT INTO [Table_1] ([Id], [CreatedOn]) VALUES ('{0}', GETDATE())", id);
command.ExecuteNonQuery();
}
}
var response = Request.CreateResponse(HttpStatusCode.Created);
response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = id }));
return response;
}
Source Code Files
Attached to this article, you can find a solution containing two projects: a client console application and a WebAPI application.
To try this solution, unzip the solution and deploy the WebAPI 2.0 application under IIS on server "A" with its own SQL Server instance. After that, install the console application on server "B" with its own SQL Server instance. As mentioned before, those servers must be in the same Network Domain.
The client application test four cases,
Commit client and server
Rollback client and server
Exception server-side (resulting in a server and client rollback)
Exception client-side (resulting in a server and client rollback)
When the client application starts, it asks you what kind of test do you want. In case of a positive commitment (0), a green message is reported, followed by a server call used to get all records inserted since now,
In negative cases (1,2,3), the resulting message will be in red, followed by a server call to retrieve all the records from the server DB to check that no new record was inserted.