Init
This commit is contained in:
123
Prefab/Handler/Decorators/HandlerContextDecorator.cs
Normal file
123
Prefab/Handler/Decorators/HandlerContextDecorator.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Prefab.Handler.Decorators;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a decorator for handler execution that establishes and manages the current HandlerContext for each request,
|
||||
/// supporting HTTP, Blazor Server, and background scenarios.
|
||||
/// </summary>
|
||||
/// <remarks>This decorator ensures that a HandlerContext is available for each handler invocation, propagating
|
||||
/// correlation and request identifiers as appropriate for the execution environment. It supports HTTP requests, Blazor
|
||||
/// Server interactive sessions, and background processing, automatically selecting the appropriate context source. If
|
||||
/// an existing HandlerContext is present, it is reused with a new request identifier. This class is typically used in
|
||||
/// middleware or pipeline scenarios to provide consistent context information for logging, tracing, or auditing
|
||||
/// purposes.</remarks>
|
||||
/// <param name="handlerAccessor">The handlerAccessor used to get or set the current HandlerContext for the executing request. Cannot be null.</param>
|
||||
/// <param name="httpAccessor">The HTTP context handlerAccessor used to extract request and user information when handling HTTP requests. May be null if
|
||||
/// HTTP context is not available.</param>
|
||||
/// <param name="auth">The authentication state provider used to obtain user information in Blazor Server scenarios. May be null if
|
||||
/// authentication state is not required.</param>
|
||||
public class HandlerContextDecorator(
|
||||
IHandlerContextAccessor handlerAccessor,
|
||||
IHttpContextAccessor? httpAccessor,
|
||||
AuthenticationStateProvider? auth = null) : IHandlerDecorator
|
||||
{
|
||||
public async Task<TResponse> Invoke<TRequest, TResponse>(Func<TRequest, CancellationToken, Task<TResponse>> next, TRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// If a parent context exists, reuse correlation; mint a new request id
|
||||
if (handlerAccessor.Current is { } parent)
|
||||
{
|
||||
var child = parent with { RequestId = NewId() };
|
||||
|
||||
using var scope = handlerAccessor.Set(child);
|
||||
return await next(request, cancellationToken);
|
||||
}
|
||||
|
||||
if (httpAccessor?.HttpContext is HttpContext httpContext)
|
||||
{
|
||||
var ctx = FromHttp(httpContext);
|
||||
var stored = TryStoreHandlerContext(httpContext, ctx);
|
||||
|
||||
using var scope = handlerAccessor.Set(ctx);
|
||||
try
|
||||
{
|
||||
return await next(request, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (stored)
|
||||
{
|
||||
httpContext.Items.Remove(HandlerContextItems.HttpContextKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auth is not null)
|
||||
{
|
||||
var state = await auth.GetAuthenticationStateAsync();
|
||||
var ctx = ForInteractive(state.User);
|
||||
|
||||
using var scope = handlerAccessor.Set(ctx);
|
||||
return await next(request, cancellationToken);
|
||||
}
|
||||
|
||||
// Background/default
|
||||
{
|
||||
var ctx = ForBackground();
|
||||
using var scope = handlerAccessor.Set(ctx);
|
||||
return await next(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static HandlerContext FromHttp(HttpContext httpContext)
|
||||
{
|
||||
httpContext.Request.Headers.TryGetValue("Idempotency-Key", out var idem);
|
||||
var userId = GetUserId(httpContext.User);
|
||||
|
||||
string FirstNonEmpty(string a, string b) => !string.IsNullOrWhiteSpace(a) ? a : b;
|
||||
var corr = FirstNonEmpty(httpContext.Request.Headers["X-Correlation-Id"].ToString(), httpContext.TraceIdentifier);
|
||||
var req = FirstNonEmpty(httpContext.Request.Headers["X-Request-Id"].ToString(), httpContext.TraceIdentifier);
|
||||
|
||||
return new HandlerContext(
|
||||
CorrelationId: string.IsNullOrWhiteSpace(corr) ? NewId() : corr,
|
||||
RequestId: string.IsNullOrWhiteSpace(req) ? NewId() : req,
|
||||
IdempotencyKey: string.IsNullOrWhiteSpace(idem) ? null : idem.ToString(),
|
||||
UserId: userId);
|
||||
}
|
||||
|
||||
private static HandlerContext ForInteractive(ClaimsPrincipal? user) => new(
|
||||
UserId: GetUserId(user), RequestId: NewId(), CorrelationId: NewId(), IdempotencyKey: null);
|
||||
|
||||
private static HandlerContext ForBackground(string? correlationId = null, string? userId = null) => new(
|
||||
UserId: userId, RequestId: NewId(), CorrelationId: correlationId ?? NewId(), IdempotencyKey: null);
|
||||
|
||||
private static string? GetUserId(ClaimsPrincipal? user)
|
||||
{
|
||||
if (user?.Identity?.IsAuthenticated != true) return null;
|
||||
return user.FindFirst("sub")?.Value
|
||||
?? user.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? user.Identity!.Name;
|
||||
}
|
||||
|
||||
private static string NewId() => Guid.NewGuid().ToString("N");
|
||||
|
||||
private static bool TryStoreHandlerContext(HttpContext httpContext, HandlerContext context)
|
||||
{
|
||||
if (httpContext.Items.ContainsKey(HandlerContextItems.HttpContextKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
httpContext.Items[HandlerContextItems.HttpContextKey] = context;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
|
||||
15
Prefab/Handler/Decorators/IHandlerDecorator.cs
Normal file
15
Prefab/Handler/Decorators/IHandlerDecorator.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Prefab.Handler.Decorators;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a contract for decorating handler execution with additional behavior, such as logging, validation, or
|
||||
/// exception handling.
|
||||
/// </summary>
|
||||
/// <remarks>Implementations of this interface can be used to add cross-cutting concerns around handler invocation
|
||||
/// in a pipeline. Decorators can inspect or modify the request, context, or response, and may perform actions before or
|
||||
/// after calling the next handler in the chain. This interface is typically used in scenarios where handler logic needs
|
||||
/// to be extended without modifying the original handler implementation.</remarks>
|
||||
public interface IHandlerDecorator
|
||||
{
|
||||
Task<TResponse> Invoke<TRequest, TResponse>(Func<TRequest, CancellationToken, Task<TResponse>> next,
|
||||
TRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
161
Prefab/Handler/Decorators/LoggingDecorator.cs
Normal file
161
Prefab/Handler/Decorators/LoggingDecorator.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Prefab.Handler.Decorators;
|
||||
|
||||
public sealed class LoggingDecoratorOptions
|
||||
{
|
||||
/// <summary>Log at Warning when handler time >= this threshold (ms).</summary>
|
||||
public int SlowThresholdInMilliseconds { get; init; } = 1000;
|
||||
}
|
||||
|
||||
public sealed class LoggingDecorator(ILogger<LoggingDecorator> logger, IHandlerContextAccessor handlerContext, IOptions<LoggingDecoratorOptions>? options = null) : IHandlerDecorator
|
||||
{
|
||||
private readonly LoggingDecoratorOptions _options = options?.Value ?? new LoggingDecoratorOptions();
|
||||
|
||||
|
||||
public async Task<TResponse> Invoke<TRequest, TResponse>(Func<TRequest, CancellationToken, Task<TResponse>> next,
|
||||
TRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Friendly operation label
|
||||
var requestType = DescribeMessageType(typeof(TRequest));
|
||||
var responseType = DescribeMessageType(typeof(TResponse));
|
||||
var operation = $"{requestType}->{responseType}";
|
||||
|
||||
var currentContext = handlerContext.Current ?? new HandlerContext(null, null, null, null);
|
||||
|
||||
using var scope = logger.BeginScope(new Dictionary<string, object?>
|
||||
{
|
||||
["Operation"] = operation,
|
||||
["RequestType"] = requestType,
|
||||
["ResponseType"] = responseType,
|
||||
["CorrelationId"] = currentContext.CorrelationId,
|
||||
["IdempotencyKey"] = currentContext.IdempotencyKey,
|
||||
["RequestId"] = currentContext.RequestId,
|
||||
["UserId"] = currentContext.UserId
|
||||
});
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
// (Optional) OpenTelemetry tagging
|
||||
var activity = Activity.Current;
|
||||
activity?.SetTag("handler.operation", operation);
|
||||
activity?.SetTag("handler.request_type", requestType);
|
||||
activity?.SetTag("handler.response_type", responseType);
|
||||
activity?.SetTag("handler.correlation_id", currentContext.CorrelationId);
|
||||
activity?.SetTag("handler.request_id", currentContext.RequestId);
|
||||
|
||||
try
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Debug))
|
||||
logger.LogDebug("Handling {Operation}", operation);
|
||||
|
||||
var result = await next(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Warning))
|
||||
logger.LogWarning("Canceled {Operation}", operation);
|
||||
|
||||
activity?.SetStatus(ActivityStatusCode.Error, "canceled");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Error))
|
||||
logger.LogError(ex, "Error handling {Operation}", operation);
|
||||
|
||||
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
var ms = stopwatch.ElapsedMilliseconds;
|
||||
activity?.SetTag("handler.duration_ms", ms);
|
||||
|
||||
if (ms >= _options.SlowThresholdInMilliseconds && logger.IsEnabled(LogLevel.Warning))
|
||||
logger.LogWarning("Handled {Operation} in {ElapsedMs} ms (slow)", operation, ms);
|
||||
else if (logger.IsEnabled(LogLevel.Information))
|
||||
logger.LogInformation("Handled {Operation} in {ElapsedMs} ms", operation, ms);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Pretty type-name helpers with caching ----
|
||||
|
||||
private static string DescribeMessageType(Type type) =>
|
||||
MessageTypeCache.GetOrAdd(type, static t =>
|
||||
{
|
||||
if (t == typeof(NoRequest))
|
||||
{
|
||||
return nameof(NoRequest);
|
||||
}
|
||||
|
||||
if (t.FullName is { } fullName &&
|
||||
fullName.StartsWith("Prefab.", StringComparison.Ordinal))
|
||||
{
|
||||
var formatted = FormatPrefabTypeName(fullName);
|
||||
if (!string.IsNullOrEmpty(formatted))
|
||||
{
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
|
||||
return PrettyTypeName(t);
|
||||
});
|
||||
|
||||
private static string FormatPrefabTypeName(string fullName)
|
||||
{
|
||||
const string prefix = "Prefab.";
|
||||
var span = fullName.AsSpan(prefix.Length);
|
||||
var buffer = new List<string>();
|
||||
|
||||
foreach (var segment in span.ToString().Replace('+', '.')
|
||||
.Split('.', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (SegmentsToSkip.Contains(segment))
|
||||
continue;
|
||||
|
||||
buffer.Add(segment);
|
||||
}
|
||||
|
||||
if (buffer.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return string.Join('.', buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a human-readable type name (e.g., <c>Dictionary<string, List<int>></c>)
|
||||
/// instead of framework names like <c>Dictionary`2</c>.
|
||||
/// </summary>
|
||||
private static string PrettyTypeName(Type type) =>
|
||||
PrettyCache.GetOrAdd(type, static t => PrettyTypeNameUncached(t));
|
||||
|
||||
private static string PrettyTypeNameUncached(Type t)
|
||||
{
|
||||
if (!t.IsGenericType) return t.Name;
|
||||
|
||||
var name = t.Name;
|
||||
var tick = name.IndexOf('`');
|
||||
var baseName = tick >= 0 ? name[..tick] : name;
|
||||
|
||||
var args = t.GetGenericArguments().Select(PrettyTypeNameUncached);
|
||||
return $"{baseName}<{string.Join(",", args)}>";
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, string> MessageTypeCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, string> PrettyCache = new();
|
||||
private static readonly HashSet<string> SegmentsToSkip = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"Shared",
|
||||
"App",
|
||||
"Handler",
|
||||
"Base"
|
||||
};
|
||||
}
|
||||
65
Prefab/Handler/Decorators/ValidationDecorator.cs
Normal file
65
Prefab/Handler/Decorators/ValidationDecorator.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Prefab.Handler.Decorators;
|
||||
|
||||
/// <summary>
|
||||
/// Decorator that runs all registered FluentValidation validators for the current request type
|
||||
/// before the handler executes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Validators are resolved on demand per request, allowing pages and modules to register request-specific
|
||||
/// validators without touching handler implementations. When no validators are registered, the decorator
|
||||
/// simply forwards execution to the next component in the pipeline.
|
||||
/// </remarks>
|
||||
public sealed class ValidationDecorator(IServiceProvider serviceProvider) : IHandlerDecorator
|
||||
{
|
||||
public async Task<TResponse> Invoke<TRequest, TResponse>(
|
||||
Func<TRequest, CancellationToken, Task<TResponse>> next,
|
||||
TRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var failures = await ValidateAsync(request, cancellationToken);
|
||||
|
||||
if (failures.Count != 0)
|
||||
{
|
||||
throw new ValidationException(failures);
|
||||
}
|
||||
|
||||
return await next(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<ValidationFailure>> ValidateAsync<TRequest>(
|
||||
TRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var validators = serviceProvider.GetServices<IValidator<TRequest>>();
|
||||
|
||||
var enumerable = validators as IValidator<TRequest>[] ?? validators.ToArray();
|
||||
if (enumerable.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var context = new ValidationContext<TRequest>(request);
|
||||
var failures = new List<ValidationFailure>();
|
||||
|
||||
foreach (var validator in enumerable)
|
||||
{
|
||||
var result = await validator.ValidateAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!result.IsValid)
|
||||
{
|
||||
failures.AddRange(result.Errors.Where(f => f is not null));
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
15
Prefab/Handler/HandlerBase.cs
Normal file
15
Prefab/Handler/HandlerBase.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Prefab.Handler.Decorators;
|
||||
|
||||
namespace Prefab.Handler;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base class for handler implementations that require access to the current handler context.
|
||||
/// </summary>
|
||||
/// <param name="accessor">The accessor used to retrieve the current handler context. Cannot be null.</param>
|
||||
public abstract class HandlerBase(IHandlerContextAccessor accessor)
|
||||
{
|
||||
protected IHandlerContextAccessor Accessor { get; } = accessor;
|
||||
|
||||
protected HandlerContext Context => Accessor.Current ??
|
||||
throw new InvalidOperationException($"HandlerContext not set ({nameof(HandlerContextDecorator)}) must run first).");
|
||||
}
|
||||
20
Prefab/Handler/HandlerContext.cs
Normal file
20
Prefab/Handler/HandlerContext.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Prefab.Handler;
|
||||
|
||||
/// <summary>
|
||||
/// Represents contextual information about the current request and user for use in request handling and logging.
|
||||
/// </summary>
|
||||
/// <remarks>This record is typically used to pass request-scoped metadata through application layers, enabling
|
||||
/// consistent logging, tracing, and idempotency handling. All properties are optional and may be null if the
|
||||
/// corresponding information is unavailable.</remarks>
|
||||
/// <param name="UserId">The unique identifier of the user associated with the request, or null if the user is unauthenticated.</param>
|
||||
/// <param name="RequestId">The unique identifier for the current request, used for tracing and diagnostics. Can be null if not set.</param>
|
||||
/// <param name="CorrelationId">The identifier used to correlate this request with related operations across services or components. Can be null if
|
||||
/// not set.</param>
|
||||
/// <param name="IdempotencyKey">A key that uniquely identifies the request for idempotency purposes, allowing safe retries. Can be null if
|
||||
/// idempotency is not required.</param>
|
||||
public sealed record HandlerContext(
|
||||
string? UserId,
|
||||
string? RequestId,
|
||||
string? CorrelationId,
|
||||
string? IdempotencyKey);
|
||||
|
||||
12
Prefab/Handler/HandlerContextItems.cs
Normal file
12
Prefab/Handler/HandlerContextItems.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Prefab.Handler;
|
||||
|
||||
/// <summary>
|
||||
/// Provides shared keys for storing handler context metadata in request-scoped containers.
|
||||
/// </summary>
|
||||
public static class HandlerContextItems
|
||||
{
|
||||
/// <summary>
|
||||
/// Key used when storing <see cref="HandlerContext"/> in <see cref="Microsoft.AspNetCore.Http.HttpContext.Items"/>.
|
||||
/// </summary>
|
||||
public const string HttpContextKey = "__prefab.handler-context";
|
||||
}
|
||||
100
Prefab/Handler/HandlerInvoker.cs
Normal file
100
Prefab/Handler/HandlerInvoker.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Prefab.Handler.Decorators;
|
||||
|
||||
namespace Prefab.Handler;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Provides a mechanism to execute handler pipelines with support for applying a sequence of handler decorators. This
|
||||
/// class enables the composition of cross-cutting concerns around handler execution.
|
||||
/// </summary>
|
||||
/// <remarks>HandlerInvoker is typically used to execute handlers with additional behaviors, such as logging,
|
||||
/// validation, or exception handling, by composing the provided decorators. The order of decorators affects the
|
||||
/// execution flow: the first-registered decorator is invoked first and wraps subsequent decorators and the handler
|
||||
/// itself. This class is thread-safe for concurrent handler executions.</remarks>
|
||||
/// <param name="decorators">The collection of handler decorators to apply to each handler invocation. Decorators are applied in the order they
|
||||
/// are provided, with the first decorator wrapping the outermost layer of the pipeline. Cannot be null.</param>
|
||||
public sealed class HandlerInvoker(IEnumerable<IHandlerDecorator> decorators, IServiceProvider serviceProvider)
|
||||
{
|
||||
private readonly IHandlerDecorator[] _decorators = decorators.Reverse().ToArray();
|
||||
|
||||
public Task<TResponse> Execute<TResponse>(CancellationToken cancellationToken)
|
||||
{
|
||||
var inner = serviceProvider.GetRequiredService<IHandler<TResponse>>();
|
||||
|
||||
// No decorators? Call the handler directly.
|
||||
if (_decorators.Length == 0)
|
||||
return inner.Execute(cancellationToken);
|
||||
|
||||
var noRequest = new NoRequestAdapter<TResponse>(inner);
|
||||
|
||||
var pipeline = BuildPipeline(noRequest);
|
||||
|
||||
return pipeline(NoRequest.Instance, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the specified handler for the given request within the provided context and cancellation token,
|
||||
/// returning the handler's response asynchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">The type of the request to be handled. Must not be null.</typeparam>
|
||||
/// <typeparam name="TResponse">The type of the response returned by the handler.</typeparam>
|
||||
/// <param name="request">The request object to be processed by the handler.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the response produced by the
|
||||
/// handler.</returns>
|
||||
public Task<TResponse> Execute<TRequest, TResponse>(TRequest request, CancellationToken cancellationToken) where TRequest : notnull
|
||||
{
|
||||
var handler = serviceProvider.GetRequiredService<IHandler<TRequest, TResponse>>();
|
||||
|
||||
// No decorators? Call the handler directly.
|
||||
if (_decorators.Length == 0)
|
||||
return handler.Execute(request, cancellationToken);
|
||||
|
||||
var pipeline = BuildPipeline(handler);
|
||||
return pipeline(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a delegate representing the request handling pipeline, applying all registered decorators to the
|
||||
/// specified handler.
|
||||
/// </summary>
|
||||
/// <remarks>Decorators are applied in the order they were registered, with the first-registered decorator
|
||||
/// wrapping the outermost layer of the pipeline. The returned delegate can be invoked with a request, handler
|
||||
/// context, and cancellation token to execute the full pipeline.</remarks>
|
||||
/// <typeparam name="TRequest">The type of the request message to be handled.</typeparam>
|
||||
/// <typeparam name="TResponse">The type of the response message returned by the handler.</typeparam>
|
||||
/// <param name="handler">The core handler to which decorators will be applied. Cannot be null.</param>
|
||||
/// <returns>A delegate that processes a request through all registered decorators and the specified handler.</returns>
|
||||
private Func<TRequest, CancellationToken, Task<TResponse>> BuildPipeline<TRequest, TResponse>(IHandler<TRequest, TResponse> handler)
|
||||
{
|
||||
// Start from the final operation: the handler itself.
|
||||
Func<TRequest, CancellationToken, Task<TResponse>> pipeline = handler.Execute;
|
||||
|
||||
// Wrap in reverse registration order so the first-registered decorator runs outermost.
|
||||
foreach (var decorator in _decorators)
|
||||
{
|
||||
var next = pipeline;
|
||||
|
||||
pipeline = (request, cancellationToken) =>
|
||||
decorator.Invoke(next, request, cancellationToken);
|
||||
}
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides an adapter that allows an existing handler to be used where a handler for a request type of NoRequest
|
||||
/// is required.
|
||||
/// </summary>
|
||||
/// <remarks>This class is useful for scenarios where a handler expects no input request data but must
|
||||
/// conform to an interface requiring a request parameter. It delegates execution to the specified inner handler,
|
||||
/// ignoring the NoRequest parameter.</remarks>
|
||||
/// <typeparam name="TResponse">The type of the response returned by the handler.</typeparam>
|
||||
/// <param name="innerHandler">The handler that processes requests and produces a response of type TResponse.</param>
|
||||
private sealed class NoRequestAdapter<TResponse>(IHandler<TResponse> innerHandler) : IHandler<NoRequest, TResponse>
|
||||
{
|
||||
public Task<TResponse> Execute(NoRequest _, CancellationToken cancellationToken)
|
||||
=> innerHandler.Execute(cancellationToken);
|
||||
}
|
||||
}
|
||||
25
Prefab/Handler/IHandler.cs
Normal file
25
Prefab/Handler/IHandler.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Prefab.Handler;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a handler that processes a request and returns a response asynchronously.
|
||||
/// </summary>
|
||||
/// <remarks>Implementations of this interface encapsulate the logic required to handle a specific request and
|
||||
/// produce a result. Handlers are typically used in command or query processing pipelines to separate request handling
|
||||
/// logic from other application concerns.</remarks>
|
||||
/// <typeparam name="TResponse">The type of the response returned by the handler.</typeparam>
|
||||
public interface IHandler<TResponse>
|
||||
{
|
||||
Task<TResponse> Execute(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a contract for handling a request and producing a response asynchronously.
|
||||
/// </summary>
|
||||
/// <remarks>Implementations of this interface should be thread-safe if they are intended to be used concurrently.
|
||||
/// The interface is typically used in request/response or command/query processing pipelines.</remarks>
|
||||
/// <typeparam name="TRequest">The type of the request to be handled.</typeparam>
|
||||
/// <typeparam name="TResponse">The type of the response returned after handling the request.</typeparam>
|
||||
public interface IHandler<in TRequest, TResponse>
|
||||
{
|
||||
Task<TResponse> Execute(TRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
66
Prefab/Handler/IHandlerContextAccessor.cs
Normal file
66
Prefab/Handler/IHandlerContextAccessor.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
namespace Prefab.Handler;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the current handler context for the executing operation.
|
||||
/// </summary>
|
||||
/// <remarks>Implementations of this interface allow components to retrieve or temporarily override the current
|
||||
/// HandlerContext within a given scope. This is typically used to flow contextual information, such as user or request
|
||||
/// data, through asynchronous or nested operations.</remarks>
|
||||
public interface IHandlerContextAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current handler context for the ongoing operation, if one is available.
|
||||
/// </summary>
|
||||
HandlerContext? Current { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the specified handler context as the current context for the duration of the returned disposable object's
|
||||
/// lifetime.
|
||||
/// </summary>
|
||||
/// <remarks>Use the returned IDisposable in a using statement to ensure the previous context is restored
|
||||
/// even if an exception occurs.</remarks>
|
||||
/// <param name="handlerContext">The handler context to set as current. Cannot be null.</param>
|
||||
/// <returns>An IDisposable that, when disposed, restores the previous handler context.</returns>
|
||||
IDisposable Set(HandlerContext handlerContext);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IHandlerContextAccessor"/>
|
||||
public sealed class HandlerContextAccessor : IHandlerContextAccessor
|
||||
{
|
||||
private static readonly AsyncLocal<HandlerContext?> CurrentContext = new();
|
||||
|
||||
/// <inheritdoc cref="IHandlerContextAccessor"/>
|
||||
public HandlerContext? Current => CurrentContext.Value;
|
||||
|
||||
/// <inheritdoc cref="IHandlerContextAccessor"/>
|
||||
public IDisposable Set(HandlerContext handlerContext)
|
||||
{
|
||||
var previous = CurrentContext.Value;
|
||||
|
||||
CurrentContext.Value = handlerContext;
|
||||
|
||||
return new Scope(() => CurrentContext.Value = previous);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a mechanism for executing a specified delegate when the scope is disposed, typically used to restore
|
||||
/// state or perform cleanup actions.
|
||||
/// </summary>
|
||||
/// <remarks>This type is intended for use with the 'using' statement to ensure that the specified
|
||||
/// delegate is executed exactly once when the scope ends. The delegate is not invoked if Dispose is called more
|
||||
/// than once.</remarks>
|
||||
/// <param name="restore">The delegate to invoke when the scope is disposed. Cannot be null.</param>
|
||||
private sealed class Scope(Action restore) : IDisposable
|
||||
{
|
||||
private bool _done;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(_done)
|
||||
return;
|
||||
|
||||
_done = true;
|
||||
restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Prefab/Handler/NoRequest.cs
Normal file
11
Prefab/Handler/NoRequest.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Prefab.Handler;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a sentinel value indicating the absence of a request.
|
||||
/// </summary>
|
||||
/// <remarks>Use this type when an operation or API requires a value to signify that no request is present. This
|
||||
/// can be useful in scenarios where a method or handler expects a request type but no data is needed.</remarks>
|
||||
public readonly record struct NoRequest
|
||||
{
|
||||
public static readonly NoRequest Instance = default;
|
||||
}
|
||||
Reference in New Issue
Block a user