Base solution for your next web application
Open Closed

using (_unitOfWorkManager.Current.SetTenantId(null)) does not reset when disposed #10435


User avatar
0
SorenRomer created

Prerequisites

Please answer the following questions before submitting an issue. YOU MAY DELETE THE PREREQUISITES SECTION.

  • What is your product version?
    • 10.3
  • What is your product type (Angular or MVC)?
    • Angular
  • What is product framework type (.net framework or .net core)?
    • .Net core

If issue related with ABP Framework

  • What is ABP Framework version?
    • 6.3

Multitenancy ENABLED

Question

I have a strange issue in the UserAppService class.

I have added a new custom property to the User class that needs to get set during update and creation of users. The issue is that in order to assign the right values for the custom property I need to get some data from the database created by the host. To avoid too much polution of the create and update user methods I have extracted the functionality to a private async method inside the UserAppService class.

While entering UpdateUserAsync() _unitOfWorkManager.Current.GetTenantId() returns the correct tenantId but after my private method is called the same statement returns null.

This is what I have done inside the private method: Attempt #1 Assuming tenant ID will be reset to AbpSession.GetTenantId() value after using is complete (it is not)

private async Task DoCustomStuffAsync(CreateOrUpdateUserInput input, User user)
{
    if(user.CustomProp == null)
        return;
        
     using (_unitOfWorkManager.Current.SetTenantId(null))
    {
        // Getting host data *** WORKS
    }
}

Attempt #2 forcing new UOW

private async Task DoCustomStuffAsync(CreateOrUpdateUserInput input, User user)
{
    if(user.CustomProp == null)
        return;
    
    using(var uow = _unitOfWorkManager.Begin())
    using (_unitOfWorkManager.Current.SetTenantId(null))
    {
        // Getting host data *** WORKS
        
        uow.Complete();
    }
}

Attempt #2 Forcing new transaction scope

private async Task DoCustomStuffAsync(CreateOrUpdateUserInput input, User user)
{
    if(user.CustomProp == null)
        return;
    
    using(var uow = _unitOfWorkManager.Begin(RequiresNew))
    using (_unitOfWorkManager.Current.SetTenantId(null))
    {
        // Getting host data *** WORKS
        
        uow.Complete();
    }
}

Attempt #3 Manual dispose of uow scope

private async Task DoCustomStuffAsync(CreateOrUpdateUserInput input, User user)
{
    if(user.CustomProp == null)
        return;
    
    using(_unitOfWorkManager.Begin())
    using (var uow = _unitOfWorkManager.Current.SetTenantId(null))
    {
        // Getting host data *** WORKS
        
        uow.Dispose(); //Should not be nessesary
    }
}

Attempt #4 Marking methods (and class) with UOW

[UnitOfWork]
public async Task UpdateUserAsync(....)

[UnitOfWork]
private async Task DoCustomStuffAsync(CreateOrUpdateUserInput input, User user)
{
    if(user.CustomProp == null)
        return;
    
    using (var uow = _unitOfWorkManager.Current.SetTenantId(null))
    {
        // Getting host data *** WORKS
    }
}

What am I doing wrong here? I can obviously set the tenantID again in the UpdateUserAsync method but that is not a good way IMO.

Best regards


4 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @SorenRomer

    Yes, this shouldn't work like that. Could you share your main method which calls DoCustomStuffAsync as well ? We can try to reproduce this on our side.

    Thanks,

  • User Avatar
    0
    SorenRomer created

    Hi @ismcagdas, Sure thing, I kept the DoCustomStuffAsync naming:

    [UnitOfWork]
    [AbpAuthorize(AppPermissions.Pages_Administration_Users_Edit)]
    protected virtual async Task UpdateUserAsync(CreateOrUpdateUserInput input)
    {
        Debug.Assert(input.User.Id != null, "input.User.Id should be set.");
    
        var user = await UserManager.GetUserByIdAsync(input.User.Id.Value);
    
        //Update user properties
        ObjectMapper.Map(input.User, user); //Passwords is not mapped (see mapping configuration)
    
        // CUSTOM CODE 
        // _unitOfWorkManager.Current.GetTenantId() is **1** at this point.
        if (input.AssignedCourseId != null && AbpSession.MultiTenancySide != Abp.MultiTenancy.MultiTenancySides.Host)
        {
            await DoCustomStuffAsync(input, user);
        }
         // _unitOfWorkManager.Current.GetTenantId() is **null** at this point.
    
        CheckErrors(await UserManager.UpdateAsync(user));
    
        if (input.SetRandomPassword)
        {
            var randomPassword = await _userManager.CreateRandomPassword();
            user.Password = _passwordHasher.HashPassword(user, randomPassword);
            input.User.Password = randomPassword;
        }
        else if (!input.User.Password.IsNullOrEmpty())
        {
            await UserManager.InitializeOptionsAsync(AbpSession.TenantId);
            CheckErrors(await UserManager.ChangePasswordAsync(user, input.User.Password));
        }
    
        //Update roles
        CheckErrors(await UserManager.SetRolesAsync(user, input.AssignedRoleNames));
    
        //update organization units
        await UserManager.SetOrganizationUnitsAsync(user, input.OrganizationUnits.ToArray());
    
        if (input.SendActivationEmail)
        {
            user.SetNewEmailConfirmationCode();
            await _userEmailer.SendEmailActivationLinkAsync(
                user,
                AppUrlService.CreateEmailActivationUrlFormat(AbpSession.TenantId),
                input.User.Password
            );
        }
    }
    

    Here is a simplified version of the custom method:

    [UnitOfWork]
    private async Task DoCustomStuffAsync(CreateOrUpdateUserInput input, User user)
    {
        if (input.CustomEnityId != null)
        {
            using (_unitOfWorkManager.Current.SetTenantId(null))
            {
            // GETTING GENERAL DATA FROM HOST
                var _customEntities = _customEntityRepository.GetAllIncluding(c => c.CourseFlows).FirstOrDefault(c => c.Id == (int)input.AssignedCourseId);
    
            // CODE REMOVED FOR CLARITY
            }
    
            EventBus.Trigger(new CustomEvent { Id = user.Id});
        }
        else
        {
            Logger.Info($"User {user.UserName} is not assigned to a custom entity");
        }
    }
    

    Thanks in advance

  • User Avatar
    0
    SorenRomer created

    I found issue - sort of.... If I avoid calling EventBus.Trigger(new CustomEvent { Id = user.Id}); the tenantId is reset to the correct value after the using is disposed. Calling await EventBus.TriggerAsync(new CustomEvent { Id = user.Id}); results in the same behaviour.

    I don't understand this behaviour of the EventBus? Why does it stick to tenantid null when it is being called after the using is disposed?

    Wrapping the call to the eventbus inside its own using solves the problem:

    using (_unitOfWorkManager.Current.SetTenantId(AbpSession.GetTenantId()))
    {
        await EventBus.TriggerAsync(new CustomEvent { Id = user.Id});
    }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    @SorenRomer

    I think it is becasue you are mixing async and sync code. If using EventBus.TriggerAsync works, I suggest you to use it.