Fix CA1873 warning and consolidate test fixtures (#313)

This commit is contained in:
Aaron Po
2026-09-12 22:14:55 -04:00
committed by GitHub
parent 40ae75808c
commit a6a9046457
6 changed files with 150 additions and 121 deletions

View File

@@ -6,18 +6,11 @@ using Microsoft.Extensions.Logging;
namespace Shared.Application.Behaviors;
/// <summary>
/// MediatR pipeline behavior that logs unhandled exceptions together with the request type and
/// payload, then rethrows them unchanged for <c>GlobalExceptionFilter</c> to map to a status code.
/// </summary>
public class UnhandledExceptionLoggingBehavior<TRequest, TResponse>(
ILogger<UnhandledExceptionLoggingBehavior<TRequest, TResponse>> logger
) : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
public abstract class JsonLogger
{
private static readonly string[] SensitiveFieldNames = ["password", "token"];
private static readonly JsonSerializerOptions SerializerOptions = new()
protected static readonly JsonSerializerOptions SerializerOptions = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
@@ -25,46 +18,6 @@ public class UnhandledExceptionLoggingBehavior<TRequest, TResponse>(
},
};
/// <inheritdoc/>
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken
)
{
try
{
return await next();
}
catch (ValidationException)
{
// Expected: ValidationBehavior already reports these as request errors, not incidents.
throw;
}
catch (Exception ex)
{
logger.LogError(
ex,
"Unhandled exception while processing {RequestName}: {RequestPayload}",
typeof(TRequest).Name,
Serialize(request)
);
throw;
}
}
private static string Serialize(TRequest request)
{
try
{
return JsonSerializer.Serialize(request, SerializerOptions);
}
catch (Exception)
{
return "<unable to serialize request payload>";
}
}
private static void RedactSensitiveProperties(JsonTypeInfo typeInfo)
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
@@ -84,3 +37,71 @@ public class UnhandledExceptionLoggingBehavior<TRequest, TResponse>(
}
}
}
/// <summary>
/// MediatR pipeline behavior that logs unhandled exceptions together with the
/// request type and
/// payload, then rethrows them unchanged for <c>GlobalExceptionFilter</c> to
/// map to a status code.
/// </summary>
public partial class UnhandledExceptionLoggingBehavior<TRequest, TResponse>
: JsonLogger,
IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<UnhandledExceptionLoggingBehavior<TRequest, TResponse>> _logger;
public UnhandledExceptionLoggingBehavior(
ILogger<UnhandledExceptionLoggingBehavior<TRequest, TResponse>> logger
)
{
_logger = logger;
}
/// <inheritdoc />
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken
)
{
try
{
return await next();
}
catch (ValidationException)
{
// Expected: ValidationBehavior already reports these as request errors
throw;
}
catch (Exception ex)
{
if (_logger.IsEnabled(LogLevel.Error))
LogUnhandledException(ex, typeof(TRequest).Name, Serialize(request));
throw;
}
}
private static string Serialize(TRequest request)
{
try
{
return JsonSerializer.Serialize(request, SerializerOptions);
}
catch (Exception)
{
return "<unable to serialize request payload>";
}
}
[LoggerMessage(
Level = LogLevel.Error,
Message = "Unhandled exception while processing {RequestName}: {RequestPayload}"
)]
private partial void LogUnhandledException(
Exception ex,
string requestName,
string requestPayload
);
}

View File

@@ -5,14 +5,15 @@ using MediatR;
namespace Shared.Application.Behaviors;
/// <summary>
/// MediatR pipeline behavior that runs registered FluentValidation validators before the handler,
/// MediatR pipeline behavior that runs registered FluentValidation validators
/// before the handler,
/// throwing <see cref="ValidationException" /> if any report failures.
/// </summary>
public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
/// <inheritdoc/>
/// <inheritdoc />
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,