Skip to main content

MessageContext Items Dictionary Usage

This document explains when to use the IMessageContext.Items dictionary versus direct properties, and catalogs legitimate Items dictionary usage across the Excalibur framework.

Overview​

IMessageContext provides two mechanisms for storing contextual data:

  1. Direct Properties - Strongly-typed properties, sub-nanosecond access
  2. Items Dictionary - Flexible key-value storage, around 6 ns per read

When to Use Direct Properties​

Use direct properties for data that:

  • Is accessed on every message dispatch (hot path)
  • Has a known, fixed schema across all transports
  • Is core to the messaging framework (correlation, validation, retry tracking)

Examples of direct properties:

  • CorrelationId, CausationId, TenantId
  • ProcessingAttempts, IsRetry, FirstAttemptTime
  • ValidationPassed, ValidationTimestamp
  • TimeoutExceeded, RateLimitExceeded

When to Use Items Dictionary​

Use the Items dictionary for data that:

  • Is transport-specific (only relevant to RabbitMQ, SQS, etc.)
  • Has unpredictable keys (custom headers, user-defined attributes)
  • Is accessed infrequently (once during message setup, not on every dispatch)
  • Would bloat the interface with transport-specific concerns

Transport-Specific Items Usage​

The following sections document legitimate Items dictionary usage by transport.

RabbitMQ Transport​

// Set by RabbitMqChannelConsumer when receiving messages
context.Items["rabbitmq.exchange"] = ea.Exchange; // Exchange name
context.Items["rabbitmq.routingKey"] = ea.RoutingKey; // Message routing key
context.Items["rabbitmq.deliveryTag"] = ea.DeliveryTag; // Delivery tag for ack/nack
context.Items["rabbitmq.redelivered"] = ea.Redelivered; // Redelivery flag

// Custom headers from RabbitMQ messages (unpredictable keys)
foreach (var header in basicProperties.Headers)
{
context.Items[header.Key] = header.Value;
}

Rationale: RabbitMQ headers are user-defined and unpredictable. The delivery tag and redelivered flag are transport-specific concepts not shared with other transports.

Google Pub/Sub Transport​

// Set by GooglePubSubChannelReceiver when receiving messages
foreach (var attr in pubsubMessage.Attributes)
{
context.Items[attr.Key] = attr.Value;
}

Rationale: Pub/Sub attributes are user-defined key-value pairs with unpredictable keys.

ASP.NET Core Integration​

// Set by HttpContextExtensions when bridging HTTP requests
foreach (var header in httpContext.Request.Headers)
{
context.Items[header.Key] = header.Value.ToString();
}

Rationale: HTTP headers are user-defined with unpredictable keys.

CloudEvents Integration​

// Set by CloudEventMiddleware for CloudEvents attributes
context.Items["cloudevent.outgoing"] = outgoingCloudEvent;
context.Items[$"ce.{attr.Name}"] = cloudEvent[attr.Name];

Rationale: CloudEvents extension attributes are defined by the CloudEvents spec and vary by event type.

Service Mesh Integration​

// Set by ServiceMeshMiddleware for Envoy/Istio integration
context.Items["X-Envoy-Available"] = isAvailable.ToString();
context.Items["X-Service-Mesh"] = "true";
context.Items["X-Service-Mesh-Version"] = "1.0";
context.Items["X-Service-Name"] = serviceName;
context.Items["X-Instance-MessageId"] = instanceId;
context.Items["X-Mesh-Timestamp"] = timestamp;
context.Items["X-Service-Endpoints"] = endpointCount;
context.Items["X-Service-Healthy"] = healthyCount;
context.Items["X-Processing-Time-Ms"] = elapsedMs;
context.Items["X-Processing-Success"] = success;

Rationale: Service mesh headers follow Envoy/Istio conventions and are only relevant when service mesh is enabled.

Routing and Scheduling​

// Set by RoutingMiddleware on strongly-typed context property
context.RoutingDecision = decision; // RoutingDecision (Transport, Endpoints, metadata)

// Legacy transport metadata (set by transport adapters)
context.Items["queue"] = meta.QueueName;
context.Items["topic"] = meta.TopicName;
context.Items["routing-key"] = meta.RoutingKey;

// Set by EnhancedScheduledMessageService
context.Items["ScheduleTimeZone"] = item.TimeZoneId;

// Set by TransportAdapterRouter
context.Items["TransportAdapterId"] = adapterId;
context.Items["RoutedTimestamp"] = DateTimeOffset.UtcNow;

Rationale: Routing decision is now strongly typed (IMessageContext.RoutingDecision) while transport-specific legacy metadata can still vary by adapter.

Distributed Tracing Context​

// Set by DispatchContextInitializer — only for keys the application allowed by name
context.Items[$"baggage.{key}"] = value;

Rationale: W3C Baggage entries are user-defined with unpredictable keys.

