I have a method that signs a user in and attempts to update the user if the viewModel
is different than user
object. The behaviour I'm seeing is confusing.
Every time the method executes, if the user
was not previously logged in, the line await _userManager.UpdateAsync(user);
fails with the exception: "There is no user with id: 99"
(or whatever id
value). However, if the user
was previously logged in, the line works.
So for example,
viewModel
to server. user
in, viewModel
is different than the existing user
's data, the server attempts to update it. "There is no user with id: 99"
user
has been logged in from previous failed post)viewModel
is still different than existing data (remember, the update failed last time)await _userManager.UpdateAsync(user);
works and the record is updated.The following is the method:
[UnitOfWork]
public async Task<AjaxResponse> Post(MyViewModel viewModel)
{
try
{
var loginResult = await _userManager.LoginAsync(viewModel.UserName, viewModel.Password, viewModel.TenancyName);
User user;
if (loginResult.Result == AbpLoginResultType.Success)
{
await SignInAsync(loginResult.User, loginResult.Identity);
user = loginResult.User;
if (user.AccessToken != viewModel.AccessToken)
{
user.AccessToken = viewModel.AccessToken;
// why does this fail the first time?
await _userManager.UpdateAsync(user);
}
}
else
{
/* do some other UnitOfWork stuff below */
}
return new AjaxResponse(new MyResult
{
Name = user.Name + " " + user.Surname,
UserName = user.UserName,
EmailAddress = user.EmailAddress,
IsActive = user.IsActive,
Success = true,
UserId = user.UserId,
});
}
catch (Exception ex)
{
throw new HttpException((int)HttpStatusCode.InternalServerError, ex.Message);
}
}
I can confirm that a user with the id 99 does in fact exist in the database.
For the record, the following is the contents of ex.StackTrace
at Abp.Authorization.Users.AbpUserManager`3.<GetUserByIdAsync>d__5b.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Abp.Authorization.Users.AbpUserManager`3.<UpdateAsync>d__64.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at MyProject.Web.Controllers.Api.AccountApiController.<Post>d__16.MoveNext() in C:\dev\MyProject\MyProject.Web\Controllers\Api\AccountApiController.cs:line 146
I think one clue might be in the following query (intercepted with SQL Server Profiler) that gets executed before the update:
exec sp_executesql N'SELECT TOP (1)
[Extent1].[Id] AS [Id],
[Extent1].[AccessToken] AS [AccessToken],
[Extent1].[UserId] AS [UserId],
[Extent1].[EmailAddress] AS [EmailAddress],
[Extent1].[TenantId] AS [TenantId],
[Extent1].[IsDeleted] AS [IsDeleted],
-- irrelevant stuff removed
FROM [dbo].[AbpUsers] AS [Extent1]
WHERE
((([Extent1].[TenantId] IS NULL) AND (@DynamicFilterParam_1 IS NULL))
OR (([Extent1].[TenantId] IS NOT NULL) AND ([Extent1].[TenantId] = @DynamicFilterParam_1))
OR (@DynamicFilterParam_2 IS NOT NULL)) AND (([Extent1].[IsDeleted] = @DynamicFilterParam_3)
OR (@DynamicFilterParam_4 IS NOT NULL)) AND ([Extent1].[EmailAddress] = @p__linq__0)',
N'@DynamicFilterParam_1 int,@DynamicFilterParam_2 bit,@DynamicFilterParam_3 bit,@DynamicFilterParam_4 bit,@p__linq__0 nvarchar(4000)'
,@DynamicFilterParam_1=NULL,@DynamicFilterParam_2=NULL,@DynamicFilterParam_3=0,@DynamicFilterParam_4=NULL,@p__linq__0=N'myemail@mail.com'
Here, we can see that @DynamicFilterParam_1=NULL
. The variable @DynamicFilterParam_1
corresponds to the value for [Extent1].[TenantId]
. If I manually assign the value 2
(which is the value associated with the record in the db) instead of NULL
and rerun the query, it returns the record as I expect.
When I execute the method the second time, I can see that TenantId is correctly assigned the value of 2.
Why is the value corresponding to TenantId being assigned NULL the first time? Why does the UpdateAsync
method fail every first time? What can I do to make it work?
In response to the request below, the definition of UpdateAsync is available in the asp.net boilerplate github
This is because of multitenancy. ABP does not know the TenantId before user login. You should manually Set tenant id to switch to the tenant of the user. My Login code is like that:
public virtual async Task<JsonResult> Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "")
{
var loginResult = await GetLoginResultAsync(loginModel.UsernameOrEmailAddress, loginModel.Password, loginModel.TenancyName);
var tenantId = loginResult.Tenant == null ? (int?)null : loginResult.Tenant.Id;
using (UnitOfWorkManager.Current.SetTenantId(tenantId))
{
if (loginResult.User.ShouldChangePasswordOnNextLogin)
{
loginResult.User.SetNewPasswordResetCode();
return Json(new AjaxResponse
{
TargetUrl = Url.Action(
"ResetPassword",
new ResetPasswordViewModel
{
TenantId = tenantId,
UserId = SimpleStringCipher.Instance.Encrypt(loginResult.User.Id.ToString()),
ResetCode = loginResult.User.PasswordResetCode
})
});
}
var signInResult = await _signInManager.SignInOrTwoFactorAsync(loginResult, loginModel.RememberMe);
if (signInResult == SignInStatus.RequiresVerification)
{
return Json(new AjaxResponse
{
TargetUrl = Url.Action(
"SendSecurityCode",
new
{
returnUrl = returnUrl + (returnUrlHash ?? ""),
rememberMe = loginModel.RememberMe
})
});
}
Debug.Assert(signInResult == SignInStatus.Success);
await UnitOfWorkManager.Current.SaveChangesAsync();
if (string.IsNullOrWhiteSpace(returnUrl))
{
returnUrl = GetAppHomeUrl();
}
if (!string.IsNullOrWhiteSpace(returnUrlHash))
{
returnUrl = returnUrl + returnUrlHash;
}
return Json(new AjaxResponse { TargetUrl = returnUrl });
}
}
The critical thing is the using (UnitOfWorkManager.Current.SetTenantId(tenantId))
statement