MessageContext Best Practices
This guide covers performance optimization patterns for IMessageContext usage in high-throughput scenarios.
Before You Start
- .NET 8.0+ (or .NET 9/10 for latest features)
- Familiarity with message context and actions and handlers
Property Access Performance
Use Direct Properties for Hot-Path Data
Direct properties on IMessageContext provide ~10x better performance than the Items dictionary:
| Access Method | Latency | Use Case |
|---|---|---|
| Direct property | 1-3ns | Core framework data, frequently accessed |
| Items dictionary | 30-50ns | Transport-specific, user-defined data |
GetItem<T>() | 40-60ns | Same as Items + type cast |
DO:
// Fast - direct property access
context.ProcessingAttempts++;
context.ValidationPassed = true;
var isRetry = context.IsRetry;
DON'T:
// Slow - dictionary access with boxing
context.Items["ProcessingAttempts"] = attempts;
var passed = (bool)context.Items["ValidationPassed"];
Available Direct Properties
Use these properties instead of Items for common patterns:
// Retry tracking
context.ProcessingAttempts // int
context.FirstAttemptTime // DateTimeOffset?
context.IsRetry // bool
// Validation
context.ValidationPassed // bool
context.ValidationTimestamp // DateTimeOffset?
// Transactions
context.Transaction // object?
context.TransactionId // string?
// Timeout
context.TimeoutExceeded // bool
context.TimeoutElapsed // TimeSpan?
// Rate limiting
context.RateLimitExceeded // bool
context.RateLimitRetryAfter // TimeSpan?
Items Dictionary Patterns
When to Use Items
Items dictionary is appropriate for:
- Transport-specific metadata - Data that only exists for certain transports
- User-defined headers - HTTP headers, AMQP headers with unpredictable keys
- Infrequently accessed data - Setup once, read once
Key Naming Conventions
Use consistent prefixes to avoid collisions:
// Transport-specific (prefix with transport name)
context.Items["rabbitmq.exchange"] = exchange;
context.Items["rabbitmq.deliveryTag"] = deliveryTag;
// Internal framework (prefix with "Dispatch:")
context.Items["Dispatch:OriginalResult"] = result;
// CloudEvents (prefix with "ce.")
context.Items["ce.type"] = eventType;
// Custom application (prefix with app name)
context.Items["MyApp.CustomData"] = data;
Avoid Boxing for Value Types
If you must use Items with value types, consider caching:
// Slow - boxes int on every access
context.Items["counter"] = count;
var c = (int)context.Items["counter"];
// Better - use direct property if available
context.ProcessingAttempts = count;
var c = context.ProcessingAttempts;
Middleware Patterns
Read Once, Use Multiple Times
Don't repeatedly access the same property in a loop:
// Good - read once
var tenantId = context.TenantId;
foreach (var item in items)
{
Process(item, tenantId);
}
// Bad - repeated property access (though minimal cost for direct properties)
foreach (var item in items)
{
Process(item, context.TenantId);
}
Short-Circuit Early
Check conditions before expensive operations:
public async ValueTask<IMessageResult> InvokeAsync(
IDispatchMessage message, IMessageContext context,
DispatchRequestDelegate nextDelegate, CancellationToken cancellationToken)
{
// Fast check first
if (context.ValidationPassed)
{
return await nextDelegate(message, context, cancellationToken);
}
// Expensive validation only if needed
var isValid = await ValidateAsync(message);
context.ValidationPassed = isValid;
context.ValidationTimestamp = DateTimeOffset.UtcNow;
if (isValid)
{
return await nextDelegate(message, context, cancellationToken);
}
return MessageResult.Empty;
}
Use Null-Coalescing Assignment
// Good - only sets if null
context.FirstAttemptTime ??= DateTimeOffset.UtcNow;
// Unnecessary - always writes
if (context.FirstAttemptTime == null)
{
context.FirstAttemptTime = DateTimeOffset.UtcNow;
}
Context Propagation
Automatic Propagation
CreateChildContext() automatically propagates cross-cutting concerns:
var childContext = context.CreateChildContext();
// Propagated: CorrelationId, TenantId, UserId, SessionId,
// WorkflowId, TraceParent, Source
// Set: CausationId = parent.MessageId
// NOT copied: Items, hot-path properties
What's NOT Propagated
Hot-path properties reset for each context:
ProcessingAttemptsstarts at 0ValidationPassedstarts at falseTransactionstarts at null
This is intentional - each message tracks its own processing state.
Memory Considerations
Don't Store Large Objects in Items
Items dictionary values are stored by reference, but large object graphs:
- Increase memory pressure
- Slow down context pooling (clearing takes longer)
- May prevent objects from being collected
// Bad - storing large objects
context.Items["FullResponse"] = largeResponseObject;
// Better - store reference/ID and fetch when needed
context.Items["ResponseId"] = responseId;
Clear Temporary Data
If you add temporary Items, consider removing them:
try
{
context.Items["temp.data"] = tempData;
await ProcessAsync(context);
}
finally
{
context.Items.Remove("temp.data");
}
Benchmarks
Typical performance at scale (100K messages/second):
| Pattern | CPU Cost per Second |
|---|---|
| 1 direct property read | ~0.2ms |
| 1 Items dictionary read | ~3.5ms |
| 10 direct property reads | ~2ms |
| 10 Items dictionary reads | ~35ms |
For middleware accessing 5-10 properties per message, direct properties save ~30ms of CPU time per second at 100K msg/s throughput.
Summary
- Use direct properties for ProcessingAttempts, ValidationPassed, IsRetry, etc.
- Use Items for transport-specific and user-defined data only
- Prefix Items keys to avoid collisions
- Read properties once if used multiple times
- Short-circuit early to avoid unnecessary work
- Don't store large objects in Items
See Also
- Message Context - Core concepts and API reference for IMessageContext
- Auto-Freeze - Automatic FrozenDictionary cache optimization on startup
- Performance Overview - Full performance guide and optimization strategies
Next Steps
- MessageContext Design - Architecture details
- MessageContext Items Usage - Items dictionary guidance