Skip to content

Event Annotations

Aryeh Citron edited this page Apr 14, 2026 · 25 revisions

Requests can be marked with RequestResponseMetaType.Event to give them special styling in diagrams. Event-annotated notes are rendered with:

  • A light blue background (#cfecf7)
  • Smaller font size (11px)
  • Rounded corners

This is useful for distinguishing genuinely asynchronous or non-HTTP interactions — such as Kafka events, EventGrid notifications, RabbitMQ messages, SNS/SQS, or webhooks — from standard HTTP request/response flows.

⚠️ Important: Do NOT use MessageTracker for HTTP-based dependencies. If the real production interaction between your SUT and a dependency is HTTP, you must route it through TestTrackingMessageHandler instead. Using MessageTracker for HTTP calls produces event-style diagram arrows (blue notes, no HTTP method, no status code, no headers) that are misleading and inconsistent with the rest of the diagram. See Tracking Dependencies#faking-dependencies-getting-proper-http-tracking for the correct approaches to faking HTTP dependencies (WireMock, JustEat HttpClient Interception, in-memory fake APIs, etc.).

MessageTracker

The MessageTracker class makes it easy to log non-HTTP interactions — such as events, messages, or commands sent via Kafka, EventGrid, RabbitMQ, SNS, or any other transport — so they appear in the generated sequence diagrams alongside regular HTTP traffic.

Registration

Register MessageTracker in your WebApplicationFactory startup, alongside your existing tracking setup:

builder.ConfigureTestServices(services =>
{
    services.TrackDependenciesForDiagrams(new XUnitTestTrackingMessageHandlerOptions { ... });
    services.TrackMessagesForDiagrams(callingServiceName: "My API");
});

This registers MessageTracker as a singleton in DI, along with IHttpContextAccessor (needed to read test-tracking headers from the current request context).

If your tests trigger behaviour via non-HTTP entry points (service bus messages, background jobs, etc.), you should also pass the testInfoFallback parameter. See Tracking Outside HTTP Request Context below.

Usage

Inject MessageTracker into any fake or stub that simulates publishing or sending messages. Call TrackMessageRequest when the message is sent, and TrackMessageResponse when the publish completes:

public class FakeEventPublisher : IEventPublisher
{
    private readonly MessageTracker _tracker;

    public FakeEventPublisher(MessageTracker tracker)
    {
        _tracker = tracker;
    }

    public Task PublishAsync(OrderCreatedEvent @event)
    {
        var correlationId = _tracker.TrackMessageRequest(
            protocol: "Send (Event Protocol)",
            destinationName: "Order Service",
            destinationUri: new Uri("event://order-service/order_events"),
            payload: @event);

        _tracker.TrackMessageResponse(
            protocol: "Send (Event Protocol)",
            destinationName: "Order Service",
            destinationUri: new Uri("event://order-service/order_events"),
            requestResponseId: correlationId);

        return Task.CompletedTask;
    }
}

Parameters

Parameter Description
protocol The transport label shown in the diagram (e.g. "Send (Event Protocol)", "Kafka", "SNS", "RabbitMQ").
destinationName The name of the destination service or topic shown in the diagram.
destinationUri A URI representing the destination (e.g. new Uri("event://order-service/order_events")). The path portion appears in the diagram arrow label.
payload The message payload — serialised to JSON and shown in the diagram note.
requestResponseId The correlation ID returned by TrackMessageRequest, used to pair the request and response.
responsePayload Optional response payload for TrackMessageResponse (e.g. an acknowledgement).

Custom JSON Serialisation

If your payloads require specific serialisation settings, pass JsonSerializerOptions to the registration:

services.TrackMessagesForDiagrams(
    callingServiceName: "My API",
    serializerOptions: new JsonSerializerOptions { WriteIndented = true });

Tracking Outside HTTP Request Context (testInfoFallback)

MessageTracker reads test-tracking headers from IHttpContextAccessor.HttpContext to determine which test a tracked message belongs to. This works when the message is published during HTTP request processing (e.g. your API receives a request and publishes an event as a side-effect).

However, if a message is published outside of an HTTP request context — e.g. triggered by a service bus message, during hosted service startup, background task processing, or test fixture setup — HttpContext will be null and there are no test-tracking headers to read.

This is a common scenario in Function-based or event-driven architectures where tests send service bus messages directly (not via HTTP), and the function handler may publish outbound messages or interact with other services. Without configuration, these interactions will silently fail to be tracked, resulting in missing diagrams.

To handle this, pass a testInfoFallback delegate when registering MessageTracker. This delegate is called whenever HttpContext is unavailable:

xUnit 3:

builder.ConfigureTestServices(services =>
{
    services.TrackDependenciesForDiagrams(new XUnitTestTrackingMessageHandlerOptions { ... });
    services.TrackMessagesForDiagrams(
        callingServiceName: "My API",
        testInfoFallback: () => (
            TestContext.Current.Test!.TestDisplayName,
            TestContext.Current.Test.UniqueID));
});

xUnit 2:

using TestTrackingDiagrams.Tracking;
using TestTrackingDiagrams.xUnit2;

services.TrackMessagesForDiagrams(
    callingServiceName: "My API",
    testInfoFallback: () => XUnit2TestTrackingContext.GetCurrentTestInfo());

TUnit:

using TUnit.Core;

services.TrackMessagesForDiagrams(
    callingServiceName: "My API",
    testInfoFallback: () => (
        TestContext.Current!.Metadata.DisplayName,
        TestContext.Current.Id));

ReqNRoll + TUnit:

using TestTrackingDiagrams.ReqNRoll.TUnit;

services.TrackMessagesForDiagrams(
    callingServiceName: "My API",
    testInfoFallback: () => ReqNRollTestContext.CurrentTestInfo
        ?? throw new InvalidOperationException("No ReqNRoll scenario is currently executing."));

Resolution order: MessageTracker first attempts to read headers from HttpContext. If HttpContext is null (or the test-tracking headers are missing), it falls back to the testInfoFallback delegate. If neither is available, it throws an InvalidOperationException with a descriptive message.

When is this needed? If your tests trigger behaviour via non-HTTP entry points (service bus messages, background jobs, timers) and the handler publishes messages or calls downstream services that are tracked via MessageTracker, you must provide testInfoFallback — otherwise those interactions will not appear in the diagrams.

See also: Tracking Dependencies — Gotcha: HTTP Calls During HttpClient Construction for a similar scenario affecting TestTrackingMessageHandler when HTTP calls occur during HttpClient construction.


When to Use MessageTracker vs TestTrackingMessageHandler

This is one of the most common sources of confusion. The two mechanisms produce visually different outputs in your diagrams, and using the wrong one leads to misleading documentation.

Visual Comparison

HTTP tracking (via TestTrackingMessageHandler) produces:

caller -> scvApi: GET: /v2/identifiers?ssoId=sub
note left                          ← white background, standard note
[Accept=application/json]

end note
scvApi --> caller: 200 OK
note right                         ← white background, status code visible
[Content-Type=application/json]

{ "customerId": "custNbr123" }
end note
  • Arrow label shows the HTTP method (GET:, POST:, etc.)
  • Response shows the HTTP status code (200 OK, 404 Not Found, etc.)
  • Request and response headers are captured
  • Request and response bodies are captured
  • Notes have a white background (standard styling)

Event tracking (via MessageTracker) produces:

caller -> scvApi: HTTP: /v2/identifiers?ssoId=sub
note<<eventNote>> left             ← BLUE background, rounded corners
"sub"
end note
scvApi --> caller: Responded       ← no status code
  • Arrow label shows the protocol string you passed (e.g. "HTTP", "Kafka", "SNS")
  • Response always shows "Responded" — no HTTP status code
  • No headers are captured
  • Only the payload you explicitly pass appears in the note
  • Notes have a light blue background with rounded corners (event styling)

Decision Guide

The real production interaction is... Use Why
HTTP (REST API, GraphQL over HTTP, gRPC-web) TestTrackingMessageHandler Captures full HTTP semantics (method, status, headers, body)
Cosmos DB CosmosTrackingMessageHandler Pre-built extension; uses RequestResponseLogger.Log() internally
EF Core / relational database SqlTrackingInterceptor Pre-built extension; intercepts DbCommand
Redis (StackExchange.Redis) RedisTrackingDatabase Pre-built extension; decorates IDatabase
Kafka event / message MessageTracker No HTTP involved; protocol is Kafka
RabbitMQ / SNS / SQS message MessageTracker No HTTP involved; protocol is the message broker
EventGrid / Azure Service Bus notification MessageTracker Asynchronous messaging, not request/response HTTP
Synchronous SDK fake (Blob Storage, Key Vault, etc.) RequestResponseLogger.Log() No HTTP pipeline; creates standard call-and-return arrows. See Tracking Custom Dependencies
Webhook (your SUT sends an HTTP call to a callback URL) TestTrackingMessageHandler It's still HTTP — track it properly through the pipeline
gRPC (over HTTP/2) TestTrackingMessageHandler gRPC uses HTTP/2 transport — track the HTTP layer

Common Mistake: Wrapping Mocks with MessageTracker

A common pattern that leads to incorrect diagrams is mocking a service client interface and then wrapping the mock to manually log calls via MessageTracker:

// ❌ WRONG — produces event-style (blue) arrows for what is actually an HTTP call
public class TrackingScvProvider : IScvProvider
{
    private readonly IScvProvider _mock;       // NSubstitute mock
    private readonly MessageTracker _tracker;

    public async Task<ScvResponse> GetIdentifiersAsync(string ssoId)
    {
        var id = _tracker.TrackMessageRequest(
            protocol: "HTTP",
            destinationName: "SCV API",
            destinationUri: new Uri($"http://scv/v2/identifiers?ssoId={ssoId}"),
            payload: ssoId);

        var result = await _mock.GetIdentifiersAsync(ssoId);

        _tracker.TrackMessageResponse(
            protocol: "HTTP",
            destinationName: "SCV API",
            destinationUri: new Uri("http://scv/v2/identifiers"),
            requestResponseId: id);

        return result;
    }
}

The fix: Replace the mock with a real HTTP fake (WireMock, JustEat HttpClient Interception, or an in-memory API) and let TestTrackingMessageHandler capture the call automatically. See Tracking Dependencies#faking-dependencies-getting-proper-http-tracking for detailed examples of each approach.


Tips for Tracking In-Memory Message Brokers

When tracking messages through a custom in-memory Service Bus or message broker fake, keep these pitfalls in mind:

Hook at the Broker Level, Not the Sender Level

Many in-memory fakes route messages through multiple paths — e.g. SendMessageAsync() on a sender vs EnqueueMessage() / ScheduleMessage() on the client or broker. If you only hook tracking into the sender, messages published internally by the SUT (routed directly through the broker) will silently not appear in diagrams.

Track where messages actually flow through — typically the client or broker's send/enqueue/schedule methods — not just the sender wrapper.

Avoid Copying MessageTracker at Construction Time

If your fake ServiceBusClient creates senders/receivers before WebApplicationFactory.Services is built, MessageTracker will be null at sender construction time. Senders that capture the tracker in a field at construction will never get the real instance.

Instead, read the tracker from the parent client at send time:

// ❌ Breaks if MessageTracker is set after sender creation
internal MessageTracker? MessageTracker { get; set; }  // copied from client at construction

// ✅ Always reads current value from parent
private void TrackMessage(ServiceBusMessage message)
{
    _client.MessageTracker?.TrackMessageRequest(
        protocol: "Service Bus",
        destinationName: "Service Bus",
        destinationUri: new Uri($"sb://servicebus/{_queueName}"),
        payload: message.Body);
}

Wrap Tracking Calls in try/catch

Tracking failures should never break test execution. For example, XUnit2TestTrackingContext.GetCurrentTestInfo() returns a fallback ("Unknown Test", ...) when no test is active, but your own code around the tracker (e.g. accessing test-specific state) might throw during fixture setup or teardown.

try
{
    _client.MessageTracker?.TrackMessageRequest(...);
}
catch
{
    // Tracking is best-effort — don't break the test
}

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally