7
votes

In windows azure we have hosted two asp.net webapi project as app service. We need to enable distributed transaction here. We initiate transaction inside one api. Then inside that transaction scope we fetch propagation token of that transaction and send it as a header during another api call. The code is something like bellow.

[HttpGet]
[Route("api/Test/Transaction/Commit")]
public async Task<string> Commit()
{
    using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions
    {
        IsolationLevel = IsolationLevel.ReadCommitted
    },
    TransactionScopeAsyncFlowOption.Enabled))
    {
        // cross app domain call
        using (var client = new HttpClient())
        {
            using (var request = new HttpRequestMessage(HttpMethod.Get, ConfigurationManager.AppSettings["IdentityServerUri"] + "api/Test/Transaction/NoCommit"))
            {
                // forward transaction token
                request.AddTransactionPropagationToken();
                var response = await client.SendAsync(request);
                response.EnsureSuccessStatusCode();
            }
        }
        this.Repository.Insert(new Currency { Ccy = "x", IsoCode = "XIS", Name = "XYZ", CurrencyId = 9 });
        await this.Repository.SaveChangesAsync();

        scope.Complete();
        return "value";
    }
}

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

Inside the api(...api/Test/Transaction/NoCommit) to which we are making the call inside transaction scope, fetch that marshaled propagation token of the transaction from header and using it create instance of that transaction and instantiate TransactionScope using that transaction. Later we use this transaction scope to complete that transaction. We have introduced a action filter to apply this and added that filter to the action which is responsible for that api call. Code for that api and action filter is something like bellow.

    [HttpGet]
    [EnlistToDistributedTransactionActionFilter]
    [Route("api/Test/Transaction/NoCommit")]
    public async Task<string> NoCommit()
    {
        this.Repository.Insert(new Client
        {
            Name = "Test",
            AllowedOrigin = "*",
            Active = true,
            ClientGuid = Guid.NewGuid(),
            RefreshTokenLifeTime = 0,
            ApplicationType = ApplicationTypes.JavaScript,
            Secret = "ffff",
            Id = "Test"
        }
        );
        await this.Repository.SaveChangesAsync();
        return "value";
    }

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, TransactionScopeAsyncFlowOption.Enabled);

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

So if any exception occurs during this call (api/Test/Transaction/Commit) inside that transaction scope (either in firt api or second api) all the database change done by the both api will be rolled back. This is working fine locally. As locally we get support of MSDTC. But in Azure there is no MSDTC support. In azure we get support from Elastic transaction. Because of this when we are trying to fetch propagation token of the transaction from the first server we are getting exception. So when we try to execute bellow code var transaction = TransactionInterop.GetTransactionFromTransmitterPropagationToken(transactionToken); We are getting exception with message "Value does not fall within the expected range". This post saying that this method would require promotion to MSDTC by System.Transactions, but for elastic transaction how we will make it work? For elastic transaction we need to marshal transaction into propagation token. How to do this? Looking for the solution.

1

1 Answers

0
votes

Elastic Transactions are designed to allow transactions across Azure SQL Database and Azure SQL Managed Instance from a single .net application in Azure.

It is not built for distributing transactions across clients.

"Only client-coordinated transactions from a .NET application are supported"

https://docs.microsoft.com/en-us/azure/azure-sql/database/elastic-transactions-overview