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