Transaction Support (#309)

* Added transactions to UnitOfWork and refactored code to use it.

* This included blank UI fix from Kavita-webui
This commit is contained in:
Joseph Milazzo 2021-06-18 07:37:48 -05:00 committed by GitHub
parent d2e444910d
commit 6e1b227e65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 168 additions and 124 deletions

View File

@ -83,42 +83,55 @@ namespace API.Controllers
[HttpPost("register")] [HttpPost("register")]
public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto) public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
{ {
if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == registerDto.Username.ToUpper())) try
{ {
return BadRequest("Username is taken."); if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == registerDto.Username.ToUpper()))
}
var user = _mapper.Map<AppUser>(registerDto);
user.UserPreferences ??= new AppUserPreferences();
var result = await _userManager.CreateAsync(user, registerDto.Password);
if (!result.Succeeded) return BadRequest(result.Errors);
var role = registerDto.IsAdmin ? PolicyConstants.AdminRole : PolicyConstants.PlebRole;
var roleResult = await _userManager.AddToRoleAsync(user, role);
if (!roleResult.Succeeded) return BadRequest(result.Errors);
// When we register an admin, we need to grant them access to all Libraries.
if (registerDto.IsAdmin)
{
_logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries", user.UserName);
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
foreach (var lib in libraries)
{ {
lib.AppUsers ??= new List<AppUser>(); return BadRequest("Username is taken.");
lib.AppUsers.Add(user);
} }
if (libraries.Any() && !await _unitOfWork.Complete()) _logger.LogError("There was an issue granting library access. Please do this manually");
var user = _mapper.Map<AppUser>(registerDto);
user.UserPreferences ??= new AppUserPreferences();
var result = await _userManager.CreateAsync(user, registerDto.Password);
if (!result.Succeeded) return BadRequest(result.Errors);
var role = registerDto.IsAdmin ? PolicyConstants.AdminRole : PolicyConstants.PlebRole;
var roleResult = await _userManager.AddToRoleAsync(user, role);
if (!roleResult.Succeeded) return BadRequest(result.Errors);
// When we register an admin, we need to grant them access to all Libraries.
if (registerDto.IsAdmin)
{
_logger.LogInformation("{UserName} is being registered as admin. Granting access to all libraries",
user.UserName);
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).ToList();
foreach (var lib in libraries)
{
lib.AppUsers ??= new List<AppUser>();
lib.AppUsers.Add(user);
}
if (libraries.Any() && !await _unitOfWork.CommitAsync())
_logger.LogError("There was an issue granting library access. Please do this manually");
}
return new UserDto
{
Username = user.UserName,
Token = await _tokenService.CreateToken(user),
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Something went wrong when registering user");
await _unitOfWork.RollbackAsync();
} }
return new UserDto return BadRequest("Something went wrong when registering user");
{
Username = user.UserName,
Token = await _tokenService.CreateToken(user),
Preferences = _mapper.Map<UserPreferencesDto>(user.UserPreferences)
};
} }
[HttpPost("login")] [HttpPost("login")]
@ -140,7 +153,7 @@ namespace API.Controllers
user.UserPreferences ??= new AppUserPreferences(); user.UserPreferences ??= new AppUserPreferences();
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
await _unitOfWork.Complete(); await _unitOfWork.CommitAsync();
_logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive); _logger.LogInformation("{UserName} logged in at {Time}", user.UserName, user.LastActive);
@ -167,7 +180,6 @@ namespace API.Controllers
{ {
var user = await _userManager.Users var user = await _userManager.Users
.Include(u => u.UserPreferences) .Include(u => u.UserPreferences)
//.Include(u => u.UserRoles)
.SingleOrDefaultAsync(x => x.NormalizedUserName == updateRbsDto.Username.ToUpper()); .SingleOrDefaultAsync(x => x.NormalizedUserName == updateRbsDto.Username.ToUpper());
if (updateRbsDto.Roles.Contains(PolicyConstants.AdminRole) || if (updateRbsDto.Roles.Contains(PolicyConstants.AdminRole) ||
updateRbsDto.Roles.Contains(PolicyConstants.PlebRole)) updateRbsDto.Roles.Contains(PolicyConstants.PlebRole))
@ -178,16 +190,22 @@ namespace API.Controllers
var existingRoles = (await _userManager.GetRolesAsync(user)) var existingRoles = (await _userManager.GetRolesAsync(user))
.Where(s => s != PolicyConstants.AdminRole && s != PolicyConstants.PlebRole) .Where(s => s != PolicyConstants.AdminRole && s != PolicyConstants.PlebRole)
.ToList(); .ToList();
// Find what needs to be added and what needs to be removed // Find what needs to be added and what needs to be removed
var rolesToRemove = existingRoles.Except(updateRbsDto.Roles); var rolesToRemove = existingRoles.Except(updateRbsDto.Roles);
var result = await _userManager.AddToRolesAsync(user, updateRbsDto.Roles); var result = await _userManager.AddToRolesAsync(user, updateRbsDto.Roles);
if (!result.Succeeded) return BadRequest("Something went wrong, unable to update user's roles"); if (!result.Succeeded)
{
await _unitOfWork.RollbackAsync();
return BadRequest("Something went wrong, unable to update user's roles");
}
if ((await _userManager.RemoveFromRolesAsync(user, rolesToRemove)).Succeeded) if ((await _userManager.RemoveFromRolesAsync(user, rolesToRemove)).Succeeded)
{ {
return Ok(); return Ok();
} }
await _unitOfWork.RollbackAsync();
return BadRequest("Something went wrong, unable to update user's roles"); return BadRequest("Something went wrong, unable to update user's roles");
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Constants; using API.Constants;
@ -33,11 +34,7 @@ namespace API.Controllers
{ {
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync(); return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
} }
else return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
{
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
}
} }
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
@ -64,7 +61,7 @@ namespace API.Controllers
if (_unitOfWork.HasChanges()) if (_unitOfWork.HasChanges())
{ {
if (await _unitOfWork.Complete()) if (await _unitOfWork.CommitAsync())
{ {
return Ok("Tag updated successfully"); return Ok("Tag updated successfully");
} }
@ -81,38 +78,42 @@ namespace API.Controllers
[HttpPost("update-series")] [HttpPost("update-series")]
public async Task<ActionResult> UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto) public async Task<ActionResult> UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto)
{ {
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(updateSeriesForTagDto.Tag.Id); try
if (tag == null) return BadRequest("Not a valid Tag");
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
// Check if Tag has updated (Summary)
if (tag.Summary == null || !tag.Summary.Equals(updateSeriesForTagDto.Tag.Summary))
{ {
tag.Summary = updateSeriesForTagDto.Tag.Summary; var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(updateSeriesForTagDto.Tag.Id);
_unitOfWork.CollectionTagRepository.Update(tag); if (tag == null) return BadRequest("Not a valid Tag");
} tag.SeriesMetadatas ??= new List<SeriesMetadata>();
foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove) // Check if Tag has updated (Summary)
{ if (tag.Summary == null || !tag.Summary.Equals(updateSeriesForTagDto.Tag.Summary))
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove)); {
} tag.Summary = updateSeriesForTagDto.Tag.Summary;
_unitOfWork.CollectionTagRepository.Update(tag);
}
if (tag.SeriesMetadatas.Count == 0) foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove)
{ {
_unitOfWork.CollectionTagRepository.Remove(tag); tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
} }
if (_unitOfWork.HasChanges() && await _unitOfWork.Complete())
if (tag.SeriesMetadatas.Count == 0)
{
_unitOfWork.CollectionTagRepository.Remove(tag);
}
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
{
return Ok("Tag updated");
}
}
catch (Exception)
{ {
return Ok("Tag updated"); await _unitOfWork.RollbackAsync();
} }
return BadRequest("Something went wrong. Please try again."); return BadRequest("Something went wrong. Please try again.");
} }
} }
} }

