Files
EnvelopeGenerator/EnvelopeGenerator.Application/Common/Extensions/MediatorExtensions.cs
TekH 9bdf24d7d5 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.
2026-04-08 15:26:23 +02:00

77 lines
3.2 KiB
C#

using DigitalData.Core.Exceptions;
using MediatR;
using System.Collections;
namespace EnvelopeGenerator.Application.Common.Extensions;
/// <summary>
/// Extension methods for <see cref="ISender"/> that provide a fluent API for enforcing non-null responses.
/// </summary>
public static class MediatorExtensions
{
/// <summary>
/// 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>
public async Task<TResponse> Throw(Func<Exception> exceptionFactory)
{
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();
return res;
}
throw exceptionFactory();
}
/// <summary>
/// Sends the request and throws <see cref="NotFoundException"/> when the response is <c>null</c> or an empty collection.
/// </summary>
public Task<TResponse> ThrowNotFound(string? message = null)
=> Throw(() => new NotFoundException(message ?? $"The requested resource of type {typeof(TResponse).Name} was not found."));
/// <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."));
}