TCMProject.Messaging
26.9.15.1
dotnet add package TCMProject.Messaging --version 26.9.15.1
NuGet\Install-Package TCMProject.Messaging -Version 26.9.15.1
<PackageReference Include="TCMProject.Messaging" Version="26.9.15.1" />
<PackageVersion Include="TCMProject.Messaging" Version="26.9.15.1" />
<PackageReference Include="TCMProject.Messaging" />
paket add TCMProject.Messaging --version 26.9.15.1
#r "nuget: TCMProject.Messaging, 26.9.15.1"
#:package TCMProject.Messaging@26.9.15.1
#addin nuget:?package=TCMProject.Messaging&version=26.9.15.1
#tool nuget:?package=TCMProject.Messaging&version=26.9.15.1
TCMProject.Messaging
Mensajería del ecosistema TCM: outbox transaccional + transporte RabbitMQ, sobre Wolverine.
Un servicio declara su nombre y qué consume; el paquete pone el resto — topología, reintentos, cola de error, propagación de trazas y validación de configuración.
Convive con MediatR. No lo reemplaza ni lo toca.
// Program.cs — la única línea de mensajería
builder.Host.AddTcmMessaging<CoreDBContext>(builder.Configuration, m =>
{
m.AddConsumer<MiConsumidor>(); // solo si el servicio consume
});
// CoreDBContext.OnModelCreating — mapea las tablas del outbox
if (provider == "Npgsql.EntityFrameworkCore.PostgreSQL")
{
modelBuilder.AddTcmOutboxTables();
}
Configuración — sección Messaging
Convención: la sección va COMPLETA en el
appsettingsde cada entorno. Nada se deja al default. Un valor implícito es un valor que nadie revisa cuando algo falla.
{
"ConnectionStrings": {
"DBConnectionString": "Host=...;Database=...;Username=...;Password=..."
},
"Messaging": {
"Enabled": true,
"ServiceName": "scheduleeasycore",
"RabbitMq": {
"Enabled": true,
"Host": "rabbitmq",
"Port": 5672,
"VHost": "/clinilize-dev",
"User": "scheduleeasycore",
"Password": "",
"UseSsl": false,
"Prefetch": 16
},
"Outbox": {
"Schema": "wolverine",
"RetentionDays": 7
}
}
}
Raíz
| Clave | Tipo | Default | Obligatoria | Qué hace |
|---|---|---|---|---|
Enabled |
bool | true |
no | Interruptor general. En false Wolverine no se registra en absoluto |
ServiceName |
string | — | sí | Identifica al emisor en el sobre y prefija las colas del servicio |
Messaging:RabbitMq
| Clave | Tipo | Default | Obligatoria | Qué hace |
|---|---|---|---|---|
Enabled |
bool | true |
no | En false: outbox sí, transporte no. Los mensajes se persisten y esperan |
Host |
string | localhost |
sí (si Enabled) |
Host del broker. Rechazado si es localhost fuera de Development |
Port |
int | 5672 |
sí (si Enabled) |
Puerto AMQP. Debe ser > 0 |
VHost |
string | /clinilize-dev |
sí (si Enabled) |
Virtual host. Uno por entorno |
User |
string | guest |
sí (si Enabled) |
Credenciales por servicio, no un usuario compartido |
Password |
string | guest |
sí fuera de Development |
Vacío fuera de Development hace fallar el arranque |
UseSsl |
bool | false |
no | amqps://. Encenderlo fuera de desarrollo local |
Prefetch |
int | 16 |
no, pero > 0 | Mensajes sin confirmar que el broker adelanta por consumidor. Es el dial de throughput contra memoria: alto llena la RAM del contenedor con mensajes sin procesar; bajo deja al consumidor pidiendo de a uno |
Messaging:Outbox
| Clave | Tipo | Default | Obligatoria | Qué hace |
|---|---|---|---|---|
Schema |
string | wolverine |
sí | Esquema Postgres de las tablas de durabilidad. Separado del de negocio a propósito |
RetentionDays |
int | 7 |
sí, > 0 | Días que se conservan los sobres ya entregados. Sin esto la tabla crece sin techo |
Fuera de la sección, pero obligatoria
| Clave | Qué hace |
|---|---|
ConnectionStrings:DBConnectionString |
El outbox no tiene connection string propia: reusa la del servicio. Es a propósito — tiene que vivir en la misma base que el cambio de negocio, porque si no haría falta una transacción distribuida, y Npgsql no las soporta |
⚠️ Los dos interruptores: cuál usar
Es lo que más confunde. La diferencia real está en la última columna:
Clave en false |
Wolverine | Outbox | ¿Necesita el esquema wolverine para arrancar? |
|---|---|---|---|
Messaging:Enabled |
no se registra | no | No |
Messaging:RabbitMq:Enabled |
se registra | sí | Sí |
Apagar solo el transporte NO evita la comprobación del almacén, porque el outbox es
independiente de RabbitMQ. Un servicio con RabbitMq:Enabled=false y sin el esquema no
arranca:
The Wolverine message storage for database 'default' is missing
(could not read '"wolverine".wolverine_incoming_envelopes')
Regla: mientras un servicio no tenga broker aprovisionado y no publique nada,
Messaging:Enabled=false es más seguro — elimina la dependencia del esquema en el arranque.
Apagado, ITcmOutbox se registra igual pero lanza al publicar. Es deliberado: descartar
mensajes en silencio convertiría un interruptor en pérdida de datos.
La configuración inválida revienta el arranque
Se valida al construir el host y se lanza con todos los problemas juntos:
Configuración de mensajería inválida (2 problema(s)):
- Messaging:RabbitMq:Host apunta a 'localhost' en el entorno 'Production'.
Casi seguro falta la sección Messaging en el appsettings de este entorno.
- Messaging:RabbitMq:Password vacío en el entorno 'Production'.
Es a propósito: el fallo contrario es invisible. Wolverine reintenta la conexión en segundo
plano, así que un servicio mal configurado arranca, responde HTTP y su /status queda
verde mientras los mensajes se acumulan sin salir.
Trampas al adoptar
| Trampa | Síntoma | Qué hacer |
|---|---|---|
Tests que arrancan la app (WebApplicationFactory) |
"The Wolverine message storage … is missing" en toda la capa de tests de API | builder.UseSetting("Messaging:Enabled", "false") en la factory |
dotnet ef migrations add para las tablas de Wolverine |
Migración con Up() y Down() vacíos |
Van marcadas ExcludeFromMigrations. Generar con db-ef-migration add |
EF Core por debajo de 10.0.4 |
NU1605 al restaurar |
WolverineFx.EntityFrameworkCore exige ese piso |
| Buscar el sobre en la tabla después del commit | Siempre 0 filas | Se entrega inline y se borra. Consultar dentro de la transacción |
Publicar desde un comando con [SkipCommandTransaction] |
El flush y el cambio de negocio quedan en transacciones distintas | No hacerlo sin TransactionScope ambiental |
API
| Miembro | Para qué |
|---|---|
AddTcmMessaging<TDbContext>(config, cfg => …) |
Registro. Extensión de IHostBuilder |
TcmMessagingOptions.AddConsumer<T>() |
Declara un consumidor. Explícito a propósito: el descubrimiento convencional de Wolverine está apagado, porque si no reclamaría todos los *CommandHandler.Handle de MediatR |
TcmMessagingOptions.ConfigureWolverine |
Escape hatch sobre las WolverineOptions crudas. Usar con criterio: cada llamada es una convención que ese servicio deja de compartir |
modelBuilder.AddTcmOutboxTables(schema) |
Mapea las tablas de durabilidad en el modelo. No las crea |
ITcmOutbox.PublishAsync<T>(msg, ct) |
Encola, no envía |
ITcmOutbox.SaveChangesAndFlushAsync(ct) |
Guarda entidades y persiste los sobres, en la misma operación |
Documentación completa
En la knowledge base de TCM:
platform/infrastructure/messaging-design.md— el diseño, las garantías y el porquéplatform/infrastructure/messaging-adoption-guide.md— paso a paso para adoptarlo
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Configuration.Binder (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- WolverineFx (>= 6.30.3)
- WolverineFx.EntityFrameworkCore (>= 6.30.3)
- WolverineFx.Postgresql (>= 6.30.3)
- WolverineFx.RabbitMQ (>= 6.30.3)
- WolverineFx.RuntimeCompilation (>= 6.30.3)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on TCMProject.Messaging:
| Package | Downloads |
|---|---|
|
TCMProject.Audit
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
TCMProject.Events: se ELIMINA AppointmentNoShowV1. Era el contrato piloto del bus, elegido a proposito por ser inofensivo -un hecho consumado que nadie consumia con efecto de negocio- para ejercitar outbox, RabbitMQ y consumidor de punta a punta. Cumplio esa funcion: el ciclo completo quedo demostrado por el pipeline de auditoria, que cruza cuatro procesos con un contrato real. Ningun servicio del ecosistema lo referenciaba fuera de scheduleeasycore, y alli solo lo usaban el consumidor de humo y los tests de outbox, que pasan a un contrato de prueba propio. CAMBIO INCOMPATIBLE si alguien lo usaba: las versiones anteriores siguen en nuget.org. AuditChangeRecordedV1 NO se toca. Nota anterior (26.9.10.1) - TCMProject.Messaging: sin TransactionScope, el flush se dispara por el evento SavedChanges del DbContext y no por un ISaveChangesInterceptor del contenedor. Medido: EF Core no descubria ese interceptor desde DI y los sobres quedaban sin soltar en jobs, consumidores y comandos con SkipCommandTransaction. El evento no depende de como se haya registrado el contexto, y este paquete no controla el AddDbContext del servicio. Nota anterior (26.9.9.5) - TCMProject.Messaging: el outbox ENTREGA SOLO tras el commit. Publicar alcanza. Antes, PublishAsync encolaba y el sobre quedaba en la tabla como propiedad del nodo esperando un flush que en algunos caminos no llegaba nunca; el agente de durabilidad no lo tocaba porque solo reclama sobres de nodos MUERTOS. Sintoma: cero errores, la publicacion parece correcta, y la cola no recibe nada mientras el outbox crece. Ahora la garantia vive en el outbox y no en quien publica. Con TransactionScope, PublishAsync se suscribe a TransactionCompleted: eso solo REGISTRA una funcion, no abre conexiones; el flush corre cuando el evento dispara, o sea despues del commit, y solo si Status es Committed. Sin TransactionScope (jobs, consumidores, SkipCommandTransaction), OutboxFlushInterceptor suelta al terminar el SaveChanges, que ahi ya commiteo por su cuenta; EF Core lo descubre solo del contenedor. NO se suelta durante el SaveChanges a proposito: con la conexion de EF abierta, el flush abre una SEGUNDA conexion, .NET promueve a transaccion distribuida y Postgres responde 55000 prepared transactions are disabled, abortando tambien el cambio de negocio. El flush va con TransactionScopeOption.Suppress o intentaria enlistarse en la transaccion recien cerrada. MultiFlushMode AllowMultiples: con el default OnlyOnce, un sobre encolado DESPUES de un flush se descarta en silencio (JasperFx/wolverine issue 1825). NUEVO Outbox:StaleMinutes (10 por defecto) libera los sobres que nadie solto; es la RED y no el camino, alto a proposito para que en operacion normal el outbox este vacio y la metrica de profundidad siga sirviendo de alarma. SaveChangesAndFlushAsync queda OBSOLETO: suelta dentro del TransactionScope, antes del commit final. TCMProject.Audit no cambia. Nota anterior (26.9.9.4) - TCMProject.Utilities: recupera PLATFORM_RESEND_EMAILS=121 en main (publicado en 26.8.25.6 y en uso por tcmadmingateway; las 26.9.x lo habian perdido y rompian la compilacion del gateway). Ademas, permisos de Ulandin (26.9.9.3): ModuleEnum.PLATFORM_ULANDIN (anexado al final, enum sin valores explicitos) y PermissionEnum PLATFORM_VIEW_ULANDIN=122 / PLATFORM_MANAGE_ULANDIN=123, dentro del rango 121-129 reservado para PLATFORM_*; el 121 sigue reclamado por la rama de reenvio de correos. El seeder de tcmaccounts los concede solo a Owner/Administrator de TCM-X al adoptar esta version. Los demas paquetes se republican solo por la version compartida, sin cambios. Nota anterior (26.9.9.2) - TCMProject.Audit: primera version. Log de cambios campo a campo -quien cambio que, cuando, valor anterior y nuevo- para las APIs del ecosistema. Se engancha via DbContextOptions: no hay que tocar ningun handler ni repositorio. Fase 0 del RFC (knowledge-base/platform/infrastructure/audit-trail-design.md). OJO AL ADOPTAR: AddTcmAudit REVIENTA el arranque si Messaging:Enabled=false o Messaging:RabbitMq:Enabled=false. No es capricho: el outbox aguanta que el broker se CAIGA -el sobre queda persistido y se recupera solo- pero SIN transporte configurado Wolverine no encuentra ruta, dispara NoRoutesFor y descarta el mensaje sin persistirlo. Para auditoria eso rompe el requisito 6 y encima en silencio. Un servicio adopta la auditoria DESPUES de tener broker, no antes. La auditoria es opt-in por entidad y por campo (leccion de Salesforce: auditar todo multiplica el volumen de escritura); lo que no se declara con audit.Entity de T o [Audited], no se audita. Deny-list global: RowVersion, AppCreatedBy, AppCreationDate, AppLastUpdatedBy y AppLastUpdatedDate nunca se auditan -son ruido, el encabezado ya los trae- sin ella cada update generaria tres diffs de basura. Valores en JSON invariante, no ToString(), para que un decimal o un DateTime no queden distintos segun la cultura del servidor. Redact() guarda solo el hash; Truncate() guarda hash mas longitud por encima del umbral. FailOpen es el unico modo: un fallo de captura NO tumba la escritura de negocio, se loguea con severidad alta. TCMProject.Events suma los contratos de auditoria (AuditChangeRecordedV1, AuditFieldChange y sus enums) y sigue SIN dependencias, para que TCM.AuditApi pueda referenciarlo sin heredar EF ni el transporte. TCMProject.Messaging no cambia de contenido; se republica porque Audit lo referencia por ProjectReference y la dependencia queda fijada a esta misma version. Verificado con 20 tests, sin Postgres ni RabbitMQ: deny-list, opt-in, redactado, truncado, delete, impersonacion, captura por SavingChanges Y SavingChangesAsync -RepositoryBase.Update usa el sincrono, cubrir solo el asincrono dejaria sin auditar dos servicios enteros-, y que la validacion reviente cuando falta transporte.