| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- using System.Collections.Concurrent;
- using System.Diagnostics;
- using Application.Abstractions.Messaging;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Logging;
- namespace Application.Messaging;
- /// <summary>
- /// MediatR ISender/IMediator를 대체하는 사내 디스패처입니다.
- /// 요청의 런타임 타입으로 Handler를 해석(리플렉션 + 캐싱)하여 호출하며,
- /// 기존 LoggingBehavior의 처리 시간/느린 요청 로깅을 그대로 수행합니다.
- /// </summary>
- internal sealed class Sender(IServiceProvider serviceProvider, ILogger<Sender> logger) : IMediator
- {
- private const long SlowThresholdMs = 500;
- private static readonly ConcurrentDictionary<Type, ResponseHandlerWrapper> ResponseWrappers = new();
- private static readonly ConcurrentDictionary<Type, VoidHandlerWrapper> VoidWrappers = new();
- public async Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
- {
- ArgumentNullException.ThrowIfNull(request);
- var requestType = request.GetType();
- var wrapper = (ResponseHandlerWrapper<TResponse>)ResponseWrappers.GetOrAdd(
- requestType,
- static (type, responseType) => ResponseHandlerWrapper.Create(type, responseType),
- typeof(TResponse));
- var requestName = requestType.Name;
- var sw = Stopwatch.StartNew();
- try
- {
- var result = await wrapper.Handle(serviceProvider, request, cancellationToken);
- sw.Stop();
- LogCompleted(requestName, sw.ElapsedMilliseconds);
- return result;
- }
- catch (Exception ex)
- {
- sw.Stop();
- logger.LogError(ex, "[Failed] {Request} after {Elapsed}ms", requestName, sw.ElapsedMilliseconds);
- throw;
- }
- }
- public async Task Send(IRequest request, CancellationToken cancellationToken = default)
- {
- ArgumentNullException.ThrowIfNull(request);
- var requestType = request.GetType();
- var wrapper = VoidWrappers.GetOrAdd(requestType, static type => VoidHandlerWrapper.Create(type));
- var requestName = requestType.Name;
- var sw = Stopwatch.StartNew();
- try
- {
- await wrapper.Handle(serviceProvider, request, cancellationToken);
- sw.Stop();
- LogCompleted(requestName, sw.ElapsedMilliseconds);
- }
- catch (Exception ex)
- {
- sw.Stop();
- logger.LogError(ex, "[Failed] {Request} after {Elapsed}ms", requestName, sw.ElapsedMilliseconds);
- throw;
- }
- }
- private void LogCompleted(string requestName, long elapsedMs)
- {
- if (elapsedMs > SlowThresholdMs)
- {
- logger.LogWarning("[Slow] {Request} took {Elapsed}ms", requestName, elapsedMs);
- }
- else if (logger.IsEnabled(LogLevel.Debug))
- {
- logger.LogDebug("[Done] {Request} {Elapsed}ms", requestName, elapsedMs);
- }
- }
- // 반환값이 있는 Command/Query 핸들러 호출 래퍼 (런타임 타입별 1회 생성 후 캐시)
- private abstract class ResponseHandlerWrapper
- {
- public static ResponseHandlerWrapper Create(Type requestType, Type responseType)
- {
- var commandInterface = typeof(ICommand<>).MakeGenericType(responseType);
- var queryInterface = typeof(IQuery<>).MakeGenericType(responseType);
- var isCommand = commandInterface.IsAssignableFrom(requestType);
- var isQuery = queryInterface.IsAssignableFrom(requestType);
- // Handler 해석은 클래스 이름이 아니라 ICommand/IQuery 상속으로 결정됩니다.
- // 요청은 ICommand<T> 또는 IQuery<T> 중 정확히 하나만 구현해야 하며,
- // 그렇지 않으면 첫 디스패치 시 모호한 예외 대신 명확히 실패시킵니다.
- if (isCommand == isQuery)
- {
- throw new InvalidOperationException(
- $"요청 타입 '{requestType.Name}'은(는) ICommand<{responseType.Name}> 또는 IQuery<{responseType.Name}> 중 정확히 하나만 구현해야 합니다.");
- }
- var openWrapper = isCommand
- ? typeof(CommandResponseWrapper<,>)
- : typeof(QueryResponseWrapper<,>);
- var closedWrapper = openWrapper.MakeGenericType(requestType, responseType);
- return (ResponseHandlerWrapper)Activator.CreateInstance(closedWrapper)!;
- }
- }
- private abstract class ResponseHandlerWrapper<TResponse> : ResponseHandlerWrapper
- {
- public abstract Task<TResponse> Handle(IServiceProvider provider, object request, CancellationToken cancellationToken);
- }
- private sealed class CommandResponseWrapper<TCommand, TResponse> : ResponseHandlerWrapper<TResponse>
- where TCommand : ICommand<TResponse>
- {
- public override Task<TResponse> Handle(IServiceProvider provider, object request, CancellationToken cancellationToken)
- {
- var handler = provider.GetRequiredService<ICommandHandler<TCommand, TResponse>>();
- return handler.Handle((TCommand)request, cancellationToken);
- }
- }
- private sealed class QueryResponseWrapper<TQuery, TResponse> : ResponseHandlerWrapper<TResponse>
- where TQuery : IQuery<TResponse>
- {
- public override Task<TResponse> Handle(IServiceProvider provider, object request, CancellationToken cancellationToken)
- {
- var handler = provider.GetRequiredService<IQueryHandler<TQuery, TResponse>>();
- return handler.Handle((TQuery)request, cancellationToken);
- }
- }
- // 반환값이 없는 Command 핸들러 호출 래퍼
- private abstract class VoidHandlerWrapper
- {
- public abstract Task Handle(IServiceProvider provider, object request, CancellationToken cancellationToken);
- public static VoidHandlerWrapper Create(Type requestType)
- {
- var closedWrapper = typeof(VoidCommandWrapper<>).MakeGenericType(requestType);
- return (VoidHandlerWrapper)Activator.CreateInstance(closedWrapper)!;
- }
- }
- private sealed class VoidCommandWrapper<TCommand> : VoidHandlerWrapper
- where TCommand : ICommand
- {
- public override Task Handle(IServiceProvider provider, object request, CancellationToken cancellationToken)
- {
- var handler = provider.GetRequiredService<ICommandHandler<TCommand>>();
- return handler.Handle((TCommand)request, cancellationToken);
- }
- }
- }
|