View File

@ -67,7 +67,7 @@ namespace API.Controllers
} }
if (!await _unitOfWork.Complete()) return BadRequest("There was a critical issue. Please try again."); if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue. Please try again.");
_logger.LogInformation("Created a new library: {LibraryName}", library.Name); _logger.LogInformation("Created a new library: {LibraryName}", library.Name);
_taskScheduler.ScanLibrary(library.Id); _taskScheduler.ScanLibrary(library.Id);
@ -133,7 +133,7 @@ namespace API.Controllers
return Ok(_mapper.Map<MemberDto>(user)); return Ok(_mapper.Map<MemberDto>(user));
} }
if (await _unitOfWork.Complete()) if (await _unitOfWork.CommitAsync())
{ {
_logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username); _logger.LogInformation("Added: {SelectedLibraries} to {Username}",libraryString, updateLibraryForUserDto.Username);
return Ok(_mapper.Map<MemberDto>(user)); return Ok(_mapper.Map<MemberDto>(user));
@ -199,7 +199,7 @@ namespace API.Controllers
_unitOfWork.LibraryRepository.Update(library); _unitOfWork.LibraryRepository.Update(library);
if (!await _unitOfWork.Complete()) return BadRequest("There was a critical issue updating the library."); if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical issue updating the library.");
if (differenceBetweenFolders.Any()) if (differenceBetweenFolders.Any())
{ {
_taskScheduler.ScanLibrary(library.Id, true); _taskScheduler.ScanLibrary(library.Id, true);

View File

@ -116,7 +116,7 @@ namespace API.Controllers
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
if (await _unitOfWork.Complete()) if (await _unitOfWork.CommitAsync())
{ {
return Ok(); return Ok();
} }
@ -157,7 +157,7 @@ namespace API.Controllers
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
if (await _unitOfWork.Complete()) if (await _unitOfWork.CommitAsync())
{ {
return Ok(); return Ok();
} }
@ -198,7 +198,7 @@ namespace API.Controllers
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
if (await _unitOfWork.Complete()) if (await _unitOfWork.CommitAsync())
{ {
return Ok(); return Ok();
} }
@ -251,7 +251,7 @@ namespace API.Controllers
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
if (await _unitOfWork.Complete()) if (await _unitOfWork.CommitAsync())
{ {
return Ok(); return Ok();
} }

View File

@ -114,7 +114,7 @@ namespace API.Controllers
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
if (!await _unitOfWork.Complete()) return BadRequest("There was a critical error."); if (!await _unitOfWork.CommitAsync()) return BadRequest("There was a critical error.");
return Ok(); return Ok();
} }
@ -139,7 +139,7 @@ namespace API.Controllers
_unitOfWork.SeriesRepository.Update(series); _unitOfWork.SeriesRepository.Update(series);
if (await _unitOfWork.Complete()) if (await _unitOfWork.CommitAsync())
{ {
return Ok(); return Ok();
} }
@ -190,61 +190,68 @@ namespace API.Controllers
[HttpPost("metadata")] [HttpPost("metadata")]
public async Task<ActionResult> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto) public async Task<ActionResult> UpdateSeriesMetadata(UpdateSeriesMetadataDto updateSeriesMetadataDto)
{ {
var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId; try
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
if (series.Metadata == null)
{ {
series.Metadata = DbFactory.SeriesMetadata(updateSeriesMetadataDto.Tags var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId;
.Select(dto => DbFactory.CollectionTag(dto.Id, dto.Title, dto.Summary, dto.Promoted)).ToList()); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
} if (series.Metadata == null)
else
{
series.Metadata.CollectionTags ??= new List<CollectionTag>();
var newTags = new List<CollectionTag>();
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
var existingTags = series.Metadata.CollectionTags.ToList();
foreach (var existing in existingTags)
{ {
if (updateSeriesMetadataDto.Tags.SingleOrDefault(t => t.Id == existing.Id) == null) series.Metadata = DbFactory.SeriesMetadata(updateSeriesMetadataDto.Tags
.Select(dto => DbFactory.CollectionTag(dto.Id, dto.Title, dto.Summary, dto.Promoted)).ToList());
}
else
{
series.Metadata.CollectionTags ??= new List<CollectionTag>();
var newTags = new List<CollectionTag>();
// I want a union of these 2 lists. Return only elements that are in both lists, but the list types are different
var existingTags = series.Metadata.CollectionTags.ToList();
foreach (var existing in existingTags)
{ {
// Remove tag if (updateSeriesMetadataDto.Tags.SingleOrDefault(t => t.Id == existing.Id) == null)
series.Metadata.CollectionTags.Remove(existing); {
// Remove tag
series.Metadata.CollectionTags.Remove(existing);
}
}
// At this point, all tags that aren't in dto have been removed.
foreach (var tag in updateSeriesMetadataDto.Tags)
{
var existingTag = series.Metadata.CollectionTags.SingleOrDefault(t => t.Title == tag.Title);
if (existingTag != null)
{
// Update existingTag
existingTag.Promoted = tag.Promoted;
existingTag.Title = tag.Title;
existingTag.NormalizedTitle = Parser.Parser.Normalize(tag.Title).ToUpper();
}
else
{
// Add new tag
newTags.Add(DbFactory.CollectionTag(tag.Id, tag.Title, tag.Summary, tag.Promoted));
}
}
foreach (var tag in newTags)
{
series.Metadata.CollectionTags.Add(tag);
} }
} }
// At this point, all tags that aren't in dto have been removed. if (!_unitOfWork.HasChanges())
foreach (var tag in updateSeriesMetadataDto.Tags)
{ {
var existingTag = series.Metadata.CollectionTags.SingleOrDefault(t => t.Title == tag.Title); return Ok("No changes to save");
if (existingTag != null)
{
// Update existingTag
existingTag.Promoted = tag.Promoted;
existingTag.Title = tag.Title;
existingTag.NormalizedTitle = Parser.Parser.Normalize(tag.Title).ToUpper();
}
else
{
// Add new tag
newTags.Add(DbFactory.CollectionTag(tag.Id, tag.Title, tag.Summary, tag.Promoted));
}
} }
foreach (var tag in newTags) if (await _unitOfWork.CommitAsync())
{ {
series.Metadata.CollectionTags.Add(tag); return Ok("Successfully updated");
} }
} }
catch (Exception)
if (!_unitOfWork.HasChanges())
{ {
return Ok("No changes to save"); await _unitOfWork.RollbackAsync();
}
if (await _unitOfWork.Complete())
{
return Ok("Successfully updated");
} }
return BadRequest("Could not update metadata"); return BadRequest("Could not update metadata");

View File

@ -95,7 +95,7 @@ namespace API.Controllers
_configuration.GetSection("Logging:LogLevel:Default").Value = updateSettingsDto.LoggingLevel + ""; _configuration.GetSection("Logging:LogLevel:Default").Value = updateSettingsDto.LoggingLevel + "";
if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated"); if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated");
if (!_unitOfWork.HasChanges() || !await _unitOfWork.Complete()) if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync())
return BadRequest("There was a critical issue. Please try again."); return BadRequest("There was a critical issue. Please try again.");
_logger.LogInformation("Server Settings updated"); _logger.LogInformation("Server Settings updated");

