Last active
May 30, 2024 17:10
-
-
Save AlexanderReaper7/368dae8fb9ff28d93942d7578285fffb to your computer and use it in GitHub Desktop.
C# ASP.NET MediatR logger and validator for ErrorOr requests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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<,>)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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