Message Context Extraction¶
Verified by tests
EnvelopeContextExtractorTests — library CI run #31657041675 (2026-08-13)
EnvelopeContextExtractor is a static helper that extracts both tracing context (OpenTelemetry ActivityContext) and security scope (IScopeContext) from message envelope hops. It consolidates extraction logic that would otherwise be duplicated across workers, invokers, and consumers.
Why a Dedicated Extractor?¶
Message envelopes carry a list of MessageHop entries - each hop records metadata about the message at a point in its journey. Extracting usable context from these hops requires:
- Trace context: Parsing
TraceParentfrom the last hop for distributed tracing - Security scope: Merging
ScopeDeltafrom all "Current" hops to rebuild the fullIScopeContext
Without EnvelopeContextExtractor, every worker and invoker would repeat this logic. The extractor provides a single source of truth.
API¶
ExtractedContext¶
The extraction result is a lightweight readonly record struct, nested inside EnvelopeContextExtractor:
ExtractedContext
// Nested type: EnvelopeContextExtractor.ExtractedContext
public readonly record struct ExtractedContext(
ActivityContext TraceContext, // For OpenTelemetry trace correlation
IScopeContext? Scope); // Security scope (null if none found)
ExtractFromEnvelope¶
The primary entry point - extracts context directly from an envelope:
ExtractFromEnvelope
ExtractFromHops¶
Lower-level method when you already have the hop list:
ExtractFromHops
Returns default ActivityContext and null scope when hops is null or empty.
Focused Extractors¶
For cases where only one type of context is needed:
Focused Extractors
// Trace context only (ActivityContext from last hop's TraceParent)
public static ActivityContext ExtractTraceContext(IReadOnlyList<MessageHop>? hops);
// Security scope only (merged ScopeDelta from all Current hops)
public static IScopeContext? ExtractScope(IReadOnlyList<MessageHop>? hops);
How Extraction Works¶
Trace Context Extraction¶
Distributed tracing relies on W3C traceparent headers. The extractor reads TraceParent from the last hop that has one, linking the worker's processing span back to the original HTTP request:
Hop 1: TraceParent = "00-abc...def-1234...5678-01" (HTTP origin)
Hop 2: TraceParent = null (internal hop)
Hop 3: TraceParent = "00-abc...def-9abc...def0-01" (outbox publish)
↑
ExtractTraceContext uses this one
The extracted ActivityContext is used to set the parent for new Activity spans, preserving the distributed trace across service boundaries.
Security Scope Extraction¶
Security context is rebuilt by merging ScopeDelta from all hops where Type == HopType.Current:
Hop 1 (Current): ScopeDelta { UserId = "alice", TenantId = "acme" }
Hop 2 (Current): ScopeDelta { Roles = ["Admin"], Permissions = [...] }
↓
MergedScope = ApplyTo() chain
↓
ImmutableScopeContext(mergedScope, shouldPropagate: true)
The merged scope is wrapped in an ImmutableScopeContext with ShouldPropagate = true, enabling security context to cascade to child messages via CascadeContext.GetSecurityFromAmbient().
Usage¶
In a Worker or Consumer¶
Worker Usage
public class OrderEventWorker {
public async Task ProcessAsync(IMessageEnvelope envelope, CancellationToken ct) {
// Extract both trace and security context
var extracted = EnvelopeContextExtractor.ExtractFromEnvelope(envelope);
// Link OpenTelemetry span to original trace
using var activity = ActivitySource.StartActivity(
"ProcessOrderEvent",
ActivityKind.Consumer,
extracted.TraceContext);
// Set ambient security scope
if (extracted.Scope is not null) {
ScopeContextAccessor.CurrentContext = extracted.Scope;
}
// Process the message with full context available
await HandleOrderEventAsync(envelope, ct);
}
}
Trace Context Only¶
Trace Context Only
var traceContext = EnvelopeContextExtractor.ExtractTraceContext(envelope.Hops);
using var activity = ActivitySource.StartActivity(
"MyOperation",
ActivityKind.Consumer,
traceContext);
Security Scope Only¶
Security Scope Only
var scope = EnvelopeContextExtractor.ExtractScope(envelope.Hops);
if (scope is not null) {
var tenantId = scope.Scope.TenantId;
var userId = scope.Scope.UserId;
// Use for authorization or multi-tenant filtering
}
Integration with CascadeContext¶
EnvelopeContextExtractor and CascadeContextFactory work together but serve different roles:
| Concern | Tool | Purpose |
|---|---|---|
| Extract trace + scope from hops | EnvelopeContextExtractor |
Low-level hop parsing |
| Create propagation context for children | CascadeContextFactory |
High-level context creation with enrichment |
CascadeContextFactory.FromEnvelope() uses envelope-level APIs (GetCorrelationId(), GetCurrentScope()) rather than calling EnvelopeContextExtractor directly. The extractor is primarily used by workers and invokers that need the raw ActivityContext and IScopeContext for ambient setup before processing begins.
Best Practices¶
DO¶
- Use
ExtractFromEnvelopeas the default entry point - Set
ScopeContextAccessor.CurrentContextfrom the extracted scope before processing - Link
ActivityContextto new spans for end-to-end distributed tracing - Handle null scope gracefully (unauthenticated or system messages have no scope)
DON'T¶
- Manually parse
TraceParentstrings - let the extractor handleActivityContext.TryParse - Skip scope extraction in workers - downstream code may rely on ambient security
- Assume hops are always present - the extractor safely returns defaults for null/empty hops
Further Reading¶
- Cascade Context & Security Propagation - How extracted context feeds into child message creation
- Message Context & Tracing - MessageId, CorrelationId, CausationId fundamentals
- Message Envelopes - Hop structure and envelope lifecycle
Version 1.0.0 - Foundation Release | Last Updated: 2026-03-26