I have an MVC system that uses .net Identity login, however, when two separate users log into the system at roughly the same time, the user who clicks the button last gets an error saying;
One or more errors occurred. A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread-safe.
This error happens on the SignInManager.PasswordSignInAsync()
line but I am stuck as to what I've done wrong.
Admittedly, I am not too familiar with OWIN, but if anyone has any clues as to what I can do to stop this happening that would be much appreciated.
To cover off a few things, I have already tried calling PasswordSignIn
instead of PasswordSingInAsync
and I've tried awaiting the call but its the same story - I believe this is because it's two entirely separate requests.
In brief, my code is set up as follows;
LoginController.cs
public ActionResult Login(LoginModel model)
{
_identityManagement.SignInManager.PasswordSignInAsync(model.Username, model.Password, model.PersistentLogin, false);
//We do some stuff here but it fails before this point so code removed.
return null;
}
IdentityManagement.cs
public class IdentityManagement
{
public ApplicationSignInManager SignInManager
{
get
{
return HttpContext.Current.GetOwinContext().Get<ApplicationSignInManager>();
}
}
}
ApplicationSignInManager.cs
public class ApplicationSignInManager : SignInManager<SystemUser, string>
{
public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) : base(userManager, authenticationManager)
{
}
public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
{
return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
}
}
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
DatabaseContext dbContext = DependencyResolver.Current.GetService<DatabaseContext>();
app.CreatePerOwinContext<IUserStore<DatabaseContext>>(() => ApplicationUserStore.Create(dbContext));
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
}
Global.asax.cs
var container = new SimpleInjector.Container();
container.Options.DefaultScopedLifestyle = new WebRequestLifestyle();
container.Register<DatabaseContext>(Lifestyle.Scoped);
container.Verify();
Many Thanks, Tom
As requested here is the stack trace
System.AggregateException
HResult=0x80131500
Message=One or more errors occurred.
Source=mscorlib
StackTrace:
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
at System.Threading.Tasks.Task`1.get_Result()
at Controllers.LoginController.Login(LoginModel model) in C:\...\Controllers\LoginController.cs:line 205
at Controllers.LoginController.Index(LoginModel model) in C:\...\Controllers\LoginController.cs:line 111
at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters)
at System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters)
at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c.<BeginInvokeSynchronousActionMethod>b__9_0(IAsyncResult asyncResult, ActionInvocation innerInvokeState)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`2.CallEndDelegate(IAsyncResult asyncResult)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResultBase`1.End()
at System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethod(IAsyncResult asyncResult)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<>c__DisplayClass11_0.<InvokeActionMethodFilterAsynchronouslyRecursive>b__0()
at System.Web.Mvc.Async.AsyncControllerActionInvoker.AsyncInvocationWithFilters.<>c__DisplayClass11_2.<InvokeActionMethodFilterAsynchronouslyRecursive>b__2()
Inner Exception 1:
NotSupportedException: A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.
The error explains what's wrong and how to fix it - an await
is missing from an asynchronous operation. The only such operation is in the first snippet :
public ActionResult Login(LoginModel model)
{
_identityManagement.SignInManager.PasswordSignInAsync(model.Username, model.Password, model.PersistentLogin, false);
//We do some stuff here but it fails before this point so code removed.
return null;
}
By convention asynchronous methods end in Async
and yet, nothing awaits for PasswordSignInAsync
to complete before continuing and returning. PasswordSignInAsync is indeed an asynchronous method.
This means that the user hasn't logged in by the time this method returns. For all anyone knows login may have failed.
Any subsequent attempts to login can easily happen before the first attempt finishes. It also means that the sign-in cookie is never set and any subsequent calls to the site will try to login again.
Change the method to :
public async Task<ActionResult> Login(LoginModel model)
{
var result=await _identityManagement.SignInManager.PasswordSignInAsync(model.Username, model.Password, model.PersistentLogin, false);
if(result.Succeeded)
{
....
}
}
The problem is that a second user ( which is running in different thread ) is trying to access the DatabaseContext while it is used by first user. You should create a new DatabaseContext for every thread
Thank you both, it put me onto the right track and I've now solved the issue.
In Startup.auth.cs I had the following code
DatabaseContext dbContext = DependencyResolver.Current.GetService<DatabaseContext>();
app.CreatePerOwinContext<IUserStore<SystemUser>>(() => ApplicationUserStore.Create(dbContext));
I've now changed it to the following which works for multiple simultaneous login requests
app.CreatePerOwinContext<IUserStore<SystemUser>>(() => ApplicationUserStore.Create(new DatabaseContext()));
Presumably, the was I was declaring the dbContext here was using the same one for every instance of "SystemUser"
Thank you both again for the help and ideas.
Tom
User contributions licensed under CC BY-SA 3.0