16 KiB
MusifyBackend — Resumen de cambios (2026-05-14)
Este documento resume todos los cambios aplicados hasta ahora en la rama master durante esta iteración.
Nota: El proyecto compila en
Releaseal cierre de estos cambios.
1) Mensajería robusta: MassTransit + EF Outbox
Objetivo: hacer la publicación de eventos confiable y transaccional (DB + mensajes) y tolerar redelivery en consumidores.
Cambios principales
- Se integró MassTransit EntityFramework Outbox con PostgreSQL (
UsePostgres+UseBusOutbox). - Se añadieron entidades/tablas de Inbox/Outbox en el
DbContext. - Se generó y añadió una migración para las tablas de outbox/inbox.
- Se ajustó la configuración de endpoints para aplicar:
- Retry (exponential backoff)
- Idempotencia en consumo usando
UseEntityFrameworkOutbox<Database>()
- Se ajustó el orden de operaciones para el patrón outbox: publicar evento y luego
SaveChanges.
Archivos involucrados (principalmente)
Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.csInfrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.csInfrastructure/Persistence/Database.csInfrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.csApplication/Abstractions/Infrastructure/IEventBus.csInfrastructure/MassTransit/EventBus/MassTransitEventBus.cs
2) Workflows: Courier RoutingSlip para imágenes y audio
Objetivo: mantener el pipeline asíncrono (descarga, resize/transcode, upload, update de entidades) con reintentos e idempotencia.
Fix importante en workflow de imágenes
- Se corrigió el nombre de actividad del paso de Upload del tamaño Medium (antes duplicaba el nombre de
ResizeMedium).
Archivos:
Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.csInfrastructure/MassTransit/RoutingSlip/ActivityNames.cs
Fix importante en compensación de uploads
UploadFileToBucketActivityahora registra correctamente el object key completo (ruta + filename) para que la compensación borre el objeto correcto.
Archivo:
Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs
3) Uploads (Opción B): Presigned PUT URL + Create/Update con nombres
Objetivo: separar la subida de binarios (S3) de la operación de dominio (Create/Update), evitando rollbacks complejos y haciendo el flujo más robusto.
Endpoint/Commands añadidos (request presigned URL)
- Playlist picture:
Application/PlayLists/Commands/RequestPlayListPictureUploadCommand.csApplication/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.csApplication/PlayLists/Responses/PlayListPictureUploadResponse.cs
- Track picture + audio:
Application/Tracks/Commands/RequestTrackUploadUrlsCommand.csApplication/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.csApplication/Tracks/Responses/TrackUploadUrlsResponse.cs
Storage: presigned PUT
- Se añadió soporte para generar presigned PUT URLs:
Application/Abstractions/Infrastructure/IStorageService.csInfrastructure/Services/StorageService.cs(GetUploadUrlAsync)
4) Limpieza: eliminar V1 y normalizar nombres (V2 → “normal”)
Objetivo: eliminar comandos/handlers “V1” (subida directa por stream/multipart) y dejar un único set de comandos “normales” con el contrato final.
Resultado
CreatePlayListCommand,UpdatePlayListCommandyCreateTrackCommandquedaron con el contrato nuevo (nombres/metadata, no streams).- Se eliminaron archivos
*V2*ya innecesarios.
Archivos clave:
Application/PlayLists/Commands/CreatePlayListCommand.csApplication/PlayLists/Handlers/CreatePlayListCommandHandler.csApplication/PlayLists/Commands/UpdatePlayListCommand.csApplication/PlayLists/Handlers/UpdatePlayListCommandHandler.csApplication/Tracks/Commands/CreateTrackCommand.csApplication/Tracks/Handler/CreateTrackCommandHandler.cs
5) Prefijo de seguridad: uploads/{userId}/... para originales
Objetivo: reducir superficie (y colisiones) haciendo que los objetos originales queden naturalmente “namespaced” por usuario.
Cambios
- Las keys de originales (playlist original picture, track original picture, track original audio) ahora se generan bajo:
uploads/{userId}/...
- Se añadieron overloads de rutas para construir keys con
userId. - Los handlers de request presigned URL ahora generan keys usando el prefijo con
userId. - Los eventos de workflows (sourceKey) se publican con la key prefijada.
- El borrado de playlist y track usan keys prefijadas dentro de sus workflows de delete.
Archivos:
Application/Configuration/PlayListConfiguration.csApplication/Configuration/TrackConfiguration.csApplication/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.csApplication/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.csApplication/PlayLists/Handlers/CreatePlayListCommandHandler.csApplication/PlayLists/Handlers/UpdatePlayListCommandHandler.csApplication/Tracks/Handler/CreateTrackCommandHandler.csApplication/PlayLists/Handlers/DeletePlayListCommandHandler.cs
6) Normalización de separadores en keys S3 (evitar \\ en Windows)
Objetivo: evitar que Path.Combine introduzca separadores \\ en keys S3 (lo cual es incorrecto/inesperado en S3, y rompe interoperabilidad).
Cambios
- Se reemplazaron combinaciones de keys que usaban
Path.Combinepor joins con/en puntos críticos.
Archivos:
Application/Configuration/PlayListConfiguration.csApplication/Configuration/TrackConfiguration.csInfrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.csInfrastructure/Services/StorageService.cs
7) Validaciones en Application eliminadas (delegadas a API)
Objetivo: evitar duplicar validaciones de formato (extensiones/strings) si la capa API ya se encarga.
Cambios
- Se eliminó el helper de validación creado inicialmente y los checks en handlers.
Archivos:
- Eliminado:
Application/Validation/UploadObjectNameValidator.cs - Ajustados:
Application/PlayLists/Handlers/CreatePlayListCommandHandler.csApplication/PlayLists/Handlers/UpdatePlayListCommandHandler.csApplication/Tracks/Handler/CreateTrackCommandHandler.cs
8) Documento de mitigaciones (presigned uploads)
Se añadió documentación de hardening/mitigaciones contra abuso/spam en endpoints presigned.
Archivo:
docs/presigned-upload-hardening.md
9) Notas de compatibilidad / impacto
- Este cambio rompe compatibilidad con la API anterior si existían endpoints que aceptaban multipart/streams para Create/Update (ahora se espera el flujo presigned y luego Create/Update con nombres).
- Los objetos originales ahora viven bajo
uploads/{userId}/.... Objetos antiguos fuera de ese prefijo pueden quedar huérfanos si ya existían de antes.
10) Estado final (iteración anterior)
dotnet build MusifyBackend.slnx -c Release✅
11) Deletes unificados con Workflows (RoutingSlip)
Objetivo: homogeneizar deletes de Track/PlayList usando Courier RoutingSlip con actividades granulares e idempotentes.
Cambios
- Se añadió
LifeCycleStatus(Active/Removing) aTrackyPlayListy se generó migración. - Track delete:
- Se eliminó la activity monolítica y el slip ahora encadena: marcar como removing → borrar N ficheros (condicional) → borrar en DB.
- PlayList delete:
- Se añadió
DeletePlayListEvent+ consumer + routing slip builder + activities para marcar como removing, borrar pictures (si no son preset) y borrar en DB.
- Se añadió
- Se endureció la idempotencia de borrado en S3: si una key no existe, se loguea y el paso se considera completado.
- Se eliminaron
RemoveFileEvent/RemoveFileConsumerpor quedar sin usos.
Archivos clave:
Domain/Entities/LifeCycleStatus.csDomain/Entities/Track.csDomain/Entities/PlayList.csApplication/Events/DeletePlayListEvent.csApplication/PlayLists/Handlers/DeletePlayListCommandHandler.csApplication/Tracks/Handler/DeleteTrackCommandHandler.csInfrastructure/MassTransit/Consumers/DeleteTrackConsumer.csInfrastructure/MassTransit/Consumers/DeletePlayListConsumer.csInfrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.csInfrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.csInfrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.csInfrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.csInfrastructure/Persistence/Migrations/20260514161518_AddLifeCycleStatus.cs
12) Hardening de pre-signed uploads (Fases 1 y 2)
Objetivo: evitar abuso de coste/almacenamiento cuando usuarios hacen spam de endpoints presigned sin crear la entidad final (objetos huérfanos en S3).
Referencia: docs/presigned-upload-hardening.md
Fase 1 — Upload Intents en DB
Nuevas entidades (Domain)
Domain/Entities/UploadIntent.cs— tablaUploadIntentscon campos:Id,UserId,Bucket,Key,ObjectName,ContentType,ExpectedSizeBytes,Purpose,Status,ExpiresAt,CreatedAt.Domain/Entities/UploadIntentStatus.cs— enum:Issued,Consumed,Expired.Domain/Entities/UploadIntentPurpose.cs— enum:PlayListPicture,TrackPicture,TrackAudio.
Nueva configuración (Application)
Application/Configuration/UploadIntentConfiguration.cs— secciónUploadIntent:UploadUrlExpiresInSeconds(default: 120)MaxActiveUploadIntentsPerUser(default: 5)MaxActiveUploadBytesPerUser(default: 200 MB)MaxUploadBytes(default: 100 MB, para validación HEAD)DefaultExpectedPictureSizeBytes/DefaultExpectedAudioSizeBytes(fallback cuando el caller no envía tamaño)ExpirationJobIntervalSeconds/ExpiredIntentsRetentionDaysTempRootPrefix/TempUploadsRetentionDays/TempCleanupJobIntervalSeconds(Fase 2)
Interfaces actualizadas
Application/Abstractions/Infrastructure/IDatabase.cs— añadidoDbSet<UploadIntent> UploadIntents.Application/Abstractions/Infrastructure/IStorageService.cs— añadidos:HeadObjectAsync(bucket, key)→ObjectMetadata?(para validación antes de consumir).ListObjectsAsync(bucket, prefix)→IAsyncEnumerable<(string Key, DateTime LastModifiedUtc)>(para el cleanup job).
Application/Abstractions/Infrastructure/ObjectMetadata.cs— record(ContentType, ContentLength).
Infrastructure actualizada
Infrastructure/Persistence/Database.cs— añadidoDbSet<UploadIntent>.Infrastructure/Services/StorageService.cs— implementaciones deHeadObjectAsyncyListObjectsAsync.
Commands actualizados (cambio de contrato)
RequestPlayListPictureUploadCommand— nuevo campo opcionalExpectedSizeBytes?.RequestTrackUploadUrlsCommand— nuevos campos opcionalesExpectedPictureSizeBytes?,ExpectedAudioSizeBytes?.CreatePlayListCommand—OriginalPictureName?→PictureIntentId?(Guid).CreateTrackCommand—OriginalPictureName+OriginalAudioName→PictureIntentId+AudioIntentId(Guid).UpdatePlayListCommand—NewOriginalPictureName?→NewPictureIntentId?(Guid).
Responses actualizadas
PlayListPictureUploadResponse— añadidoIntentId(Guid).TrackUploadUrlsResponse— añadidosPictureIntentId+AudioIntentId(Guid).
Handlers actualizados
Request upload (RequestPlayListPictureUploadCommandHandler, RequestTrackUploadUrlsCommandHandler):
- Comprueban cuota de usuario (conteo de intents activos + suma de bytes) antes de generar la URL.
- Crean un registro
UploadIntent(Status =Issued) en la misma llamada. - TTL reducido a
UploadUrlExpiresInSeconds(configurable, default 120 s vs. los anteriores 600 s hardcodeados).
Create/Update (CreatePlayListCommandHandler, CreateTrackCommandHandler, UpdatePlayListCommandHandler):
- Reciben
IntentIden lugar del nombre de fichero. - Validan el intent: existe, pertenece al usuario, Status =
Issued, no caducado. - Realizan
HEADdel objeto en S3: debe existir y tamaño ≤MaxUploadBytes. - Marcan el intent como
Consumedy guardan todo en el mismoSaveChangesAsync.
Job de expiración
Infrastructure/Jobs/UploadIntentExpirationJob.cs—BackgroundServiceque:- Marca como
Expiredlos intentsIssuedconExpiresAt < now. - Elimina intents
Expiredcuya fecha de expiración supera la ventana de retención configurable.
- Marca como
Migración
Infrastructure/Persistence/Migrations/*_AddUploadIntentTable.cs
Fase 2 — Prefijo temporal + cleanup de S3
Rutas temporales
TrackRoutes.BuildTempPicturePath/BuildTempAudioPath— construyen{TempRootPrefix}/{userId}/{ParentFolder}/{objectName}.PlayListRoutes.BuildTempPicturePath— ídem para playlists.
Handlers de request upload
- La presigned URL ahora apunta a
temp/{userId}/...(prefijo temporal) en lugar de la ruta final. - El intent almacena la temp key.
Handlers de create/update (consume)
- Al consumir un intent, se llama
CopyFileAsync(tempKey → finalKey)antes de publicar el evento de procesado. - El objeto temporal queda en S3 y es eliminado posteriormente por el cleanup job.
Job de cleanup de temporales
Infrastructure/Jobs/TempUploadsCleanupJob.cs—BackgroundServiceque:- Lista objetos bajo
TempRootPrefix/usandoListObjectsAsync. - Borra los que superan
TempUploadsRetentionDays(default 3 días). - Tolerante a errores por objeto (continúa si un borrado falla).
- Lista objetos bajo
DI
Infrastructure/Services/ServicesDependencyInjection.AddUploadIntentJobs()— registraUploadIntentConfiguration+ ambos background jobs.
13) Refactor: IntentValidationResult (eliminar tuplas)
Objetivo: evitar retornar tuplas (Result?, UploadIntent?) desde métodos privados — patrón no idiomático en C#.
Cambios
- Creado
Application/UploadIntents/IntentValidationResult.cs—internal sealed recordcon:Result? Error/UploadIntent? Intentbool IsSuccess- Factory methods
Ok(intent)/Fail(error)
- Los tres handlers (
CreatePlayListCommandHandler,CreateTrackCommandHandler,UpdatePlayListCommandHandler) ahora usanIntentValidationResultenValidateAndLoadIntentAsync. - El tipo es
internal(noprivate) porque es compartido entre 3 handlers de distintas carpetas.
14) Fix: race condition en cuota de intents
Objetivo: garantizar que el check de cuota + insert del intent sean atómicos y no admitan concurrent bypass.
Cambios
- Añadida
IDatabaseTransactionenApplication/Abstractions/Infrastructure/IDatabaseTransaction.cs— interfaz limpia (CommitAsync,DisposeAsync) que no expone tipos EF. - Añadido
BeginTransactionAsync(IsolationLevel, CancellationToken)aIDatabase. - Implementado en
Infrastructure/Persistence/Database.cscon unprivate sealed class DatabaseTransactionque envuelveIDbContextTransactionde EF. RequestPlayListPictureUploadCommandHandleryRequestTrackUploadUrlsCommandHandler: el bloque quota check + insert ahora se ejecuta dentro de una transacciónSERIALIZABLE, eliminando la ventana de race condition entre el check y el commit.
15) Fix: separación AddUploadIntentConfiguration / AddUploadIntentJobs
Objetivo: UploadIntentConfiguration es necesaria tanto en el host del API (para los handlers) como en el worker (para los jobs). El método anterior mezclaba config + jobs en uno solo, haciendo imposible registrar la config sin registrar los BackgroundService.
Cambios
AddUploadIntentConfiguration(services, configuration)— registra solo la config. Lo llama el host del API y el worker.AddUploadIntentJobs(services, configuration)— llama internamente aAddUploadIntentConfigurationy además registraUploadIntentExpirationJob+TempUploadsCleanupJob. Solo lo llama el worker.
Archivo: Infrastructure/Services/ServicesDependencyInjection.cs
16) Estado final
dotnet build✅ — 0 errores, 0 advertencias.