View File

@ -26,7 +26,7 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
_unitOfWork.UserRepository.Delete(user); _unitOfWork.UserRepository.Delete(user);
if (await _unitOfWork.Complete()) return Ok(); if (await _unitOfWork.CommitAsync()) return Ok();
return BadRequest("Could not delete the user."); return BadRequest("Could not delete the user.");
} }
@ -71,7 +71,7 @@ namespace API.Controllers
_unitOfWork.UserRepository.Update(existingPreferences); _unitOfWork.UserRepository.Update(existingPreferences);
if (await _unitOfWork.Complete()) if (await _unitOfWork.CommitAsync())
{ {
return Ok(preferencesDto); return Ok(preferencesDto);
} }

View File

@ -30,7 +30,11 @@ namespace API.Data
public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context); public IAppUserProgressRepository AppUserProgressRepository => new AppUserProgressRepository(_context);
public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper); public ICollectionTagRepository CollectionTagRepository => new CollectionTagRepository(_context, _mapper);
public async Task<bool> Complete() public bool Commit()
{
return _context.SaveChanges() > 0;
}
public async Task<bool> CommitAsync()
{ {
return await _context.SaveChangesAsync() > 0; return await _context.SaveChangesAsync() > 0;
} }
@ -39,5 +43,16 @@ namespace API.Data
{ {
return _context.ChangeTracker.HasChanges(); return _context.ChangeTracker.HasChanges();
} }
public async Task<bool> RollbackAsync()
{
await _context.DisposeAsync();
return true;
}
public bool Rollback()
{
_context.Dispose();
return true;
}
} }
} }

