Change editor rule

This commit is contained in:
2026-09-24 13:24:43 +02:00
parent 16478678b5
commit 73e37dbecd
401 changed files with 14735 additions and 12995 deletions
@@ -1,62 +1,63 @@
namespace Musify.Infrastructure.MassTransit.RoutingSlip;
internal static class ActivityNames
namespace Musify.Infrastructure.MassTransit.RoutingSlip
{
internal const string MarkTrackAsRemoving = "MarkTrackAsRemoving";
internal const string MarkTrackAsFailed = "MarkTrackAsFailed";
internal const string DeleteTrackFromDb = "DeleteTrackFromDb";
internal const string MarkPlayListAsRemoving = "MarkPlayListAsRemoving";
internal const string MarkPlayListAsFailed = "MarkPlayListAsFailed";
internal const string DeletePlayListFromDb = "DeletePlayListFromDb";
internal const string MarkAlbumAsFailed = "MarkAlbumAsFailed";
internal const string MarkAlbumAsRemoving = "MarkAlbumAsRemoving";
internal const string DeleteAlbumFromDb = "DeleteAlbumFromDb";
internal static class ActivityNames
{
internal const string MarkTrackAsRemoving = "MarkTrackAsRemoving";
internal const string MarkTrackAsFailed = "MarkTrackAsFailed";
internal const string DeleteTrackFromDb = "DeleteTrackFromDb";
internal const string MarkPlayListAsRemoving = "MarkPlayListAsRemoving";
internal const string MarkPlayListAsFailed = "MarkPlayListAsFailed";
internal const string DeletePlayListFromDb = "DeletePlayListFromDb";
internal const string MarkAlbumAsFailed = "MarkAlbumAsFailed";
internal const string MarkAlbumAsRemoving = "MarkAlbumAsRemoving";
internal const string DeleteAlbumFromDb = "DeleteAlbumFromDb";
internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths";
internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths";
internal const string GenerateAudioWorkflowPaths = "GenerateAudioWorkflowPaths";
internal const string GeneratePictureWorkflowPaths = "GeneratePictureWorkflowPaths";
internal const string DownloadFile = "DownloadFile";
internal const string DownloadFile = "DownloadFile";
internal const string TranscodeAudio = "TranscodeAudio";
internal const string TranscodeAudio = "TranscodeAudio";
internal const string ResizeSmall = "ResizeSmall";
internal const string ResizeMedium = "ResizeMedium";
internal const string ResizeLarge = "ResizeLarge";
internal const string ResizeSmall = "ResizeSmall";
internal const string ResizeMedium = "ResizeMedium";
internal const string ResizeLarge = "ResizeLarge";
internal const string UploadSmall = "UploadSmall";
internal const string UploadMedium = "UploadMedium";
internal const string UploadLarge = "UploadLarge";
internal const string UploadSmall = "UploadSmall";
internal const string UploadMedium = "UploadMedium";
internal const string UploadLarge = "UploadLarge";
internal const string TransferFiles = "TransferFiles";
internal const string TransferFiles = "TransferFiles";
internal const string UpdateTrackPicture = "UpdateTrackPicture";
internal const string UpdatePlayListPicture = "UpdatePlayListPicture";
internal const string UpdateAlbumPicture = "UpdateAlbumPicture";
internal const string UpdateTrackAudio = "UpdateTrackAudio";
internal const string UpdateTrackPicture = "UpdateTrackPicture";
internal const string UpdatePlayListPicture = "UpdatePlayListPicture";
internal const string UpdateAlbumPicture = "UpdateAlbumPicture";
internal const string UpdateTrackAudio = "UpdateTrackAudio";
internal const string CopyPictureToFinal = "CopyPictureToFinal";
internal const string CopyAudioToFinal = "CopyAudioToFinal";
internal const string CopyPictureToFinal = "CopyPictureToFinal";
internal const string CopyAudioToFinal = "CopyAudioToFinal";
internal const string ConsumeUploadIntents = "ConsumeUploadIntents";
internal const string ConsumeUploadIntents = "ConsumeUploadIntents";
internal const string PublishTrackProcessingEvents = "PublishTrackProcessingEvents";
internal const string PublishPlayListPictureProcessingEvent = "PublishPlayListPictureProcessingEvent";
internal const string PublishAlbumPictureProcessingEvent = "PublishAlbumPictureProcessingEvent";
internal const string PublishTrackProcessingEvents = "PublishTrackProcessingEvents";
internal const string PublishPlayListPictureProcessingEvent = "PublishPlayListPictureProcessingEvent";
internal const string PublishAlbumPictureProcessingEvent = "PublishAlbumPictureProcessingEvent";
internal const string RemoveTrackOriginalPicture = "RemoveTrackOriginalPicture";
internal const string RemoveTrackSmallPicture = "RemoveTrackSmallPicture";
internal const string RemoveTrackMediumPicture = "RemoveTrackMediumPicture";
internal const string RemoveTrackLargePicture = "RemoveTrackLargePicture";
internal const string RemoveTrackOriginalAudio = "RemoveTrackOriginalAudio";
internal const string RemoveTrackProcessedAudio = "RemoveTrackProcessedAudio";
internal const string RemoveTrackOriginalPicture = "RemoveTrackOriginalPicture";
internal const string RemoveTrackSmallPicture = "RemoveTrackSmallPicture";
internal const string RemoveTrackMediumPicture = "RemoveTrackMediumPicture";
internal const string RemoveTrackLargePicture = "RemoveTrackLargePicture";
internal const string RemoveTrackOriginalAudio = "RemoveTrackOriginalAudio";
internal const string RemoveTrackProcessedAudio = "RemoveTrackProcessedAudio";
internal const string RemovePlayListOriginalPicture = "RemovePlayListOriginalPicture";
internal const string RemovePlayListSmallPicture = "RemovePlayListSmallPicture";
internal const string RemovePlayListMediumPicture = "RemovePlayListMediumPicture";
internal const string RemovePlayListLargePicture = "RemovePlayListLargePicture";
internal const string RemovePlayListOriginalPicture = "RemovePlayListOriginalPicture";
internal const string RemovePlayListSmallPicture = "RemovePlayListSmallPicture";
internal const string RemovePlayListMediumPicture = "RemovePlayListMediumPicture";
internal const string RemovePlayListLargePicture = "RemovePlayListLargePicture";
internal const string RemoveAlbumOriginalPicture = "RemoveAlbumOriginalPicture";
internal const string RemoveAlbumSmallPicture = "RemoveAlbumSmallPicture";
internal const string RemoveAlbumMediumPicture = "RemoveAlbumMediumPicture";
internal const string RemoveAlbumLargePicture = "RemoveAlbumLargePicture";
internal const string RemoveAlbumOriginalPicture = "RemoveAlbumOriginalPicture";
internal const string RemoveAlbumSmallPicture = "RemoveAlbumSmallPicture";
internal const string RemoveAlbumMediumPicture = "RemoveAlbumMediumPicture";
internal const string RemoveAlbumLargePicture = "RemoveAlbumLargePicture";
}
}
@@ -5,40 +5,41 @@ using Musify.Infrastructure.MassTransit.Activities.Audio;
using Musify.Infrastructure.MassTransit.Activities.Files;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
public class AudioWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration)
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
public RoutingSlipBuilder Build(UpdateTrackAudioEvent message, Guid? correlationId) =>
RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.GenerateAudioWorkflowPaths,
GenerateAudioWorkflowPathsActivity.ExecuteEndpointName,
new GenerateAudioWorkflowPathsArguments(
message.TrackId, workerConfiguration.Routes.TemporaryFilesDirectory, message.SourceKey))
.AddStep(
ActivityNames.DownloadFile,
DownloadFileFromBucketActivity.ExecuteEndpointName,
new DownloadFileFromBucketArguments(
message.SourceBucket, message.SourceKey, RoutingSlipVariableNames.Audio.SourceFilePath))
.AddStep(
ActivityNames.TranscodeAudio,
TranscodeAudioActivity.ExecuteEndpointName,
new TranscodeAudioArguments(
message.TrackId,
RoutingSlipVariableNames.Audio.SourceFilePath,
RoutingSlipVariableNames.Workflow.TemporalDirectory))
.AddStep(
ActivityNames.TransferFiles,
TransferFilesToBucketActivity.ExecuteEndpointName,
new TransferFilesToBucketArguments(
message.DestinationBucket,
RoutingSlipVariableNames.Audio.TranscodedDirectory,
message.DestinationFolderKey))
.AddStep(
ActivityNames.UpdateTrackAudio,
UpdateTrackAudioActivity.ExecuteEndpointName,
new UpdateTrackAudioArguments(
message.TrackId, message.DestinationFolderKey, RoutingSlipVariableNames.Audio.DurationSeconds))
.TrackFaults(message.TrackId, RoutingSlipVariableNames.ProcessKinds.TrackAudio);
public class AudioWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration)
{
public RoutingSlipBuilder Build(UpdateTrackAudioEvent message, Guid? correlationId) =>
RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.GenerateAudioWorkflowPaths,
GenerateAudioWorkflowPathsActivity.ExecuteEndpointName,
new GenerateAudioWorkflowPathsArguments(
message.TrackId, workerConfiguration.Routes.TemporaryFilesDirectory, message.SourceKey))
.AddStep(
ActivityNames.DownloadFile,
DownloadFileFromBucketActivity.ExecuteEndpointName,
new DownloadFileFromBucketArguments(
message.SourceBucket, message.SourceKey, RoutingSlipVariableNames.Audio.SourceFilePath))
.AddStep(
ActivityNames.TranscodeAudio,
TranscodeAudioActivity.ExecuteEndpointName,
new TranscodeAudioArguments(
message.TrackId,
RoutingSlipVariableNames.Audio.SourceFilePath,
RoutingSlipVariableNames.Workflow.TemporalDirectory))
.AddStep(
ActivityNames.TransferFiles,
TransferFilesToBucketActivity.ExecuteEndpointName,
new TransferFilesToBucketArguments(
message.DestinationBucket,
RoutingSlipVariableNames.Audio.TranscodedDirectory,
message.DestinationFolderKey))
.AddStep(
ActivityNames.UpdateTrackAudio,
UpdateTrackAudioActivity.ExecuteEndpointName,
new UpdateTrackAudioArguments(
message.TrackId, message.DestinationFolderKey, RoutingSlipVariableNames.Audio.DurationSeconds))
.TrackFaults(message.TrackId, RoutingSlipVariableNames.ProcessKinds.TrackAudio);
}
}
@@ -5,41 +5,42 @@ using Musify.Infrastructure.MassTransit.Activities.Tracks;
using Musify.Infrastructure.MassTransit.Activities.UploadIntents;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
public class CreateTrackRoutingSlipBuilder
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
public RoutingSlipBuilder Build(CreateTrackResourcesEvent message, Guid? correlationId)
public class CreateTrackRoutingSlipBuilder
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.CopyPictureToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(message.Bucket, message.PictureSourceKey, message.Bucket, message.PictureDestinationKey))
.AddStep(
ActivityNames.CopyAudioToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(message.Bucket, message.AudioSourceKey, message.Bucket, message.AudioDestinationKey))
.AddStep(
ActivityNames.ConsumeUploadIntents,
ConsumeUploadIntentsActivity.ExecuteEndpointName,
new ConsumeUploadIntentsArguments([message.PictureIntentId, message.AudioIntentId]))
.AddStep(
ActivityNames.PublishTrackProcessingEvents,
PublishTrackProcessingEventsActivity.ExecuteEndpointName,
new PublishTrackProcessingEventsArguments(
message.TrackId,
message.Bucket,
message.PictureDestinationKey,
message.AudioDestinationKey,
message.AudioProcessedFolderKey,
message.Sizes))
.TrackFaults(message.TrackId, RoutingSlipVariableNames.ProcessKinds.TrackCreation);
public RoutingSlipBuilder Build(CreateTrackResourcesEvent message, Guid? correlationId)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.CopyPictureToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(message.Bucket, message.PictureSourceKey, message.Bucket, message.PictureDestinationKey))
.AddStep(
ActivityNames.CopyAudioToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(message.Bucket, message.AudioSourceKey, message.Bucket, message.AudioDestinationKey))
.AddStep(
ActivityNames.ConsumeUploadIntents,
ConsumeUploadIntentsActivity.ExecuteEndpointName,
new ConsumeUploadIntentsArguments([message.PictureIntentId, message.AudioIntentId]))
.AddStep(
ActivityNames.PublishTrackProcessingEvents,
PublishTrackProcessingEventsActivity.ExecuteEndpointName,
new PublishTrackProcessingEventsArguments(
message.TrackId,
message.Bucket,
message.PictureDestinationKey,
message.AudioDestinationKey,
message.AudioProcessedFolderKey,
message.Sizes))
.TrackFaults(message.TrackId, RoutingSlipVariableNames.ProcessKinds.TrackCreation);
builder.AddVariable(RoutingSlipVariableNames.Workflow.Bucket, message.Bucket);
builder.AddVariable(RoutingSlipVariableNames.Workflow.PictureKey, message.PictureDestinationKey);
builder.AddVariable(RoutingSlipVariableNames.Workflow.AudioKey, message.AudioDestinationKey);
builder.AddVariable(RoutingSlipVariableNames.Workflow.Bucket, message.Bucket);
builder.AddVariable(RoutingSlipVariableNames.Workflow.PictureKey, message.PictureDestinationKey);
builder.AddVariable(RoutingSlipVariableNames.Workflow.AudioKey, message.AudioDestinationKey);
return builder;
return builder;
}
}
}
@@ -6,130 +6,131 @@ using Musify.Domain.ValueObjects;
using Musify.Infrastructure.MassTransit.Activities.LifeCycle;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
/// <summary>Marks an entity as removing, deletes its files from the bucket and finally deletes its row.</summary>
public class DeleteRoutingSlipBuilder(
IDatabase database,
TrackConfiguration trackConfiguration,
AlbumConfiguration albumConfiguration,
PlayListConfiguration playListConfiguration,
ApplicationStorageConfiguration storageConfiguration)
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
private sealed record PictureStepNames(string Original, string Small, string Medium, string Large);
private static readonly PictureStepNames AlbumSteps = new(
ActivityNames.RemoveAlbumOriginalPicture, ActivityNames.RemoveAlbumSmallPicture,
ActivityNames.RemoveAlbumMediumPicture, ActivityNames.RemoveAlbumLargePicture);
private static readonly PictureStepNames PlayListSteps = new(
ActivityNames.RemovePlayListOriginalPicture, ActivityNames.RemovePlayListSmallPicture,
ActivityNames.RemovePlayListMediumPicture, ActivityNames.RemovePlayListLargePicture);
public async Task<RoutingSlipBuilder> BuildTrackAsync(
Guid trackId, long userId, Guid? correlationId, CancellationToken cancellationToken)
/// <summary>Marks an entity as removing, deletes its files from the bucket and finally deletes its row.</summary>
public class DeleteRoutingSlipBuilder(
IDatabase database,
TrackConfiguration trackConfiguration,
AlbumConfiguration albumConfiguration,
PlayListConfiguration playListConfiguration,
ApplicationStorageConfiguration storageConfiguration)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkTrackAsRemoving,
MarkTrackLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(trackId, LifeCycleStatus.Removing));
private sealed record PictureStepNames(string Original, string Small, string Medium, string Large);
var track = await database.Tracks.AsNoTracking().SingleOrDefaultAsync(t => t.Id == trackId, cancellationToken);
if (track != null)
private static readonly PictureStepNames AlbumSteps = new(
ActivityNames.RemoveAlbumOriginalPicture, ActivityNames.RemoveAlbumSmallPicture,
ActivityNames.RemoveAlbumMediumPicture, ActivityNames.RemoveAlbumLargePicture);
private static readonly PictureStepNames PlayListSteps = new(
ActivityNames.RemovePlayListOriginalPicture, ActivityNames.RemovePlayListSmallPicture,
ActivityNames.RemovePlayListMediumPicture, ActivityNames.RemovePlayListLargePicture);
public async Task<RoutingSlipBuilder> BuildTrackAsync(
Guid trackId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var bucket = storageConfiguration.Bucket;
var routes = trackConfiguration.Routes;
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkTrackAsRemoving,
MarkTrackLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(trackId, LifeCycleStatus.Removing));
if (!string.IsNullOrEmpty(track.Pictures.OriginalName))
var track = await database.Tracks.AsNoTracking().SingleOrDefaultAsync(t => t.Id == trackId, cancellationToken);
if (track != null)
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackOriginalPicture, bucket,
routes.BuildOriginalPicturePath(userId, track.Pictures.OriginalName));
var bucket = storageConfiguration.Bucket;
var routes = trackConfiguration.Routes;
if (!string.IsNullOrEmpty(track.Pictures.OriginalName))
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackOriginalPicture, bucket,
routes.BuildOriginalPicturePath(userId, track.Pictures.OriginalName));
}
// Unprocessed tracks still point at the shared preset pictures, which must never be deleted.
if (track.Pictures.IsProcessed)
{
builder.AddRemoveFile(ActivityNames.RemoveTrackSmallPicture, bucket, routes.BuildPicturePath(PictureSize.Small, track.Pictures.SmallName));
builder.AddRemoveFile(ActivityNames.RemoveTrackMediumPicture, bucket, routes.BuildPicturePath(PictureSize.Medium, track.Pictures.MediumName));
builder.AddRemoveFile(ActivityNames.RemoveTrackLargePicture, bucket, routes.BuildPicturePath(PictureSize.Large, track.Pictures.LargeName));
}
if (!string.IsNullOrEmpty(track.Audio.OriginalName))
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackOriginalAudio, bucket,
routes.BuildOriginalAudioPath(userId, track.Audio.OriginalName));
}
if (track.Audio.IsProcessed)
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackProcessedAudio, bucket,
routes.BuildProcessedAudioPath(track.Audio.FolderName));
}
}
// Unprocessed tracks still point at the shared preset pictures, which must never be deleted.
if (track.Pictures.IsProcessed)
{
builder.AddRemoveFile(ActivityNames.RemoveTrackSmallPicture, bucket, routes.BuildPicturePath(PictureSize.Small, track.Pictures.SmallName));
builder.AddRemoveFile(ActivityNames.RemoveTrackMediumPicture, bucket, routes.BuildPicturePath(PictureSize.Medium, track.Pictures.MediumName));
builder.AddRemoveFile(ActivityNames.RemoveTrackLargePicture, bucket, routes.BuildPicturePath(PictureSize.Large, track.Pictures.LargeName));
}
if (!string.IsNullOrEmpty(track.Audio.OriginalName))
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackOriginalAudio, bucket,
routes.BuildOriginalAudioPath(userId, track.Audio.OriginalName));
}
if (track.Audio.IsProcessed)
{
builder.AddRemoveFile(
ActivityNames.RemoveTrackProcessedAudio, bucket,
routes.BuildProcessedAudioPath(track.Audio.FolderName));
}
return builder.AddStep(
ActivityNames.DeleteTrackFromDb, DeleteTrackActivity.ExecuteEndpointName, new DeleteEntityArguments(trackId));
}
return builder.AddStep(
ActivityNames.DeleteTrackFromDb, DeleteTrackActivity.ExecuteEndpointName, new DeleteEntityArguments(trackId));
}
public async Task<RoutingSlipBuilder> BuildAlbumAsync(
Guid albumId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkAlbumAsRemoving,
MarkAlbumLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(albumId, LifeCycleStatus.Removing));
public async Task<RoutingSlipBuilder> BuildAlbumAsync(
Guid albumId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkAlbumAsRemoving,
MarkAlbumLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(albumId, LifeCycleStatus.Removing));
var pictures = await database.Albums.AsNoTracking()
.Where(a => a.Id == albumId)
.Select(a => a.Pictures)
.SingleOrDefaultAsync(cancellationToken);
var pictures = await database.Albums.AsNoTracking()
.Where(a => a.Id == albumId)
.Select(a => a.Pictures)
.SingleOrDefaultAsync(cancellationToken);
AddPictureRemovals(builder, AlbumSteps, albumConfiguration.Routes, userId, pictures);
AddPictureRemovals(builder, AlbumSteps, albumConfiguration.Routes, userId, pictures);
return builder.AddStep(
ActivityNames.DeleteAlbumFromDb, DeleteAlbumActivity.ExecuteEndpointName, new DeleteEntityArguments(albumId));
}
return builder.AddStep(
ActivityNames.DeleteAlbumFromDb, DeleteAlbumActivity.ExecuteEndpointName, new DeleteEntityArguments(albumId));
}
public async Task<RoutingSlipBuilder> BuildPlayListAsync(
Guid playListId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkPlayListAsRemoving,
MarkPlayListLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(playListId, LifeCycleStatus.Removing));
public async Task<RoutingSlipBuilder> BuildPlayListAsync(
Guid playListId, long userId, Guid? correlationId, CancellationToken cancellationToken)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.MarkPlayListAsRemoving,
MarkPlayListLifeCycleActivity.ExecuteEndpointName,
new MarkLifeCycleArguments(playListId, LifeCycleStatus.Removing));
var pictures = await database.PlayLists.AsNoTracking()
.Where(p => p.Id == playListId)
.Select(p => p.Pictures)
.SingleOrDefaultAsync(cancellationToken);
var pictures = await database.PlayLists.AsNoTracking()
.Where(p => p.Id == playListId)
.Select(p => p.Pictures)
.SingleOrDefaultAsync(cancellationToken);
AddPictureRemovals(builder, PlayListSteps, playListConfiguration.Routes, userId, pictures);
AddPictureRemovals(builder, PlayListSteps, playListConfiguration.Routes, userId, pictures);
return builder.AddStep(
ActivityNames.DeletePlayListFromDb, DeletePlayListActivity.ExecuteEndpointName, new DeleteEntityArguments(playListId));
}
return builder.AddStep(
ActivityNames.DeletePlayListFromDb, DeletePlayListActivity.ExecuteEndpointName, new DeleteEntityArguments(playListId));
}
private void AddPictureRemovals(
RoutingSlipBuilder builder, PictureStepNames steps, PictureRoutes routes, long userId, EntityPictures? pictures)
{
if (pictures == null)
return;
private void AddPictureRemovals(
RoutingSlipBuilder builder, PictureStepNames steps, PictureRoutes routes, long userId, EntityPictures? pictures)
{
if (pictures == null)
return;
var bucket = storageConfiguration.Bucket;
var bucket = storageConfiguration.Bucket;
if (pictures.OriginalName is { } original)
builder.AddRemoveFile(steps.Original, bucket, routes.BuildOriginalPicturePath(userId, original));
if (pictures.SmallName is { } small)
builder.AddRemoveFile(steps.Small, bucket, routes.BuildPicturePath(PictureSize.Small, small));
if (pictures.MediumName is { } medium)
builder.AddRemoveFile(steps.Medium, bucket, routes.BuildPicturePath(PictureSize.Medium, medium));
if (pictures.LargeName is { } large)
builder.AddRemoveFile(steps.Large, bucket, routes.BuildPicturePath(PictureSize.Large, large));
if (pictures.OriginalName is { } original)
builder.AddRemoveFile(steps.Original, bucket, routes.BuildOriginalPicturePath(userId, original));
if (pictures.SmallName is { } small)
builder.AddRemoveFile(steps.Small, bucket, routes.BuildPicturePath(PictureSize.Small, small));
if (pictures.MediumName is { } medium)
builder.AddRemoveFile(steps.Medium, bucket, routes.BuildPicturePath(PictureSize.Medium, medium));
if (pictures.LargeName is { } large)
builder.AddRemoveFile(steps.Large, bucket, routes.BuildPicturePath(PictureSize.Large, large));
}
}
}
@@ -6,69 +6,70 @@ using Musify.Infrastructure.MassTransit.Activities.PlayLists;
using Musify.Infrastructure.MassTransit.Activities.UploadIntents;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
/// <summary>
/// Moves an uploaded album/playlist picture to its final key, consumes the upload intent and hands over to the picture workflow.
/// Creation faults mark the whole entity as failed; update faults only report the picture as failed.
/// </summary>
public class PictureSourceRoutingSlipBuilder
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
public RoutingSlipBuilder Build(CreateAlbumResourcesEvent message, Guid? correlationId) =>
BuildSlip(
message.AlbumId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.AlbumCreation,
ActivityNames.PublishAlbumPictureProcessingEvent, PublishAlbumPictureProcessingEventActivity.ExecuteEndpointName,
new PublishAlbumPictureProcessingEventArguments(message.AlbumId, message.Bucket, message.PictureDestinationKey, message.Sizes));
public RoutingSlipBuilder Build(UpdateAlbumPictureSourceEvent message, Guid? correlationId) =>
BuildSlip(
message.AlbumId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.AlbumPicture,
ActivityNames.PublishAlbumPictureProcessingEvent, PublishAlbumPictureProcessingEventActivity.ExecuteEndpointName,
new PublishAlbumPictureProcessingEventArguments(message.AlbumId, message.Bucket, message.PictureDestinationKey, message.Sizes));
public RoutingSlipBuilder Build(CreatePlayListResourcesEvent message, Guid? correlationId) =>
BuildSlip(
message.PlayListId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.PlayListCreation,
ActivityNames.PublishPlayListPictureProcessingEvent, PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName,
new PublishPlayListPictureProcessingEventArguments(message.PlayListId, message.Bucket, message.PictureDestinationKey, message.Sizes));
public RoutingSlipBuilder Build(UpdatePlayListPictureSourceEvent message, Guid? correlationId) =>
BuildSlip(
message.PlayListId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.PlayListPicture,
ActivityNames.PublishPlayListPictureProcessingEvent, PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName,
new PublishPlayListPictureProcessingEventArguments(message.PlayListId, message.Bucket, message.PictureDestinationKey, message.Sizes));
private static RoutingSlipBuilder BuildSlip(
Guid subjectId,
Guid pictureIntentId,
string bucket,
string pictureSourceKey,
string pictureDestinationKey,
Guid? correlationId,
string processKind,
string publishStepName,
string publishEndpointName,
object publishArguments)
/// <summary>
/// Moves an uploaded album/playlist picture to its final key, consumes the upload intent and hands over to the picture workflow.
/// Creation faults mark the whole entity as failed; update faults only report the picture as failed.
/// </summary>
public class PictureSourceRoutingSlipBuilder
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.CopyPictureToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(bucket, pictureSourceKey, bucket, pictureDestinationKey))
.AddStep(
ActivityNames.ConsumeUploadIntents,
ConsumeUploadIntentsActivity.ExecuteEndpointName,
new ConsumeUploadIntentsArguments([pictureIntentId]))
.AddStep(publishStepName, publishEndpointName, publishArguments)
.TrackFaults(subjectId, processKind);
public RoutingSlipBuilder Build(CreateAlbumResourcesEvent message, Guid? correlationId) =>
BuildSlip(
message.AlbumId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.AlbumCreation,
ActivityNames.PublishAlbumPictureProcessingEvent, PublishAlbumPictureProcessingEventActivity.ExecuteEndpointName,
new PublishAlbumPictureProcessingEventArguments(message.AlbumId, message.Bucket, message.PictureDestinationKey, message.Sizes));
builder.AddVariable(RoutingSlipVariableNames.Workflow.Bucket, bucket);
builder.AddVariable(RoutingSlipVariableNames.Workflow.PictureKey, pictureDestinationKey);
public RoutingSlipBuilder Build(UpdateAlbumPictureSourceEvent message, Guid? correlationId) =>
BuildSlip(
message.AlbumId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.AlbumPicture,
ActivityNames.PublishAlbumPictureProcessingEvent, PublishAlbumPictureProcessingEventActivity.ExecuteEndpointName,
new PublishAlbumPictureProcessingEventArguments(message.AlbumId, message.Bucket, message.PictureDestinationKey, message.Sizes));
return builder;
public RoutingSlipBuilder Build(CreatePlayListResourcesEvent message, Guid? correlationId) =>
BuildSlip(
message.PlayListId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.PlayListCreation,
ActivityNames.PublishPlayListPictureProcessingEvent, PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName,
new PublishPlayListPictureProcessingEventArguments(message.PlayListId, message.Bucket, message.PictureDestinationKey, message.Sizes));
public RoutingSlipBuilder Build(UpdatePlayListPictureSourceEvent message, Guid? correlationId) =>
BuildSlip(
message.PlayListId, message.PictureIntentId, message.Bucket, message.PictureSourceKey, message.PictureDestinationKey,
correlationId, RoutingSlipVariableNames.ProcessKinds.PlayListPicture,
ActivityNames.PublishPlayListPictureProcessingEvent, PublishPlayListPictureProcessingEventActivity.ExecuteEndpointName,
new PublishPlayListPictureProcessingEventArguments(message.PlayListId, message.Bucket, message.PictureDestinationKey, message.Sizes));
private static RoutingSlipBuilder BuildSlip(
Guid subjectId,
Guid pictureIntentId,
string bucket,
string pictureSourceKey,
string pictureDestinationKey,
Guid? correlationId,
string processKind,
string publishStepName,
string publishEndpointName,
object publishArguments)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.CopyPictureToFinal,
CopyFileInBucketActivity.ExecuteEndpointName,
new CopyFileInBucketArguments(bucket, pictureSourceKey, bucket, pictureDestinationKey))
.AddStep(
ActivityNames.ConsumeUploadIntents,
ConsumeUploadIntentsActivity.ExecuteEndpointName,
new ConsumeUploadIntentsArguments([pictureIntentId]))
.AddStep(publishStepName, publishEndpointName, publishArguments)
.TrackFaults(subjectId, processKind);
builder.AddVariable(RoutingSlipVariableNames.Workflow.Bucket, bucket);
builder.AddVariable(RoutingSlipVariableNames.Workflow.PictureKey, pictureDestinationKey);
return builder;
}
}
}
@@ -6,81 +6,82 @@ using Musify.Infrastructure.MassTransit.Activities.Files;
using Musify.Infrastructure.MassTransit.Activities.Pictures;
using Musify.Infrastructure.MassTransit.Arguments;
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders;
/// <summary>Downloads a picture, resizes it to the three sizes, uploads them and records the result on the owner.</summary>
public class PictureWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration)
namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders
{
public RoutingSlipBuilder Build(UpdateTrackPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.TrackId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdateTrackPicture, UpdateTrackPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.TrackPicture);
public RoutingSlipBuilder Build(UpdatePlayListPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.PlayListId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdatePlayListPicture, UpdatePlayListPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.PlayListPicture);
public RoutingSlipBuilder Build(UpdateAlbumPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.AlbumId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdateAlbumPicture, UpdateAlbumPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.AlbumPicture);
private RoutingSlipBuilder BuildPictureWorkflow(
Guid subjectId,
string bucket,
string sourceKey,
ImageSizes sizes,
Guid? correlationId,
string updateStepName,
string updateEndpointName,
string processKind)
/// <summary>Downloads a picture, resizes it to the three sizes, uploads them and records the result on the owner.</summary>
public class PictureWorkflowRoutingSlipBuilder(WorkerConfiguration workerConfiguration)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.GeneratePictureWorkflowPaths,
GeneratePictureWorkflowPathsActivity.ExecuteEndpointName,
new GeneratePictureWorkflowPathsArguments(workerConfiguration.Routes.TemporaryFilesDirectory, sourceKey))
.AddStep(
ActivityNames.DownloadFile,
DownloadFileFromBucketActivity.ExecuteEndpointName,
new DownloadFileFromBucketArguments(bucket, sourceKey, RoutingSlipVariableNames.Picture.OriginalFilePath));
public RoutingSlipBuilder Build(UpdateTrackPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.TrackId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdateTrackPicture, UpdateTrackPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.TrackPicture);
AddResize(builder, ActivityNames.ResizeSmall, ActivityNames.UploadSmall, bucket, RoutingSlipVariableNames.Picture.SmallResizedFilePath, sizes.Small);
AddResize(builder, ActivityNames.ResizeMedium, ActivityNames.UploadMedium, bucket, RoutingSlipVariableNames.Picture.MediumResizedFilePath, sizes.Medium);
AddResize(builder, ActivityNames.ResizeLarge, ActivityNames.UploadLarge, bucket, RoutingSlipVariableNames.Picture.LargeResizedFilePath, sizes.Large);
public RoutingSlipBuilder Build(UpdatePlayListPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.PlayListId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdatePlayListPicture, UpdatePlayListPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.PlayListPicture);
return builder
.AddStep(
updateStepName,
updateEndpointName,
new UpdatePicturesArguments(
subjectId,
sourceKey,
RoutingSlipVariableNames.Picture.SmallResizedFilePath,
RoutingSlipVariableNames.Picture.MediumResizedFilePath,
RoutingSlipVariableNames.Picture.LargeResizedFilePath))
.TrackFaults(subjectId, processKind);
public RoutingSlipBuilder Build(UpdateAlbumPictureEvent message, Guid? correlationId) =>
BuildPictureWorkflow(
message.AlbumId, message.Bucket, message.SourceKey, message.Sizes, correlationId,
ActivityNames.UpdateAlbumPicture, UpdateAlbumPictureActivity.ExecuteEndpointName,
RoutingSlipVariableNames.ProcessKinds.AlbumPicture);
private RoutingSlipBuilder BuildPictureWorkflow(
Guid subjectId,
string bucket,
string sourceKey,
ImageSizes sizes,
Guid? correlationId,
string updateStepName,
string updateEndpointName,
string processKind)
{
var builder = RoutingSlips.Create(correlationId)
.AddStep(
ActivityNames.GeneratePictureWorkflowPaths,
GeneratePictureWorkflowPathsActivity.ExecuteEndpointName,
new GeneratePictureWorkflowPathsArguments(workerConfiguration.Routes.TemporaryFilesDirectory, sourceKey))
.AddStep(
ActivityNames.DownloadFile,
DownloadFileFromBucketActivity.ExecuteEndpointName,
new DownloadFileFromBucketArguments(bucket, sourceKey, RoutingSlipVariableNames.Picture.OriginalFilePath));
AddResize(builder, ActivityNames.ResizeSmall, ActivityNames.UploadSmall, bucket, RoutingSlipVariableNames.Picture.SmallResizedFilePath, sizes.Small);
AddResize(builder, ActivityNames.ResizeMedium, ActivityNames.UploadMedium, bucket, RoutingSlipVariableNames.Picture.MediumResizedFilePath, sizes.Medium);
AddResize(builder, ActivityNames.ResizeLarge, ActivityNames.UploadLarge, bucket, RoutingSlipVariableNames.Picture.LargeResizedFilePath, sizes.Large);
return builder
.AddStep(
updateStepName,
updateEndpointName,
new UpdatePicturesArguments(
subjectId,
sourceKey,
RoutingSlipVariableNames.Picture.SmallResizedFilePath,
RoutingSlipVariableNames.Picture.MediumResizedFilePath,
RoutingSlipVariableNames.Picture.LargeResizedFilePath))
.TrackFaults(subjectId, processKind);
}
private static void AddResize(
RoutingSlipBuilder builder,
string resizeStepName,
string uploadStepName,
string bucket,
string resizedFileVariable,
ImageSize size) =>
builder
.AddStep(
resizeStepName,
ResizePictureActivity.ExecuteEndpointName,
new ResizePictureLocalArguments(
RoutingSlipVariableNames.Picture.OriginalFilePath, resizedFileVariable, size.Width, size.Height))
.AddStep(
uploadStepName,
UploadFileToBucketActivity.ExecuteEndpointName,
new UploadFileToBucketArguments(resizedFileVariable, bucket, size.SavePath));
}
private static void AddResize(
RoutingSlipBuilder builder,
string resizeStepName,
string uploadStepName,
string bucket,
string resizedFileVariable,
ImageSize size) =>
builder
.AddStep(
resizeStepName,
ResizePictureActivity.ExecuteEndpointName,
new ResizePictureLocalArguments(
RoutingSlipVariableNames.Picture.OriginalFilePath, resizedFileVariable, size.Width, size.Height))
.AddStep(
uploadStepName,
UploadFileToBucketActivity.ExecuteEndpointName,
new UploadFileToBucketArguments(resizedFileVariable, bucket, size.SavePath));
}
@@ -1,41 +1,42 @@
namespace Musify.Infrastructure.MassTransit.RoutingSlip;
internal static class RoutingSlipVariableNames
namespace Musify.Infrastructure.MassTransit.RoutingSlip
{
internal static class Workflow
internal static class RoutingSlipVariableNames
{
internal const string TemporalDirectory = "Workflow.TemporalDirectory";
internal const string CorrelationId = "Workflow.CorrelationId";
internal const string SubjectId = "Workflow.SubjectId";
internal const string ProcessKind = "Workflow.ProcessKind";
internal const string Bucket = "Workflow.Bucket";
internal const string PictureKey = "Workflow.PictureKey";
internal const string AudioKey = "Workflow.AudioKey";
}
internal static class Workflow
{
internal const string TemporalDirectory = "Workflow.TemporalDirectory";
internal const string CorrelationId = "Workflow.CorrelationId";
internal const string SubjectId = "Workflow.SubjectId";
internal const string ProcessKind = "Workflow.ProcessKind";
internal const string Bucket = "Workflow.Bucket";
internal const string PictureKey = "Workflow.PictureKey";
internal const string AudioKey = "Workflow.AudioKey";
}
internal static class ProcessKinds
{
internal const string TrackPicture = "TrackPicture";
internal const string TrackAudio = "TrackAudio";
internal const string PlayListPicture = "PlayListPicture";
internal const string AlbumPicture = "AlbumPicture";
internal const string TrackCreation = "TrackCreation";
internal const string AlbumCreation = "AlbumCreation";
internal const string PlayListCreation = "PlayListCreation";
}
internal static class ProcessKinds
{
internal const string TrackPicture = "TrackPicture";
internal const string TrackAudio = "TrackAudio";
internal const string PlayListPicture = "PlayListPicture";
internal const string AlbumPicture = "AlbumPicture";
internal const string TrackCreation = "TrackCreation";
internal const string AlbumCreation = "AlbumCreation";
internal const string PlayListCreation = "PlayListCreation";
}
internal static class Audio
{
internal const string SourceFilePath = "Audio.SourceFilePath";
internal const string TranscodedDirectory = "Audio.TranscodedDirectory";
internal const string DurationSeconds = "Audio.DurationSeconds";
}
internal static class Audio
{
internal const string SourceFilePath = "Audio.SourceFilePath";
internal const string TranscodedDirectory = "Audio.TranscodedDirectory";
internal const string DurationSeconds = "Audio.DurationSeconds";
}
internal static class Picture
{
internal const string OriginalFilePath = "Picture.OriginalFilePath";
internal const string SmallResizedFilePath = "Picture.SmallResizedFilePath";
internal const string MediumResizedFilePath = "Picture.MediumResizedFilePath";
internal const string LargeResizedFilePath = "Picture.LargeResizedFilePath";
internal static class Picture
{
internal const string OriginalFilePath = "Picture.OriginalFilePath";
internal const string SmallResizedFilePath = "Picture.SmallResizedFilePath";
internal const string MediumResizedFilePath = "Picture.MediumResizedFilePath";
internal const string LargeResizedFilePath = "Picture.LargeResizedFilePath";
}
}
}
@@ -4,45 +4,46 @@ using Musify.Infrastructure.MassTransit.Activities.Files;
using Musify.Infrastructure.MassTransit.Arguments;
using Musify.Infrastructure.MassTransit.Consumers;
namespace Musify.Infrastructure.MassTransit.RoutingSlip;
/// <summary>Shared building blocks for every routing slip in the worker.</summary>
internal static class RoutingSlips
namespace Musify.Infrastructure.MassTransit.RoutingSlip
{
public static Uri ExecuteUri(string endpointName) => new($"queue:{endpointName}_execute");
public static Uri ConsumerUri(string queueName) => new($"queue:{queueName}");
/// <summary>A new slip that carries the correlation id and always cleans its temporary directory up.</summary>
public static RoutingSlipBuilder Create(Guid? correlationId)
/// <summary>Shared building blocks for every routing slip in the worker.</summary>
internal static class RoutingSlips
{
var builder = new RoutingSlipBuilder(NewId.NextGuid());
builder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty);
builder.AddSubscription(
ConsumerUri(RoutingSlipCleanUpConsumer.QueueName),
RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted);
public static Uri ExecuteUri(string endpointName) => new($"queue:{endpointName}_execute");
return builder;
public static Uri ConsumerUri(string queueName) => new($"queue:{queueName}");
/// <summary>A new slip that carries the correlation id and always cleans its temporary directory up.</summary>
public static RoutingSlipBuilder Create(Guid? correlationId)
{
var builder = new RoutingSlipBuilder(NewId.NextGuid());
builder.AddVariable(RoutingSlipVariableNames.Workflow.CorrelationId, correlationId ?? Guid.Empty);
builder.AddSubscription(
ConsumerUri(RoutingSlipCleanUpConsumer.QueueName),
RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted);
return builder;
}
public static RoutingSlipBuilder AddStep(
this RoutingSlipBuilder builder, string name, string executeEndpointName, object arguments)
{
builder.AddActivity(name, ExecuteUri(executeEndpointName), arguments);
return builder;
}
/// <summary>Publishes a failure event for the subject when the slip faults.</summary>
public static RoutingSlipBuilder TrackFaults(
this RoutingSlipBuilder builder, Guid subjectId, string processKind)
{
builder.AddVariable(RoutingSlipVariableNames.Workflow.SubjectId, subjectId);
builder.AddVariable(RoutingSlipVariableNames.Workflow.ProcessKind, processKind);
builder.AddSubscription(ConsumerUri(ProcessingSlipFaultConsumer.QueueName), RoutingSlipEvents.Faulted);
return builder;
}
public static RoutingSlipBuilder AddRemoveFile(
this RoutingSlipBuilder builder, string stepName, string bucket, string key) =>
builder.AddStep(stepName, RemoveFileFromBucketActivity.ExecuteEndpointName, new RemoveFileFromBucketArguments(bucket, key));
}
public static RoutingSlipBuilder AddStep(
this RoutingSlipBuilder builder, string name, string executeEndpointName, object arguments)
{
builder.AddActivity(name, ExecuteUri(executeEndpointName), arguments);
return builder;
}
/// <summary>Publishes a failure event for the subject when the slip faults.</summary>
public static RoutingSlipBuilder TrackFaults(
this RoutingSlipBuilder builder, Guid subjectId, string processKind)
{
builder.AddVariable(RoutingSlipVariableNames.Workflow.SubjectId, subjectId);
builder.AddVariable(RoutingSlipVariableNames.Workflow.ProcessKind, processKind);
builder.AddSubscription(ConsumerUri(ProcessingSlipFaultConsumer.QueueName), RoutingSlipEvents.Faulted);
return builder;
}
public static RoutingSlipBuilder AddRemoveFile(
this RoutingSlipBuilder builder, string stepName, string bucket, string key) =>
builder.AddStep(stepName, RemoveFileFromBucketActivity.ExecuteEndpointName, new RemoveFileFromBucketArguments(bucket, key));
}