调试技巧
🔍 高效排查 MediatR 应用中的问题
📖 概述
调试 MediatR 应用需要掌握一些特殊技巧,因为请求处理涉及管道行为链、依赖注入和异步执行。本章介绍实用的调试方法和工具。
🛠️ 日志输出请求执行流程
增强版日志行为
csharp
public class DetailedLoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<DetailedLoggingBehavior<TRequest, TResponse>> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
public DetailedLoggingBehavior(
ILogger<DetailedLoggingBehavior<TRequest, TResponse>> logger,
IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var requestId = Guid.NewGuid();
var requestName = typeof(TRequest).Name;
var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
_logger.LogInformation(
"═══════════════════════════════════════\n" +
"📨 开始处理请求\n" +
" 请求ID: {RequestId}\n" +
" 请求类型: {RequestName}\n" +
" 用户ID: {UserId}\n" +
" 时间戳: {Timestamp}\n" +
" 请求数据: {@Request}\n" +
"═══════════════════════════════════════",
requestId,
requestName,
userId ?? "Anonymous",
DateTime.UtcNow,
request);
var stopwatch = Stopwatch.StartNew();
try
{
var response = await next();
stopwatch.Stop();
_logger.LogInformation(
"✅ 请求处理成功\n" +
" 请求ID: {RequestId}\n" +
" 耗时: {ElapsedMilliseconds}ms\n" +
" 响应数据: {@Response}",
requestId,
stopwatch.ElapsedMilliseconds,
response);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(
ex,
"❌ 请求处理失败\n" +
" 请求ID: {RequestId}\n" +
" 耗时: {ElapsedMilliseconds}ms\n" +
" 异常类型: {ExceptionType}\n" +
" 异常消息: {Message}",
requestId,
stopwatch.ElapsedMilliseconds,
ex.GetType().Name,
ex.Message);
throw;
}
}
}日志输出示例
═══════════════════════════════════════
📨 开始处理请求
请求ID: a3f8b2c1-4d5e-6f7g-8h9i-0j1k2l3m4n5o
请求类型: CreateOrderCommand
用户ID: user123
时间戳: 2026-04-12 10:30:45.123
请求数据: {"ProductName":"iPhone","Quantity":2,"Price":7999}
═══════════════════════════════════════
✅ 请求处理成功
请求ID: a3f8b2c1-4d5e-6f7g-8h9i-0j1k2l3m4n5o
耗时: 125ms
响应数据: {"OrderId":"b4c9d3e2-...","TotalAmount":15998}📊 可视化管道执行顺序
调试行为链
csharp
public class PipelineTraceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private static int _behaviorCounter = 0;
private readonly int _order;
private readonly string _behaviorName;
public PipelineTraceBehavior()
{
_order = Interlocked.Increment(ref _behaviorCounter);
_behaviorName = GetType().Name;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var indent = new string(' ', _order * 2);
Console.WriteLine($"{indent}➡️ 进入 [{_order}] {_behaviorName}");
try
{
var response = await next();
Console.WriteLine($"{indent}⬅️ 离开 [{_order}] {_behaviorName}");
return response;
}
catch (Exception ex)
{
Console.WriteLine($"{indent}❌ 异常 [{_order}] {_behaviorName}: {ex.Message}");
throw;
}
}
}
// 注册(按顺序)
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PipelineTraceBehavior<,>)); // Logging
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PipelineTraceBehavior<,>)); // Validation
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PipelineTraceBehavior<,>)); // Transaction输出示例
➡️ 进入 [1] LoggingBehavior
➡️ 进入 [2] ValidationBehavior
➡️ 进入 [3] TransactionBehavior
➡️ 进入 [4] CachingBehavior
➡️ 进入 [5] PerformanceBehavior
🎯 Handler 执行业务逻辑
⬅️ 离开 [5] PerformanceBehavior
⬅️ 离开 [4] CachingBehavior
⬅️ 离开 [3] TransactionBehavior
⬅️ 离开 [2] ValidationBehavior
⬅️ 离开 [1] LoggingBehavior🔬 诊断监听器与 DiagnosticSource
创建诊断监听器
csharp
public class MediatRDiagnosticsObserver : IObserver<KeyValuePair<string, object>>
{
private readonly ILogger<MediatRDiagnosticsObserver> _logger;
private readonly ActivitySource _activitySource;
public MediatRDiagnosticsObserver(
ILogger<MediatRDiagnosticsObserver> logger,
ActivitySource activitySource)
{
_logger = logger;
_activitySource = activitySource;
}
public void OnNext(KeyValuePair<string, object> kvp)
{
switch (kvp.Key)
{
case "MediatR.Request.Start":
HandleRequestStart(kvp.Value);
break;
case "MediatR.Request.Stop":
HandleRequestStop(kvp.Value);
break;
case "MediatR.Request.Error":
HandleRequestError(kvp.Value);
break;
case "MediatR.Notification.Start":
HandleNotificationStart(kvp.Value);
break;
}
}
private void HandleRequestStart(object data)
{
var requestData = (dynamic)data;
_logger.LogInformation("🚀 请求开始: {RequestName}", requestData.RequestName);
// 创建 Activity 用于分布式追踪
var activity = _activitySource.StartActivity($"MediatR.{requestData.RequestName}");
activity?.SetTag("request.name", requestData.RequestName);
}
private void HandleRequestStop(object data)
{
var responseData = (dynamic)data;
_logger.LogInformation(
"✅ 请求完成: {RequestName}, 耗时: {Duration}ms",
responseData.RequestName,
responseData.Duration);
}
private void HandleRequestError(object data)
{
var errorData = (dynamic)data;
_logger.LogError(
errorData.Exception,
"❌ 请求失败: {RequestName}, 错误: {Message}",
errorData.RequestName,
errorData.Exception.Message);
}
public void OnCompleted() { }
public void OnError(Exception error) { }
}注册诊断监听器
csharp
// Program.cs
var diagnosticListener = new DiagnosticListener("MediatR");
var observer = new MediatRDiagnosticsObserver(
loggerFactory.CreateLogger<MediatRDiagnosticsObserver>(),
new ActivitySource("MediatR"));
diagnosticListener.Subscribe(observer);
builder.Services.AddSingleton(diagnosticListener);⚡ 性能分析(BenchmarkDotNet)
安装
bash
dotnet add package BenchmarkDotNet创建基准测试
csharp
[MemoryDiagnoser]
[HtmlExporter]
public class MediatRBenchmarks
{
private IMediator _mediator;
private CreateOrderCommand _command;
[GlobalSetup]
public void Setup()
{
var services = new ServiceCollection();
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(CreateOrderHandler).Assembly));
services.AddScoped<IOrderRepository, InMemoryOrderRepository>();
var provider = services.BuildServiceProvider();
_mediator = provider.GetRequiredService<IMediator>();
_command = new CreateOrderCommand
{
ProductName = "Benchmark Product",
Quantity = 1,
Price = 100
};
}
[Benchmark]
public async Task<OrderResult> SendCommand()
{
return await _mediator.Send(_command);
}
[Benchmark]
public async Task PublishNotification()
{
await _mediator.Publish(new OrderCreatedNotification
{
OrderId = Guid.NewGuid(),
TotalAmount = 100
});
}
}
// 运行基准测试
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<MediatRBenchmarks>();
}
}基准测试结果示例
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|-------------------- |---------:|--------:|--------:|-------:|----------:|
| SendCommand | 125.3 μs | 2.14 μs | 1.89 μs | 5.3711 | 44.2 KB |
| PublishNotification | 89.7 μs | 1.56 μs | 1.38 μs | 3.1738 | 26.1 KB |🐛 常见问题排查
问题 1:Handler 未被发现
症状:InvalidOperationException: No service for type 'IRequestHandler'
排查步骤:
csharp
// 1. 检查程序集扫描
var assembly = typeof(Program).Assembly;
Console.WriteLine($"扫描程序集: {assembly.FullName}");
// 2. 列出所有注册的 Handler
var handlers = assembly.GetTypes()
.Where(t => t.GetInterfaces().Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IRequestHandler<,>)))
.ToList();
Console.WriteLine($"找到 {handlers.Count} 个 Handler:");
foreach (var handler in handlers)
{
Console.WriteLine($" - {handler.FullName}");
}
// 3. 确认 Handler 是 public 且非抽象
public class MyHandler : IRequestHandler<MyCommand> // ✅ 正确
internal class MyHandler : IRequestHandler<MyCommand> // ❌ 错误:必须是 public解决方案:
csharp
// 确保扫描正确的程序集
builder.Services.AddMediatR(cfg => {
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
cfg.RegisterServicesFromAssembly(typeof(SharedLibraryMarker).Assembly);
});问题 2:管道行为未执行
症状:日志行为没有输出日志
排查步骤:
csharp
// 1. 检查行为是否注册
var behaviors = builder.Services
.Where(s => s.ServiceType == typeof(IPipelineBehavior<,>))
.ToList();
Console.WriteLine($"注册了 {behaviors.Count} 个管道行为:");
foreach (var behavior in behaviors)
{
Console.WriteLine($" - {behavior.ImplementationType?.Name}");
}
// 2. 确认注册顺序
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));问题 3:循环依赖
症状:InvalidOperationException: A circular dependency was detected
排查方法:
csharp
// 使用依赖注入容器检测循环依赖
try
{
var mediator = serviceProvider.GetRequiredService<IMediator>();
}
catch (InvalidOperationException ex) when (ex.Message.Contains("circular"))
{
Console.WriteLine("检测到循环依赖!");
Console.WriteLine(ex.StackTrace);
}解决方案:
csharp
// ❌ 错误:循环依赖
public class OrderHandler : IRequestHandler<OrderCommand>
{
private readonly PaymentHandler _paymentHandler; // 循环依赖
}
// ✅ 正确:提取共享服务
public class OrderProcessingService
{
public async Task ProcessPayment(Guid orderId) { /* ... */ }
}
public class OrderHandler : IRequestHandler<OrderCommand>
{
private readonly OrderProcessingService _orderProcessingService;
}
public class PaymentHandler : IRequestHandler<PaymentCommand>
{
private readonly OrderProcessingService _orderProcessingService;
}问题 4:通知处理器异常中断
症状:一个处理器失败导致其他处理器不执行
解决方案:使用容错 Mediator
csharp
public class ResilientMediator : Mediator
{
private readonly ILogger<ResilientMediator> _logger;
protected override async Task PublishCore(
IEnumerable<NotificationHandlerExecutor> handlerExecutors,
INotification notification,
CancellationToken cancellationToken)
{
foreach (var handlerExecutor in handlerExecutors)
{
try
{
await handlerExecutor.HandlerCallback(notification, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"通知处理器失败,但继续执行其他处理器: {HandlerType}",
handlerExecutor.HandlerInstance.GetType().Name);
}
}
}
}🔍 Visual Studio 调试技巧
1. 条件断点
csharp
// 在 Handler 中设置条件断点
public async Task<OrderResult> Handle(CreateOrderCommand request, CancellationToken ct)
{
// 右键点击行号 → 条件 → 输入条件
// 例如:request.ProductName == "iPhone"
var order = await _repo.CreateAsync(request);
return new OrderResult { OrderId = order.Id };
}2. 数据断点(监视变量变化)
csharp
// 在 Watch 窗口中添加变量
// 当变量值改变时中断
order.Status3. 调用堆栈分析
// 当异常发生时,查看完整调用堆栈
MediatR.Pipeline.RequestPostProcessorBehavior.Handle()
MediatR.Pipeline.RequestPreProcessorBehavior.Handle()
YourApp.Handlers.CreateOrderHandler.Handle() ← 你的代码
Controllers.OrdersController.CreateOrder()🎯 最佳实践
✅ 推荐做法
csharp
// 1. 始终记录请求 ID
var requestId = Guid.NewGuid();
_logger.LogInformation("处理请求: {RequestId}", requestId);
// 2. 记录性能指标
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > 500)
_logger.LogWarning("慢查询: {Duration}ms", stopwatch.ElapsedMilliseconds);
// 3. 使用结构化日志
_logger.LogInformation("订单创建: {@Order}", order);
// 4. 捕获并记录异常上下文
catch (Exception ex)
{
_logger.LogError(ex, "处理失败: {RequestName} {@Request}",
typeof(TRequest).Name, request);
throw;
}❌ 避免的做法
csharp
// 1. 不要吞掉异常
catch (Exception ex)
{
_logger.LogError(ex.ToString());
return default; // ❌ 静默失败
}
// 2. 不要在生产环境输出敏感数据
_logger.LogInformation("用户密码: {Password}", password); // ❌ 安全风险
// 3. 不要过度日志
foreach (var item in list)
{
_logger.LogInformation("处理项: {Item}", item); // ❌ 可能产生大量日志
}🎓 总结
调试工具箱
| 工具 | 用途 | 适用场景 |
|---|---|---|
| 详细日志 | 追踪请求流程 | 日常开发和问题排查 |
| 管道追踪 | 可视化行为链 | 理解执行顺序 |
| DiagnosticSource | 深度诊断 | 性能分析和监控 |
| BenchmarkDotNet | 性能基准 | 优化前后对比 |
| VS 调试器 | 逐步调试 | 复杂逻辑排查 |
关键要点
- ✅ 使用结构化日志记录关键信息
- ✅ 为每个请求分配唯一 ID
- ✅ 监控性能指标和慢查询
- ✅ 实现容错的通知发布
- ✅ 使用基准测试验证优化效果
💡 提示: 良好的日志和监控是快速定位问题的关键!