Remove docs and agents
This commit is contained in:
@@ -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`.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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`
|
|
||||||
Reference in New Issue
Block a user