-
Notifications
You must be signed in to change notification settings - Fork 1
Tracking Custom Dependencies
This page explains how to track interactions with dependencies that bypass HTTP entirely — such as Azure Blob Storage, Queue Storage, Key Vault, or any SDK that uses virtual methods or interfaces rather than HttpClient.
TestTrackingDiagrams provides several tracking APIs, each suited to a different type of dependency:
| Your dependency is... | Use | Why |
|---|---|---|
| An HTTP service (REST API, downstream microservice) |
TestTrackingMessageHandler / TrackDependenciesForDiagrams
|
Automatically tracks HTTP request/response through the pipeline |
| Cosmos DB (via CosmosDB.InMemoryEmulator) | 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
|
| An async message broker (Service Bus, Kafka, RabbitMQ) | MessageTracker |
Designed for fire-and-forget or pub/sub messaging; produces event-styled arrows |
| A synchronous dependency with no HTTP (Blob Storage, SDK fakes) | RequestResponseLogger.Log() |
Creates synchronous call-and-return arrows in diagrams |
Use RequestResponseLogger.Log() when your dependency is faked at the SDK level (e.g. overriding BlobClient virtual methods) and there is no HTTP pipeline to intercept. This is the same API that CosmosTrackingMessageHandler uses internally.
Why not
MessageTracker?MessageTrackermarks entries withRequestResponseMetaType.Event, which produces event-styled diagram arrows (light blue background, rounded corners, "Responded" instead of status codes). This is semantically incorrect for synchronous request/response dependencies like blob storage.RequestResponseLogger.Log()produces standard-styled arrows (white background, HTTP method/status labels) — matching whatTestTrackingMessageHandlerproduces for HTTP dependencies. See Event & Message Tracking for a visual comparison.
using System.Net;
using TestTrackingDiagrams.Tracking;
// Use the appropriate import for your test framework:
// using TestTrackingDiagrams.xUnit2; // XUnit2TestTrackingContext.GetCurrentTestInfo()
// using TUnit.Core; // TestContext.Current
// For xUnit 3: TestContext.Current.Test (built-in)
// For ReqNRoll.TUnit: ReqNRollTestContext.CurrentTestInfo
internal static class BlobTracker
{
internal static class BlobOperation
{
internal const string Exists = "Blob Exists";
internal const string Delete = "Blob Delete";
internal const string Download = "Blob Download";
internal const string Upload = "Blob Upload";
}
private const int MaxPayloadLength = 1000;
private const string TruncationSuffix = "...(Truncated)";
private const string ServiceName = "Blob Storage";
private const string CallingServiceName = "My Service";
internal static void TrackBlobOperation(
string container,
string fileName,
string operation,
string? content = null,
HttpStatusCode statusCode = HttpStatusCode.OK)
{
try
{
var testInfo = XUnit2TestTrackingContext.GetCurrentTestInfo();
// For TUnit, use instead:
// var testInfo = (TestContext.Current!.Metadata.DisplayName, TestContext.Current.Id);
// For ReqNRoll + TUnit:
// var testInfo = ReqNRollTestContext.CurrentTestInfo
// ?? throw new InvalidOperationException("No scenario executing.");
var requestResponseId = Guid.NewGuid();
var traceId = Guid.NewGuid();
var requestUri = new Uri($"https://blob.core.windows.net/{container}/{fileName}");
string? truncatedContent = content is not null && content.Length > MaxPayloadLength
? string.Concat(content.AsSpan(0, MaxPayloadLength), TruncationSuffix)
: content;
TestTrackingDiagrams.Tracking.OneOf<HttpMethod, string> method = operation;
RequestResponseLogger.Log(new RequestResponseLog(
testInfo.Name,
testInfo.Id,
method,
truncatedContent,
requestUri,
[],
ServiceName,
CallingServiceName,
RequestResponseType.Request,
traceId,
requestResponseId,
false
));
RequestResponseLogger.Log(new RequestResponseLog(
testInfo.Name,
testInfo.Id,
method,
null,
requestUri,
[],
ServiceName,
CallingServiceName,
RequestResponseType.Response,
traceId,
requestResponseId,
false,
statusCode
));
}
catch
{
// Silently skip tracking if something goes wrong (e.g. during fixture setup)
}
}
}public class TestBlobClient(InMemoryBlobStorage blobStorage, string container, string fileName)
: BlobClient(new Uri($"https://test.blob.core.windows.net/{container}/{fileName}"))
{
public override async Task<Response<BlobContentInfo>> UploadAsync(
BinaryData content, bool overwrite = false, CancellationToken ct = default)
{
var contentString = content.ToString();
blobStorage.CreateBlob(container, fileName, contentString);
BlobTracker.TrackBlobOperation(container, fileName, BlobTracker.BlobOperation.Upload, contentString);
return await Task.FromResult(new FakeAzureResponse<BlobContentInfo>(null!));
}
public override async Task<Response<bool>> ExistsAsync(CancellationToken ct = default)
{
var exists = blobStorage.BlobExists(container, fileName);
BlobTracker.TrackBlobOperation(container, fileName, BlobTracker.BlobOperation.Exists);
return await Task.FromResult(new FakeAzureResponse<bool>(exists));
}
}Always log a Request followed by a Response with the same traceId and requestResponseId. This creates the call-and-return arrow pair in sequence diagrams.
Service A ──────────▶ Blob Storage ← Request log
Service A ◀────────── Blob Storage ← Response log (same traceId + requestResponseId)
If you only log a Request without a matching Response (or vice versa), the diagram will show an unpaired arrow.
The Method parameter is OneOf<HttpMethod, string>. For non-HTTP dependencies, pass a string label that describes the operation — e.g. "Blob Upload", "Blob Download", "Cache Get". This label appears on the arrow in the sequence diagram.
// HTTP dependency — use HttpMethod
OneOf<HttpMethod, string> method = HttpMethod.Get;
// Non-HTTP dependency — use a descriptive string
OneOf<HttpMethod, string> method = "Blob Upload";The ServiceName parameter determines the target participant name in the diagram. The CallerName parameter determines the calling participant. These should match the names used by your other tracking mechanisms to keep the diagram consistent.
The StatusCode parameter is OneOf<HttpStatusCode, string>?. For non-HTTP dependencies, you can pass either an HttpStatusCode enum value (e.g. HttpStatusCode.OK) if it semantically maps, or a custom string label (e.g. "Success", "Not Found"). Pass null to omit the status from the response arrow.
TestTrackingDiagrams defines its own OneOf<T1, T2> type in the TestTrackingDiagrams.Tracking namespace. If your project also references the popular OneOf NuGet package, you will get a CS0104 ambiguity error:
error CS0104: 'OneOf<,>' is an ambiguous reference between
'OneOf.OneOf<T0, T1>' and 'TestTrackingDiagrams.Tracking.OneOf<TOption1, TOption2>'
Fix this by using the fully qualified type:
TestTrackingDiagrams.Tracking.OneOf<HttpMethod, string> method = "Blob Upload";The framework-specific test context (e.g. XUnit2TestTrackingContext.GetCurrentTestInfo()) provides the test name and ID needed by RequestResponseLog. Behaviour varies by framework:
-
xUnit 2:
XUnit2TestTrackingContext.GetCurrentTestInfo()returns a fallback("Unknown Test", <new GUID>)when no test is active (e.g. during fixture setup/teardown), rather than throwing. -
xUnit 3:
TestContext.Current.Testmay benulloutside a test context. -
TUnit:
TestContext.Currentis available during test execution. UseTestContext.Current!.Metadata.DisplayNamefor the test name andTestContext.Current.Idfor the test ID. -
ReqNRoll + TUnit: Use
ReqNRollTestContext.CurrentTestInfowhich returns(string Name, string Id)?—nulloutside a scenario context. -
LightBDD + TUnit: Use
ScenarioExecutionContext.CurrentScenario.Infofor the test name andRuntimeId.
Even though GetCurrentTestInfo() won't throw, wrapping tracking calls in try/catch is still recommended — your own code around the tracking call (e.g. accessing test-specific state, serialising payloads) could throw during fixture setup or teardown. Tracking failures should never break tests.
This is the same pattern used by CosmosTrackingMessageHandler, which checks CurrentTestInfoFetcher?.Invoke() and skips tracking entirely if the result is null.
When tracking dependencies that handle large payloads (e.g. blob storage uploads containing entire JSON files, large Cosmos DB documents), the tracked content can significantly increase report size and reduce diagram readability.
Unlike CosmosTrackingMessageHandler (which provides a Verbosity setting — use CosmosTrackingVerbosity.Summarised to suppress response bodies), RequestResponseLogger.Log() has no built-in truncation. The caller must handle it:
const int MaxPayloadLength = 1000;
const string TruncationSuffix = "...(Truncated)";
string? truncatedContent = content is not null && content.Length > MaxPayloadLength
? string.Concat(content.AsSpan(0, MaxPayloadLength), TruncationSuffix)
: content;Alternatively, pass null for content to track the interaction without recording the payload at all.
RequestResponseLog(
string TestName, // Name of the current test
string TestId, // Unique ID of the current test
OneOf<HttpMethod, string> Method, // HTTP method or custom label for the diagram arrow
string? Content, // Request/response body (shown as note in diagram)
Uri Uri, // URI of the target (appears in diagram arrow)
(string Key, string? Value)[] Headers, // Headers (shown in detailed/raw views)
string ServiceName, // Target participant name in the diagram
string CallerName, // Calling participant name in the diagram
RequestResponseType Type, // Request or Response
Guid TraceId, // Groups related request/response pairs
Guid RequestResponseId, // Unique ID for this request/response pair
bool TrackingIgnore, // Whether to exclude from diagram rendering
OneOf<HttpStatusCode, string>? StatusCode = null, // Status (only for Response type)
RequestResponseMetaType MetaType = default // Default = standard styling, Event = blue event styling
)| Parameter | Notes |
|---|---|
TestName / TestId
|
Obtained from your framework's test context (e.g. XUnit2TestTrackingContext.GetCurrentTestInfo(), or TestContext.Current for TUnit/xUnit 3). |
Method |
Pass HttpMethod.Get etc. for HTTP, or a string like "Blob Upload" for non-HTTP. |
Content |
Request/response body. Pass null to omit. Truncate large payloads before passing. |
Uri |
The target URI. The path portion appears in the diagram arrow label. |
Headers |
Pass [] (empty array) if not applicable. |
ServiceName |
The name shown as the target participant in the sequence diagram. |
CallerName |
The name shown as the calling participant in the sequence diagram. |
Type |
RequestResponseType.Request or RequestResponseType.Response. Always log one of each per interaction. |
TraceId |
Use the same Guid for a paired request and response. Groups them in the diagram. |
RequestResponseId |
Use the same Guid for a paired request and response. Identifies the specific pair. |
TrackingIgnore |
Set to true to exclude this entry from diagrams (useful for health checks, etc.). Typically false. |
StatusCode |
Pass an HttpStatusCode or a custom string status. Only meaningful for Response entries. |
MetaType |
Leave as default for standard styling. Only set to Event if using MessageTracker-style event annotations. |
For a complete example of building a custom tracker, see the CosmosTrackingMessageHandler source code. This demonstrates:
- Pairing request/response logs with matching
TraceIdandRequestResponseId - Using
OneOf<HttpMethod, string>for custom diagram labels - Filtering content by verbosity level
- Gracefully handling missing test context (
CurrentTestInfoFetcher?.Invoke())
Getting Started
Common Tasks
Integration Guides
- Integration xUnit3
- Integration xUnit2
- Integration NUnit
- Integration MSTest
- Integration TUnit
- Integration BDDfy xUnit3
- Integration LightBDD xUnit2
- Integration LightBDD xUnit3
- Integration LightBDD TUnit
- Integration ReqNRoll xUnit2
- Integration ReqNRoll xUnit3
- Integration ReqNRoll TUnit
Extensions
- Integration AtlasDataApi Extension
- Integration BigQuery Extension
- Integration Bigtable Extension
- Integration BlobStorage Extension
- Integration ClickHouse Extension
- Integration CloudStorage Extension
- Integration CosmosDB Extension
- Integration Dapper Extension
- Integration DynamoDB Extension
- Integration EF Core Relational Extension
- Integration Elasticsearch Extension
- Integration EventBridge Extension
- Integration EventHubs Extension
- Integration Grpc Extension
- Integration Kafka Extension
- Integration MassTransit Extension
- Integration MongoDB Extension
- Integration MySqlConnector Extension
- Integration Npgsql Extension
- Integration Oracle Extension
- Integration PubSub Extension
- Integration Redis Extension
- Integration S3 Extension
- Integration ServiceBus Extension
- Integration SNS Extension
- Integration Spanner Extension
- Integration SqlClient Extension
- Integration Sqlite Extension
- Integration SQS Extension
- Integration StorageQueues Extension
- Integration OpenTelemetry Extension
- Integration DispatchProxy Extension
- Integration MediatR Extension
- Integration PlantUML IKVM
Configuration
- Tracking Dependencies
- Tracking Custom Dependencies
- HTTP Tracking Setup
- Report Configuration
- Diagram Customisation
- Phase-Aware Tracking
- Content Formatting
- PlantUML Server Configuration
Features
- Generated Reports
- Search Syntax
- Component Diagrams
- PlantUML Browser Rendering
- Inline SVG Rendering
- Internal Flow Tracking
- Tags and Attributes
- Excluding Requests
- Excluded Headers
- Multi-Host Test Architectures
- Event-Driven Architecture Testing
- Service Bus Tracking Patterns
- Background Thread Correlation
- Parallel-Safe Background Correlation
- Event & Message Tracking
- Assertion Tracking
- Step Tracking
- Tabular Attributes
- Large Response and Diagram Handling
- Diagnostics and Debugging
- CI Summary Integration
- CI Artifact Upload
- Merging Parallel Reports
Reference