Nothing is copied unless you opt the key in. The runtime parses an inbound baggage header into the ambient activity without being asked, so on a publicly reachable endpoint those keys and values are chosen by the caller. Copying them here would put caller-supplied data onto the wire for remote transports and into every downstream log, telemetry sink and store. Name the keys you want:

services.Configure<BaggagePropagationOptions>(o =>
{
o.AllowedKeys.Add("tenant-hint"); // copied as context.Items["baggage.tenant-hint"]
o.MaxEntries = 8; // caps apply after the allowlist
o.MaxValueLength = 256; // an oversized value is DROPPED, never truncated
o.MaxTotalLength = 1024;
});

The allowlist and the caps solve different problems and neither substitutes for the other: the allowlist keeps untrusted values out of trusted sinks, and the caps bound how much a permitted key can amplify — one cheap request riding every downstream message.

Internal Framework Usage​

Some Items usage is internal to the framework and may be migrated to direct properties in future versions:

Caching Middleware​

// Used internally by CachingMiddleware
context.Items["Dispatch:Result"] = returnValue;
context.Items["Dispatch:OriginalResult"] = messageResult;

Upcasting​

// Used by UpcastingMessageBusDecorator for event versioning
context.Items["Dispatch:OriginalMessageType"] = evt.GetType();
context.Items["Dispatch:UpcastedMessageType"] = upcasted.GetType();
context.Items["Dispatch:OriginalVersion"] = versioned.Version;
context.Items["Dispatch:UpcastedVersion"] = latestVersion;

Poison Message Handling​

// Some Items still used by PoisonMessageMiddleware
// Note: ProcessingAttempts, FirstAttemptTime, IsRetry are now direct properties
context.Items["CurrentAttemptTime"] = DateTimeOffset.UtcNow;
context.Items["ProcessingHistory"] = history;
context.Items["IsReplay"] = true;
context.Items["OriginalDeadLetterId"] = deadLetterMessage.Id;

Validation​

// Used by InputValidationMiddleware and profile validation
context.Items["PayloadSizeWarning"] = warningMessage;
// Note: ValidationPassed and ValidationTimestamp are now direct properties

Best Practices​

DO​

  • Use Items for transport-specific metadata that varies by transport
  • Use Items for user-defined headers/attributes with unpredictable keys
  • Prefix internal framework keys with Dispatch: to avoid collisions
  • Prefix transport-specific keys with the transport name (e.g., rabbitmq.)

DON'T​

  • Use Items for data accessed on every dispatch (use direct properties)
  • Use Items for cross-cutting concerns like correlation or tenancy
  • Store large objects in Items (impacts memory and pooling)
  • Rely on Items for data that should propagate to child contexts

Migration from Items to Direct Properties​

If you previously used Items for hot-path data, migrate to the new direct properties:

// Before (Items dictionary - around 6 ns per read)
context.Items["ValidationPassed"] = true;
var passed = (bool)context.Items["ValidationPassed"];

// After (Direct property - under one CPU cycle)
context.ValidationPassed = true;
var passed = context.ValidationPassed;

Available Direct Properties​

PropertyTypePurpose
ProcessingAttemptsintRetry tracking
FirstAttemptTimeDateTimeOffset?First processing timestamp
IsRetryboolRetry flag
ValidationPassedboolValidation result
ValidationTimestampDateTimeOffset?Validation completion time
Transactionobject?Active transaction
TransactionIdstring?Transaction identifier
TimeoutExceededboolTimeout flag
TimeoutElapsedTimeSpan?Elapsed time before timeout
RateLimitExceededboolRate limit flag
RateLimitRetryAfterTimeSpan?Retry-after duration

Performance Comparison​

Access MethodMeasuredAllocatedNotes
Direct property (CorrelationId, MessageId)~0.2 ns0 BA field read. See the note below.
Feature extension (GetTenantId(), GetUserId())5.7-7.7 ns0 BFeature lookup then field read
Items dictionary (Items["key"])5.7-6.9 ns0 BDictionary lookup + boxing
GetItem<T>()4.3-4.9 ns0 BDictionary + type check

Measured 2026-09-04, MessageContextBenchmarks, BenchmarkDotNet 0.15.8 in-process on .NET 10.0.11 (i9-14900K). Errors 0.006-0.13 ns.

What these numbers do and do not say

The direct-property figure is at the resolution limit. ~0.2 ns is well under one cycle on this CPU, which is what a field read the JIT can hoist looks like. Read it as effectively free, not as a precise 0.2.

A feature extension is not cheaper than the Items dictionary. They measure the same, around 6 ns, and typed GetItem<T>() is the cheapest of the three non-property options. If you were choosing a feature extension over Items for speed, that reason does not hold -- choose on type safety and intent instead.

The gap that is real is the first row against all the others -- roughly 30x, not the 10-20x previously stated here. Everything below the first row costs single-digit nanoseconds and allocates nothing.

Next Steps​

See Also​