Remove docs and agents

This commit is contained in:
2026-05-29 15:45:53 +02:00
parent 098f97473d
commit 53067358df
5 changed files with 0 additions and 1028 deletions
-41
View File
@@ -1,41 +0,0 @@
# Contexto del proyecto para agentes de IA
## Resumen rápido
- Proyecto backend en `.NET 10`.
- Estructura principal en capas: `Domain`, `Application`, `Infrastructure`.
- Patrón arquitectónico: `Clean Architecture` con enfoque `CQRS`.
## Arquitectura
- **Domain (`Domain`)**: entidades de negocio puras.
- **Application (`Application`)**:
- Casos de uso (comandos/queries y handlers).
- Contratos (`Application.Contracts`).
- Configuración de aplicación (`Application.Configuration`).
- Mensajería orientada a eventos a través de `IEventBus`.
- **Infrastructure (`Infrastructure`)**:
- Persistencia y acceso a datos.
- Integraciones externas (Keycloak, almacenamiento, procesamiento de imágenes).
- Implementación de mensajería y consumidores.
## CQRS / Mediación
- Se usa **DispatchR** (paquete `DispatchR.Mediator`) como variante de MediatR para orquestar comandos y consultas.
- Los casos de uso están organizados por feature con handlers dedicados (`Commands`, `Queries`).
## Mensajería y workers
- Se usa **MassTransit** para publicación/consumo de eventos.
- Broker actual: **RabbitMQ** (`MassTransit.RabbitMQ`).
- Hay modo de registro para workers con consumidores en `Infrastructure.Messaging.Consumers`.
- Actualmente existen consumidores como:
- `ResizePictureConsumer`
- `RemoveFileConsumer`
## Persistencia e integraciones
- `Entity Framework Core` + proveedor `Npgsql` (PostgreSQL).
- Integración de identidad con `Keycloak`.
- Almacenamiento con `AWS S3`.
## Notas para agentes
- Mantener separación de responsabilidades por capa.
- Evitar lógica de infraestructura dentro de `Application` y `Domain`.
- Agregar nuevos casos de uso siguiendo el patrón CQRS existente (comando/query + handler).
- Para nuevas integraciones asíncronas, reutilizar el pipeline de `MassTransit` y la abstracción `IEventBus`.
-333
View File
@@ -1,333 +0,0 @@
# 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.
-205
View File
@@ -1,205 +0,0 @@
# Plan — Unificar Deletes con Workflows (RoutingSlip)
## Contexto
Actualmente hay dos problemas de diseño en los deletes:
**PlayLists:** el borrado disparaba múltiples `RemoveFileEvent` sueltos desde `DeletePlayListCommandHandler` (4 llamadas separadas para original/small/medium/large) y luego eliminaba la entidad en DB. Sin workflow, sin trazabilidad.
**Tracks:** existía un workflow (`DeleteTrackEvent` → consumer → RoutingSlip → `DeleteTrackActivity`), pero `DeleteTrackActivity` hacía **demasiado en un único paso**: carga la entidad, borra todos los archivos de S3 (pictures + audios), y elimina de DB. Esto rompe el principio de actividades granulares y reutilizables, hace que el slip no sea "seguible" paso a paso, y dificulta los reintentos parciales.
La idea es **homogeneizar y corregir ambos**: que los deletes de Track y PlayList sigan un RoutingSlip con actividades pequeñas, ordenadas, repetibles e independientes.
---
## Objetivos
- **Un único camino** de borrado por aggregate (Track / PlayList), basado en **RoutingSlip con actividades granulares**.
- El handler de comando hace lo mínimo: validar ownership/reglas, cambiar estado a `Removing`, y publicar el evento de borrado (outbox).
- El workflow ejecuta pasos **ordenados, reutilizables y reiniciables** individualmente.
- **Reutilizar** `RemoveFileFromBucketActivity` (ya existente) para cada archivo a borrar, en lugar de lógica ad-hoc dentro de una activity monolítica.
- Eliminar `RemoveFileEvent`, `RemoveFileConsumer` y su registration si no tienen otros usos.
No objetivos (por ahora):
- No introducir observabilidad extra (tracing/telemetría) fuera de lo ya existente.
---
## Problemas del enfoque actual
### PlayLists
- El handler implementa orquestación (decide qué archivos borrar y cómo) → acoplamiento alto.
- Publica múltiples `RemoveFileEvent` sueltos, sin agruparlos en un proceso único.
- No hay log de RoutingSlip que permita seguir el progreso/estado.
### Tracks ← **nuevo problema identificado**
- `DeleteTrackActivity` es monolítica: carga entidad, borra pictures, borra audios, elimina de DB, todo en un solo `Execute`.
- No reutiliza `RemoveFileFromBucketActivity` ya existente.
- Si falla a mitad, no hay granularidad de reintento (no se sabe qué paso falló).
- El RoutingSlip tiene un único nodo → no es realmente un "slip" con pasos.
---
## Diseño propuesto
## Implementación final (nombres reales)
Este plan se implementó con los siguientes nombres finales (para facilitar traceo/debug):
### Colas (consumers)
- `delete-track` (`DeleteTrackConsumer`)
- `delete-playList` (`DeletePlayListConsumer`)
### Execute endpoints (activities)
MassTransit crea las colas de execute como `<endpointName>_execute`.
- `remove-file-from-bucket` → cola `remove-file-from-bucket_execute`
- `mark-track-as-removing` → cola `mark-track-as-removing_execute`
- `delete-track-from-db` → cola `delete-track-from-db_execute`
- `mark-playList-as-removing` → cola `mark-playList-as-removing_execute`
- `delete-playList-from-db` → cola `delete-playList-from-db_execute`
### Estado de borrado (DB)
- Se introdujo `LifeCycleStatus` en `Track` y `PlayList` con valores `Active` y `Removing`.
- El handler marca `Removing` antes de publicar el evento (y persiste con `SaveChanges`).
> Nota: se eliminó el enfoque de “estado Removing” implícito en `ProcessingStatus` para deletes. El tracking de delete queda en `LifeCycleStatus`.
### Actividades compartidas / reutilizables
Ya existe (y se mantiene):
- `RemoveFileFromBucketActivity` — borra un único objeto de S3. Se usará N veces en cada slip.
Se añaden actividades de DB por aggregate:
- `MarkTrackAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste.
- `DeleteTrackFromDbActivity` — elimina el Track de EF (cascade elimina relaciones).
- `MarkPlayListAsRemovingActivity` — cambia estado `LifeCycleStatus` a `Removing` y persiste.
- `DeletePlayListFromDbActivity` — elimina la PlayList de EF (cascade elimina relaciones).
### Estructura del RoutingSlip para Track
```
DeleteTrackEvent
└─► DeleteTrackConsumer (queue: `delete-track`)
└─► DeleteTrackRoutingSlipBuilder.BuildAsync(...)
├─ MarkTrackAsRemovingActivity (endpoint: `mark-track-as-removing`)
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original picture
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small (si IsPicturesProcessed)
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium (si IsPicturesProcessed)
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large (si IsPicturesProcessed)
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original audio
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — processed audio (si IsAudioProcessed)
└─ DeleteTrackFromDbActivity (endpoint: `delete-track-from-db`)
```
> El builder carga la entidad para construir el plan (keys condicionales) antes de arrancar el slip. Las actividades de S3 son idempotentes (NotFound → log Information + continue).
### Estructura del RoutingSlip para PlayList
```
DeletePlayListEvent
└─► DeletePlayListConsumer (queue: `delete-playList`)
└─► DeletePlayListRoutingSlipBuilder.BuildAsync(...)
├─ MarkPlayListAsRemovingActivity (endpoint: `mark-playList-as-removing`)
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — original (uploads/{userId}/...) ← solo si no es preset
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — small ← solo si no es preset
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — medium ← solo si no es preset
├─ RemoveFileFromBucketActivity (endpoint: `remove-file-from-bucket`) — large ← solo si no es preset
└─ DeletePlayListFromDbActivity (endpoint: `delete-playList-from-db`)
```
### Handler de comando (ambos aggregates)
```csharp
// DeletePlayListCommandHandler — ANTES
await PublishRemoveFileEventsAsync(...); // 4 eventos sueltos
database.PlayLists.Remove(playlist);
await database.SaveChangesAsync();
// DESPUÉS
playlist.LifeCycleStatus = LifeCycleStatus.Removing;
await eventBus.PublishAsync(new DeletePlayListEvent(command.PlayListId, command.UserId));
await database.SaveChangesAsync();
// El workflow se encarga de borrar ficheros y eliminar en DB.
```
```csharp
// DeleteTrackCommandHandler — ANTES
// (ya publicaba DeleteTrackEvent, se mantiene igual)
// DESPUÉS
track.LifeCycleStatus = LifeCycleStatus.Removing;
await eventBus.PublishAsync(new DeleteTrackEvent(command.TrackId, command.UserId));
await database.SaveChangesAsync();
// La diferencia es que ahora el slip tiene pasos granulares, no una activity monolítica.
```
---
## Reglas de negocio a preservar
- Ownership se valida en el handler del comando (igual que ahora), antes de publicar el evento.
- PlayList: solo borrar original/sizes si no son presets.
- PlayList: el original está bajo `uploads/{userId}/...` (cambio reciente).
- Track: respetar estados de procesamiento — si audio está `Pending/Processing`, el handler ya bloquea el delete antes de llegar al workflow.
- EF cascade elimina `PlayListHasTrack` al borrar la PlayList → no hace falta activity explícita para ello.
---
## Idempotencia / reintentos
- RoutingSlip + reintentos de endpoint ya configurados en MassTransit.
- Operaciones S3: idempotentes por diseño. Si el key no existe (`NoSuchKey`/`NotFound`) → log `Information` y `Completed()` (no throw).
- DB: si la entidad ya no existe cuando llega `DeleteTrackFromDbActivity` / `DeletePlayListFromDbActivity`, el workflow se considera `Completed` (misma lógica que ahora).
- Estado `Removing`: permite identificar entidades en proceso de borrado si el sistema se reinicia a mitad del slip.
---
## Plan de implementación (paso a paso)
### 1. Inventario
- Confirmar todos los usos de `RemoveFileEvent` / `RemoveFileConsumer` en la solución.
- Si no tienen otros usos fuera de playlist delete → marcarlos para eliminación en el paso 7.
### 2. Refactorizar Track delete ← **nuevo**
- Crear `MarkTrackAsRemovingActivity` (execute-activity): carga Track, setea estado `Removing`, persiste.
- Crear `DeleteTrackFromDbActivity` (execute-activity): carga Track, llama `Remove()`, persiste.
- Actualizar `DeleteTrackRoutingSlipBuilder`:
- Carga el Track para calcular el plan (keys condicionales según `IsPicturesProcessed`, `IsAudioProcessed`).
- Encadena: `MarkTrackAsRemoving` → N × `RemoveFileFromBucket` → `DeleteTrackFromDb`.
- Eliminar `DeleteTrackActivity` monolítica (ya no se usa).
- Ajustar registration DI de MassTransit (quitar activity vieja, registrar las nuevas).
### 3. Crear PlayList delete: evento + consumer + builder
- Añadir `DeletePlayListEvent(PlayListId, UserId)` en `Application/Events`.
- Crear `DeletePlayListConsumer` en Infrastructure: recibe el evento y ejecuta el builder.
- Crear `DeletePlayListRoutingSlipBuilder`:
- Carga la PlayList para calcular el plan (keys condicionales por presets, ruta de original).
- Encadena: `MarkPlayListAsRemoving` → N × `RemoveFileFromBucket` → `DeletePlayListFromDb`.
### 4. Crear actividades de PlayList
- `MarkPlayListAsRemovingActivity` (execute-activity): carga PlayList, setea `Removing`, persiste.
- `DeletePlayListFromDbActivity` (execute-activity): carga PlayList, llama `Remove()`, persiste (cascade).
### 5. Actualizar `RemoveFileFromBucketActivity` para NotFound
- Si el SDK lanza excepción por key inexistente → capturar, log `Information("Key {Key} not found, skipping")`, retornar `Completed()`.
- Si es otro error → log `Error` + throw (reintento normal).
### 6. Cambiar `DeletePlayListCommandHandler`
- Reemplazar `PublishRemoveFileEventsAsync` por `eventBus.PublishAsync(new DeletePlayListEvent(...))`.
- No publicar más `RemoveFileEvent` para playlist delete.
### 7. Ajustar DI / registration MassTransit
- Registrar: `DeletePlayListConsumer`, `MarkPlayListAsRemovingActivity`, `DeletePlayListFromDbActivity`.
- Registrar: `MarkTrackAsRemovingActivity`, `DeleteTrackFromDbActivity`.
- Desregistrar: `DeleteTrackActivity` (monolítica), y si procede `RemoveFileConsumer`.
### 8. Eliminar código muerto
- `DeleteTrackActivity` monolítica.
- `RemoveFileEvent` y `RemoveFileConsumer` (si no tienen otros usos).
- Endpoints y registrations asociados.
### 9. Build y verificación
- `dotnet build -c Release` sin warnings de referencias muertas.
- Smoke test manual: borrar un Track y una PlayList, verificar logs de cada activity en orden.
-263
View File
@@ -1,263 +0,0 @@
# Hardening: abuso de pre-signed uploads (Opción B)
## Contexto
Con la Opción B el backend genera URLs pre-firmadas (pre-signed) para que el cliente suba ficheros directamente a S3, y luego el backend crea/actualiza entidades en DB referenciando `pictureName/audioName`.
Esto mejora latencia y descarga el host/servicio, pero abre un vector de abuso: un usuario puede llamar muchas veces a `RequestPlayListPictureUpload` / `RequestTrackUploadUrls` y subir muchos objetos a S3 sin llegar a crear la entidad final (objetos “huérfanos”).
## Qué pasa hoy si el usuario spamea
- Se generan muchas URLs pre-firmadas (costo bajo pero no cero).
- El usuario puede subir muchos objetos a S3 sin relación con una entidad en DB.
- El impacto principal es:
- coste/uso de S3 (storage + requests)
- posible saturación del componente de generación de URLs
## Objetivo
Reducir abuso y coste, manteniendo:
- UX simple
- backend sin streams
- workflows asíncronos + MassTransit Outbox
## Qué significa “sólido” aquí
Como el upload es directo a S3 (o compatible), el backend no puede garantizar “single-use” del upload en el lado del storage mientras una URL sea válida. Por tanto, el objetivo realista es:
- Asegurar que **cualquier abuso solo pueda impactar un prefijo temporal** barato de limpiar.
- Poner límites duros (cuotas) en DB para que el coste tenga un techo.
- No aceptar referencias a objetos no válidos (verificación antes de consumir).
- Evitar pérdida de datos en reemplazos (no borrar la versión previa hasta éxito).
## Principio de configuración (no hardcode)
Todas las rutas/prefijos/TTLs/límites deben ser **configurables**.
Regla:
- En `Application`/`Infrastructure` no debe haber strings hardcodeadas de keys S3 (p.ej. `temp/`, `uploads/`, `PlayLists/`, `Tracks/`).
- Las rutas deben centralizarse en opciones/config (p.ej. `StorageRoutes`, `PlayListRoutes`, `TrackRoutes`, o similar), de forma que cambiar la convención de keys no requiera tocar lógica.
Los ejemplos de este documento son **convenciones sugeridas** y deben leerse como plantillas parametrizadas.
## Invariantes de seguridad (reglas que no se rompen)
- El cliente solo puede escribir en rutas temporales bajo `{TempRootPrefix}/...` (o el prefijo temporal equivalente). La ruta final **nunca** recibe credenciales pre-firmadas.
- Cada intent emite un `Key` **nuevo** (GUID). No hay keys “estables” para reusar.
- Una entidad solo apunta a un `pictureName/audioName` si su intent asociado se valida y se consume.
- Reemplazos: el borrado de la versión anterior ocurre **al final** del workflow, tras actualizar DB.
## Prioridad (coste alto)
Si el coste es el principal riesgo, el orden recomendado es:
1) **Cuotas duras en DB** (intents activos + bytes activos) + job de expiración.
2) Prefijo temporal + cleanup por prefijo (lifecycle si existe, y job como fallback).
3) Validación de objeto (HEAD) antes de consumir.
4) (Opcional) Migrar a pre-signed POST si el backend de storage lo soporta bien.
## Decisiones validadas (no tocaría)
- El orden de prioridad (cuotas → prefijo temporal → HEAD → POST) es el más sensato cuando el **coste** es el riesgo principal.
- La advertencia sobre S3-compatible / SeaweedFS es importante: el diseño debe ser seguro sin depender de features avanzadas.
- El principio de no hardcodear rutas y centralizar prefijos/convenciones en config es coherente con el estilo del repo.
- La semántica de errores debe permanecer separada del mapeo a códigos/respuestas (responsabilidad del host/presentación).
- En reemplazos, no borrar la versión anterior hasta éxito del workflow es clave para resiliencia con Courier.
## Mitigaciones recomendadas (pueden coexistir)
### 1) “Upload intents” en DB (control y conciliación)
Persistir una intención de upload cuando se genera un pre-signed URL.
> Importante: **no usar el nombre original del fichero** como `pictureName/audioName`. La key debe usar un **identificador generado** (p.ej. GUID) para evitar colisiones, inputs “raros” y facilitar cleanup.
Sugerencia práctica:
- `objectName = {guid}.{ext}` (o directamente `{guid}` si la extensión no aporta valor)
- Guardar `originalFileName` solo como metadata opcional (si se necesita para UI), pero **nunca** como key.
Modelo sugerido (mínimo):
- `UploadIntent`
- `Id`
- `UserId`
- `Bucket`
- `Key`
- `ContentType`
- `ExpectedSizeBytes` (opcional; si el caller conoce el tamaño)
- `ExpiresAt`
- `Status`: `Issued | Consumed | Expired`
- `CreatedAt`
Opcional (útil para auditoría/UX):
- `OriginalFileName`
- `Purpose` (`TrackPicture | TrackAudio | PlayListPicture`)
Reglas:
- Al pedir URL (Request*Upload*):
- insertar intent `Issued`
- imponer límites por usuario (ver “Cuotas”)
- si supera el límite: fallar con un error semántico (p.ej. `UploadIntentLimitExceeded`)
- Al crear/actualizar entidad (Create*/Update* V2):
- validar que el `pictureName/audioName` recibido corresponde a un intent `Issued` del mismo `UserId` y no expirado
- validar que el objeto existe en S3 (HEAD) y que tamaño / `Content-Type` cumplen la policy del backend
- marcar intent como `Consumed`
#### Cuotas (anti-cost) 🔴 Alta
Además del conteo de intents, imponer **cuotas por bytes** para que el coste tenga techo.
Recomendación mínima:
- `MaxActiveUploadIntentsPerUser` (número)
- `MaxActiveUploadBytesPerUser` (bytes) — suma de objetos temporales “en vuelo” por usuario
Cómo aproximarlo sin streams:
- Guardar `ExpectedSizeBytes` en el intent (si está disponible).
- Si no se dispone del tamaño, usar un valor conservador por `Purpose` (p.ej. imagen <= X, audio <= Y) y validarlo en HEAD antes de consumir.
> Nota: si el caller puede mentir sobre tamaño, no confíes ciegamente en `ExpectedSizeBytes`; úsalo como guardrail y valida al consumir.
#### Semántica de errores (intent inválido) 🟡 Media
Definir errores semánticos y dejar el mapeo al “caller” (host/presentación):
- `UploadIntentLimitExceeded`
- `UploadIntentNotFoundOrNotAccessible` (si se quiere no filtrar existencia)
- `UploadIntentAlreadyConsumed`
- `UploadIntentExpired`
- `UploadObjectNotFound`
- `UploadObjectPolicyViolation` (tamaño, `Content-Type`, extensión, etc.)
#### Nota sobre race conditions en el límite 🟡 Media
El check “`count(Issued) < Max`” es vulnerable a concurrencia (dos requests paralelas pueden pasar el check y crear 2 intents extra).
Mitigaciones (de menor a mayor robustez):
- Ejecutar **check + insert** en la **misma transacción** con aislamiento fuerte (p.ej. `SERIALIZABLE`).
- Usar un mecanismo de lock por usuario (p.ej. advisory lock en PostgreSQL) durante la emisión.
- Modelar el límite con una tabla/contador por usuario y aplicar **update condicional** (estilo “compare-and-swap”).
#### Job de expiración de intents 🔴 Alta
Aunque `ExpiresAt` esté en DB, hace falta un mecanismo que marque y limpie intents caducados:
- Un job periódico (worker/cron) que:
- marque intents `Issued` con `ExpiresAt < now` como `Expired`
- opcionalmente encole el borrado del objeto S3 (si existe)
Si se aplica lifecycle en S3 (ver sección 2), el job puede ser “solo DB” (marcar `Expired`) y dejar el borrado real a S3.
Adicionalmente, este job es un buen sitio para:
- borrar intents `Expired` antiguos (retención de auditoría configurable)
- emitir métricas/contadores (intents expirados por usuario)
Ventajas:
- Previene spam con reglas duras y auditables.
- Permite cleanup de huérfanos con certeza.
Coste:
- entidad + migración + lógica de validación.
### 2) Limitar el scope del pre-signed URL (reducción de daño)
Aplicar restricciones conservadoras:
- Reducir TTL:
- `ExpiresInSeconds` más bajo (p.ej. 60–120s en vez de 600s)
- Validar/limitar:
- `ContentType` permitido
- extensiones permitidas (`FileType`)
- Usar keys temporales por usuario:
- Opción recomendada (facilita lifecycle por prefijo):
- `{TempRootPrefix}/{userId}/playlists/{guid}.{ext}`
- `{TempRootPrefix}/{userId}/tracks/{guid}.{ext}`
- Alternativa (si se quiere mantener todo bajo `uploads/{userId}`):
- `{UploadsRootPrefix}/{userId}/PlayLists/TempUploads/{guid}.{ext}`
- `{UploadsRootPrefix}/{userId}/Tracks/TempUploads/{guid}.{ext}`
Luego, en el workflow/worker:
- mover/copiar desde la ruta temporal (p.ej. `temp/...` o `.../TempUploads/...`) a la ruta final (`OriginalPictures/...` / `OriginalAudios/...`)
- borrar el objeto temporal
Ventajas:
- Cleanup masivo sencillo (por prefijo `{TempRootPrefix}/`)
- Reduce el impacto de huérfanos.
#### S3 Lifecycle Rule en `TempUploads` 🔴 Alta
Configurar una **Lifecycle Rule** en el bucket para auto-expirar objetos bajo el prefijo de temporales, por ejemplo:
- Prefijo: `{TempRootPrefix}/`
- Expiración: 1–7 días (según tolerancia a reintentos)
Nota: las reglas de lifecycle por **prefijo** no soportan comodines en medio (p.ej. `uploads/*/...`).
Si se usa la alternativa `uploads/{userId}/.../TempUploads/...`, para filtrar correctamente se recomienda:
- cambiar la convención de key para que el prefijo sea común (p.ej. `{TempRootPrefix}/...`), o
- usar **tags** en los objetos (p.ej. `temporary=true`) y filtrar la lifecycle rule por tag (si se puede firmar el tagging en el upload).
Esto evita que el coste de huérfanos crezca indefinidamente incluso si el job de cleanup falla.
#### Cleanup backend (fallback) 🔴 Alta
No dependas solo de lifecycle del storage (en S3 compatibles puede variar). Añadir un job de cleanup que:
- liste por prefijo temporal `{TempRootPrefix}/`
- borre objetos con edad > `TempUploadsRetentionDays`
Este job debe ser:
- incremental (paginación)
- tolerante a errores (reintentos)
- con límites de throughput (para no auto-generar costes por borrar en masa)
#### Restricción de `Content-Type` en S3 🟡 Media
Si el objetivo es enforcement “duro” a nivel de S3 (no solo validación posterior), **pre-signed POST** suele ser más robusto que PUT.
**pre-signed PUT** (lo actual en este repo):
- Permite fijar `Content-Type` si se pre-firma incluyendo ese header.
- Es más limitado para imponer condiciones ricas (tamaño, tags, etc.).
- Recomendación: combinar con validación posterior (HEAD) antes de consumir el intent.
**pre-signed POST** (recomendado si buscas más control):
- Permite policy conditions como:
- `content-length-range` (tamaño máximo)
- `eq`/`starts-with` para `Content-Type`
- key/prefijo (el cliente no elige keys fuera del scope)
- headers requeridos (p.ej. SSE)
- opcional: tagging (p.ej. `temporary=true`) para lifecycle por tag
Trade-off: el cliente sube con `multipart/form-data` (POST), pero el backend sigue sin manejar streams.
#### Nota S3-compatible / SeaweedFS 🟡 Media
No todos los backends S3 compatibles implementan al 100%:
- policy conditions de POST
- tagging en upload
- respuestas/errores idénticos a AWS
Recomendación:
- Diseñar el sistema para ser seguro **sin depender** de condiciones avanzadas (keys únicas + prefijo temporal + cuotas + verificación HEAD).
- Tratar POST/condiciones como una mejora opcional, tras validar compatibilidad del backend.
#### Reemplazos: no borrar originales antiguos hasta éxito ✅
Al actualizar una imagen/audio existente:
- Subir el nuevo binario a la ruta temporal (p.ej. `{TempRootPrefix}/.../{newGuid}`).
- Procesar (resize/transcode) y subir a rutas finales con nombres nuevos.
- Actualizar la entidad en DB para apuntar a los nuevos `*PictureName/*AudioFolderName`.
- **Solo al final** borrar los objetos antiguos (original + sizes / audio procesado), cuando el workflow haya completado.
Esto evita perder la versión anterior si el worker falla a mitad (punto crítico para resiliencia).
## Validación del objeto antes de consumir (HEAD) 🔴 Alta
Antes de consumir un intent y actualizar DB:
- `HEAD Bucket/Key`
- validar `Content-Type` esperado (según `Purpose`)
- validar tamaño máximo (`MaxUploadBytes` o por propósito)
- (opcional) validar extensión inferida de `Key` si se usa
Si falla la validación:
- no consumir el intent
- marcarlo como violación o dejarlo expirar
- el cleanup se encarga del objeto
## Orden de implementación sugerido
1) Upload intents en DB (límite de activos + validación al crear/actualizar)
2) Reducir TTL y mover a rutas temporales + S3 lifecycle + cleanup
3) Si se requiere enforcement fuerte: migrar a pre-signed POST + policy conditions
## Parámetros recomendados (config)
- `UploadUrlExpiresInSeconds` (default: 120)
- `MaxActiveUploadIntentsPerUser` (default: 5)
- `MaxActiveUploadBytesPerUser` (default sugerido: depende de coste; p.ej. 200MB)
- `AllowedPictureExtensions` / `AllowedAudioExtensions`
- `AllowedContentTypes` (lista blanca)
- `TempUploadsRetentionDays` (default: 3)
- `PresignedUploadMethod` (default sugerido: `POST` si se quiere enforcement fuerte; `PUT` si se prioriza simplicidad)
- `MaxUploadBytes` (límite máximo de tamaño por upload)
Jobs (config):
- `UploadIntentsExpirationJobIntervalSeconds`
- `TempUploadsCleanupJobIntervalSeconds`
- `ExpiredIntentsRetentionDays`
Rutas/prefijos (ejemplos):
- `TempRootPrefix` (default sugerido: `temp`)
- `UploadsRootPrefix` (default sugerido: `uploads`)
## Notas
- Estas defensas complementan el Outbox: el Outbox garantiza entrega de eventos tras commit, pero no evita el abuso de generación de URLs o almacenamiento de objetos huérfanos.
-186
View File
@@ -1,186 +0,0 @@
# Tech stack y arquitectura (MusifyBackend)
Este documento describe **qué tecnologías usa este repo** y **cómo encajan** (capas, workers, mensajería, storage, observabilidad). Está pensado para alguien que llega de cero o para una IA que necesita contexto.
## Foto rápida
- Runtime: **.NET 10** (C#)
- Estilo: **Clean Architecture** (capas `Domain` / `Application` / `Infrastructure`)
- Casos de uso: **CQRS** con **DispatchR** (similar a MediatR)
- Persistencia: **Entity Framework Core** + **PostgreSQL** (provider `Npgsql`)
- Workers / mensajería: **MassTransit** + **RabbitMQ**
- Orquestación de workflows: **MassTransit Courier / RoutingSlip** (Activities)
- Storage de objetos: **SeaweedFS** expuesto como **S3-compatible** (vía AWS SDK `IAmazonS3`)
- Observabilidad: **OpenTelemetry** (traces + metrics, export OTLP)
- Procesado multimedia:
- Imágenes: **SixLabors.ImageSharp** (resize, WebP)
- Audio: **ffmpeg** (transcode a DASH)
## Estructura del repo (capas)
### `Domain/` (núcleo de negocio)
- Entidades puras (sin dependencias de infraestructura).
- Ejemplos: `Track`, `PlayList`, `Upload`, etc.
- Estados de negocio relevantes:
- `ProcessingStatus` (procesado en curso/finalizado, etc.).
- `LifeCycleStatus` (p.ej. `Active`, `Removing`) para deletes asíncronos.
### `Application/` (casos de uso + contratos)
- Contiene comandos/queries/handlers por feature.
- CQRS:
- `Commands/` y `Queries/` por feature (`PlayLists`, `Tracks`, `Users`, ...).
- `Handlers/` ejecutan la lógica de aplicación (coordinar DB + eventos), pero **no implementan** detalles de infraestructura.
- Contratos con infraestructura: `Application/Abstractions/Infrastructure/*`
- `IDatabase` (abstracto de EF DbContext)
- `IEventBus` (publicación de eventos)
- `IStorageService`, `IPictureService`, `IAudioTranscoderService`, etc.
- Eventos de integración: `Application/Events/*`
- Se publican desde `Application` y se consumen en `Infrastructure`.
### `Infrastructure/` (implementaciones + integraciones)
- Implementa las abstracciones de `Application`.
- Contiene:
- EF Core DbContext y migraciones
- MassTransit (consumers, activities, routing slips, outbox)
- Integración S3 (SeaweedFS) mediante AWS SDK
- Observabilidad OpenTelemetry
- Servicios de procesado (ImageSharp/ffmpeg)
## CQRS con DispatchR
- Los “use cases” se modelan como:
- **Commands**: mutaciones (crear/actualizar/borrar).
- **Queries**: lecturas.
- Los handlers viven en `Application/*/Handlers/` y dependen de:
- `IDatabase` para persistencia (sin acoplarse al DbContext concreto).
- `IEventBus` para emitir eventos cuando hay trabajo asíncrono.
## Persistencia (EF Core + PostgreSQL)
- DbContext: `Infrastructure/Persistence/Database.cs`
- Usa `UseNpgsql(connectionString)`.
- Incluye tablas de MassTransit Outbox/Inbox (ver sección de mensajería).
- DI: `Infrastructure/Persistence/DatabaseDependencyInjection.cs`
- Config: `Infrastructure/Configuration/DatabaseConfiguration.cs` (sección `Database`)
- `ConnectionString`
## Mensajería y workers (MassTransit + RabbitMQ)
### Qué se usa
- Broker: RabbitMQ (`MassTransit.RabbitMQ`).
- MassTransit está configurado en dos modos:
1) **Cliente** (publica eventos): `Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs` → `AddMassTransitClient()`
2) **Workers** (consumen y ejecutan activities): `AddMassTransitConsumers()`
### Outbox / Inbox (robustez)
- Se usa **EntityFramework Outbox** (`AddEntityFrameworkOutbox<Database>()`) con:
- `UsePostgres()`
- `UseBusOutbox()`
- `QueryDelay = 1s`
- `DuplicateDetectionWindow = 30m`
- En el DbContext se registran entidades de MassTransit:
- `AddInboxStateEntity()`
- `AddOutboxMessageEntity()`
- `AddOutboxStateEntity()`
Idea clave:
- La outbox asegura que, tras confirmar la transacción de DB, los mensajes/eventos pendientes se publican de forma fiable.
### Courier / RoutingSlip (workflows asíncronos)
Para operaciones multi-paso (p.ej. imagen/audio y deletes), se usa **MassTransit Courier**:
- Un “RoutingSlip” es una lista de pasos.
- Cada paso es una **Activity** (con args + log opcional).
- Existen dos tipos que aparecen en este repo:
- `ExecuteActivity` (endpoint `<name>_execute`), típicamente acciones atómicas.
- `Activity` (con compensación/log), típico para pasos de workflow.
Dónde mirar:
- Builders: `Infrastructure/MassTransit/RoutingSlip/Builders/*`
- Activities: `Infrastructure/MassTransit/Activities/*`
- Consumers (punto de entrada del workflow): `Infrastructure/MassTransit/Consumers/*`
- Registro de endpoints: `Infrastructure/MassTransit/DependencyInjection/*`
Ejemplos de workflows presentes:
- Procesado de imagen (resize + upload + update DB)
- Procesado de audio (transcode DASH + upload + update DB)
- Deletes asíncronos (marcar “removing”, borrar objetos, borrar filas)
### Config de MassTransit
- `Infrastructure/Configuration/MassTransitConfiguration.cs` (sección `MassTransit`)
- `Address`
- `Username`
- `Password`
## Storage de objetos (SeaweedFS como S3-compatible)
### Qué se usa
- SDK: `Amazon.S3` (`IAmazonS3`).
- Implementación: `Infrastructure/Services/StorageService.cs` (usa `IAmazonS3`).
- DI: `Infrastructure/Services/ServicesDependencyInjection.cs` → `AddStorageService()`
### Config
- `Infrastructure/Configuration/InfrastructureStorageConfiguration.cs` (sección `InfrastructureStorage`)
- `Address` (ServiceURL)
- `AccessKey` / `SecretAccessKey`
- `ForcePathStyle` (útil en S3-compatibles)
- `UseHttp`
Notas prácticas (S3-compatible):
- Algunas features avanzadas de AWS S3 pueden no comportarse igual (depende de SeaweedFS). Diseñar workflows para que sean correctos con el subconjunto básico: PUT/GET/HEAD/LIST/DELETE y paths bien definidos.
## Procesado multimedia
### Imágenes
- Librería: `SixLabors.ImageSharp`
- Servicio: `Infrastructure/Services/PictureService.cs`
- Contrato: `Application/Abstractions/Infrastructure/IPictureService.cs`
- Patrón: workers descargan/transforman/suben; la app coordina por eventos.
### Audio
- Herramienta: `ffmpeg` (binario externo)
- Config: `Infrastructure/Configuration/AudioTranscoderConfiguration.cs` (sección `AudioTranscoder`)
- Timeout, codec/bitrate/channels/sample-rate, segmentación DASH, etc.
- Servicio: `Infrastructure/Services/AudioTranscoderService.cs`
- Contrato: `Application/Abstractions/Infrastructure/IAudioTranscoderService.cs`
### Directorio temporal (workers)
- Config: `Infrastructure/Configuration/WorkerConfiguration.cs` (sección `Workers`)
- `Routes.TemporaryFilesDirectory`
## Observabilidad (OpenTelemetry)
- DI: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs`
- Config: `Infrastructure/Configuration/OpenTelemetryConfiguration.cs` (sección `OpenTelemetry`)
- `OtlpEndpoint`
Qué instrumenta:
- Tracing:
- `MassTransit`
- ASP.NET Core
- HttpClient
- EF Core
- Exporter OTLP
- Métricas:
- `MassTransit`
- ASP.NET Core
- HttpClient
- Runtime + Process
- Exporter OTLP
## Identidad (Keycloak)
- El repo asume integración con **Keycloak** (ver `AGENTS.md` y nombres como `GetUserByKeycloakQuery`).
- La capa `Application` trata el “Keycloak Id” como identificador de usuario en algunos casos de uso.
- La integración concreta (tokens, validación, etc.) puede vivir fuera de estas 3 capas o en otro proyecto/host.
## Glosario rápido
- **Command / Query**: operación de escritura / lectura.
- **Handler**: clase que ejecuta el caso de uso (CQRS).
- **EventBus**: abstracción para publicar eventos (MassTransit por debajo).
- **Consumer**: “listener” de mensajes en una cola.
- **RoutingSlip**: workflow compuesto por Activities.
- **Activity**: paso del workflow (puede tener log/compensación).
- **Outbox**: patrón para publicar eventos de forma fiable tras commit de DB.
## “Dónde empiezo a leer” (ruta recomendada)
1) Arquitectura general: `AGENTS.md`
2) Casos de uso: `Application/*/Commands`, `Application/*/Queries` y sus `Handlers/`
3) Persistencia: `Infrastructure/Persistence/Database.cs` + migraciones
4) Mensajería/workflows: `Infrastructure/MassTransit/*`
5) Integraciones: `Infrastructure/Services/*` (S3, imagen, audio)
6) Observabilidad: `Infrastructure/Observability/OpenTelemetryDependencyInjection.cs`