Проблемы TransactionScope с OnAuthorizationAsync с нашей собственной реализацией AuthorizeAttribute

Мы расширили использование TransactionScope в нашем веб-приложении ASP.NET с помощью базы данных AzureSQL. Кажется, все работает нормально локально и на нашем сервере разработки. Даже на нашем производственном сервере изначально все работало нормально. Но через некоторое время мы получили тонны

System.InvalidOperationException: The current TransactionScope is already complete.

Оказывается, они происходят внутри нашей собственной реализации AuthorizeAttribute, которую мы используем для защиты метода контроллера. Если быть точным, так бывает только с OnAuthorizationAsync. У нас есть асинхронные методы контроллера, поэтому я предполагаю, что это идет рука об руку.
Но я не уверен, что здесь происходит?
Может ли проблема быть в том, что наши вызовы db внутри OnAuthorizationAsync происходят после того, как мы открываем TransactionScope в методе контроллера из-за всей асинхронности? Как мы можем это исправить?
Сложность в том, что это не происходит постоянно. Одна и та же функция может работать десять раз, а затем внезапно выйти из строя.

Обновление:
Я узнал о TransactionScopeAsyncFlowOption, но мы нигде не используем TransactionScope внутри метода OnAuthorizationAsync. Не уверен, что это вообще применимо?

Это мой код AuthorizeAttribute:

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

А вот часть для 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 спросил, где на самом деле OnAuthorizationAsync? Я предполагаю, что это происходит в базовом классе: Когда WebApi2 использует OnAuthorizationAsync против OnAuthorization (1-й ответ).
Вы также можете увидеть это в трассировке стека:

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()

person Remy    schedule 18.12.2017    source источник
comment
Было бы неплохо, если бы мы действительно могли видеть код, не глядя на свой монитор.   -  person Camilo Terevinto    schedule 19.12.2017
comment
Вы используете TransactionScopeAsyncFlowOption? (См. stackoverflow.com/questions/24593070/)   -  person desmondgc    schedule 19.12.2017
comment
Я использовал его в некоторых местах, где у меня были проблемы с async и TransactionScope, но поскольку я фактически не использую TransactionScope внутри OnAuthorizationAsync, я не думал, что это будет необходимо.   -  person Remy    schedule 19.12.2017
comment
Извините, я не вижу OnAuthorizationAsync во всем вашем коде, вы забыли скопировать это или вы имели в виду OnAuthorization?   -  person Camilo Terevinto    schedule 19.12.2017
comment
Этого нет в МОЕМ коде. Но насколько я понимаю, базовый код вызывает OnAuthorization. См. Обновление моего вопроса.   -  person Remy    schedule 19.12.2017
comment
Понятно (правда, не знаю, почему меня не уведомили о вашем ответе). Можете ли вы добавить код для DatabaseUtil.GetConnection() и conn.CreateCommand()?   -  person Camilo Terevinto    schedule 19.12.2017
comment
Выполнено. conn.CreateCommand () от нас ничего не говорит.   -  person Remy    schedule 19.12.2017