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.