Distributed Trace Context Propagation in C
Distributed telemetry tracing allows developers to monitor, debug, and understand the path of a request as it navigates complex multi-service architectures. This article defines the fundamentals of telemetry tracing and demonstrates how to reliably propagate distributed trace contexts across outbound HTTP calls in modern C# and .NET applications using native diagnostic APIs.
What is Telemetry Tracing?
Telemetry tracing—often called distributed tracing—is the process of recording the lifecycle of an execution path across disparate systems, services, and network boundaries. While traditional logging records isolated events within a single application, distributed tracing stitches together the entire journey of an end-to-end operation.
A distributed trace consists of one or more "spans."
- Trace: Represents the entire transaction across all systems from start to finish. It shares a globally unique identifier (TraceId).
- Span: Represents a single unit of contiguous work within that trace (e.g., an HTTP request, a database query, or an internal method execution). Each span has its own identifier (SpanId), a start time, a duration, and a reference to its parent span.
By linking these spans together in a parent-child hierarchy, tracing tools visualize the exact execution flow, identify performance bottlenecks, and pinpoint where errors originate in a microservice ecosystem.
The Role of Trace Context Propagation
For tracing to work across network calls, services must pass the trace state to downstream systems. This is achieved via Trace Context Propagation.
Most modern platforms adhere to the W3C Trace Context specification, which relies on standardized HTTP headers:
traceparent: Encodes the version, TraceId, ParentSpanId, and trace flags (e.g., whether the request is sampled).- Example:
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
- Example:
tracestate: Carries vendor-specific routing and filtering metadata as opaque key-value pairs.
Automatic Trace Propagation in .NET
In C# and modern .NET (Core 3.1 and .NET 5+), the runtime natively
supports the W3C Trace Context specification via the
System.Diagnostics.Activity API, which maps directly to the
OpenTelemetry Span concept.
When using standard .NET components, context propagation over HTTP occurs automatically:
- An inbound request to an ASP.NET Core application creates an
Activity(extracting thetraceparentheader if present). - Any outbound HTTP request made using
HttpClientautomatically inspects the ambientActivity.Current. - If an active activity exists,
HttpClientgenerates a new child span ID and automatically injects thetraceparentandtracestateheaders into the outgoing request.
Basic Setup Example
using System.Diagnostics;
// 1. Define an ActivitySource for creating custom spans
var activitySource = new ActivitySource("OrderService");
// 2. Start a parent span
using (var activity = activitySource.StartActivity("ProcessOrder"))
{
// The activity is now ambient (Activity.Current is populated)
using var httpClient = new HttpClient();
// 3. Outgoing call: HttpClient automatically injects W3C headers
var response = await httpClient.GetAsync("https://api.inventory.local/check-stock");
// The receiving service will extract the header and continue the same TraceId
Console.WriteLine($"Dispatched request with TraceId: {activity?.TraceId}");
}Manual Context Propagation Using DistributedContextPropagator
In scenarios where HttpClient cannot be used directly,
or when working with custom message brokers, raw sockets, or third-party
HTTP wrappers, you must inject the context manually.
.NET provides the DistributedContextPropagator class to
handle standards-compliant context injection and extraction.
Manually Injecting Trace Headers
using System.Diagnostics;
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.inventory.local/check-stock");
// Retrieve the default W3C propagator
var propagator = DistributedContextPropagator.Current;
// Inject the current ambient Activity into the HttpRequestMessage headers
propagator.Inject(Activity.Current, request, (carrier, key, value) =>
{
if (carrier is HttpRequestMessage req)
{
req.Headers.TryAddWithoutValidation(key, value);
}
});
var response = await client.SendAsync(request);Manually Extracting Trace Headers on the Receiving End
When writing a custom receiver (such as a generic TCP server or a
custom HTTP listener), use ExtractTraceIdAndState to read
the headers and initialize the next Activity:
using System.Diagnostics;
// Simulated incoming header retrieval
string? incomingTraceParent = requestHeaders["traceparent"];
string? incomingTraceState = requestHeaders["tracestate"];
var propagator = DistributedContextPropagator.Current;
// Extract parent context
propagator.ExtractTraceIdAndState(
requestHeaders,
getter: (carrier, key) => carrier.TryGetValue(key, out var val) ? new[] { val } : null,
out string? traceParent,
out string? traceState);
// Start child activity linked to the incoming trace context
var activitySource = new ActivitySource("InventoryService");
using var activity = activitySource.StartActivity(
"CheckStock",
ActivityKind.Server,
parentContext: ActivityContext.Parse(traceParent, traceState));Summary
Modern C# applications leverage the
System.Diagnostics.Activity library to achieve
standards-based distributed tracing. For standard HTTP operations,
HttpClient handles W3C header injection automatically
whenever an ambient Activity is active. For non-standard or
decoupled transport mechanisms,
DistributedContextPropagator provides explicit programmatic
control over the injection and extraction of trace contexts.