View File

@ -11,7 +11,10 @@ namespace API.Interfaces
ISettingsRepository SettingsRepository { get; } ISettingsRepository SettingsRepository { get; }
IAppUserProgressRepository AppUserProgressRepository { get; } IAppUserProgressRepository AppUserProgressRepository { get; }
ICollectionTagRepository CollectionTagRepository { get; } ICollectionTagRepository CollectionTagRepository { get; }
Task<bool> Complete(); bool Commit();
Task<bool> CommitAsync();
bool HasChanges(); bool HasChanges();
bool Rollback();
Task<bool> RollbackAsync();
} }
} }

View File

@ -158,7 +158,7 @@ namespace API.Services
} }
if (_unitOfWork.HasChanges() && Task.Run(() => _unitOfWork.Complete()).Result) if (_unitOfWork.HasChanges() && Task.Run(() => _unitOfWork.CommitAsync()).Result)
{ {
_logger.LogInformation("Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds); _logger.LogInformation("Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds);
} }
@ -191,7 +191,7 @@ namespace API.Services
_unitOfWork.SeriesRepository.Update(series); _unitOfWork.SeriesRepository.Update(series);
if (_unitOfWork.HasChanges() && Task.Run(() => _unitOfWork.Complete()).Result) if (_unitOfWork.HasChanges() && Task.Run(() => _unitOfWork.CommitAsync()).Result)
{ {
_logger.LogInformation("Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); _logger.LogInformation("Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds);
} }

View File

@ -89,7 +89,7 @@ namespace API.Services.Tasks
UpdateLibrary(library, series); UpdateLibrary(library, series);
_unitOfWork.LibraryRepository.Update(library); _unitOfWork.LibraryRepository.Update(library);
if (Task.Run(() => _unitOfWork.Complete()).Result) if (Task.Run(() => _unitOfWork.CommitAsync()).Result)
{ {
_logger.LogInformation("Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}", totalFiles, series.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, library.Name); _logger.LogInformation("Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}", totalFiles, series.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, library.Name);
} }