mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 18:14:44 +00:00
Fix CA1873 warning and consolidate test fixtures (#313)
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Shared.Application.Tests.Behaviors;
|
||||||
|
|
||||||
|
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||||
|
public sealed record TestRequest(string Username, string Password) : IRequest<TestResponse>;
|
||||||
|
|
||||||
|
public sealed record TestResponse(string Value);
|
||||||
|
// ReSharper restore NotAccessedPositionalProperty.Global
|
||||||
@@ -14,15 +14,18 @@ public class UnhandledExceptionLoggingBehaviorTests
|
|||||||
ILogger<UnhandledExceptionLoggingBehavior<TestRequest, TestResponse>>
|
ILogger<UnhandledExceptionLoggingBehavior<TestRequest, TestResponse>>
|
||||||
> _loggerMock = new();
|
> _loggerMock = new();
|
||||||
|
|
||||||
private UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> CreateBehavior() =>
|
private UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> CreateBehavior()
|
||||||
new(_loggerMock.Object);
|
{
|
||||||
|
_loggerMock.Setup(l => l.IsEnabled(LogLevel.Error)).Returns(true);
|
||||||
|
return new(_loggerMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_WhenHandlerThrowsGenericException_LogsOnceAndRethrows()
|
public async Task Handle_WhenHandlerThrowsGenericException_LogsOnceAndRethrows()
|
||||||
{
|
{
|
||||||
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
||||||
TestRequest request = new("aaron", "hunter2");
|
TestRequest request = new("aaron", "pa$$word123");
|
||||||
InvalidOperationException thrown = new("boom");
|
InvalidOperationException thrown = new("Something went wrong.");
|
||||||
RequestHandlerDelegate<TestResponse> next = () => throw thrown;
|
RequestHandlerDelegate<TestResponse> next = () => throw thrown;
|
||||||
|
|
||||||
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
||||||
@@ -37,7 +40,7 @@ public class UnhandledExceptionLoggingBehaviorTests
|
|||||||
public async Task Handle_WhenValidationExceptionThrown_DoesNotLogAndRethrows()
|
public async Task Handle_WhenValidationExceptionThrown_DoesNotLogAndRethrows()
|
||||||
{
|
{
|
||||||
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
||||||
TestRequest request = new("aaron", "hunter2");
|
TestRequest request = new("aaron", "pa$$word123");
|
||||||
ValidationException thrown = new([new ValidationFailure("Username", "is required")]);
|
ValidationException thrown = new([new ValidationFailure("Username", "is required")]);
|
||||||
RequestHandlerDelegate<TestResponse> next = () => throw thrown;
|
RequestHandlerDelegate<TestResponse> next = () => throw thrown;
|
||||||
|
|
||||||
@@ -51,7 +54,7 @@ public class UnhandledExceptionLoggingBehaviorTests
|
|||||||
public async Task Handle_WhenHandlerSucceeds_DoesNotLogAndReturnsResponse()
|
public async Task Handle_WhenHandlerSucceeds_DoesNotLogAndReturnsResponse()
|
||||||
{
|
{
|
||||||
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
||||||
TestRequest request = new("aaron", "hunter2");
|
TestRequest request = new("aaron", "pa$$word123");
|
||||||
TestResponse response = new("ok");
|
TestResponse response = new("ok");
|
||||||
RequestHandlerDelegate<TestResponse> next = () => Task.FromResult(response);
|
RequestHandlerDelegate<TestResponse> next = () => Task.FromResult(response);
|
||||||
|
|
||||||
@@ -65,15 +68,15 @@ public class UnhandledExceptionLoggingBehaviorTests
|
|||||||
public async Task Handle_WhenHandlerThrows_RedactsSensitiveFieldsInLoggedPayload()
|
public async Task Handle_WhenHandlerThrows_RedactsSensitiveFieldsInLoggedPayload()
|
||||||
{
|
{
|
||||||
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
UnhandledExceptionLoggingBehavior<TestRequest, TestResponse> behavior = CreateBehavior();
|
||||||
TestRequest request = new("aaron", "hunter2");
|
TestRequest request = new("aaron", "pa$$word123");
|
||||||
RequestHandlerDelegate<TestResponse> next = () =>
|
RequestHandlerDelegate<TestResponse> next = () =>
|
||||||
throw new InvalidOperationException("boom");
|
throw new InvalidOperationException("Something went wrong.");
|
||||||
|
|
||||||
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
||||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||||
|
|
||||||
string loggedMessage = CaptureLoggedMessage();
|
string loggedMessage = CaptureLoggedMessage();
|
||||||
loggedMessage.Should().Contain("aaron").And.NotContain("hunter2");
|
loggedMessage.Should().Contain("aaron").And.NotContain("pa$$word123");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VerifyLogError(Times times) =>
|
private void VerifyLogError(Times times) =>
|
||||||
@@ -91,21 +94,17 @@ public class UnhandledExceptionLoggingBehaviorTests
|
|||||||
|
|
||||||
private string CaptureLoggedMessage()
|
private string CaptureLoggedMessage()
|
||||||
{
|
{
|
||||||
var invocation = _loggerMock
|
IInvocation? invocation = _loggerMock
|
||||||
.Invocations.Where(i => i.Method.Name == nameof(ILogger.Log))
|
.Invocations.Where(i => i.Method.Name == nameof(ILogger.Log))
|
||||||
.Should()
|
.Should()
|
||||||
.ContainSingle()
|
.ContainSingle()
|
||||||
.Subject;
|
.Subject;
|
||||||
|
|
||||||
object state = invocation.Arguments[2];
|
object state = invocation.Arguments[2];
|
||||||
object? exception = invocation.Arguments[3];
|
object exception = invocation.Arguments[3];
|
||||||
object formatter = invocation.Arguments[4];
|
object formatter = invocation.Arguments[4];
|
||||||
System.Reflection.MethodInfo invokeMethod = formatter.GetType().GetMethod("Invoke")!;
|
System.Reflection.MethodInfo invokeMethod = formatter.GetType().GetMethod("Invoke")!;
|
||||||
|
|
||||||
return (string)invokeMethod.Invoke(formatter, [state, exception])!;
|
return (string)invokeMethod.Invoke(formatter, [state, exception])!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record TestRequest(string Username, string Password) : IRequest<TestResponse>;
|
|
||||||
|
|
||||||
public record TestResponse(string Value);
|
|
||||||
|
|||||||
@@ -9,35 +9,35 @@ namespace Shared.Application.Tests.Behaviors;
|
|||||||
|
|
||||||
public class ValidationBehaviorTests
|
public class ValidationBehaviorTests
|
||||||
{
|
{
|
||||||
private static ValidationBehavior<ValidationTestRequest, ValidationTestResponse> CreateBehavior(
|
private static ValidationBehavior<TestRequest, TestResponse> CreateBehavior(
|
||||||
params IValidator<ValidationTestRequest>[] validators
|
params IValidator<TestRequest>[] validators
|
||||||
) => new(validators);
|
) => new(validators);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_WhenRequestPassesAllValidators_InvokesNextUnchanged()
|
public async Task Handle_WhenRequestPassesAllValidators_InvokesNextUnchanged()
|
||||||
{
|
{
|
||||||
Mock<IValidator<ValidationTestRequest>> validatorMock = new();
|
Mock<IValidator<TestRequest>> validatorMock = new();
|
||||||
validatorMock
|
validatorMock
|
||||||
.Setup(v =>
|
.Setup(v =>
|
||||||
v.ValidateAsync(
|
v.ValidateAsync(
|
||||||
It.IsAny<ValidationContext<ValidationTestRequest>>(),
|
It.IsAny<ValidationContext<TestRequest>>(),
|
||||||
It.IsAny<CancellationToken>()
|
It.IsAny<CancellationToken>()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ReturnsAsync(new ValidationResult());
|
.ReturnsAsync(new ValidationResult());
|
||||||
ValidationBehavior<ValidationTestRequest, ValidationTestResponse> behavior = CreateBehavior(
|
ValidationBehavior<TestRequest, TestResponse> behavior = CreateBehavior(
|
||||||
validatorMock.Object
|
validatorMock.Object
|
||||||
);
|
);
|
||||||
ValidationTestRequest request = new("aaron");
|
TestRequest request = new("aaron", "pa$$word123");
|
||||||
ValidationTestResponse response = new("ok");
|
TestResponse response = new("ok");
|
||||||
bool nextInvoked = false;
|
bool nextInvoked = false;
|
||||||
RequestHandlerDelegate<ValidationTestResponse> next = () =>
|
RequestHandlerDelegate<TestResponse> next = () =>
|
||||||
{
|
{
|
||||||
nextInvoked = true;
|
nextInvoked = true;
|
||||||
return Task.FromResult(response);
|
return Task.FromResult(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
ValidationTestResponse result = await behavior.Handle(
|
TestResponse result = await behavior.Handle(
|
||||||
request,
|
request,
|
||||||
next,
|
next,
|
||||||
CancellationToken.None
|
CancellationToken.None
|
||||||
@@ -50,30 +50,31 @@ public class ValidationBehaviorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_WhenValidatorReportsFailures_ThrowsValidationExceptionAndNeverInvokesNext()
|
public async Task Handle_WhenValidatorReportsFailures_ThrowsValidationExceptionAndNeverInvokesNext()
|
||||||
{
|
{
|
||||||
ValidationFailure failure = new("Name", "is required");
|
ValidationFailure failure = new("Username", "is required");
|
||||||
Mock<IValidator<ValidationTestRequest>> validatorMock = new();
|
Mock<IValidator<TestRequest>> validatorMock = new();
|
||||||
validatorMock
|
validatorMock
|
||||||
.Setup(v =>
|
.Setup(v =>
|
||||||
v.ValidateAsync(
|
v.ValidateAsync(
|
||||||
It.IsAny<ValidationContext<ValidationTestRequest>>(),
|
It.IsAny<ValidationContext<TestRequest>>(),
|
||||||
It.IsAny<CancellationToken>()
|
It.IsAny<CancellationToken>()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ReturnsAsync(new ValidationResult([failure]));
|
.ReturnsAsync(new ValidationResult([failure]));
|
||||||
ValidationBehavior<ValidationTestRequest, ValidationTestResponse> behavior = CreateBehavior(
|
ValidationBehavior<TestRequest, TestResponse> behavior = CreateBehavior(
|
||||||
validatorMock.Object
|
validatorMock.Object
|
||||||
);
|
);
|
||||||
ValidationTestRequest request = new("");
|
TestRequest request = new("", "pa$$word123");
|
||||||
bool nextInvoked = false;
|
bool nextInvoked = false;
|
||||||
RequestHandlerDelegate<ValidationTestResponse> next = () =>
|
RequestHandlerDelegate<TestResponse> next = () =>
|
||||||
{
|
{
|
||||||
nextInvoked = true;
|
nextInvoked = true;
|
||||||
return Task.FromResult(new ValidationTestResponse("ok"));
|
return Task.FromResult(new TestResponse("ok"));
|
||||||
};
|
};
|
||||||
|
|
||||||
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
||||||
|
|
||||||
(await act.Should().ThrowAsync<ValidationException>()).Which.Errors.Should()
|
(await act.Should().ThrowAsync<ValidationException>())
|
||||||
|
.Which.Errors.Should()
|
||||||
.ContainSingle()
|
.ContainSingle()
|
||||||
.Which.Should()
|
.Which.Should()
|
||||||
.BeSameAs(failure);
|
.BeSameAs(failure);
|
||||||
@@ -83,38 +84,38 @@ public class ValidationBehaviorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_WhenMultipleValidatorsAreRegistered_RunsAllOfThem()
|
public async Task Handle_WhenMultipleValidatorsAreRegistered_RunsAllOfThem()
|
||||||
{
|
{
|
||||||
Mock<IValidator<ValidationTestRequest>> firstValidatorMock = new();
|
Mock<IValidator<TestRequest>> firstValidatorMock = new();
|
||||||
firstValidatorMock
|
firstValidatorMock
|
||||||
.Setup(v =>
|
.Setup(v =>
|
||||||
v.ValidateAsync(
|
v.ValidateAsync(
|
||||||
It.IsAny<ValidationContext<ValidationTestRequest>>(),
|
It.IsAny<ValidationContext<TestRequest>>(),
|
||||||
It.IsAny<CancellationToken>()
|
It.IsAny<CancellationToken>()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ReturnsAsync(new ValidationResult());
|
.ReturnsAsync(new ValidationResult());
|
||||||
Mock<IValidator<ValidationTestRequest>> secondValidatorMock = new();
|
Mock<IValidator<TestRequest>> secondValidatorMock = new();
|
||||||
secondValidatorMock
|
secondValidatorMock
|
||||||
.Setup(v =>
|
.Setup(v =>
|
||||||
v.ValidateAsync(
|
v.ValidateAsync(
|
||||||
It.IsAny<ValidationContext<ValidationTestRequest>>(),
|
It.IsAny<ValidationContext<TestRequest>>(),
|
||||||
It.IsAny<CancellationToken>()
|
It.IsAny<CancellationToken>()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ReturnsAsync(new ValidationResult());
|
.ReturnsAsync(new ValidationResult());
|
||||||
ValidationBehavior<ValidationTestRequest, ValidationTestResponse> behavior = CreateBehavior(
|
ValidationBehavior<TestRequest, TestResponse> behavior = CreateBehavior(
|
||||||
firstValidatorMock.Object,
|
firstValidatorMock.Object,
|
||||||
secondValidatorMock.Object
|
secondValidatorMock.Object
|
||||||
);
|
);
|
||||||
ValidationTestRequest request = new("aaron");
|
TestRequest request = new("aaron", "pa$$word123");
|
||||||
RequestHandlerDelegate<ValidationTestResponse> next = () =>
|
RequestHandlerDelegate<TestResponse> next = () =>
|
||||||
Task.FromResult(new ValidationTestResponse("ok"));
|
Task.FromResult(new TestResponse("ok"));
|
||||||
|
|
||||||
await behavior.Handle(request, next, CancellationToken.None);
|
await behavior.Handle(request, next, CancellationToken.None);
|
||||||
|
|
||||||
firstValidatorMock.Verify(
|
firstValidatorMock.Verify(
|
||||||
v =>
|
v =>
|
||||||
v.ValidateAsync(
|
v.ValidateAsync(
|
||||||
It.IsAny<ValidationContext<ValidationTestRequest>>(),
|
It.IsAny<ValidationContext<TestRequest>>(),
|
||||||
It.IsAny<CancellationToken>()
|
It.IsAny<CancellationToken>()
|
||||||
),
|
),
|
||||||
Times.Once()
|
Times.Once()
|
||||||
@@ -122,7 +123,7 @@ public class ValidationBehaviorTests
|
|||||||
secondValidatorMock.Verify(
|
secondValidatorMock.Verify(
|
||||||
v =>
|
v =>
|
||||||
v.ValidateAsync(
|
v.ValidateAsync(
|
||||||
It.IsAny<ValidationContext<ValidationTestRequest>>(),
|
It.IsAny<ValidationContext<TestRequest>>(),
|
||||||
It.IsAny<CancellationToken>()
|
It.IsAny<CancellationToken>()
|
||||||
),
|
),
|
||||||
Times.Once()
|
Times.Once()
|
||||||
@@ -132,49 +133,51 @@ public class ValidationBehaviorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_WhenMultipleValidatorsReportFailures_AggregatesErrorsFromAll()
|
public async Task Handle_WhenMultipleValidatorsReportFailures_AggregatesErrorsFromAll()
|
||||||
{
|
{
|
||||||
ValidationFailure firstFailure = new("Name", "is required");
|
ValidationFailure firstFailure = new("Username", "is required");
|
||||||
ValidationFailure secondFailure = new("Name", "must be at least 3 characters");
|
ValidationFailure secondFailure = new("Username", "must be at least 3 characters");
|
||||||
Mock<IValidator<ValidationTestRequest>> firstValidatorMock = new();
|
Mock<IValidator<TestRequest>> firstValidatorMock = new();
|
||||||
firstValidatorMock
|
firstValidatorMock
|
||||||
.Setup(v =>
|
.Setup(v =>
|
||||||
v.ValidateAsync(
|
v.ValidateAsync(
|
||||||
It.IsAny<ValidationContext<ValidationTestRequest>>(),
|
It.IsAny<ValidationContext<TestRequest>>(),
|
||||||
It.IsAny<CancellationToken>()
|
It.IsAny<CancellationToken>()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ReturnsAsync(new ValidationResult([firstFailure]));
|
.ReturnsAsync(new ValidationResult([firstFailure]));
|
||||||
Mock<IValidator<ValidationTestRequest>> secondValidatorMock = new();
|
Mock<IValidator<TestRequest>> secondValidatorMock = new();
|
||||||
secondValidatorMock
|
secondValidatorMock
|
||||||
.Setup(v =>
|
.Setup(v =>
|
||||||
v.ValidateAsync(
|
v.ValidateAsync(
|
||||||
It.IsAny<ValidationContext<ValidationTestRequest>>(),
|
It.IsAny<ValidationContext<TestRequest>>(),
|
||||||
It.IsAny<CancellationToken>()
|
It.IsAny<CancellationToken>()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.ReturnsAsync(new ValidationResult([secondFailure]));
|
.ReturnsAsync(new ValidationResult([secondFailure]));
|
||||||
ValidationBehavior<ValidationTestRequest, ValidationTestResponse> behavior = CreateBehavior(
|
ValidationBehavior<TestRequest, TestResponse> behavior = CreateBehavior(
|
||||||
firstValidatorMock.Object,
|
firstValidatorMock.Object,
|
||||||
secondValidatorMock.Object
|
secondValidatorMock.Object
|
||||||
);
|
);
|
||||||
ValidationTestRequest request = new("");
|
TestRequest request = new("", "pa$$word123");
|
||||||
RequestHandlerDelegate<ValidationTestResponse> next = () =>
|
RequestHandlerDelegate<TestResponse> next = () =>
|
||||||
Task.FromResult(new ValidationTestResponse("ok"));
|
Task.FromResult(new TestResponse("ok"));
|
||||||
|
|
||||||
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
Func<Task> act = async () => await behavior.Handle(request, next, CancellationToken.None);
|
||||||
|
|
||||||
(await act.Should().ThrowAsync<ValidationException>()).Which.Errors.Should()
|
(await act.Should().ThrowAsync<ValidationException>())
|
||||||
|
.Which.Errors.Should()
|
||||||
.BeEquivalentTo([firstFailure, secondFailure]);
|
.BeEquivalentTo([firstFailure, secondFailure]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Handle_WhenNoValidatorsAreRegistered_InvokesNext()
|
public async Task Handle_WhenNoValidatorsAreRegistered_InvokesNext()
|
||||||
{
|
{
|
||||||
ValidationBehavior<ValidationTestRequest, ValidationTestResponse> behavior = CreateBehavior();
|
ValidationBehavior<TestRequest, TestResponse> behavior =
|
||||||
ValidationTestRequest request = new("aaron");
|
CreateBehavior();
|
||||||
ValidationTestResponse response = new("ok");
|
TestRequest request = new("aaron", "pa$$word123");
|
||||||
RequestHandlerDelegate<ValidationTestResponse> next = () => Task.FromResult(response);
|
TestResponse response = new("ok");
|
||||||
|
RequestHandlerDelegate<TestResponse> next = () => Task.FromResult(response);
|
||||||
|
|
||||||
ValidationTestResponse result = await behavior.Handle(
|
TestResponse result = await behavior.Handle(
|
||||||
request,
|
request,
|
||||||
next,
|
next,
|
||||||
CancellationToken.None
|
CancellationToken.None
|
||||||
@@ -183,7 +186,3 @@ public class ValidationBehaviorTests
|
|||||||
result.Should().BeSameAs(response);
|
result.Should().BeSameAs(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ValidationTestRequest(string Name) : IRequest<ValidationTestResponse>;
|
|
||||||
|
|
||||||
public record ValidationTestResponse(string Value);
|
|
||||||
|
|||||||
@@ -8,19 +8,19 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2"/>
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"/>
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
<PackageReference Include="Moq" Version="4.20.72"/>
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="FluentAssertions" Version="7.2.0"/>
|
<PackageReference Include="FluentAssertions" Version="7.2.0" />
|
||||||
<PackageReference Include="FluentValidation" Version="11.3.0"/>
|
<PackageReference Include="FluentValidation" Version="11.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Xunit"/>
|
<Using Include="Xunit" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Shared.Application\Shared.Application.csproj"/>
|
<ProjectReference Include="..\Shared.Application\Shared.Application.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -6,18 +6,11 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Shared.Application.Behaviors;
|
namespace Shared.Application.Behaviors;
|
||||||
|
|
||||||
/// <summary>
|
public abstract class JsonLogger
|
||||||
/// 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>
|
|
||||||
{
|
{
|
||||||
private static readonly string[] SensitiveFieldNames = ["password", "token"];
|
private static readonly string[] SensitiveFieldNames = ["password", "token"];
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
protected static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
{
|
{
|
||||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver
|
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)
|
private static void RedactSensitiveProperties(JsonTypeInfo typeInfo)
|
||||||
{
|
{
|
||||||
if (typeInfo.Kind != JsonTypeInfoKind.Object)
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ using MediatR;
|
|||||||
namespace Shared.Application.Behaviors;
|
namespace Shared.Application.Behaviors;
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// throwing <see cref="ValidationException" /> if any report failures.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
|
public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
|
||||||
: IPipelineBehavior<TRequest, TResponse>
|
: IPipelineBehavior<TRequest, TResponse>
|
||||||
where TRequest : IRequest<TResponse>
|
where TRequest : IRequest<TResponse>
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc />
|
||||||
public async Task<TResponse> Handle(
|
public async Task<TResponse> Handle(
|
||||||
TRequest request,
|
TRequest request,
|
||||||
RequestHandlerDelegate<TResponse> next,
|
RequestHandlerDelegate<TResponse> next,
|
||||||
|
|||||||
Reference in New Issue
Block a user