Мы расширили использование 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()
OnAuthorizationAsync
во всем вашем коде, вы забыли скопировать это или вы имели в видуOnAuthorization
? - person Camilo Terevinto   schedule 19.12.2017DatabaseUtil.GetConnection()
иconn.CreateCommand()
? - person Camilo Terevinto   schedule 19.12.2017