Skip to content

Instantly share code, notes, and snippets.

@AlexanderReaper7
Last active May 30, 2024 17:10
Show Gist options
  • Save AlexanderReaper7/368dae8fb9ff28d93942d7578285fffb to your computer and use it in GitHub Desktop.
Save AlexanderReaper7/368dae8fb9ff28d93942d7578285fffb to your computer and use it in GitHub Desktop.
C# ASP.NET MediatR logger and validator for ErrorOr requests
using ErrorOr;
using global::MediatR;
/// <summary>
/// A logging behavior for MediatR. Logs the request and any exceptions that occur.
/// </summary>
public class MediatRLoggerIErrorOr<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
where TResponse : IErrorOr
{
private readonly ILogger<MediatRLoggerIErrorOr<TRequest, TResponse>> _logger;
public MediatRLoggerIErrorOr(ILogger<MediatRLoggerIErrorOr<TRequest, TResponse>> logger)
{
_logger = logger;
}
/// <inheritdoc />
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
_logger.LogInformation("Handling {typeName}", typeof(TRequest).Name);
try
{
var response = await next();
if (response.IsError)
{
_logger.LogWarning("{typeName} returned an error: {firstError}",typeof(TRequest).Name, response.Errors?.First());
}
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred during handling of {typeName}", typeof(TRequest).Name);
throw;
}
}
}
/// <summary>
/// A logging behavior for MediatR with no return type (<see cref="IRequest"/>). Logs the request and any exceptions that occur.
/// </summary>
public class MediatRLoggerEmptyResponse<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest
{
private readonly ILogger<MediatRLoggerEmptyResponse<TRequest, TResponse>> _logger;
public MediatRLoggerEmptyResponse(ILogger<MediatRLoggerEmptyResponse<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
_logger.LogInformation("Handling {typeName}", typeof(TRequest).Name);
try
{
return await next();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred during handling of {typeName}", typeof(TRequest).Name);
throw;
}
}
}
// Add MediatR
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(MediatRLoggerIErrorOr<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(MediatRLoggerEmptyResponse<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>));
using MediatR;
using ErrorOr;
using System.Reflection;
/// <summary>
/// Enforces that all request implements <see cref="IErrorOr{TValue}"/> or <see cref="IErrorOr"/> so that we use errors as values.
/// <br/>
/// Also catches any exceptions thrown by the request and converts them to <see cref="ErrorOr{TValue}"/> if the <see cref="TResponse"/> also is <see cref="ErrorOr{TValue}"/>.
/// </summary>
/// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TResponse"></typeparam>
public class RequestValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
/// <inheritdoc />
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
var requestType = request.GetType();
var interfaces = requestType.GetInterfaces();
// Check if the request implements IRequest<ErrorOr<T>> or IRequest
// Store the ErrorOr<T> type if it implements IRequest<ErrorOr<T>>, saving later reflection
Type? errorOrType = null;
// Check if the interface is IRequest<ErrorOr<T>>
var isRequestErrorOr = interfaces.Any(i =>
{
var isMatch = i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IRequest<>) &&
i.GetGenericArguments()[0].IsGenericType &&
i.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(ErrorOr<>);
if (isMatch) errorOrType = i.GetGenericArguments()[0];
return isMatch;
});
// Check if the interface is IRequest
var isRequest = interfaces.Any(i => i == typeof(IRequest));
// If the request does not implement IRequest<ErrorOr<T>> or IRequest, throw an exception
if (!isRequestErrorOr && !isRequest)
{
throw new RequestTypeException(typeof(TRequest));
}
// Catch any exceptions thrown by the request and convert them to ErrorOr<T>
try
{
// Continue the pipeline
return await next();
}
catch (Exception ex)
{
// If the request doesn't have a return type, throw the exception
if (isRequest)
{
throw;
}
if (errorOrType == null)
{
throw new RequestTypeException(typeof(TRequest));
}
var innerType = errorOrType.GetGenericArguments()[0];
// Try to get the cached implicit conversion operator method for the inner type
if (!RequestValidationBehaviorCache.ImplicitOperatorCache.TryGetValue(innerType, out var implicitOperator))
{
// If the implicit operator is not cached, create it
implicitOperator = typeof(ErrorOr<>).MakeGenericType(innerType)
.GetMethod("op_Implicit", [typeof(Error)]);
}
if (implicitOperator == null)
{
throw new InvalidOperationException("No implicit conversion operator found in ErrorOr<T>");
}
// Create the Error with metadata of the exception
var metadata = new Dictionary<string, object>
{
{ "Exception", ex },
};
var errorResponse = Error.Unexpected(ex.Message, metadata: metadata);
// Use the implicit conversion operator to convert the Error to ErrorOr<T>
var convertedResponse = implicitOperator.Invoke(null, [errorResponse]);
if (convertedResponse == null)
{
throw new InvalidOperationException("Error converting Error to ErrorOr<T>");
}
return (TResponse)convertedResponse;
}
}
}
/// <summary>
/// Cache for the RequestValidationBehavior to store the implicit conversion operators for from <see cref="Error"/> to <see cref="ErrorOr{TValue}"/>
/// </summary>
internal static class RequestValidationBehaviorCache
{
/// <summary>
/// Cache for the implicit conversion operators from <see cref="Error"/> to <see cref="ErrorOr{TValue}"/>
/// </summary>
public static readonly Dictionary<Type, MethodInfo> ImplicitOperatorCache;
static RequestValidationBehaviorCache()
{
var cachedTypes = new[]
{
typeof(Nullable),
typeof(int),
typeof(string),
typeof(bool),
typeof(byte),
typeof(sbyte),
typeof(short),
typeof(ushort),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(float),
typeof(double),
typeof(decimal),
typeof(char),
typeof(DateTime),
typeof(TimeSpan),
};
ImplicitOperatorCache = new Dictionary<Type, MethodInfo>(cachedTypes.Length);
foreach (var type in cachedTypes)
{
var implicitOperator = typeof(ErrorOr<>).MakeGenericType(type)
.GetMethod("op_Implicit", [typeof(Error)]);
ImplicitOperatorCache[type] = implicitOperator!;
}
}
}
/// <summary>
/// Exception thrown when a request does not implement <see cref="IRequest"/> or <see cref="IRequest{T}"/> Where <typeparamref name="TResponse"/> is <see cref="ErrorOr{TValue}"/> type.
/// </summary>
public class RequestTypeException : InvalidOperationException
{
public Type RequestType { get; }
/// <param name="requestType">Type of the request that was made</param>
public RequestTypeException(Type requestType)
: base($"Request of type {requestType} must implement IRequest<ErrorOr<T>> or IRequest.\nThis is required to ensure that all requests use errors as values or have no return type and instead uses exceptions.\nThis is enforced by the RequestValidationBehavior.")
{
RequestType = requestType;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment