Files
Musify/Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs
T
2026-05-14 01:00:44 +02:00

199 lines
8.3 KiB
C#

using Ardalis.Result;
using Mediator;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Configuration;
using Musify.Application.Contracts.Application;
using Musify.Application.Contracts.Infrastructure;
using Musify.Application.Events;
using Musify.Application.PlayLists.Commands;
using Musify.Application.PlayLists.Responses;
using Musify.Domain.Entities;
namespace Musify.Application.PlayLists.Handlers
{
public class UpdatePlayListCommandHandler(IEventBus eventBus, IDatabase database, IStorageService storageHandler,
ILogger<UpdatePlayListCommandHandler> logger, ApplicationStorageConfiguration storageConfiguration,
PlayListConfiguration playListConfiguration)
: ICommandHandler<UpdatePlayListCommand, Result<PlayListResponse>>
{
record UpdatePictureOutcome(Result Result, bool PictureUpdated, string? UploadedPictureKey);
record PreviousPlayListState(string Name, string NormalizedName, string Description, string OriginalPictureName);
public async ValueTask<Result<PlayListResponse>> Handle(UpdatePlayListCommand request, CancellationToken cancellationToken)
{
var playListEntity = await database.PlayLists.SingleOrDefaultAsync(pl => pl.Id == request.PlayListId, cancellationToken);
if (playListEntity is null)
{
logger.LogInformation("Playlist {PlayListId} not found", request.PlayListId);
return Result.NotFound();
}
if (playListEntity.UserId != request.UserId)
{
logger.LogWarning("User {UserId} is not the owner of playlist {PlayListId}", request.UserId, request.PlayListId);
return Result.Unauthorized();
}
var previousState = CapturePreviousState(playListEntity);
UpdateName(playListEntity, request.NewName);
UpdateDescription(playListEntity, request.NewDescription);
var pictureOutcome = await UpdatePicture(playListEntity, request.NewPicture, cancellationToken);
if (!pictureOutcome.Result.IsSuccess)
return pictureOutcome.Result;
try
{
database.PlayLists.Update(playListEntity);
await database.SaveChangesAsync(cancellationToken);
}
catch (Exception exception)
{
if (pictureOutcome.PictureUpdated && pictureOutcome.UploadedPictureKey is not null)
await Rollback(pictureOutcome.UploadedPictureKey, cancellationToken);
logger.LogError(exception, "Failed to update playlist {PlayListId}", playListEntity.Id);
return Result.Error($"Failed to update playlist {playListEntity.Id}");
}
if (pictureOutcome.PictureUpdated)
{
var publishResult = await PublishPlayListPictureEvent(playListEntity, cancellationToken);
if (!publishResult.IsSuccess)
{
await TryRollbackAfterPublishFailureAsync(
playListEntity,
previousState,
pictureOutcome.UploadedPictureKey,
cancellationToken);
return Result.Error($"Failed to update playlist {playListEntity.Id}");
}
}
logger.LogInformation("Updated playlist {PlayListId}", playListEntity.Id);
return Result.Success(PlayListResponse.FromEntity(playListEntity));
}
void UpdateName(PlayList playList, string? newName)
{
if (newName is null)
return;
playList.Name = newName;
playList.NormalizedName = newName.Trim().ToUpperInvariant();
}
void UpdateDescription(PlayList playList, string? newDescription)
{
if (newDescription is null)
return;
playList.Description = newDescription;
}
static PreviousPlayListState CapturePreviousState(PlayList playList)
=> new(playList.Name, playList.NormalizedName, playList.Description, playList.OriginalPictureName);
async Task<UpdatePictureOutcome> UpdatePicture(
PlayList playList,
IFileData? newPicture,
CancellationToken cancellationToken)
{
if (newPicture is null)
return new UpdatePictureOutcome(Result.Success(), false, null);
var pictureName = Guid.NewGuid() + newPicture.FileType;
var pictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(pictureName);
try
{
await storageHandler.UploadFileAsync(
newPicture.FileStream,
newPicture.ContentType,
storageConfiguration.Bucket,
pictureKey,
cancellationToken);
}
catch
{
return new UpdatePictureOutcome(Result.Error("Failed to upload picture to storage"), false, null);
}
playList.OriginalPictureName = pictureName;
return new UpdatePictureOutcome(Result.Success(), true, pictureKey);
}
async Task<Result> PublishPlayListPictureEvent(PlayList playList, CancellationToken cancellationToken)
{
try
{
await eventBus.PublishAsync(new UpdatePlayListPictureEvent(
playList.Id,
storageConfiguration.Bucket,
playListConfiguration.Routes.BuildOriginalPicturePath(playList.OriginalPictureName),
new ImageSize(
playListConfiguration.Routes.SmallPicturesPath,
playListConfiguration.PicturesSizes.SmallPictureWidth,
playListConfiguration.PicturesSizes.SmallPictureHeight),
new ImageSize(
playListConfiguration.Routes.MediumPicturesPath,
playListConfiguration.PicturesSizes.MediumPictureWidth,
playListConfiguration.PicturesSizes.MediumPictureHeight),
new ImageSize(
playListConfiguration.Routes.LargePicturesPath,
playListConfiguration.PicturesSizes.LargePictureWidth,
playListConfiguration.PicturesSizes.LargePictureHeight)),
cancellationToken);
return Result.Success();
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to publish playlist picture update event for playlist {PlayListId}", playList.Id);
return Result.Error($"Failed to publish playlist {playList.Id} picture update event");
}
}
async Task Rollback(string pictureKey, CancellationToken cancellationToken)
{
await storageHandler.RemoveFileAsync(
storageConfiguration.Bucket,
pictureKey,
cancellationToken);
logger.LogInformation("Rolled back uploaded picture {PictureKey} from storage", pictureKey);
}
async Task TryRollbackAfterPublishFailureAsync(
PlayList playList,
PreviousPlayListState previousState,
string? uploadedPictureKey,
CancellationToken cancellationToken)
{
playList.Name = previousState.Name;
playList.NormalizedName = previousState.NormalizedName;
playList.Description = previousState.Description;
playList.OriginalPictureName = previousState.OriginalPictureName;
try
{
await database.SaveChangesAsync(cancellationToken);
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to rollback playlist state after publish failure for playlist {PlayListId}", playList.Id);
}
if (uploadedPictureKey is null)
return;
try
{
await Rollback(uploadedPictureKey, cancellationToken);
}
catch (Exception exception)
{
logger.LogError(exception, "Failed to rollback uploaded picture after publish failure for playlist {PlayListId}", playList.Id);
}
}
}
}