Use explicit Razor interpolation for create and edit URLs and cover the rendered link syntax with a regression test. Ref: IT-1039
7.7 KiB
ItemTracker Outbox Email Consumer
Contract and routing
The Worker consumes durable messages from queue notification.send-email. ItemTracker publishes through RabbitMQ's default exchange, so the queue name is also the routing key. No custom exchange binding is required.
HrynCo.RabbitMq owns the transport lifecycle: connection, queue declaration, deserialization, prefetch, structured delivery context, retry, terminal-failure notification, and manual ACK/NACK. Notification Service only supplies its queue, contract validation, email handler, and result publisher through the package's generic extension points. This integration requires package version 1.0.17 or newer.
Accepted messages must have all of the following:
- AMQP
Type:Notification.SendEmail.v1 - AMQP
MessageId: the non-empty stable ItemTracker Outbox ID - JSON content using the PascalCase
SendEmailMessageenvelope - non-empty
CorrelationContext.CorrelationId - non-empty
ServiceName,TemplateKey,RecipientEmail,RecipientName, andLanguageCode - a valid recipient address and a non-null
Variablesobject
The service resolves a template by the exact ServiceName, TemplateKey, and normalized lower-case LanguageCode. It does not silently fall back to another language. Every variable marked Required by the selected template must have a non-empty value before rendering.
Delivery and acknowledgement policy
The consumer uses manual acknowledgements and prefetches one message at a time.
- Unsupported contract metadata, malformed JSON, or invalid required fields are logged and nacked without requeue.
- Template, provider, quota, SMTP, or usage-accounting failures are retried in-process three times with a five-second delay.
- The message is acknowledged only after SMTP accepts the email and provider usage is incremented.
- If
CorrelationContext.ReplyTois present, result publication is attempted after delivery. It is best-effort: a reply-queue failure is logged but does not make the SMTP delivery fail, because retrying after SMTP success could send a duplicate email. - After the final processing retry fails, the shared terminal-failure hook publishes one
negative result to the same
ReplyTo. It retains the original correlation and contains a normalized diagnostic string limited to 2000 characters. Common SMTP DNS, connection-refused, timeout, and recipient-rejection failures receive safe explanatory context while retaining the low-level provider reason. Hostnames, recipient addresses, and credentials are not added. Client outboxes store this as a neutral delivery error rather than interpreting SMTP-specific exception types.
Success and terminal failure result publication remain best-effort. If the result queue cannot be reached, the internal exception is logged and the original delivery follows its normal ACK/NACK policy. Clients therefore must not interpret the absence of a result as a confirmed failure.
Delivery remains at least once. There is no inbox/deduplication store in this MVP, so a broker redelivery or a process failure after SMTP acceptance but before acknowledgement can produce a duplicate. MessageId, CorrelationId, and MessageType are added to the structured log scope for correlation. Recipient addresses are masked, and template variable names/values and rendered links are not logged.
No database migration is required. Existing channel selection, SMTP settings, quota checks, and usage counters remain in place.
Local end-to-end setup
With Docker Desktop running, install and start the complete Notification Service development stack from the repository root:
.\docker\environments\install-development.cmd
The script uses the tracked docker/environments/.env.Development defaults. The
following manual setup is only needed when overriding those values locally.
Create a local environment file outside source control, for example docker/environments/.env.local, with these values:
DB_NAME=notification_service
DB_USER=postgres
DB_PASS=postgres
VOLUME_PREFIX=ns-dev
RABBITMQ_USER=guest
RABBITMQ_PASSWORD=guest
RABBITMQ_VIRTUAL_HOST=/
RABBITMQ_AMQP_PORT=5672
RABBITMQ_MANAGEMENT_PORT=15672
DEVELOPMENT_EMAIL_SERVICE_NAME=TestService
Start the Notification Service stack from the repository root:
docker compose --env-file docker/environments/.env.local `
-f docker/environments/docker-compose.yml `
-f docker/environments/docker-compose.Development.yml `
up --build -d
Development compose exposes:
- Notification Service admin:
http://localhost:5200/admin/channelsand/admin/templates - RabbitMQ management:
http://localhost:15672 - Mailpit inbox:
http://localhost:8025 - Seq:
http://localhost:5342
The development migrator idempotently creates an active SMTP channel for
DEVELOPMENT_EMAIL_SERVICE_NAME using host mailpit, port 1025, SSL disabled, and
blank credentials. It also creates a neutral English TestEmail template with
RecipientName and Message variables. Existing channels and templates are left
unchanged. Use the admin UI to inspect or customize them and to add client-specific
service names, template keys, or languages.
Configure ItemTracker to use the same RabbitMQ host, port, credentials, virtual host, queue notification.send-email, and service name. When ItemTracker runs outside Docker against the development stack, the broker is localhost:5672; from a Docker container it is the reachable host or shared-network name. Start the ItemTracker Outbox Worker in RabbitMq mode, trigger a production-safe notification to an owned test address, then verify:
- the ItemTracker Outbox row becomes published;
- RabbitMQ delivers and removes the message from
notification.send-email; - Notification Service logs show the same
MessageIdandCorrelationIdwithout the full address; - Mailpit shows exactly one rendered email;
- the Notification Service channel usage counter increments;
- the client result queue is consumed and the matching outbox row records successful delivery.
Production configuration and smoke test
Production requires the same App__RabbitMq__Host, Port, User, Password, and VirtualHost values as ItemTracker's publisher, plus the Notification Service database connection. The production compose file obtains these from deployment environment variables; secrets must stay in the deployment secret store. The queue is declared durable by both producer and consumer.
This Notification Service revision restores the published immutable
HrynCo.RabbitMq version 1.0.17 from NuGet.org. Build and deployment environments
therefore require NuGet.org access or a trusted package mirror containing that exact
version.
Before enabling the Worker, verify that the target service has an active SMTP channel and exact-language templates for every queued ItemTracker template key. Inspect any delayed ItemTracker backlog for expired password-reset, verification, or invitation messages before draining it.
The template administration list preserves optional Service Name and Key filters when an
administrator opens the create or edit screen. These links use explicit Razor expression
boundaries so the filter query is appended as query parameters rather than rendered as a
literal @filterQuery path segment. With no active filters, the create route is exactly
/admin/templates/create.
For the smoke test, use an owned test account and a non-sensitive notification template. Record the Outbox Id and CorrelationId, trigger only one message, follow those identifiers through Outbox publication and Notification Service logs, and confirm receipt with the configured SMTP provider. Do not copy payloads, tokens, credentials, full recipient addresses, or rendered URLs into tickets or logs.