2

I have a method which call Web Api from it. Data get inserted into multiple tables, so added transaction. Below is layout of code.

{

    TransactionObject objTransaction = new TransactionObject(); 
    try
    {

    //Create Order
    order.insert(objTransaction);

    //Create Delivery
    address.insert(objTransaction);

    //Call Generic Web API method to calculate TAX.
    using (HttpClient client = new HttpClient())
        {
            client.BaseAddress = new Uri("http://localhost:85766/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            var response = client.PostAsJsonAsync("api/Tax/UpdateTax", order).Result;

            if (response.IsSuccessStatusCode)
            {    
                //Commit transactio     
                 objTransaction.EndTransaction(true);                                            
            }
            else
            {
                //Commit transactio 
                objTransaction.EndTransaction(false);   
            }

        }
    }
}
catch(Exception ex)
{
    //Commit transactio 
    objTransaction.EndTransaction(false);   
}

This method call a web api to do another database operation. Below Web Api is a generic method called form multiple places.

        [HttpPost]
        public HttpResponseMessage UpdateTax(Order order)
        {            
            TransactionObject objTransaction = new TransactionObject();            
            try
            {

                //DO calculation logic
                .
                .
                //Insert Invoice 
                invoice.insert(objTransaction);
                objTransaction.EndTransaction(true);
                return Request.CreateResponse(HttpStatusCode.Created, "Invoice data added successfully");
            }
            catch (Exception ex)
            {
                objTransaction.EndTransaction(false);
                return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, "Erorr while adding invoice");
            }

        }

Now if exception occur before Web API getting called , then all transaction will be rolled back. It is fine. Same for if exception occur in Web Api.

If exception will occur after Web API get executed successfully, while committing transaction in main method, how to handle it, I mean in below code

if (response.IsSuccessStatusCode)
   {    
     //Commit transactio        
      objTransaction.EndTransaction(true);                                            
   }

Is there any better approach to handle the logic?

Khanh TO
  • 48,509
  • 13
  • 99
  • 115
user1926138
  • 1,464
  • 7
  • 34
  • 53
  • One option is to use [`TransactionScope`](https://msdn.microsoft.com/en-us/library/system.transactions.transactionscope(v=vs.110).aspx) with ambient transaction option. – kayess Jun 07 '17 at 10:16
  • @kayess: ambient transaction option does not work across network calls. I posted an answer to flow the transaction id across the network. Please give feedback if you have any, thanks – Khanh TO Jun 10 '17 at 04:25

1 Answers1

4

Your current implementation does not guarantee atomicity, consider the case when there is an error after the UpdateTax method finishes (network problem or whatever), so your main method receives an error even your data has been saved.

You could improve it by using TransactionScope to create a distributed transaction.

In order for distributed transaction to work, you need to turn on the Distributed Transaction Coordinator (DTC): https://msdn.microsoft.com/en-us/library/dd327979.aspx . I also assume that your web api application is in the same domain network of your main application.

In your case, you can create an ambient transaction to flow the transaction across method calls within the application boundary as suggested by @kayess in his comment. But ambient transaction does not work across network calls, you have to implement additional logic for that.

The idea is to flow your transaction id to your web api application so it can participate in the same transaction of your main application. Below is example code for your main application:

    using (var scope = new TransactionScope()) //create ambient transaction by default.
    { 
        //Create Order (insert Order using your local method)

        //Create Delivery (insert Delivery using your local method)
 
        using (var client = new HttpClient()) 
        { 
            client.BaseAddress = new Uri("http://localhost:85766/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            //Send current transaction id as header in the http request
            var token = TransactionInterop.GetTransmitterPropagationToken(Transaction.Current); 
            //You could improve it by creating a HttpRequestMessage, here is just to demonstrate the idea.
            client.DefaultRequestHeaders.Add("TransactionToken", Convert.ToBase64String(token));      

            var response = client.PostAsJsonAsync("api/Tax/UpdateTax", order).Result;
           //Do some more stuff
           ..

           //If there is no exception and no error from the response
           if (response.IsSuccessStatusCode) {
               scope.Complete(); //Calling this let the transaction manager commit the transaction, without this call, the transaction is rolled back          
           }  
         }
    }

Your web api method:

    [HttpPost]
    public HttpResponseMessage UpdateTax(Order order)
    {               
        try
        {
            //Retrieve the transaction id in the header
             var values = Request.Headers.GetValues("TransactionToken"); 
            if (values != null && values.Any()) 
            { 
                byte[] transactionToken = Convert.FromBase64String(values.FirstOrDefault()); 
                var transaction = TransactionInterop.GetTransactionFromTransmitterPropagationToken(transactionToken); 
               //Enlist in the same transaction
               using(var transactionScope = new TransactionScope(transaction)
               {
                  //DO calculation logic
                  .
                  .
                  //Insert Invoice 
             
                  transactionScope.Complete();
                }
            }
            return Request.CreateResponse(HttpStatusCode.Created, "Invoice data added successfully");
        }
        catch (Exception ex)
        {
            return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, "Erorr while adding invoice");
        }

    }

Note:

Please help to adjust your code to work with this. I just demonstrate the idea (there is no check for null, no adjusting the old database method to work with TransactionScope, extracting to ActionMethod to have better code reusability,...)

Update in 2020: Distributed transaction is a very strong consistency protocol but it also has problems:

  • It's a synchronous (blocking protocol) and could place a lot of locks across many systems.
  • Distributed transactions have long delays with RPC calls, especially when integrating with external services. Therefore the locks could be held for long time and become performance bottleneck.

If possible, we should try integration patterns using message queue like RabbitMq or architect the systems with a distributed streaming platform like Kafka

Khanh TO
  • 48,509
  • 13
  • 99
  • 115