Files
Musify/docs/changes-summary.md
T
2026-05-14 19:53:26 +02:00

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 Release al 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.cs
  • Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Endpoints.cs
  • Infrastructure/Persistence/Database.cs
  • Infrastructure/Persistence/Migrations/20260514095113_AddMassTransitOutbox.cs
  • Application/Abstractions/Infrastructure/IEventBus.cs
  • Infrastructure/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.cs
  • Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs

Fix importante en compensación de uploads

  • UploadFileToBucketActivity ahora 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.cs
    • Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs
    • Application/PlayLists/Responses/PlayListPictureUploadResponse.cs
  • Track picture + audio:
    • Application/Tracks/Commands/RequestTrackUploadUrlsCommand.cs
    • Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs
    • Application/Tracks/Responses/TrackUploadUrlsResponse.cs

Storage: presigned PUT

  • Se añadió soporte para generar presigned PUT URLs:
    • Application/Abstractions/Infrastructure/IStorageService.cs
    • Infrastructure/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, UpdatePlayListCommand y CreateTrackCommand quedaron con el contrato nuevo (nombres/metadata, no streams).
  • Se eliminaron archivos *V2* ya innecesarios.

Archivos clave:

  • Application/PlayLists/Commands/CreatePlayListCommand.cs
  • Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs
  • Application/PlayLists/Commands/UpdatePlayListCommand.cs
  • Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs
  • Application/Tracks/Commands/CreateTrackCommand.cs
  • Application/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.cs
  • Application/Configuration/TrackConfiguration.cs
  • Application/PlayLists/Handlers/RequestPlayListPictureUploadCommandHandler.cs
  • Application/Tracks/Handler/RequestTrackUploadUrlsCommandHandler.cs
  • Application/PlayLists/Handlers/CreatePlayListCommandHandler.cs
  • Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs
  • Application/Tracks/Handler/CreateTrackCommandHandler.cs
  • Application/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.Combine por joins con / en puntos críticos.

Archivos:

  • Application/Configuration/PlayListConfiguration.cs
  • Application/Configuration/TrackConfiguration.cs
  • Infrastructure/MassTransit/Activities/Files/UploadFileToBucketActivity.cs
  • Infrastructure/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.cs
    • Application/PlayLists/Handlers/UpdatePlayListCommandHandler.cs
    • Application/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) a Track y PlayList y 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 endureció la idempotencia de borrado en S3: si una key no existe, se loguea y el paso se considera completado.
  • Se eliminaron RemoveFileEvent/RemoveFileConsumer por quedar sin usos.

Archivos clave:

  • Domain/Entities/LifeCycleStatus.cs
  • Domain/Entities/Track.cs
  • Domain/Entities/PlayList.cs
  • Application/Events/DeletePlayListEvent.cs
  • Application/PlayLists/Handlers/DeletePlayListCommandHandler.cs
  • Application/Tracks/Handler/DeleteTrackCommandHandler.cs
  • Infrastructure/MassTransit/Consumers/DeleteTrackConsumer.cs
  • Infrastructure/MassTransit/Consumers/DeletePlayListConsumer.cs
  • Infrastructure/MassTransit/RoutingSlip/Builders/DeleteTrackRoutingSlipBuilder.cs
  • Infrastructure/MassTransit/RoutingSlip/Builders/DeletePlayListRoutingSlipBuilder.cs
  • Infrastructure/MassTransit/Activities/Files/RemoveFileFromBucketActivity.cs
  • Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs
  • Infrastructure/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 — tabla UploadIntents con 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ón UploadIntent:
    • 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 / ExpiredIntentsRetentionDays
    • TempRootPrefix / TempUploadsRetentionDays / TempCleanupJobIntervalSeconds (Fase 2)

Interfaces actualizadas

  • Application/Abstractions/Infrastructure/IDatabase.cs — añadido DbSet<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ñadido DbSet<UploadIntent>.
  • Infrastructure/Services/StorageService.cs — implementaciones de HeadObjectAsync y ListObjectsAsync.

Commands actualizados (cambio de contrato)

  • RequestPlayListPictureUploadCommand — nuevo campo opcional ExpectedSizeBytes?.
  • RequestTrackUploadUrlsCommand — nuevos campos opcionales ExpectedPictureSizeBytes?, ExpectedAudioSizeBytes?.
  • CreatePlayListCommand — OriginalPictureName? → PictureIntentId? (Guid).
  • CreateTrackCommand — OriginalPictureName + OriginalAudioName → PictureIntentId + AudioIntentId (Guid).
  • UpdatePlayListCommand — NewOriginalPictureName? → NewPictureIntentId? (Guid).

Responses actualizadas

  • PlayListPictureUploadResponse — añadido IntentId (Guid).
  • TrackUploadUrlsResponse — añadidos PictureIntentId + 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 IntentId en lugar del nombre de fichero.
  • Validan el intent: existe, pertenece al usuario, Status = Issued, no caducado.
  • Realizan HEAD del objeto en S3: debe existir y tamaño ≤ MaxUploadBytes.
  • Marcan el intent como Consumed y guardan todo en el mismo SaveChangesAsync.

Job de expiración

  • Infrastructure/Jobs/UploadIntentExpirationJob.cs — BackgroundService que:
    • Marca como Expired los intents Issued con ExpiresAt < now.
    • Elimina intents Expired cuya fecha de expiración supera la ventana de retención configurable.

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 — BackgroundService que:
    • Lista objetos bajo TempRootPrefix/ usando ListObjectsAsync.
    • Borra los que superan TempUploadsRetentionDays (default 3 días).
    • Tolerante a errores por objeto (continúa si un borrado falla).

DI

  • Infrastructure/Services/ServicesDependencyInjection.AddUploadIntentJobs() — registra UploadIntentConfiguration + 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 record con:
    • Result? Error / UploadIntent? Intent
    • bool IsSuccess
    • Factory methods Ok(intent) / Fail(error)
  • Los tres handlers (CreatePlayListCommandHandler, CreateTrackCommandHandler, UpdatePlayListCommandHandler) ahora usan IntentValidationResult en ValidateAndLoadIntentAsync.
  • El tipo es internal (no private) 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 IDatabaseTransaction en Application/Abstractions/Infrastructure/IDatabaseTransaction.cs — interfaz limpia (CommitAsync, DisposeAsync) que no expone tipos EF.
  • Añadido BeginTransactionAsync(IsolationLevel, CancellationToken) a IDatabase.
  • Implementado en Infrastructure/Persistence/Database.cs con un private sealed class DatabaseTransaction que envuelve IDbContextTransaction de EF.
  • RequestPlayListPictureUploadCommandHandler y RequestTrackUploadUrlsCommandHandler: el bloque quota check + insert ahora se ejecuta dentro de una transacción SERIALIZABLE, 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 a AddUploadIntentConfiguration y además registra UploadIntentExpirationJob + TempUploadsCleanupJob. Solo lo llama el worker.

Archivo: Infrastructure/Services/ServicesDependencyInjection.cs


16) Estado final

  • dotnet build ✅ — 0 errores, 0 advertencias.