1

We've been expanding the use of TransactionScope in our ASP.NET Web Application with an AzureSQL DB. Everything seems to work fine locally and on our development server. Even on our production server everything worked fine initially. But after a while we got tons of

System.InvalidOperationException: The current TransactionScope is already complete.

Turns out they happen inside our own implementation of an AuthorizeAttribute that we use to protect the controller method. To be exact, it only happens with OnAuthorizationAsync. We do have controller methods that are async, so I assume this goes hand in hand.
But I'm not sure what's going on here?
Could the problem be, that our db calls inside OnAuthorizationAsync happen after we open a TransactionScope in the controller method because of the whole async stuff? How could we fix this?
The tricky stuff is that this does not happen all the time. The same function can work ten times and then suddenly fail.

Update:
I found out about TransactionScopeAsyncFlowOption, but we are not using TransactionScope inside the OnAuthorizationAsync method anywhere. So not sure if this even applies?

This is my AuthorizeAttribute code:

public class BasicHttpAuthorizeAttribute : AuthorizeAttribute
{
    private static readonly ILog Log = LogManager.GetLogger(typeof (BasicHttpAuthorizeAttribute));
    private bool _requireSsl = Convert.ToBoolean(ConfigurationManager.AppSettings["RequireSsl"]);
    private bool _requireAuthentication = true;


    public bool RequireSsl
    {
        get { return _requireSsl; }
        set { _requireSsl = value; }
    }


    public bool RequireAuthentication
    {
        get { return _requireAuthentication; }
        set { _requireAuthentication = value; }
    }


    public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        //actionContext.Request

        if (Authenticate(actionContext) || !RequireAuthentication)
        {
            return;
        }

        HandleUnauthorizedRequest(actionContext);
    }


    protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        HttpResponseMessage challengeMessage = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        challengeMessage.Headers.Add("WWW-Authenticate", "Basic");
        throw new HttpResponseException(challengeMessage);
        //throw new HttpResponseException();
    }


    private bool Authenticate(System.Web.Http.Controllers.HttpActionContext actionContext) //HttpRequestMessage input)
    {
        if (RequireSsl && !HttpContext.Current.Request.IsSecureConnection && !HttpContext.Current.Request.IsLocal)
        {
            return false;
        }

        //if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization")) return false;
        // no longer exit here because we also now check for an authentication cookie

        string authHeader = HttpContext.Current.Request.Headers["Authorization"];

        IPrincipal principal;
        if ((TryGetPrincipal(authHeader, out principal) || TryGetAuthCookie(out principal)) && principal != null)
        {
            HttpContext.Current.User = principal;
            return true;
        }

        Log.Error("Unable to provide authorization");

        return false;
    }


    public static bool TryGetPrincipal(string authHeader, out IPrincipal principal)
    {
        var creds = ParseAuthHeader(authHeader);
        if (creds != null)
        {
            if (TryGetPrincipal(creds[0], creds[1], out principal))
            {
                Log.Info("Authorized by the HTTP Authorization header.");
                return true;
            }
            Log.Info("Tried to authorize using the HTTP Authorization header; although header present, unable to obtain principal.");
        }

        principal = null;

        return false;
    }


    public static bool TryGetAuthCookie(out IPrincipal principal)
    {
        var context = HttpContext.Current;

        var authCookie = context.Request.Cookies[FormsAuthentication.FormsCookieName];
        if (authCookie != null)
        {
            // Get the forms authentication ticket.
            var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
            var identity = new GenericIdentity(authTicket.Name, "Forms");

            principal = new GenericPrincipal(new GenericIdentity(identity.Name), System.Web.Security.Roles.GetRolesForUser(identity.Name));
            Log.Info("Authorized by the Forms Authentication cookie.");
            return true;
        }

        Log.Info("Tried to authorize using the Forms Authentication cookie but this could not be found.");
        principal = null;

        return false;
    }


    private static string[] ParseAuthHeader(string authHeader)
    {
        // Check this is a Basic Auth header 
        if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic"))
        {
            return null;
        }

        // Pull out the Credentials with are seperated by ':' and Base64 encoded 
        string base64Credentials = authHeader.Substring(6);
        string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(base64Credentials)).Split(new[] { ':' });

        if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[1])) return null;

        // Okay this is the credentials 
        return credentials;
    }


    private static bool TryGetPrincipal(string username, string password, out IPrincipal principal)
    {
        try
        {
            // this is the method that does the authentication 

            //users often add a copy/paste space at the end of the username
            username = username.Trim();
            password = password.Trim();

            Person person = AccountManagement.ApiLogin(username, password);

            if (person != null)
            {
                // once the user is verified, assign it to an IPrincipal with the identity name and applicable roles
                principal = new GenericPrincipal(new GenericIdentity(username), System.Web.Security.Roles.GetRolesForUser(username));
                return true;
            }

            principal = null;
            return false;
        }
        catch(Exception ex)
        {
            Log.Error("TryGetPrincipal username=" + username, ex);
            throw ex;
        }
    }
}

And here the part for AccountManagement.ApiLogin

