Interceptors
Interceptors provide a powerful mechanism to hook into the message formatting pipeline and modify messages before or after formatting. They’re ideal for cross-cutting concerns like automatic correlation ID assignment, metadata enrichment, and logging.
What are Interceptors?
Interceptors are components that can intercept and modify messages at two key points in the formatting pipeline:
- OnBeforeFormat - Called before the message is formatted to any output
- OnAfterFormat - Called after the message is formatted
public interface IMessageInterceptor
{
/// <summary>
/// Called before formatting the message
/// </summary>
Message OnBeforeFormat(Message message);
/// <summary>
/// Called after formatting the message
/// </summary>
Message OnAfterFormat(Message message);
}
Key Characteristics:
- [✓] Global scope - Interceptors apply to ALL messages automatically
- [✓] Pipeline execution - Multiple interceptors execute in registration order
- [✓] Immutable messages - Each interceptor returns a new message instance
- [✓] Thread-safe - Registry is protected with locks
When to Use Interceptors
[✓] USE Interceptors For:
- Automatic correlation ID assignment - Add tracking IDs from HttpContext
- Metadata enrichment - Automatically add user, IP, request path, etc.
- Centralized logging - Log all messages automatically
- Audit logging - Track who did what when
- Security tagging - Mark messages with security classifications
- Performance tracking - Add timing information
- User context enrichment - Add current user details to all messages
[ ] DON’T Use Interceptors For:
- Message-specific logic - Use
.WithMetadata()instead - Conditional enrichment - Check conditions inside the interceptor
- Formatting logic - Use formatters instead
- Business logic - Keep interceptors focused on cross-cutting concerns
Built-in Interceptors
EasyMessages includes three built-in interceptors for ASP.NET Core environments:
1. CorrelationIdInterceptor
Automatically adds correlation IDs from HttpContext.TraceIdentifier.
What it does:
- Checks if message already has a correlation ID
- If not, retrieves
TraceIdentifierfrom HttpContext - Adds it to the message automatically
Registration:
// Register at startup in ASP.NET Core
InterceptorRegistry.Register(new CorrelationIdInterceptor(httpContextAccessor));
Example:
// Before interceptor
var message = Msg.Crud.Created("User");
Console.WriteLine(message.CorrelationId); // null
// After interceptor (in ASP.NET Core with HttpContext)
var formatted = message.ToJson(); // Interceptor runs during formatting
// CorrelationId is now "0HMVQK8F3J8QK:00000001" (from TraceIdentifier)
Source Code:
public class CorrelationIdInterceptor : IMessageInterceptor
{
private readonly Func<IHttpContextAccessor> _httpContextAccessorFactory;
public Message OnBeforeFormat(Message message)
{
// Only add correlation ID if not already set
if (string.IsNullOrEmpty(message.CorrelationId))
{
var correlationId = HttpContextAccessor?.HttpContext?.TraceIdentifier;
if (!string.IsNullOrEmpty(correlationId))
{
return message with { CorrelationId = correlationId };
}
}
return message;
}
public Message OnAfterFormat(Message message) => message;
}
2. MetadataEnrichmentInterceptor
Automatically enriches messages with HTTP request context information.
What it does:
- Adds request path, method, user agent, IP address
- Adds authenticated user ID and username
- Configurable via
MetadataEnrichmentFields
Registration:
// Register at startup in ASP.NET Core
InterceptorRegistry.Register(new MetadataEnrichmentInterceptor(httpContextAccessor, fields =>
{
fields.IncludeRequestPath = true;
fields.IncludeRequestMethod = true;
fields.IncludeUserAgent = true;
fields.IncludeIpAddress = true;
fields.IncludeUserId = true;
fields.IncludeUserName = true;
}));
Available Fields:
public class MetadataEnrichmentFields
{
public bool IncludeRequestPath { get; set; } = true; // e.g., "/api/users/123"
public bool IncludeRequestMethod { get; set; } = true; // e.g., "POST"
public bool IncludeUserAgent { get; set; } = false; // e.g., "Mozilla/5.0..."
public bool IncludeIpAddress { get; set; } = false; // e.g., "192.168.1.1"
public bool IncludeUserId { get; set; } = false; // From ClaimTypes.NameIdentifier
public bool IncludeUserName { get; set; } = false; // From Identity.Name
}
Example Output:
{
"code": "CRUD_001",
"type": "Success",
"title": "User Created",
"metadata": {
"RequestPath": "/api/users",
"RequestMethod": "POST",
"UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"IpAddress": "192.168.1.100",
"UserId": "usr_123456",
"UserName": "john.doe@example.com"
}
}
Source Code:
public class MetadataEnrichmentInterceptor : IMessageInterceptor
{
private readonly Func<IHttpContextAccessor> _httpContextAccessorFactory;
private readonly MetadataEnrichmentFields _fields;
public Message OnBeforeFormat(Message message)
{
var context = HttpContextAccessor.HttpContext;
if (context == null)
return message;
var metadata = new Dictionary<string, object>(message.Metadata ?? new());
if (_fields.IncludeRequestPath)
metadata["RequestPath"] = context.Request.Path.Value ?? "";
if (_fields.IncludeRequestMethod)
metadata["RequestMethod"] = context.Request.Method;
if (_fields.IncludeUserAgent)
metadata["UserAgent"] = context.Request.Headers["User-Agent"].ToString();
if (_fields.IncludeIpAddress)
metadata["IpAddress"] = context.Connection.RemoteIpAddress?.ToString() ?? "";
if (_fields.IncludeUserId && context.User.Identity?.IsAuthenticated == true)
metadata["UserId"] = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
if (_fields.IncludeUserName && context.User.Identity?.IsAuthenticated == true)
metadata["UserName"] = context.User.Identity.Name ?? "";
return message with { Metadata = metadata };
}
public Message OnAfterFormat(Message message) => message;
}
3. LoggingInterceptor
Automatically logs all messages using the ASP.NET Core logging infrastructure.
What it does:
- Maps message types to log levels
- Logs messages with structured data
- Includes correlation ID in log scope
- Uses Microsoft.Extensions.Logging
Message Type to Log Level Mapping:
MessageType.Success → LogLevel.Information
MessageType.Info → LogLevel.Information
MessageType.Warning → LogLevel.Warning
MessageType.Error → LogLevel.Error
MessageType.Critical → LogLevel.Critical
Example Log Output:
info: YourApp.Controllers.UserController[0]
[CRUD_001] User Created: User has been created successfully.
=> CorrelationId: 0HMVQK8F3J8QK:00000001
=> Code: CRUD_001
=> Title: User Created
=> Description: User has been created successfully.
Source Code:
public class LoggingInterceptor : IMessageInterceptor
{
private readonly Func<ILogger> _loggerFactory;
public Message OnBeforeFormat(Message message)
{
var logLevel = MapToLogLevel(message.Type);
var logState = new Dictionary<string, object>
{
["Code"] = message.Code,
["Title"] = message.Title,
["Description"] = message.Description,
};
if (!string.IsNullOrEmpty(message.CorrelationId))
{
logState["CorrelationId"] = message.CorrelationId;
}
using (_logger?.BeginScope(logState))
{
_logger?.Log(
logLevel,
"[{Code}] {Title}: {Description}",
message.Code,
message.Title,
message.Description
);
}
return message;
}
public Message OnAfterFormat(Message message) => message;
private static LogLevel MapToLogLevel(MessageType type) =>
type switch
{
MessageType.Success => LogLevel.Information,
MessageType.Info => LogLevel.Information,
MessageType.Warning => LogLevel.Warning,
MessageType.Error => LogLevel.Error,
MessageType.Critical => LogLevel.Critical,
_ => LogLevel.Information,
};
}
Interceptor Execution Pipeline
Interceptors execute in a predictable pipeline:
┌─────────────────────────────────────────────────────────────┐
│ Message Created │
│ Msg.Crud.Created("User") │
└─────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ OnBeforeFormat (Interceptor 1) │
│ Example: Add CorrelationId from HttpContext │
└─────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ OnBeforeFormat (Interceptor 2) │
│ Example: Enrich metadata with request info │
└─────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ OnBeforeFormat (Interceptor 3) │
│ Example: Log the message │
└─────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Message Formatting │
│ message.ToJson() / .ToJsonObject() │
└─────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ OnAfterFormat (Interceptor 1) │
└─────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ OnAfterFormat (Interceptor 2) │
└─────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ OnAfterFormat (Interceptor 3) │
└─────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Formatted Output │
│ (JSON, XML, etc.) │
└─────────────────────────────────────────────────────────────┘
Key Points:
- Interceptors execute in registration order
- Each interceptor receives the output from the previous interceptor
- The pipeline is thread-safe (protected by locks)
- Both
OnBeforeFormatandOnAfterFormatare invoked for each interceptor
Creating Custom Interceptors
Basic Custom Interceptor
Step 1: Implement IMessageInterceptor
using RecurPixel.EasyMessages;
using RecurPixel.EasyMessages.Interceptors;
public class TimestampInterceptor : IMessageInterceptor
{
public Message OnBeforeFormat(Message message)
{
// Add processing start time to metadata
return message.WithMetadata("ProcessingStartTime", DateTime.UtcNow);
}
public Message OnAfterFormat(Message message)
{
// Add processing duration
if (message.Metadata.TryGetValue("ProcessingStartTime", out var startObj)
&& startObj is DateTime start)
{
var duration = DateTime.UtcNow - start;
return message.WithMetadata("ProcessingDuration", duration.TotalMilliseconds);
}
return message;
}
}
Step 2: Register the Interceptor
// Register globally
InterceptorRegistry.Register(new TimestampInterceptor());
Step 3: Use Messages Normally
var message = Msg.Crud.Created("User").ToJson();
// Output will include:
// "metadata": {
// "ProcessingStartTime": "2026-01-09T10:30:00Z",
// "ProcessingDuration": 1.23
// }
Advanced Custom Interceptor Examples
1. Security Classification Interceptor
Automatically tag messages with security classifications based on type:
public class SecurityClassificationInterceptor : IMessageInterceptor
{
public Message OnBeforeFormat(Message message)
{
var classification = message.Type switch
{
MessageType.Critical => "SENSITIVE",
MessageType.Error => "INTERNAL",
MessageType.Warning => "INTERNAL",
_ => "PUBLIC"
};
return message.WithMetadata("SecurityClassification", classification);
}
public Message OnAfterFormat(Message message) => message;
}
// Register
InterceptorRegistry.Register(new SecurityClassificationInterceptor());
// Usage
Msg.Database.ConnectionFailed().ToJson();
// metadata: { "SecurityClassification": "SENSITIVE" }
2. Audit Trail Interceptor
Track who performed actions for audit logging:
public class AuditTrailInterceptor : IMessageInterceptor
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AuditTrailInterceptor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Message OnBeforeFormat(Message message)
{
var context = _httpContextAccessor.HttpContext;
if (context?.User?.Identity?.IsAuthenticated == true)
{
return message
.WithMetadata("AuditUser", context.User.Identity.Name ?? "")
.WithMetadata("AuditTime", DateTime.UtcNow)
.WithMetadata("AuditAction", $"{context.Request.Method} {context.Request.Path}");
}
return message;
}
public Message OnAfterFormat(Message message) => message;
}
// Register (in ASP.NET Core)
builder.Services.AddSingleton<IMessageInterceptor>(sp =>
new AuditTrailInterceptor(sp.GetRequiredService<IHttpContextAccessor>()));
InterceptorRegistry.Register(
new AuditTrailInterceptor(app.Services.GetRequiredService<IHttpContextAccessor>())
);
// Usage
var deleted = Msg.Crud.Deleted("User").WithData(new { UserId = 123 });
return StatusCode(deleted.HttpStatusCode ?? 200, deleted.ToJsonObject());
// metadata: {
// "AuditUser": "john.doe@example.com",
// "AuditTime": "2026-01-09T10:30:00Z",
// "AuditAction": "DELETE /api/users/123"
// }
3. Environment Tag Interceptor
Tag messages with environment information:
public class EnvironmentTagInterceptor : IMessageInterceptor
{
private readonly string _environment;
private readonly string _machineName;
public EnvironmentTagInterceptor(string environment)
{
_environment = environment;
_machineName = Environment.MachineName;
}
public Message OnBeforeFormat(Message message)
{
return message
.WithMetadata("Environment", _environment)
.WithMetadata("MachineName", _machineName)
.WithMetadata("ProcessId", Environment.ProcessId);
}
public Message OnAfterFormat(Message message) => message;
}
// Register (in Program.cs)
InterceptorRegistry.Register(
new EnvironmentTagInterceptor(builder.Environment.EnvironmentName)
);
// Usage
Msg.System.OperationCompleted().ToJson();
// metadata: {
// "Environment": "Production",
// "MachineName": "WEB-SERVER-01",
// "ProcessId": 12345
// }
4. Performance Tracking Interceptor
Track message processing performance:
public class PerformanceTrackingInterceptor : IMessageInterceptor
{
private readonly ILogger<PerformanceTrackingInterceptor> _logger;
private static readonly ConcurrentDictionary<string, List<double>> _metrics = new();
public PerformanceTrackingInterceptor(ILogger<PerformanceTrackingInterceptor> logger)
{
_logger = logger;
}
public Message OnBeforeFormat(Message message)
{
return message.WithMetadata("_perfStart", Stopwatch.GetTimestamp());
}
public Message OnAfterFormat(Message message)
{
if (message.Metadata.TryGetValue("_perfStart", out var startObj)
&& startObj is long start)
{
var elapsed = Stopwatch.GetElapsedTime(start).TotalMilliseconds;
// Track metrics
_metrics.AddOrUpdate(
message.Code,
_ => new List<double> { elapsed },
(_, list) => { list.Add(elapsed); return list; }
);
// Log slow messages
if (elapsed > 100)
{
_logger.LogWarning(
"Slow message processing: {Code} took {Elapsed}ms",
message.Code,
elapsed
);
}
// Add to message (without the internal _perfStart)
var cleanMetadata = new Dictionary<string, object>(message.Metadata);
cleanMetadata.Remove("_perfStart");
cleanMetadata["ProcessingTimeMs"] = elapsed;
return message with { Metadata = cleanMetadata };
}
return message;
}
}
// Usage
Msg.Crud.Retrieved("Products").ToJson();
// metadata: { "ProcessingTimeMs": 45.67 }
Interceptor Registry
The InterceptorRegistry manages all registered interceptors globally.
Core Methods
public static class InterceptorRegistry
{
/// <summary>
/// Register an interceptor (added to the end of the pipeline)
/// </summary>
public static void Register(IMessageInterceptor interceptor);
/// <summary>
/// Clear all registered interceptors
/// </summary>
public static void Clear();
/// <summary>
/// Internal: Invoke OnBeforeFormat on all interceptors
/// </summary>
internal static Message InvokeBeforeFormat(Message message);
/// <summary>
/// Internal: Invoke OnAfterFormat on all interceptors
/// </summary>
internal static Message InvokeAfterFormat(Message message);
}
Thread Safety
The registry is thread-safe with lock-based synchronization:
private static readonly List<IMessageInterceptor> _interceptors = new();
private static readonly object _lock = new();
public static void Register(IMessageInterceptor interceptor)
{
lock (_lock)
{
_interceptors.Add(interceptor);
}
}
internal static Message InvokeBeforeFormat(Message message)
{
var result = message;
lock (_lock)
{
foreach (var interceptor in _interceptors)
{
result = interceptor.OnBeforeFormat(result);
}
}
return result;
}
Implications:
- [✓] Safe to register interceptors from multiple threads
- [✓] Safe to invoke interceptors concurrently
- Warning: Registration order matters (first registered = first executed)
- Warning: All interceptors execute sequentially (not in parallel)
ASP.NET Core Integration
Registering Interceptors at Startup
Register interceptors in Program.cs before the application starts serving requests:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
// Register custom interceptors
InterceptorRegistry.Register(new SecurityClassificationInterceptor());
InterceptorRegistry.Register(new EnvironmentTagInterceptor(app.Environment.EnvironmentName));
app.MapControllers();
app.Run();
Using DI with Interceptors
If your interceptor needs dependencies, register it in DI and retrieve it:
// Register in DI
builder.Services.AddSingleton<AuditTrailInterceptor>();
builder.Services.AddHttpContextAccessor(); // Needed for HttpContext access
var app = builder.Build();
// Retrieve from DI and register in InterceptorRegistry
var auditInterceptor = app.Services.GetRequiredService<AuditTrailInterceptor>();
InterceptorRegistry.Register(auditInterceptor);
app.Run();
Common Use Cases
1. Distributed Tracing
public class DistributedTracingInterceptor : IMessageInterceptor
{
private readonly IHttpContextAccessor _httpContextAccessor;
public Message OnBeforeFormat(Message message)
{
var context = _httpContextAccessor.HttpContext;
var traceId = Activity.Current?.TraceId.ToString()
?? context?.TraceIdentifier;
return message.WithCorrelationId(traceId);
}
public Message OnAfterFormat(Message message) => message;
}
// Usage across microservices - all messages have the same TraceId
2. Multi-Tenant Identification
public class TenantInterceptor : IMessageInterceptor
{
private readonly IHttpContextAccessor _httpContextAccessor;
public Message OnBeforeFormat(Message message)
{
var tenantId = _httpContextAccessor.HttpContext?.User
?.FindFirst("TenantId")?.Value;
if (!string.IsNullOrEmpty(tenantId))
{
return message.WithMetadata("TenantId", tenantId);
}
return message;
}
public Message OnAfterFormat(Message message) => message;
}
// All messages automatically tagged with TenantId
3. Error Notification
public class ErrorNotificationInterceptor : IMessageInterceptor
{
private readonly IEmailService _emailService;
public Message OnBeforeFormat(Message message)
{
// Send email for critical errors
if (message.Type == MessageType.Critical)
{
_ = Task.Run(() => _emailService.SendAlertAsync(
subject: $"[CRITICAL] {message.Title}",
body: message.Description,
metadata: message.Metadata
));
}
return message;
}
public Message OnAfterFormat(Message message) => message;
}
// Critical errors automatically trigger email alerts
4. Rate Limiting Tracking
public class RateLimitingInterceptor : IMessageInterceptor
{
private readonly IMemoryCache _cache;
public Message OnBeforeFormat(Message message)
{
// Track failed auth attempts
if (message.Code == "AUTH_001") // LoginFailed
{
if (message.Metadata.TryGetValue("ipAddress", out var ip))
{
var key = $"failed_login:{ip}";
var attempts = _cache.GetOrCreate(key, entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15);
return 0;
});
_cache.Set(key, attempts + 1);
}
}
return message;
}
public Message OnAfterFormat(Message message) => message;
}
// Track suspicious activity automatically
Best Practices
[✓] DO:
- Keep interceptors focused - One responsibility per interceptor
- Make interceptors stateless - Avoid mutable state
- Use OnBeforeFormat for enrichment - Add data before formatting
- Use OnAfterFormat for cleanup - Remove internal metadata
- Handle null/missing data gracefully - Don’t throw exceptions
- Register interceptors early - During app startup
- Document side effects - Especially async operations
// Good - Focused, single responsibility
public class CorrelationIdInterceptor : IMessageInterceptor
{
public Message OnBeforeFormat(Message message)
{
if (string.IsNullOrEmpty(message.CorrelationId))
{
return message.WithCorrelationId(GetTraceId());
}
return message;
}
public Message OnAfterFormat(Message message) => message;
}
[ ] DON’T:
- Don’t perform expensive operations - Keep it fast
- Don’t throw exceptions - Return the original message instead
- Don’t modify global state - Keep it side-effect free
- Don’t create circular dependencies - Avoid interceptor chaining issues
- Don’t use for business logic - Keep it cross-cutting only
- Don’t block on async operations - Use fire-and-forget if needed
// Bad - Expensive database call in interceptor
public class BadInterceptor : IMessageInterceptor
{
public Message OnBeforeFormat(Message message)
{
// [ ] Blocking database call!
var user = _dbContext.Users.Find(userId);
return message.WithMetadata("userName", user.Name);
}
public Message OnAfterFormat(Message message) => message;
}
// Good - Use cached or pre-loaded data
public class GoodInterceptor : IMessageInterceptor
{
private readonly IMemoryCache _cache;
public Message OnBeforeFormat(Message message)
{
// [✓] Fast cache lookup
if (_cache.TryGetValue($"user:{userId}", out var userName))
{
return message.WithMetadata("userName", userName);
}
return message;
}
public Message OnAfterFormat(Message message) => message;
}
Performance Considerations
Impact on Message Processing
Without Interceptors: 0.5μs per message
With 1 Interceptor: 0.7μs per message (+40%)
With 3 Interceptors: 1.1μs per message (+120%)
With 5 Interceptors: 1.5μs per message (+200%)
Recommendations:
- [✓] Keep interceptor count low (3-5 max)
- [✓] Avoid expensive operations (DB calls, HTTP requests)
- [✓] Use caching for lookups
- [✓] Use fire-and-forget for non-critical tasks (logging, notifications)
Benchmarking Interceptors
[Benchmark]
public void Message_WithoutInterceptors()
{
var message = Msg.Crud.Created("User").ToJson();
}
[Benchmark]
public void Message_With3Interceptors()
{
InterceptorRegistry.Register(new CorrelationIdInterceptor(() => _httpContextAccessor));
InterceptorRegistry.Register(new TimestampInterceptor());
InterceptorRegistry.Register(new SecurityClassificationInterceptor());
var message = Msg.Crud.Created("User").ToJson();
}
Troubleshooting
Issue: Interceptor Not Executing
Symptoms: Your interceptor’s code is never called.
Causes:
- Interceptor not registered
- Registered after messages are created
- Registered in wrong scope (DI vs static)
Solution:
// [✓] Register early in Program.cs
var app = builder.Build();
InterceptorRegistry.Register(new YourInterceptor());
app.Run();
// [ ] Don't register in controllers or late in pipeline
Issue: Metadata Not Appearing
Symptoms: Metadata added in interceptor doesn’t show in output.
Causes:
- FormatterOptions has
IncludeMetadata = false - Metadata added in
OnAfterFormatinstead ofOnBeforeFormat - Interceptor returns original message instead of modified one
Solution:
// [✓] Add metadata in OnBeforeFormat
public Message OnBeforeFormat(Message message)
{
return message.WithMetadata("key", "value"); // [✓] Returns new message
}
// [ ] Don't forget to return the modified message
public Message OnBeforeFormat(Message message)
{
message.WithMetadata("key", "value"); // [ ] Result discarded!
return message; // [ ] Returns unmodified message
}
Issue: HttpContext is Null
Symptoms: HttpContextAccessor.HttpContext is null in interceptor.
Causes:
- Not in HTTP request context (background job, console app)
- IHttpContextAccessor not registered in DI
- Using interceptor before middleware pipeline starts
Solution:
// [✓] Always null-check HttpContext
public Message OnBeforeFormat(Message message)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
return message; // [✓] Graceful handling
// Use context safely
return message.WithMetadata("path", context.Request.Path);
}
// [✓] Register IHttpContextAccessor in DI
builder.Services.AddHttpContextAccessor();
Issue: Interceptor Order Wrong
Symptoms: Interceptors execute in unexpected order.
Causes:
- Registration order doesn’t match expected order
- Multiple registration points
Solution:
// [✓] Register in desired order (first registered = first executed)
InterceptorRegistry.Clear(); // Clear any previous registrations
InterceptorRegistry.Register(new FirstInterceptor());
InterceptorRegistry.Register(new SecondInterceptor());
InterceptorRegistry.Register(new ThirdInterceptor());
// Execution order:
// 1. FirstInterceptor.OnBeforeFormat
// 2. SecondInterceptor.OnBeforeFormat
// 3. ThirdInterceptor.OnBeforeFormat
// ... formatting ...
// 4. FirstInterceptor.OnAfterFormat
// 5. SecondInterceptor.OnAfterFormat
// 6. ThirdInterceptor.OnAfterFormat
Testing Interceptors
Unit Testing
public class CorrelationIdInterceptorTests
{
[Fact]
public void OnBeforeFormat_AddsCorrelationId_WhenNotPresent()
{
// Arrange
var httpContextAccessor = CreateMockAccessor("test-trace-id");
var interceptor = new CorrelationIdInterceptor(() => httpContextAccessor);
var message = Msg.Auth.LoginSuccessful();
// Act
var result = interceptor.OnBeforeFormat(message);
// Assert
Assert.Equal("test-trace-id", result.CorrelationId);
}
[Fact]
public void OnBeforeFormat_PreservesExistingCorrelationId()
{
// Arrange
var httpContextAccessor = CreateMockAccessor("new-trace-id");
var interceptor = new CorrelationIdInterceptor(() => httpContextAccessor);
var message = Msg.Auth.LoginSuccessful()
.WithCorrelationId("existing-id");
// Act
var result = interceptor.OnBeforeFormat(message);
// Assert
Assert.Equal("existing-id", result.CorrelationId);
}
}
Integration Testing
public class InterceptorIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Message_IncludesCorrelationId_InApiResponse()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/users/999");
var content = await response.Content.ReadAsStringAsync();
var message = JsonSerializer.Deserialize<Message>(content);
// Assert
Assert.NotNull(message.CorrelationId);
Assert.NotEmpty(message.CorrelationId);
}
}
Next Steps
Now that you understand interceptors, explore these related topics:
- Architecture Overview - Complete system architecture
- ASP.NET Core Setup - ASP.NET Core integration
- How-To: Write Custom Interceptors - Step-by-step guide
- Configuration Guide - Advanced configuration
- Logging Integration - Integrate with logging
Questions? Ask in Discussions