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