The Book Store API implements causation and correlation IDs for distributed tracing and event chain tracking. This enables you to trace the entire lifecycle of a business transaction across multiple services and events.
- Purpose: Tracks an entire business transaction from start to finish
- Scope: Remains the same throughout the entire workflow
- Use Case: Trace all events related to a single user action (e.g., "Create a book with authors and categories")
- Purpose: Tracks the immediate cause of an event
- Scope: Changes with each event in the chain
- Use Case: Understand what triggered a specific event (e.g., "This projection update was caused by a BookAdded event")
- Purpose: Unique identifier for each specific event
- Scope: Unique per event
- Use Case: Reference a specific event in the event store
| Header | Description | Required | Example |
|---|---|---|---|
X-Correlation-ID |
Business transaction identifier | No* | 019541ab-1234-7000-a000-000000000001 |
X-Causation-ID |
Immediate cause identifier | No* | 019541ab-5678-7000-b000-000000000002 |
*If not provided, the system auto-generates these IDs using Guid.CreateVersion7().
MartenMetadataMiddleware automatically echoes the correlation ID in every response.
| Header | Description | Example |
|---|---|---|
X-Correlation-ID |
Echo of the request correlation ID (or generated) | 019541ab-1234-7000-a000-000000000001 |
Every event in the system includes metadata stored in Marten's dedicated columns and a technical headers JSON column:
- CorrelationId: Tracks the entire business transaction.
- CausationId: Tracks the immediate cause (usually the message ID).
Additional context is stored in the JSON headers column for auditability:
{
"tenant-id": "my-tenant",
"user-id": "019541ab-0000-7000-0000-000000000000",
"remote-ip": "::1",
"user-agent": "Mozilla/5.0..."
}Tip
Metadata Propagation: AuthorizationMessageHandler in the Blazor frontend forwards the original browser's User-Agent and remote IP (X-Forwarded-For) on every API call, so these values are preserved even when requests cross the Blazor → API boundary.
Request:
curl -X POST http://localhost:5000/api/admin/books \
-H "Content-Type: application/json" \
-H "X-Correlation-ID: 019541ab-1234-7000-a000-000000000001" \
-d '{
"title": "Clean Code",
"isbn": "978-0132350884",
"description": "A handbook of agile software craftsmanship",
"publisherId": "019541ab-0001-7000-a000-000000000001",
"authorIds": ["019541ab-0002-7000-a000-000000000001"],
"categoryIds": ["019541ab-0003-7000-a000-000000000001"]
}'Response Headers:
X-Correlation-ID: 019541ab-1234-7000-a000-000000000001
Event Stored:
{
"eventType": "BookAdded",
"data": {
"id": "019541ab-0004-7000-a000-000000000001",
"title": "Clean Code"
},
"correlationId": "019541ab-1234-7000-a000-000000000001",
"causationId": "019541ab-1234-7000-a000-000000000001"
}Step 1: Create Book
curl -X POST http://localhost:5000/api/admin/books \
-H "X-Correlation-ID: 019541ab-aaaa-7000-a000-000000000001" \
-H "Content-Type: application/json" \
-d '{"title": "Domain-Driven Design", ...}'Step 2: Update Book (using previous event as causation)
curl -X PUT http://localhost:5000/api/admin/books/019541ab-0004-7000-a000-000000000001 \
-H "X-Correlation-ID: 019541ab-aaaa-7000-a000-000000000001" \
-H "X-Causation-ID: 019541ab-bbbb-7000-a000-000000000001" \
-H "Content-Type: application/json" \
-d '{"title": "Domain-Driven Design (Revised)", ...}'Event Chain:
graph TD
Corr[019541ab-aaaa...<br/>Correlation ID]
Create[019541ab-bbbb...<br/>BookAdded]
Update[019541ab-cccc...<br/>BookUpdated]
Corr -->|Causes| Create
Create -->|Causes| Update
Imagine a workflow where creating a book triggers multiple operations:
1. Create Publisher
POST /api/admin/publishers
X-Correlation-ID: 019541ab-1111-7000-a000-0000000000012. Create Author
POST /api/admin/authors
X-Correlation-ID: 019541ab-1111-7000-a000-000000000001
X-Causation-ID: 019541ab-2222-7000-a000-0000000000013. Create Category
POST /api/admin/categories
X-Correlation-ID: 019541ab-1111-7000-a000-000000000001
X-Causation-ID: 019541ab-3333-7000-a000-0000000000014. Create Book
POST /api/admin/books
X-Correlation-ID: 019541ab-1111-7000-a000-000000000001
X-Causation-ID: 019541ab-4444-7000-a000-000000000001
{
"publisherId": "...",
"authorIds": ["..."],
"categoryIds": ["..."]
}Complete Event Chain:
graph TD
Corr[019541ab-1111...<br/>Correlation ID]
Pub[019541ab-2222...<br/>PublisherAdded]
Auth[019541ab-3333...<br/>AuthorAdded]
Cat[019541ab-4444...<br/>CategoryAdded]
Book[019541ab-5555...<br/>BookAdded]
Corr -->|Causes| Pub
Pub -->|Causes| Auth
Auth -->|Causes| Cat
Cat -->|Causes| Book
You can query the Marten event store to find all events related to a correlation ID and inspect their technical headers:
-- PostgreSQL query in Marten event store
SELECT
id,
stream_id,
type,
correlation_id,
causation_id,
headers->>'tenant-id' as tenant_id,
headers->>'user-id' as user_id,
headers->>'remote-ip' as remote_ip,
headers->>'user-agent' as user_agent,
timestamp
FROM mt_events
WHERE correlation_id = '019541ab-1111-7000-a000-000000000001'
ORDER BY timestamp;When making multiple related API calls, always use the same correlation ID:
CORRELATION_ID="$(uuidgen)"
# All related calls use the same correlation ID
curl -H "X-Correlation-ID: $CORRELATION_ID" ...
curl -H "X-Correlation-ID: $CORRELATION_ID" ...When one operation triggers another, use the previous event ID as the causation ID:
# First call (capture created entity/event details from response body or stream)
RESPONSE=$(curl -i -X POST ... -H "X-Correlation-ID: $CORRELATION_ID")
# Note: the API does not currently return X-Event-ID response headers.
# Second call caused by first
curl -H "X-Correlation-ID: $CORRELATION_ID" \
-H "X-Causation-ID: <previous-event-id>" \
...The system generates Guid.CreateVersion7() values for all IDs. Use the same format when constructing IDs externally so they sort chronologically:
# Generate a time-ordered UUID v7 (e.g., via uuidgen or a UUID v7 library)
CORRELATION_ID="$(uuidgen)" # Use a UUID v7 generator if availableAll logging in this codebase must use the [LoggerMessage] source generator. Never call _logger.LogInformation(...) directly:
// ✅ Correct — use source-generated log methods
[LoggerMessage(Level = LogLevel.Information, Message = "Processing {EventType} for {EntityId}. CorrelationId: {CorrelationId}")]
static partial void LogProcessing(ILogger logger, string eventType, Guid entityId, string correlationId);
// ❌ Wrong — forbidden in this codebase
_logger.LogInformation("Processing {EventType}. CorrelationId: {CorrelationId}", eventType, correlationId);- Get the correlation ID from your application logs or error message
- Query the event store —
correlation_idis a first-class column onmt_events:SELECT * FROM mt_events WHERE correlation_id = '019541ab-1111-7000-a000-000000000001' ORDER BY timestamp;
- Analyze the event chain to find where the workflow failed
- Find the source event that triggered a projection update
- Use the causation ID to link back to the original command
- Follow the correlation ID to see the entire business transaction
-
MartenMetadataMiddleware(Infrastructure/MartenMetadataMiddleware.cs):- Registered explicitly via
app.UseMartenMetadata()inProgram.cs. - Reads
X-Correlation-IDfrom the request header; falls back toActivity.Current?.RootIdthenGuid.CreateVersion7(). - Reads
X-Causation-IDfrom the request header; falls back toActivity.Current?.ParentIdthen the correlation ID. - Sets
session.CorrelationIdandsession.CausationIdon the MartenIDocumentSession. - Caches values in
context.Items["CorrelationId"]andcontext.Items["CausationId"]for downstream components. - Sets
correlation_idandcausation_idtags on the currentActivity. - Sets technical headers on the session:
tenant-id,user-id,remote-ip,user-agent. - Echoes
X-Correlation-IDin the response.
- Registered explicitly via
-
WolverineCorrelationMiddleware(Infrastructure/WolverineCorrelationMiddleware.cs):- A Wolverine handler middleware (not an ASP.NET middleware), registered via
opts.Policies.AddMiddleware(typeof(WolverineCorrelationMiddleware)). - Runs
Beforeevery Wolverine handler to propagate IDs from the HTTP scope to Wolverine's nested Marten session. CorrelationIdlookup order:HttpContext.Items→X-Correlation-IDheader → Wolverinecontext.CorrelationId→Activitytag.CausationIdlookup order:envelope.Headers["X-Causation-ID"]→HttpContext.Items→Activitytag →envelope.Id.- Also propagates
user-id,remote-ip, anduser-agenttechnical headers to the session.
- A Wolverine handler middleware (not an ASP.NET middleware), registered via
-
LoggingEnricherMiddleware(Infrastructure/LoggingEnricher.cs):- Registered explicitly via
app.UseLoggingEnricher()inProgram.cs. - Begins a structured log scope for every request containing:
CorrelationId,CausationId,TraceId,SpanId,UserId,TenantId,RequestPath,RequestMethod,RemoteIp,UserAgent. - Reads
CorrelationId/CausationIdpreferringHttpContext.Items(set byMartenMetadataMiddleware) then falls back to headers andActivity.
- Registered explicitly via
-
EventMetadataService(Infrastructure/EventMetadataService.cs):- Not a middleware — a scoped service available for endpoint/handler use.
CreateMetadata()builds anEventMetadatarecord containing a freshGuid.CreateVersion7()event ID plusCorrelationId,CausationId, andUserIdread fromIHttpContextAccessor.SetResponseHeaders()exists, but endpoints do not currently call it;X-Event-IDis not emitted in API responses.
The Blazor frontend automatically manages and propagates these IDs using a dedicated service and handler pipeline.
-
ClientContextService(src/BookStore.Client/Services/ClientContextService.cs):- Registered as
Scoped— one instance per Blazor circuit. CorrelationId: Initialized once withGuid.CreateVersion7()on construction; never changes for the lifetime of the circuit.CausationId: Starts equal toCorrelationId; updated viaUpdateCausationId(string id)and reset viaResetCausationId().- Thread-safe; all mutations use a lock.
- Also holds
BrowserInfo(UserAgent, Screen, Language, Timezone) set from JS interop.
- Registered as
-
AuthorizationMessageHandler(src/BookStore.Web/Services/AuthorizationMessageHandler.cs):- Runs outermost in the handler chain (before Tenant, Headers, and network handlers).
- Injects
X-Correlation-IDandX-Causation-IDheaders fromClientContextServiceon every outgoing API request. - Attaches the JWT Bearer token.
- Forwards the original browser's
User-AgentandX-Forwarded-ForIP fromIHttpContextAccessor. - After each response: attempts to read
X-Event-IDand updateClientContextServicewhen present. - On HTTP 401: clears stored tokens.
-
BookStoreHeaderHandler(src/BookStore.Client/Infrastructure/BookStoreHeaderHandler.cs):- Runs after
AuthorizationMessageHandlerin the chain (inner handler). - Adds
api-version: 1.0andAccept-Languageif missing. - Adds
X-Correlation-IDfromActivity.TraceIdandX-Causation-IDfromActivity.ParentIdonly if those headers are not already present — acts as a fallback for non-authenticated clients.
- Runs after
-
BookStoreEventsService(src/BookStore.Client/BookStoreEventsService.cs):- Subscribes to the SSE stream from the API.
- When a notification with a non-empty
EventIdarrives, callsClientContextService.UpdateCausationId(eventId), ensuring that reactive UI reloads triggered by SSE events are linked to the correct cause.
Handler chain order (outer → inner):
ResilienceHandler → AuthorizationMessageHandler → TenantHeaderHandler → BookStoreHeaderHandler → HttpClientHandler
The Blazor frontend enriches logs with correlation and causation identifiers in both HTTP and SignalR flows:
-
LogEnrichmentMiddleware(src/BookStore.Web/Infrastructure/LogEnrichmentMiddleware.cs):- Registered via
app.UseMiddleware<LogEnrichmentMiddleware>()inProgram.cs. - Adds a structured scope with
CorrelationId,CausationId,TenantId, and browser metadata.
- Registered via
-
LoggingHubFilter(src/BookStore.Web/Infrastructure/LoggingHubFilter.cs):- Registered as
IHubFilterfor Blazor Server SignalR hub invocations. - Adds the same tracing scope for component-triggered real-time operations.
- Registered as
- Correlation ID: Tracks the entire business workflow
- Causation ID: Tracks immediate event causes
- Event ID: Unique identifier for each event
- Headers:
X-Correlation-ID,X-Causation-ID - Automatic: System generates
Guid.CreateVersion7()IDs if not provided - Propagation:
X-Correlation-IDis echoed in responses; causation chaining can use upstream IDs and SSE event identifiers