Refactor MediatorExtensions to fluent GetOr/Throw API
Replaced GetOrThrow methods with a fluent GetOr/Throw pattern for handling null or empty MediatR responses. Introduced GetOrContext<TResponse> struct with Throw, ThrowNotFound, ThrowInvalidOperation, and ThrowBadRequest methods. Updated all tests to use the new API and added coverage for new exception types. Improved XML docs and performed minor code cleanup.
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IMediator"/> that enforce non-null and non-empty responses.
|
||||
/// Extension methods for <see cref="ISender"/> that provide a fluent API for enforcing non-null responses.
|
||||
/// </summary>
|
||||
public static class MediatorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a request via MediatR and throws a custom exception produced by <paramref name="exceptionFactory"/>
|
||||
/// Begins a fluent chain that sends <paramref name="request"/> and lets you choose how to handle a <c>null</c> or empty response.
|
||||
/// <para>Usage:</para>
|
||||
/// <code>
|
||||
/// await sender.GetOr(query).ThrowNotFound();
|
||||
/// await sender.GetOr(query, cancel).Throw(() => new MyException());
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static GetOrContext<TResponse> GetOr<TResponse>(this ISender sender, IRequest<TResponse?> request, CancellationToken cancel = default)
|
||||
=> new(sender, request, cancel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds a pending MediatR request and exposes <c>Throw…</c> methods that send the request
|
||||
/// and throw a chosen exception when the response is <c>null</c> or an empty collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">The expected response type.</typeparam>
|
||||
public readonly struct GetOrContext<TResponse>
|
||||
{
|
||||
private readonly ISender _sender;
|
||||
private readonly IRequest<TResponse?> _request;
|
||||
private readonly CancellationToken _cancel;
|
||||
|
||||
internal GetOrContext(ISender sender, IRequest<TResponse?> request, CancellationToken cancel)
|
||||
{
|
||||
_sender = sender;
|
||||
_request = request;
|
||||
_cancel = cancel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the request and throws the exception produced by <paramref name="exceptionFactory"/>
|
||||
/// when the response is <c>null</c> or an empty collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">The expected response type.</typeparam>
|
||||
/// <typeparam name="TException">The exception type to throw.</typeparam>
|
||||
/// <param name="sender">The mediator instance.</param>
|
||||
/// <param name="request">The MediatR request whose response may be <c>null</c>.</param>
|
||||
/// <param name="exceptionFactory">A factory that creates the exception to throw when the response is absent.</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
/// <returns>A guaranteed non-null <typeparamref name="TResponse"/>.</returns>
|
||||
/// <exception cref="Exception">The exception produced by <paramref name="exceptionFactory"/>.</exception>
|
||||
public static async Task<TResponse> GetOrThrow<TResponse, TException>(this ISender sender, IRequest<TResponse?> request, Func<TException> exceptionFactory, CancellationToken cancel = default)
|
||||
where TException : Exception
|
||||
public async Task<TResponse> Throw(Func<Exception> exceptionFactory)
|
||||
{
|
||||
if (await sender.Send(request, cancel) is TResponse res)
|
||||
if (await _sender.Send(_request, _cancel) is TResponse res)
|
||||
{
|
||||
// string implements IEnumerable<char>, so "" would be treated as an empty collection without this guard.
|
||||
if (res is not string && res is IEnumerable enumerable && !enumerable.Cast<object>().Any())
|
||||
throw exceptionFactory();
|
||||
|
||||
@@ -36,19 +58,20 @@ public static class MediatorExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request via MediatR and throws <see cref="NotFoundException"/> when the response is <c>null</c> or an empty collection.
|
||||
/// Sends the request and throws <see cref="NotFoundException"/> when the response is <c>null</c> or an empty collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">The expected response type.</typeparam>
|
||||
/// <param name="sender">The mediator instance.</param>
|
||||
/// <param name="request">The MediatR request whose response may be <c>null</c>.</param>
|
||||
/// <param name="exceptionMessage">Optional message for the <see cref="NotFoundException"/>.</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
/// <returns>A guaranteed non-null <typeparamref name="TResponse"/>.</returns>
|
||||
/// <exception cref="NotFoundException">Thrown when the response is <c>null</c> or an empty collection.</exception>
|
||||
public static async Task<TResponse> GetOrThrow<TResponse>(this ISender sender, IRequest<TResponse?> 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<TResponse> ThrowNotFound(string? message = null)
|
||||
=> Throw(() => new NotFoundException(message ?? $"The requested resource of type {typeof(TResponse).Name} was not found."));
|
||||
|
||||
/// <inheritdoc cref="GetOrThrow{TResponse}(ISender, IRequest{TResponse}, string, CancellationToken)"/>
|
||||
public static async Task<TResponse> GetOrThrow<TResponse>(this ISender sender, IRequest<TResponse?> request, CancellationToken cancel = default)
|
||||
=> await sender.GetOrThrow(request, null, cancel);
|
||||
/// <summary>
|
||||
/// Sends the request and throws <see cref="InvalidOperationException"/> when the response is <c>null</c> or an empty collection.
|
||||
/// </summary>
|
||||
public Task<TResponse> ThrowInvalidOperation(string? message = null)
|
||||
=> Throw(() => new InvalidOperationException(message ?? $"The operation for {typeof(TResponse).Name} returned no result."));
|
||||
|
||||
/// <summary>
|
||||
/// Sends the request and throws <see cref="BadRequestException"/> when the response is <c>null</c> or an empty collection.
|
||||
/// </summary>
|
||||
public Task<TResponse> ThrowBadRequest(string? message = null)
|
||||
=> Throw(() => new BadRequestException(message ?? $"The request for {typeof(TResponse).Name} is invalid."));
|
||||
}
|
||||
Reference in New Issue
Block a user