diff --git a/EnvelopeGenerator.Application/Common/Extensions/MediatorExtensions.cs b/EnvelopeGenerator.Application/Common/Extensions/MediatorExtensions.cs index 9a6a35dc..010c3154 100644 --- a/EnvelopeGenerator.Application/Common/Extensions/MediatorExtensions.cs +++ b/EnvelopeGenerator.Application/Common/Extensions/MediatorExtensions.cs @@ -1,31 +1,53 @@ -using System.Collections; -using DigitalData.Core.Exceptions; +using DigitalData.Core.Exceptions; using MediatR; +using System.Collections; namespace EnvelopeGenerator.Application.Common.Extensions; /// -/// Extension methods for that enforce non-null and non-empty responses. +/// Extension methods for that provide a fluent API for enforcing non-null responses. /// public static class MediatorExtensions { /// - /// Sends a request via MediatR and throws a custom exception produced by + /// Begins a fluent chain that sends and lets you choose how to handle a null or empty response. + /// Usage: + /// + /// await sender.GetOr(query).ThrowNotFound(); + /// await sender.GetOr(query, cancel).Throw(() => new MyException()); + /// + /// + public static GetOrContext GetOr(this ISender sender, IRequest request, CancellationToken cancel = default) + => new(sender, request, cancel); +} + +/// +/// Holds a pending MediatR request and exposes Throw… methods that send the request +/// and throw a chosen exception when the response is null or an empty collection. +/// +/// The expected response type. +public readonly struct GetOrContext +{ + private readonly ISender _sender; + private readonly IRequest _request; + private readonly CancellationToken _cancel; + + internal GetOrContext(ISender sender, IRequest request, CancellationToken cancel) + { + _sender = sender; + _request = request; + _cancel = cancel; + } + + /// + /// Sends the request and throws the exception produced by /// when the response is null or an empty collection. /// - /// The expected response type. - /// The exception type to throw. - /// The mediator instance. - /// The MediatR request whose response may be null. - /// A factory that creates the exception to throw when the response is absent. - /// Cancellation token. - /// A guaranteed non-null . - /// The exception produced by . - public static async Task GetOrThrow(this ISender sender, IRequest request, Func exceptionFactory, CancellationToken cancel = default) - where TException : Exception + public async Task Throw(Func exceptionFactory) { - if (await sender.Send(request, cancel) is TResponse res) + if (await _sender.Send(_request, _cancel) is TResponse res) { + // string implements IEnumerable, so "" would be treated as an empty collection without this guard. if (res is not string && res is IEnumerable enumerable && !enumerable.Cast().Any()) throw exceptionFactory(); @@ -36,19 +58,20 @@ public static class MediatorExtensions } /// - /// Sends a request via MediatR and throws when the response is null or an empty collection. + /// Sends the request and throws when the response is null or an empty collection. /// - /// The expected response type. - /// The mediator instance. - /// The MediatR request whose response may be null. - /// Optional message for the . - /// Cancellation token. - /// A guaranteed non-null . - /// Thrown when the response is null or an empty collection. - public static async Task GetOrThrow(this ISender sender, IRequest request, string? exceptionMessage, CancellationToken cancel = default) - => await sender.GetOrThrow(request, () => new NotFoundException(exceptionMessage ?? $"The requested resource of type {typeof(TResponse).Name} was not found."), cancel); + public Task ThrowNotFound(string? message = null) + => Throw(() => new NotFoundException(message ?? $"The requested resource of type {typeof(TResponse).Name} was not found.")); - /// - public static async Task GetOrThrow(this ISender sender, IRequest request, CancellationToken cancel = default) - => await sender.GetOrThrow(request, null, cancel); + /// + /// Sends the request and throws when the response is null or an empty collection. + /// + public Task ThrowInvalidOperation(string? message = null) + => Throw(() => new InvalidOperationException(message ?? $"The operation for {typeof(TResponse).Name} returned no result.")); + + /// + /// Sends the request and throws when the response is null or an empty collection. + /// + public Task ThrowBadRequest(string? message = null) + => Throw(() => new BadRequestException(message ?? $"The request for {typeof(TResponse).Name} is invalid.")); } \ No newline at end of file diff --git a/EnvelopeGenerator.Tests/Application/MediatorExtensionsTests.cs b/EnvelopeGenerator.Tests/Application/MediatorExtensionsTests.cs index bec0c4a1..9c4722cd 100644 --- a/EnvelopeGenerator.Tests/Application/MediatorExtensionsTests.cs +++ b/EnvelopeGenerator.Tests/Application/MediatorExtensionsTests.cs @@ -49,212 +49,239 @@ public class MediatorExtensionsTests #endregion - #region GetOrThrow — non-null scalar + #region Throw — non-null scalar [Test] - public async Task GetOrThrow_WithNonNullResponse_ReturnsResponse() + public async Task Throw_WithNonNullResponse_ReturnsResponse() { var sender = CreateSender("hello"); var request = new StubRequest(); - var result = await sender.GetOrThrow(request, () => new InvalidOperationException()); + var result = await sender.GetOr(request).Throw(() => new InvalidOperationException()); Assert.That(result, Is.EqualTo("hello")); } #endregion - #region GetOrThrow — null response + #region Throw — null response [Test] - public void GetOrThrow_WithNullResponse_ThrowsCustomException() + public void Throw_WithNullResponse_ThrowsCustomException() { var sender = CreateSender(null); var request = new StubRequest(); var ex = Assert.ThrowsAsync( - () => sender.GetOrThrow(request, () => new InvalidOperationException("custom"))); + () => sender.GetOr(request).Throw(() => new InvalidOperationException("custom"))); Assert.That(ex!.Message, Is.EqualTo("custom")); } #endregion - #region GetOrThrow — empty collection + #region Throw — empty collection [Test] - public void GetOrThrow_WithEmptyList_ThrowsCustomException() + public void Throw_WithEmptyList_ThrowsCustomException() { var sender = CreateSender>(new List()); var request = new StubRequest?>(); Assert.ThrowsAsync( - () => sender.GetOrThrow(request, () => new ArgumentException("empty"))); + () => sender.GetOr(request).Throw(() => new ArgumentException("empty"))); } #endregion - #region GetOrThrow — non-empty collection + #region Throw — non-empty collection [Test] - public async Task GetOrThrow_WithNonEmptyList_ReturnsResponse() + public async Task Throw_WithNonEmptyList_ReturnsResponse() { var expected = new List { 1, 2 }; var sender = CreateSender>(expected); var request = new StubRequest?>(); - var result = await sender.GetOrThrow(request, () => new InvalidOperationException()); + var result = await sender.GetOr(request).Throw(() => new InvalidOperationException()); Assert.That(result, Is.EqualTo(expected)); } #endregion - #region GetOrThrow — string edge case (string implements IEnumerable) + #region Throw — string edge case (string implements IEnumerable) [Test] - public async Task GetOrThrow_WithEmptyString_ReturnsEmptyString() + public async Task Throw_WithEmptyString_ReturnsEmptyString() { var sender = CreateSender(""); var request = new StubRequest(); - var result = await sender.GetOrThrow(request, () => new InvalidOperationException("should not throw")); + var result = await sender.GetOr(request).Throw(() => new InvalidOperationException("should not throw")); Assert.That(result, Is.EqualTo("")); } #endregion - #region GetOrThrow (NotFoundException) — non-null scalar + #region ThrowNotFound — non-null scalar [Test] - public async Task GetOrThrow_NotFound_WithNonNullResponse_ReturnsResponse() + public async Task ThrowNotFound_WithNonNullResponse_ReturnsResponse() { var sender = CreateSender("hello"); var request = new StubRequest(); - var result = await sender.GetOrThrow(request); + var result = await sender.GetOr(request).ThrowNotFound(); Assert.That(result, Is.EqualTo("hello")); } [Test] - public async Task GetOrThrow_NotFound_WithExceptionMessage_AndNonNullResponse_ReturnsResponse() + public async Task ThrowNotFound_WithExceptionMessage_AndNonNullResponse_ReturnsResponse() { var sender = CreateSender(42); var request = new StubRequest(); - var result = await sender.GetOrThrow(request, "not found", CancellationToken.None); + var result = await sender.GetOr(request).ThrowNotFound("not found"); Assert.That(result, Is.EqualTo(42)); } #endregion - #region GetOrThrow (NotFoundException) — null response + #region ThrowNotFound — null response [Test] - public void GetOrThrow_NotFound_WithNullResponse_ThrowsNotFoundException() + public void ThrowNotFound_WithNullResponse_ThrowsNotFoundException() { var sender = CreateSender(null); var request = new StubRequest(); - Assert.ThrowsAsync(() => sender.GetOrThrow(request)); + Assert.ThrowsAsync(() => sender.GetOr(request).ThrowNotFound()); } [Test] - public void GetOrThrow_NotFound_WithNullResponse_AndCustomMessage_ContainsMessage() + public void ThrowNotFound_WithNullResponse_AndCustomMessage_ContainsMessage() { const string message = "Entity not found"; var sender = CreateSender(null); var request = new StubRequest(); var ex = Assert.ThrowsAsync( - () => sender.GetOrThrow(request, message, CancellationToken.None)); + () => sender.GetOr(request).ThrowNotFound(message)); Assert.That(ex!.Message, Does.Contain(message)); } [Test] - public void GetOrThrow_NotFound_WithNullResponse_HasDefaultMessageWithTypeName() + public void ThrowNotFound_WithNullResponse_HasDefaultMessageWithTypeName() { var sender = CreateSender(null); var request = new StubRequest(); - var ex = Assert.ThrowsAsync(() => sender.GetOrThrow(request)); + var ex = Assert.ThrowsAsync(() => sender.GetOr(request).ThrowNotFound()); Assert.That(ex!.Message, Does.Contain(nameof(String))); } #endregion - #region GetOrThrow (NotFoundException) — empty collection + #region ThrowNotFound — empty collection [Test] - public void GetOrThrow_NotFound_WithEmptyList_ThrowsNotFoundException() + public void ThrowNotFound_WithEmptyList_ThrowsNotFoundException() { var sender = CreateSender>(new List()); var request = new StubRequest?>(); - Assert.ThrowsAsync(() => sender.GetOrThrow(request)); + Assert.ThrowsAsync(() => sender.GetOr(request).ThrowNotFound()); } [Test] - public void GetOrThrow_NotFound_WithEmptyArray_ThrowsNotFoundException() + public void ThrowNotFound_WithEmptyArray_ThrowsNotFoundException() { var sender = CreateSender(Array.Empty()); var request = new StubRequest(); - Assert.ThrowsAsync(() => sender.GetOrThrow(request)); + Assert.ThrowsAsync(() => sender.GetOr(request).ThrowNotFound()); } [Test] - public void GetOrThrow_NotFound_WithEmptyCollection_AndCustomMessage_ContainsMessage() + public void ThrowNotFound_WithEmptyCollection_AndCustomMessage_ContainsMessage() { const string message = "No items found"; var sender = CreateSender>(new List()); var request = new StubRequest?>(); var ex = Assert.ThrowsAsync( - () => sender.GetOrThrow(request, message, CancellationToken.None)); + () => sender.GetOr(request).ThrowNotFound(message)); Assert.That(ex!.Message, Does.Contain(message)); } #endregion - #region GetOrThrow (NotFoundException) — non-empty collection + #region ThrowNotFound — non-empty collection [Test] - public async Task GetOrThrow_NotFound_WithNonEmptyList_ReturnsResponse() + public async Task ThrowNotFound_WithNonEmptyList_ReturnsResponse() { var expected = new List { "a", "b" }; var sender = CreateSender>(expected); var request = new StubRequest?>(); - var result = await sender.GetOrThrow(request); + var result = await sender.GetOr(request).ThrowNotFound(); Assert.That(result, Is.EqualTo(expected)); } [Test] - public async Task GetOrThrow_NotFound_WithNonEmptyArray_ReturnsResponse() + public async Task ThrowNotFound_WithNonEmptyArray_ReturnsResponse() { var expected = new[] { 1, 2, 3 }; var sender = CreateSender(expected); var request = new StubRequest(); - var result = await sender.GetOrThrow(request); + var result = await sender.GetOr(request).ThrowNotFound(); Assert.That(result, Is.EqualTo(expected)); } #endregion + #region ThrowInvalidOperation — null response + + [Test] + public void ThrowInvalidOperation_WithNullResponse_ThrowsInvalidOperationException() + { + var sender = CreateSender(null); + var request = new StubRequest(); + + Assert.ThrowsAsync( + () => sender.GetOr(request).ThrowInvalidOperation()); + } + + [Test] + public void ThrowInvalidOperation_WithNullResponse_AndCustomMessage_ContainsMessage() + { + const string message = "Something went wrong"; + var sender = CreateSender(null); + var request = new StubRequest(); + + var ex = Assert.ThrowsAsync( + () => sender.GetOr(request).ThrowInvalidOperation(message)); + + Assert.That(ex!.Message, Does.Contain(message)); + } + + #endregion + #region CancellationToken [Test] - public void GetOrThrow_WithCancelledToken_ThrowsOperationCanceledException() + public void Throw_WithCancelledToken_ThrowsOperationCanceledException() { var sender = CreateSender("value"); var request = new StubRequest(); @@ -262,11 +289,11 @@ public class MediatorExtensionsTests cts.Cancel(); Assert.ThrowsAsync( - () => sender.GetOrThrow(request, () => new InvalidOperationException(), cts.Token)); + () => sender.GetOr(request, cts.Token).Throw(() => new InvalidOperationException())); } [Test] - public void GetOrThrow_NotFound_WithCancelledToken_ThrowsOperationCanceledException() + public void ThrowNotFound_WithCancelledToken_ThrowsOperationCanceledException() { var sender = CreateSender("value"); var request = new StubRequest(); @@ -274,7 +301,7 @@ public class MediatorExtensionsTests cts.Cancel(); Assert.ThrowsAsync( - () => sender.GetOrThrow(request, cts.Token)); + () => sender.GetOr(request, cts.Token).ThrowNotFound()); } #endregion