Sender.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. using System.Collections.Concurrent;
  2. using System.Diagnostics;
  3. using Application.Abstractions.Messaging;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Logging;
  6. namespace Application.Messaging;
  7. /// <summary>
  8. /// MediatR ISender/IMediator를 대체하는 사내 디스패처입니다.
  9. /// 요청의 런타임 타입으로 Handler를 해석(리플렉션 + 캐싱)하여 호출하며,
  10. /// 기존 LoggingBehavior의 처리 시간/느린 요청 로깅을 그대로 수행합니다.
  11. /// </summary>
  12. internal sealed class Sender(IServiceProvider serviceProvider, ILogger<Sender> logger) : IMediator
  13. {
  14. private const long SlowThresholdMs = 500;
  15. private static readonly ConcurrentDictionary<Type, ResponseHandlerWrapper> ResponseWrappers = new();
  16. private static readonly ConcurrentDictionary<Type, VoidHandlerWrapper> VoidWrappers = new();
  17. public async Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
  18. {
  19. ArgumentNullException.ThrowIfNull(request);
  20. var requestType = request.GetType();
  21. var wrapper = (ResponseHandlerWrapper<TResponse>)ResponseWrappers.GetOrAdd(
  22. requestType,
  23. static (type, responseType) => ResponseHandlerWrapper.Create(type, responseType),
  24. typeof(TResponse));
  25. var requestName = requestType.Name;
  26. var sw = Stopwatch.StartNew();
  27. try
  28. {
  29. var result = await wrapper.Handle(serviceProvider, request, cancellationToken);
  30. sw.Stop();
  31. LogCompleted(requestName, sw.ElapsedMilliseconds);
  32. return result;
  33. }
  34. catch (Exception ex)
  35. {
  36. sw.Stop();
  37. logger.LogError(ex, "[Failed] {Request} after {Elapsed}ms", requestName, sw.ElapsedMilliseconds);
  38. throw;
  39. }
  40. }
  41. public async Task Send(IRequest request, CancellationToken cancellationToken = default)
  42. {
  43. ArgumentNullException.ThrowIfNull(request);
  44. var requestType = request.GetType();
  45. var wrapper = VoidWrappers.GetOrAdd(requestType, static type => VoidHandlerWrapper.Create(type));
  46. var requestName = requestType.Name;
  47. var sw = Stopwatch.StartNew();
  48. try
  49. {
  50. await wrapper.Handle(serviceProvider, request, cancellationToken);
  51. sw.Stop();
  52. LogCompleted(requestName, sw.ElapsedMilliseconds);
  53. }
  54. catch (Exception ex)
  55. {
  56. sw.Stop();
  57. logger.LogError(ex, "[Failed] {Request} after {Elapsed}ms", requestName, sw.ElapsedMilliseconds);
  58. throw;
  59. }
  60. }
  61. private void LogCompleted(string requestName, long elapsedMs)
  62. {
  63. if (elapsedMs > SlowThresholdMs)
  64. {
  65. logger.LogWarning("[Slow] {Request} took {Elapsed}ms", requestName, elapsedMs);
  66. }
  67. else if (logger.IsEnabled(LogLevel.Debug))
  68. {
  69. logger.LogDebug("[Done] {Request} {Elapsed}ms", requestName, elapsedMs);
  70. }
  71. }
  72. // 반환값이 있는 Command/Query 핸들러 호출 래퍼 (런타임 타입별 1회 생성 후 캐시)
  73. private abstract class ResponseHandlerWrapper
  74. {
  75. public static ResponseHandlerWrapper Create(Type requestType, Type responseType)
  76. {
  77. var commandInterface = typeof(ICommand<>).MakeGenericType(responseType);
  78. var queryInterface = typeof(IQuery<>).MakeGenericType(responseType);
  79. var isCommand = commandInterface.IsAssignableFrom(requestType);
  80. var isQuery = queryInterface.IsAssignableFrom(requestType);
  81. // Handler 해석은 클래스 이름이 아니라 ICommand/IQuery 상속으로 결정됩니다.
  82. // 요청은 ICommand<T> 또는 IQuery<T> 중 정확히 하나만 구현해야 하며,
  83. // 그렇지 않으면 첫 디스패치 시 모호한 예외 대신 명확히 실패시킵니다.
  84. if (isCommand == isQuery)
  85. {
  86. throw new InvalidOperationException(
  87. $"요청 타입 '{requestType.Name}'은(는) ICommand<{responseType.Name}> 또는 IQuery<{responseType.Name}> 중 정확히 하나만 구현해야 합니다.");
  88. }
  89. var openWrapper = isCommand
  90. ? typeof(CommandResponseWrapper<,>)
  91. : typeof(QueryResponseWrapper<,>);
  92. var closedWrapper = openWrapper.MakeGenericType(requestType, responseType);
  93. return (ResponseHandlerWrapper)Activator.CreateInstance(closedWrapper)!;
  94. }
  95. }
  96. private abstract class ResponseHandlerWrapper<TResponse> : ResponseHandlerWrapper
  97. {
  98. public abstract Task<TResponse> Handle(IServiceProvider provider, object request, CancellationToken cancellationToken);
  99. }
  100. private sealed class CommandResponseWrapper<TCommand, TResponse> : ResponseHandlerWrapper<TResponse>
  101. where TCommand : ICommand<TResponse>
  102. {
  103. public override Task<TResponse> Handle(IServiceProvider provider, object request, CancellationToken cancellationToken)
  104. {
  105. var handler = provider.GetRequiredService<ICommandHandler<TCommand, TResponse>>();
  106. return handler.Handle((TCommand)request, cancellationToken);
  107. }
  108. }
  109. private sealed class QueryResponseWrapper<TQuery, TResponse> : ResponseHandlerWrapper<TResponse>
  110. where TQuery : IQuery<TResponse>
  111. {
  112. public override Task<TResponse> Handle(IServiceProvider provider, object request, CancellationToken cancellationToken)
  113. {
  114. var handler = provider.GetRequiredService<IQueryHandler<TQuery, TResponse>>();
  115. return handler.Handle((TQuery)request, cancellationToken);
  116. }
  117. }
  118. // 반환값이 없는 Command 핸들러 호출 래퍼
  119. private abstract class VoidHandlerWrapper
  120. {
  121. public abstract Task Handle(IServiceProvider provider, object request, CancellationToken cancellationToken);
  122. public static VoidHandlerWrapper Create(Type requestType)
  123. {
  124. var closedWrapper = typeof(VoidCommandWrapper<>).MakeGenericType(requestType);
  125. return (VoidHandlerWrapper)Activator.CreateInstance(closedWrapper)!;
  126. }
  127. }
  128. private sealed class VoidCommandWrapper<TCommand> : VoidHandlerWrapper
  129. where TCommand : ICommand
  130. {
  131. public override Task Handle(IServiceProvider provider, object request, CancellationToken cancellationToken)
  132. {
  133. var handler = provider.GetRequiredService<ICommandHandler<TCommand>>();
  134. return handler.Handle((TCommand)request, cancellationToken);
  135. }
  136. }
  137. }