public static Person ApiLogin(string username, string token)
{
    using (var conn = DatabaseUtil.GetConnection())
    using (var cmd = conn.CreateCommand())
    {
        try
        {
            cmd.CommandText = @"SELECT
                                    id
                                FROM
                                    st_person
                                WHERE
                                    user_name = @user_name
                                AND
                                    api_token = @api_token";

            cmd.Parameters.Add("@user_name", SqlDbType.NVarChar, 100).Value = username;
            cmd.Parameters.Add("@api_token", SqlDbType.NVarChar, 40).Value = token;

            long personId = Convert.ToInt64(cmd.ExecuteScalarWithRetry(DatabaseUtil.RetryPolicy));

            if (personId != 0)
            {
                Log.Info("ApiLogin sucessful for username=" + username + "; token=" + token);

                return (Person) Membership.GetUser(personId);
            }

            Log.Info("ApiLogin invalid for username=" + username + "; token=" + token);

            return null;
        }
        catch (Exception ex)
        {
            Log.Error("ApiLogin failed for username=" + username + "; token=" + token, ex);
            throw;
        }
    }
}

public static ReliableSqlConnection GetConnection(string dbName = "Supertext")
{
    try
    {
        var conn = new ReliableSqlConnection(ConfigurationManager.ConnectionStrings[dbName].ConnectionString, RetryPolicy);

        conn.Open(RetryPolicy);

        return conn;
    }
    catch(Exception ex)
    {
        Log.Error("Error", ex);
        throw;
    }
}

@Camilo asked where OnAuthorizationAsync actually is? I assume this happens in the baseclass: When does WebApi2 use OnAuthorizationAsync vs OnAuthorization (1st answer).
You can see this in the stacktrace too:

System.InvalidOperationException: The current TransactionScope is already complete.
   at System.Transactions.Transaction.get_Current()
   at System.Data.ProviderBase.DbConnectionPool.GetFromTransactedPool(Transaction& transaction)
   at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection)
   at System.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions)
   at System.Data.SqlClient.SqlConnection.TryOpenInner(TaskCompletionSource`1 retry)
   at System.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry)
   at System.Data.SqlClient.SqlConnection.Open()
   at Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.ReliableSqlConnection.<Open>b__1()
   at Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.RetryPolicy.<>c__DisplayClass1.<ExecuteAction>b__0()
   at Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.RetryPolicy.ExecuteAction[TResult](Func`1 func)
   at Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.RetryPolicy.<>c__DisplayClass1.<ExecuteAction>b__0()
   at Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.RetryPolicy.ExecuteAction[TResult](Func`1 func)
   at Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling.ReliableSqlConnection.Open(RetryPolicy retryPolicy)
   at Collaboral.Common.DB.DatabaseUtil.GetConnection(String dbName) in d:\a\3\s\SupertextCommonSQL\Util\DatabaseUtil.cs:line 135
   at Supertext.BL.CustomerManagement.AccountManagement.ApiLogin(String username, String token) in d:\a\3\s\SupertextCommonSQL\CustomerManagement\AccountManagement.cs:line 81
   at SupertextCommon.Authorization.BasicHttpAuthorizeAttribute.TryGetPrincipal(String username, String password, IPrincipal& principal) in d:\a\3\s\SupertextCommonSQL\Authorization\BasicHttpAuthorizeAttribute.cs:line 152
   at SupertextCommon.Authorization.BasicHttpAuthorizeAttribute.TryGetPrincipal(String authHeader, IPrincipal& principal) in d:\a\3\s\SupertextCommonSQL\Authorization\BasicHttpAuthorizeAttribute.cs:line 86
   at SupertextCommon.Authorization.BasicHttpAuthorizeAttribute.Authenticate(HttpActionContext actionContext) in d:\a\3\s\SupertextCommonSQL\Authorization\BasicHttpAuthorizeAttribute.cs:line 68
   at SupertextCommon.Authorization.BasicHttpAuthorizeAttribute.OnAuthorization(HttpActionContext actionContext) in d:\a\3\s\SupertextCommonSQL\Authorization\BasicHttpAuthorizeAttribute.cs:line 40
   at System.Web.Http.Filters.AuthorizationFilterAttribute.OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Web.Http.Controllers.ExceptionFilterResult.<ExecuteAsync>d__0.MoveNext()
Remy
  • 12,555
  • 14
  • 64
  • 104
  • It'd be quite good if we could actually see the code without needing to look at your monitor – Camilo Terevinto Dec 18 '17 at 23:04
  • Are you using TransactionScopeAsyncFlowOption? (See https://stackoverflow.com/questions/24593070/how-to-support-async-methods-in-a-transactionscope-with-microsoft-bcl-async-in) – desmondgc Dec 18 '17 at 23:09
  • I've used it in some places where I had issues with async and the TransactionScope, but since I'm not actually using a TransactionScope inside OnAuthorizationAsync I didn't think it would be necessary. – Remy Dec 18 '17 at 23:11
  • I'm sorry, I don't see `OnAuthorizationAsync` in your entire code, did you forget to copy that or did you mean `OnAuthorization`? – Camilo Terevinto Dec 18 '17 at 23:13
  • It's not in MY code. But from what I understand, the base code calls OnAuthorization. See update to my question. – Remy Dec 18 '17 at 23:16
  • I see (not sure why I wasn't notified of your response, though). Can you add the code for `DatabaseUtil.GetConnection()` and `conn.CreateCommand()`? – Camilo Terevinto Dec 18 '17 at 23:33
  • Done. conn.CreateCommand() is nothing from us. – Remy Dec 19 '17 at 10:49

0 Answers0