Home > Net >  ASP.NET Core Web API - How to resolve The current TransactionScope is already complete
ASP.NET Core Web API - How to resolve The current TransactionScope is already complete

Time:01-12

In my ASP.NET Core-6 Web API Entity Framework Core, I have this code for user Registration:

public async Task<Response<string>> RegisterUserAsync(string userId)
{
    var response = new Response<string>();
    using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        try
        {
            var userDetail = _dataAccess.GetSaffUserProfile(userId);
            adminAccess = _config.GetSection("DDMAdminCredentials").GetValue<string>("adminaccess");
            if (string.IsNullOrEmpty(userDetail.user_logon_name))
            {
                _logger.Error("Failed Authentication");
                transaction.Dispose();
                response.Successful = false;
                response.StatusCode = (int)HttpStatusCode.BadRequest;
                response.Message = "Staff Record Not Found!";
                return response;
            }
            var user = new ApplicationUser()
            {
                UserName = userDetail.user_logon_name.ToLower().Trim(),
                Email = string.IsNullOrWhiteSpace(userDetail.email) ? null : userDetail.email.Trim().ToLower(),
                FirstName = string.IsNullOrWhiteSpace(userDetail.firstname) ? null : userDetail.firstname.Trim(),
                LastName = string.IsNullOrWhiteSpace(userDetail.lastname) ? null : userDetail.lastname.Trim(),
                MobileNumber = string.IsNullOrWhiteSpace(userDetail.mobile_phone) ? null : userDetail.mobile_phone.Trim()
                CreatedAt = DateTime.Now
            };
            var result = await _userManager.CreateAsync(user, adminAccess);
            if (result.Succeeded)
            {
                // Insert User Role
                await _userManager.AddToRoleAsync(user, userRole);
                userBranchNo = userDetail.branch_code.TrimStart('0');
                var bankBranchId = _dbContext.BankBranches.Where(u => u.BranchNumber.ToString() == userBranchNo.ToString()).Select(m => m.Id).FirstOrDefault();
                var bankUser = new BankUser()
                {
                    UserId = user.Id,
                    BankBranchId = bankBranchId,
                    CreatedBy = "System"
                };
                await _unitOfWork.AdminUsers.InsertAsync(bankUser);
                await _unitOfWork.Save();

                response.StatusCode = (int)HttpStatusCode.Created;
                response.Successful = true;
                response.Data = user.Id;
                response.Message = "Bank User Created Successfully!";
                transaction.Complete();
                await _signInManager.SignInAsync(user, isPersistent: false);// user, true, false
                return response;
            }
            response.Message = GetErrors(result);
            response.StatusCode = (int)HttpStatusCode.BadRequest;
            response.Successful = false;
            transaction.Dispose();
            return response;
        }
        catch (Exception ex)
        {
            response.Message = "An error occured :"   ex.Message;
            response.Successful = false;
            response.StatusCode = (int)HttpStatusCode.BadRequest;
            return response;
        }
    };
}

The application should immediately log in as soon as the user is created.

However, I got this error in the log:

An Error occured The current TransactionScope is already complete.

How do I get this resolved?

Thanks

CodePudding user response:

You complete the transaction scope and then SignInManager tries to use it. You should complete the scope after signing user in:

await _signInManager.SignInAsync(user, isPersistent: false);
transaction.Complete();

What is also very important is the maintainability of your code - it's a single method, long and imperative. Split it to improve readability. You can have small top-level method which just gets required for registration data and passes it further:

public async Task<Response<string>> RegisterUserAsync(string userId)
{
    try
    {
        var userProfile = GetStaffUserProfile(userId);
        if (userProfile == null)
            return BadRequestResponse("Staff Record Not Found!");

        var user = _mapper.Map<ApplicationUser>(userProfile);
        return await RegisterUserAsync(user, userProfile.GetBranchNo());
    }
    catch(Exception ex)
    {
        return BadRequestResponse(ex);
    }
}

A few things were extracted here. Getting user profile:

private UserDetail? GetStaffUserProfile(string userId)
{
    var userDetail = _dataAccess.GetSaffUserProfile(userId);
    if (!userDetail.IsAuthenticated())
    {
        _logger.Error("Failed Authentication");
        return null;
    }

    return userDetail;
}

Mapping exceptions, strings, and IdentityResults to responses (Note that you should not wrap all exceptions into a bad request response, because not all exceptions will be caused by bad requests from clients. There could be also internal server errors):

private Response<string> BadRequestResponse(IdentityResult result) =>
    BadRequestResponse(GetErrors(result)); // create this on your own

private Response<string> BadRequestResponse(Exception ex) =>
    new() {
        Message = "An error occured :"   ex.Message,
        Successful = false,
        StatusCode = (int)HttpStatusCode.BadRequest
    };

Declarative access to user details data:

public static class UserDetailExtensions
{
    public static bool IsAuthenticated(UserDetail userDetail) =>
       !String.IsNullOrEmpty(userDetail.user_logon_name);
    public static string GetBranchNo(this UserDetail userDetail) =>
       userDetail.branch_code.TrimStart('0');
}

Mapping user details to ApplicationUser is done by something like AutoMapper but you can extract method for manual mapping. And now you have a relatively small method for registering user:

private async Task<Response<string>> RegisterUserAsync(
   ApplicationUser user, string userBranchNo)
{
    using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
        var result = await _userManager.CreateAsync(user, adminAccessPassword);
        if (!result.Succeeded)
            return BadRequestResponse(result);

        await _userManager.AddToRoleAsync(user, userRole);
        await CreateBankUserAsync(user.Id, userBranchNo);
        await _signInManager.SignInAsync(user, isPersistent: false);
        transaction.Complete();
    }

    return new() {
        StatusCode = (int)HttpStatusCode.Created,
        Successful = true,
        Data = user.Id,
        Message = "Bank User Created Successfully!"
    };
}

With a small helper method:

private async Task CreateBankUserAsync(string userId, string userBranchNo)
{
    var bankBranchId = _dbContext.BankBranches
        .Where(b => b.BranchNumber.ToString() == userBranchNo)
        .Select(b => b.Id)
        .FirstOrDefault();

    var bankUser = new BankUser() {
        UserId = userId,
        BankBranchId = bankBranchId,
        CreatedBy = "System"
    };

    await _unitOfWork.AdminUsers.InsertAsync(bankUser);
    await _unitOfWork.Save();
}

Config reading of adminAccessPassword should be moved to the application startup logic.

  • Related