feat: create request projects for basic CQRS

This commit is contained in:
Louis Seubert 2026-05-08 20:26:26 +02:00
commit d614788e06
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
190 changed files with 12236 additions and 0 deletions

View file

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>true</IsPackable>
</PropertyGroup>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<InternalsVisibleTo Include="Geekeey.Request.Dispatcher.Tests" />
</ItemGroup>
<PropertyGroup>
<PackageReadmeFile>package-readme.md</PackageReadmeFile>
<PackageDescription>Simple mediator implementation in .NET with minimal dependencies.</PackageDescription>
<PackageIcon>package-icon.png</PackageIcon>
<PackageProjectUrl>https://code.geekeey.de/geekeey/request/src/branch/main/src/request.dispatcher</PackageProjectUrl>
<PackageLicenseExpression>EUPL-1.2</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include=".\package-icon.png" Pack="true" PackagePath="\" Visible="false" />
<None Include=".\package-readme.md" Pack="true" PackagePath="\" Visible="false" />
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Visible="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Options" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,28 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Defines functionality to dispatch requests to their corresponding handlers.
/// </summary>
public interface IRequestDispatcher
{
/// <summary>
/// Asynchronously send a request to a handler producing a scalar value.
/// </summary>
/// <param name="request">Request object</param>
/// <param name="cancellationToken">Optional cancellation token</param>
/// <typeparam name="TResponse">Response type</typeparam>
/// <returns>A task that represents the send operation. The task result contains the handler response</returns>
Task<TResponse> DispatchAsync<TResponse>(IScalarRequest<TResponse> request, CancellationToken cancellationToken = default);
/// <summary>
/// Asynchronously send a request to a handler producing a stream value.
/// </summary>
/// <param name="request">Request object</param>
/// <param name="cancellationToken">Optional cancellation token</param>
/// <typeparam name="TResponse">Response type</typeparam>
/// <returns>The created async enumerable, representing the stream of responses.</returns>
IAsyncEnumerable<TResponse> DispatchAsync<TResponse>(IStreamRequest<TResponse> request, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,19 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Represents a builder for configuring and registering request dispatchers and their related components.
/// </summary>
public interface IRequestDispatcherBuilder
{
/// <summary>
/// Gets the <see cref="IServiceCollection"/> associated with the request dispatcher builder.
/// Provides access to the underlying service collection for configuring dependencies
/// and registering services required by the request dispatcher.
/// </summary>
IServiceCollection Services { get; }
}

View file

@ -0,0 +1,35 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
#pragma warning disable CA1711
#pragma warning disable CA1716
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Represents a behavior in the request pipeline, allowing interception, modification,
/// or chaining of asynchronous stream requests and responses.
/// </summary>
/// <typeparam name="TRequest">The type of the request being handled. Must implement <see cref="IScalarRequest{TResponse}"/>.</typeparam>
/// <typeparam name="TResponse">The type of the response produced by the implementing request handler.</typeparam>
public interface IScalarRequestBehavior<in TRequest, TResponse> where TRequest : IScalarRequest<TResponse>
{
/// <summary>
/// Handles the asynchronous processing of a request, allowing behavior customization
/// such as interception, modification, or chaining of the request and its response.
/// </summary>
/// <param name="request">The request instance being processed.</param>
/// <param name="next">The next delegate in the pipeline to execute after the custom behavior.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response from the request.</returns>
Task<TResponse> HandleAsync(TRequest request, ScalarHandlerDelegate<TResponse> next, CancellationToken cancellationToken);
}
/// <summary>
/// Represents the delegate responsible for handling asynchronous requests in a pipeline.
/// </summary>
/// <typeparam name="TResponse">The type of the response produced by the implementing request handler.</typeparam>
/// <param name="request">The asynchronous request being processed.</param>
/// <param name="cancellationToken">A token for monitoring cancellation requests.</param>
/// <returns>A task, that represents the asynchronous operation, containing the response of type <typeparamref name="TResponse"/>.</returns>
public delegate Task<TResponse> ScalarHandlerDelegate<TResponse>(IScalarRequest<TResponse> request, CancellationToken cancellationToken);

View file

@ -0,0 +1,14 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Represents a request that produces a response of a specified type.
/// This interface serves as a marker for distinguishing request types,
/// enabling their integration into request-based pipelines or dispatch mechanisms.
/// </summary>
/// <typeparam name="TResponse">The type of the response produced by the implementing request handler.</typeparam>
public interface IScalarRequest<out TResponse>
{
}

View file

@ -0,0 +1,20 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Defines a handler for processing requests of a specific type and returning a response.
/// </summary>
/// <typeparam name="TRequest">The type of the request to be handled. Must implement <see cref="IScalarRequest{TResponse}"/>.</typeparam>
/// <typeparam name="TResponse">The type of the response produced by the implementing request handler.</typeparam>
public interface IScalarRequestHandler<in TRequest, TResponse> where TRequest : IScalarRequest<TResponse>
{
/// <summary>
/// Processes a scalar request and returns a response asynchronously.
/// </summary>
/// <param name="request">The request object to be processed.</param>
/// <param name="cancellationToken">A token to observe for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation, containing the response of type <typeparamref name="TResponse"/>.</returns>
Task<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,35 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
#pragma warning disable CA1711
#pragma warning disable CA1716
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Represents a behavior in the request pipeline, allowing interception, modification,
/// or chaining of asynchronous stream requests and responses.
/// </summary>
/// <typeparam name="TRequest">The type of the request being processed. Must implement <see cref="IStreamRequest{TResponse}"/>.</typeparam>
/// <typeparam name="TResponse">The type of the response produced by the implementing request handler.</typeparam>
public interface IStreamRequestBehavior<in TRequest, TResponse> where TRequest : IStreamRequest<TResponse>
{
/// <summary>
/// Handles the asynchronous processing of a request, allowing behavior customization
/// such as interception, modification, or chaining of the request and its response.
/// </summary>
/// <param name="request">The request instance being processed.</param>
/// <param name="next">The next delegate in the pipeline to execute after the custom behavior.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>An asynchronous stream of <typeparamref name="TResponse"/> representing the processed response.</returns>
IAsyncEnumerable<TResponse> HandleAsync(TRequest request, StreamHandlerDelegate<TResponse> next, CancellationToken cancellationToken);
}
/// <summary>
/// Represents the delegate responsible for handling asynchronous requests in a pipeline.
/// </summary>
/// <typeparam name="TResponse">The type of the response produced by the implementing request handler.</typeparam>
/// <param name="request">The asynchronous request being processed.</param>
/// <param name="cancellationToken">A token for monitoring cancellation requests.</param>
/// <returns>An asynchronous stream of responses of type <typeparamref name="TResponse"/>.</returns>
public delegate IAsyncEnumerable<TResponse> StreamHandlerDelegate<TResponse>(IStreamRequest<TResponse> request, CancellationToken cancellationToken);

View file

@ -0,0 +1,14 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Represents a request that produces a response of a specified type.
/// This interface serves as a marker for distinguishing request types,
/// enabling their integration into request-based pipelines or dispatch mechanisms.
/// </summary>
/// <typeparam name="TResponse">The type of the response produced by the implementing request handler.</typeparam>
public interface IStreamRequest<out TResponse>
{
}

View file

@ -0,0 +1,21 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Defines a handler for processing streaming requests of a specific type and producing
/// a stream of responses.
/// </summary>
/// <typeparam name="TRequest">The type of the request to handle. This must implement <see cref="IStreamRequest{TResponse}"/>.</typeparam>
/// <typeparam name="TResponse">The type of the response produced by the implementing request handler.</typeparam>
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse>
{
/// <summary>
/// Handles a streaming request and returns a stream of responses asynchronously.
/// </summary>
/// <param name="request">The request object to be processed.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>An asynchronous stream of responses of type <typeparamref name="TResponse"/>.</returns>
IAsyncEnumerable<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken);
}

View file

@ -0,0 +1,48 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Collections.Concurrent;
using System.Reflection.Metadata;
[assembly: MetadataUpdateHandler(typeof(Geekeey.Request.Dispatcher.RequestDispatcher))]
namespace Geekeey.Request.Dispatcher;
internal sealed class RequestDispatcher : IRequestDispatcher
{
private static readonly ConcurrentDictionary<(Type, Type), ScalarRequestInvoker> ScalarRequestHandlers = new();
private static readonly ConcurrentDictionary<(Type, Type), StreamRequestInvoker> StreamRequestHandlers = new();
private readonly IServiceProvider _serviceProvider;
public RequestDispatcher(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task<TResponse> DispatchAsync<TResponse>(IScalarRequest<TResponse> request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
var handler = ScalarRequestHandlers.GetOrAdd((request.GetType(), typeof(TResponse)), static key =>
{
var type = typeof(ScalarRequestInvoker<,>).MakeGenericType(key.Item1, key.Item2);
return (ScalarRequestInvoker)Activator.CreateInstance(type)!;
});
return ((ScalarRequestInvoker<TResponse>)handler).HandleAsync(request, _serviceProvider, cancellationToken);
}
public IAsyncEnumerable<TResponse> DispatchAsync<TResponse>(IStreamRequest<TResponse> request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
var handler = StreamRequestHandlers.GetOrAdd((request.GetType(), typeof(TResponse)), static key =>
{
var type = typeof(StreamRequestInvoker<,>).MakeGenericType(key.Item1, key.Item2);
return (StreamRequestInvoker)Activator.CreateInstance(type)!;
});
return ((StreamRequestInvoker<TResponse>)handler).HandleAsync(request, _serviceProvider, cancellationToken);
}
}

View file

@ -0,0 +1,174 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using static Geekeey.Request.Dispatcher.RequestDispatcherOptions;
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Provides extension methods for configuring <see cref="IRequestDispatcherBuilder"/>
/// with additional capabilities such as searching and registering request handlers in assemblies or adding types directly.
/// </summary>
public static class RequestDispatcherBuilderExtensions
{
/// <summary>
/// Searches for request handler types within the specified assembly and adds them to the request dispatcher
/// configuration.
/// </summary>
/// <remarks>
/// Prefer the generated <c>Add&lt;Name&gt;(builder)</c> extensions for assemblies that directly reference
/// <c>Geekeey.Request.Dispatcher</c>. This runtime scanning API remains supported, but nested request handlers are rejected
/// during registration and generated registration should not be mixed with <see cref="SearchHandlerInAssembly(IRequestDispatcherBuilder, Assembly)"/>
/// for the same assembly.
/// </remarks>
/// <param name="builder">The <see cref="IRequestDispatcherBuilder"/> to configure.</param>
/// <param name="assembly">The assembly to search for request handler types.</param>
/// <returns>The <see cref="IRequestDispatcherBuilder"/> instance for further configuration.</returns>
public static IRequestDispatcherBuilder SearchHandlerInAssembly(this IRequestDispatcherBuilder builder, Assembly assembly)
{
ArgumentNullException.ThrowIfNull(builder);
var exports = assembly.GetTypes()
.Where(type => type is { IsClass: true, IsAbstract: false })
.Where(IsRequestHandlerImplementationType);
builder.Add(exports);
return builder;
}
/// <summary>
/// Searches for request handler types within the specified assembly and adds them to the request dispatcher
/// configuration with the given service lifetime.
/// </summary>
/// <remarks>
/// Prefer the generated <c>Add&lt;Name&gt;(builder, lifetime)</c> extensions for assemblies that directly reference
/// <c>Geekeey.Request.Dispatcher</c>. This runtime scanning API remains supported, but nested request handlers are rejected
/// during registration and generated registration should not be mixed with <see cref="SearchHandlerInAssembly(IRequestDispatcherBuilder, Assembly, ServiceLifetime)"/>
/// for the same assembly.
/// </remarks>
/// <param name="builder">The <see cref="IRequestDispatcherBuilder"/> to configure.</param>
/// <param name="assembly">The assembly to search for request handler types.</param>
/// <param name="lifetime">The lifetime with which the request handlers are registered in the dependency injection container.</param>
/// <returns>The <see cref="IRequestDispatcherBuilder"/> instance for further configuration.</returns>
public static IRequestDispatcherBuilder SearchHandlerInAssembly(this IRequestDispatcherBuilder builder, Assembly assembly, ServiceLifetime lifetime)
{
ArgumentNullException.ThrowIfNull(builder);
var exports = assembly.GetTypes()
.Where(type => type is { IsClass: true, IsAbstract: false })
.Where(IsRequestHandlerImplementationType);
builder.Add(exports, lifetime);
return builder;
}
/// <summary>
/// Adds the specified type to the request dispatcher configuration for inspection.
/// </summary>
/// <param name="builder">The <see cref="IRequestDispatcherBuilder"/> to configure.</param>
/// <param name="type">The type to be added to the request dispatcher configuration.</param>
/// <returns>The <see cref="IRequestDispatcherBuilder"/> instance for further configuration.</returns>
public static IRequestDispatcherBuilder Add(this IRequestDispatcherBuilder builder, Type type)
{
ArgumentNullException.ThrowIfNull(builder);
ValidateNoNestedRequestHandlers([type]);
builder.Services.AddOptions<RequestDispatcherOptions>()
.Configure(options => options.Inspect([type]));
return builder;
}
/// <summary>
/// Adds the specified type to the request dispatcher configuration.
/// This also adds the type to the service collection with the specified lifetime,
/// allowing it to be resolved as a dependency in request handlers and behaviors.
/// </summary>
/// <param name="builder">The <see cref="IRequestDispatcherBuilder"/> used to configure the request dispatcher.</param>
/// <param name="type">The type to be added to the request dispatcher configuration.</param>
/// <param name="lifetime">The lifetime scope of the type in the service container.</param>
/// <returns>The <see cref="IRequestDispatcherBuilder"/> instance for further configuration.</returns>
public static IRequestDispatcherBuilder Add(this IRequestDispatcherBuilder builder, Type type, ServiceLifetime lifetime)
{
ArgumentNullException.ThrowIfNull(builder);
ValidateNoNestedRequestHandlers([type]);
builder.Services.AddOptions<RequestDispatcherOptions>()
.Configure(options => options.Inspect([type]));
builder.Services.Add(new ServiceDescriptor(type, type, lifetime));
return builder;
}
/// <summary>
/// Adds the specified collection of types to the request dispatcher configuration for inspection.
/// </summary>
/// <param name="builder">The <see cref="IRequestDispatcherBuilder"/> to configure.</param>
/// <param name="type">The collection of types to be added to the request dispatcher configuration.</param>
/// <returns>The <see cref="IRequestDispatcherBuilder"/> instance for further configuration.</returns>
public static IRequestDispatcherBuilder Add(this IRequestDispatcherBuilder builder, IEnumerable<Type> type)
{
ArgumentNullException.ThrowIfNull(builder);
var types = ValidateNoNestedRequestHandlers([.. type]);
builder.Services.AddOptions<RequestDispatcherOptions>()
.Configure(options => options.Inspect(types));
return builder;
}
/// <summary>
/// Adds the specified collection of types to the request dispatcher configuration for inspection.
/// This also adds the specified collection of types to the service collection with the specified lifetime,
/// allowing it to be resolved as a dependency in request handlers and behaviors.
/// </summary>
/// <param name="builder">The <see cref="IRequestDispatcherBuilder"/> to configure.</param>
/// <param name="type">The collection of types to be added to the request dispatcher configuration.</param>
/// <param name="lifetime">The lifetime scope of the types in the service container.</param>
/// <returns>The <see cref="IRequestDispatcherBuilder"/> instance for further configuration.</returns>
public static IRequestDispatcherBuilder Add(this IRequestDispatcherBuilder builder, IEnumerable<Type> type, ServiceLifetime lifetime)
{
ArgumentNullException.ThrowIfNull(builder);
var types = ValidateNoNestedRequestHandlers([.. type]);
builder.Services.AddOptions<RequestDispatcherOptions>()
.Configure(options => options.Inspect(types));
builder.Services.Add(types.Select(export => new ServiceDescriptor(export, export, lifetime)));
return builder;
}
private static IReadOnlyCollection<Type> ValidateNoNestedRequestHandlers(IReadOnlyCollection<Type> types, [CallerMemberName] string? invoker = null)
{
var nestedHandlers = types
.Where(type => type is { IsClass: true, IsAbstract: false, IsNested: true })
.Where(IsRequestHandlerImplementationType)
.Select(type => type.FullName ?? type.Name)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
if (nestedHandlers.Length > 0)
{
throw new InvalidOperationException($"Nested request handlers are not supported by {invoker}: {string.Join(", ", nestedHandlers)}");
}
return types;
}
private static bool IsRequestHandlerImplementationType(Type type)
{
return type.GetInterfaces().Any(IsRequestHandlerType);
}
}

View file

@ -0,0 +1,207 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Collections;
using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Dispatcher;
internal sealed class RequestDispatcherOptions
{
private readonly List<Type> _search = [];
private readonly Lazy<TypeIndex> _behaviorsTypeIndex;
private readonly Lazy<TypeIndex> _handlersTypeIndex;
public RequestDispatcherOptions()
{
_behaviorsTypeIndex = new Lazy<TypeIndex>(() => new BehaviorTypeIndex(_search.Distinct()));
_handlersTypeIndex = new Lazy<TypeIndex>(() => new HandlerTypeIndex(_search.Distinct()));
}
public void Inspect(IEnumerable<Type> assembly)
{
if (_behaviorsTypeIndex.IsValueCreated || _handlersTypeIndex.IsValueCreated)
{
throw new InvalidOperationException("The type index has already been created. Cannot inspect new assemblies.");
}
_search.AddRange(assembly);
}
public IEnumerable<T> GetRequestBehaviors<T>(IServiceProvider services)
{
return _behaviorsTypeIndex.Value.Resolve<T>(services);
}
public IEnumerable<T> GetRequestHandlers<T>(IServiceProvider services)
{
return _handlersTypeIndex.Value.Resolve<T>(services);
}
private abstract class TypeIndex
{
private readonly ConcurrentDictionary<Type, Func<IServiceProvider, IEnumerable>> _cache = new();
protected readonly Dictionary<Type, List<Type>> _closedTypeInfo = [];
protected readonly List<Type> _openTypeInfo = [];
protected TypeIndex(IEnumerable<Type> collection, Func<Type, bool> predicate)
{
foreach (var type in collection)
{
if (type.IsGenericTypeDefinition)
{
if (type.GetInterfaces().Any(predicate))
{
_openTypeInfo.Add(type);
}
}
else
{
foreach (var @interface in type.GetInterfaces().Where(predicate))
{
(_closedTypeInfo.TryGetValue(@interface, out var list) ? list : _closedTypeInfo[@interface] = []).Add(type);
}
}
}
}
public IEnumerable<T> Resolve<T>(IServiceProvider services)
{
return (IEnumerable<T>)_cache.GetOrAdd(typeof(T), CreateResolverFactory<T>)(services);
}
protected abstract IReadOnlyList<Type> IsAssignableTo(Type type);
private Func<IServiceProvider, IEnumerable<T>> CreateResolverFactory<T>(Type @interface)
{
var list = IsAssignableTo(@interface);
return ResolverFactory;
IEnumerable<T> ResolverFactory(IServiceProvider services)
{
foreach (var type in list)
{
yield return (T)ActivatorUtilities.GetServiceOrCreateInstance(services, type);
}
}
}
}
internal static bool IsRequestHandlerType(Type type)
{
return type.IsGenericType &&
(type.GetGenericTypeDefinition() == typeof(IScalarRequestHandler<,>) ||
type.GetGenericTypeDefinition() == typeof(IStreamRequestHandler<,>));
}
private sealed class HandlerTypeIndex(IEnumerable<Type> collection)
: TypeIndex(collection, IsRequestHandlerType)
{
protected override IReadOnlyList<Type> IsAssignableTo(Type @interface)
{
var result = new List<Type>();
if (_closedTypeInfo.TryGetValue(@interface, out var list))
{
result.AddRange(list);
}
var requestType = @interface.GetGenericArguments()[0];
_ = @interface.GetGenericArguments()[1];
foreach (var type in _openTypeInfo)
{
try
{
// open type case one: Handler<TRequest> : IRequestHandler<TRequest, TOutput>
// We try to close it with the request type.
var impl = type.MakeGenericType(requestType);
if (impl.IsAssignableTo(@interface))
{
result.Add(impl);
}
}
catch (ArgumentException)
{
}
try
{
// open type case two: Handler<T> : IRequestHandler<WrapperRequest<T>, TOutput>
// If the request is generic, we try to close the handler with the request's generic arguments.
if (requestType.IsGenericType)
{
var impl = type.MakeGenericType(requestType.GetGenericArguments());
if (impl.IsAssignableTo(@interface))
{
result.Add(impl);
}
}
}
catch (ArgumentException)
{
}
}
return result;
}
}
internal static bool IsRequestBehaviorType(Type type)
{
return type.IsGenericType &&
(type.GetGenericTypeDefinition() == typeof(IScalarRequestBehavior<,>) ||
type.GetGenericTypeDefinition() == typeof(IStreamRequestBehavior<,>));
}
private sealed class BehaviorTypeIndex(IEnumerable<Type> collection)
: TypeIndex(collection, IsRequestBehaviorType)
{
protected override IReadOnlyList<Type> IsAssignableTo(Type @interface)
{
var result = new List<Type>();
if (_closedTypeInfo.TryGetValue(@interface, out var list))
{
result.AddRange(list);
}
var requestType = @interface.GetGenericArguments()[0];
var responseType = @interface.GetGenericArguments()[1];
foreach (var behaviour in _openTypeInfo)
{
try
{
// open type case one: Behaviour<TRequest, TResponse> : IRequestBehaviour<TRequest, TResponse>
var impl = behaviour.MakeGenericType(requestType, responseType);
if (impl.IsAssignableTo(@interface))
{
result.Add(impl);
}
}
catch (ArgumentException)
{
}
try
{
// open type case two: Behaviour<T> : IRequestBehaviour<WrapperRequest<T>, TResponse>
var impl = behaviour.MakeGenericType(requestType.GetGenericArguments());
if (impl.IsAssignableTo(@interface))
{
result.Add(impl);
}
}
catch (ArgumentException)
{
}
}
return result;
}
}
}

View file

@ -0,0 +1,47 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Geekeey.Request.Dispatcher;
internal abstract class ScalarRequestInvoker
{
public abstract Task<object?> HandleAsync(object request, IServiceProvider serviceProvider, CancellationToken cancellationToken);
}
internal abstract class ScalarRequestInvoker<TResponse> : ScalarRequestInvoker
{
public abstract Task<TResponse> HandleAsync(IScalarRequest<TResponse> request, IServiceProvider serviceProvider, CancellationToken cancellationToken);
}
internal sealed class ScalarRequestInvoker<TRequest, TResponse> : ScalarRequestInvoker<TResponse>
where TRequest : IScalarRequest<TResponse>
{
public override async Task<object?> HandleAsync(object request, IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
return await HandleAsync((IScalarRequest<TResponse>)request, serviceProvider, cancellationToken).ConfigureAwait(false);
}
public override Task<TResponse> HandleAsync(IScalarRequest<TResponse> request, IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var options = serviceProvider.GetRequiredService<IOptions<RequestDispatcherOptions>>().Value;
var pipeline = options.GetRequestBehaviors<IScalarRequestBehavior<TRequest, TResponse>>(serviceProvider)
.Reverse()
.Aggregate((ScalarHandlerDelegate<TResponse>)Head, Chain);
return pipeline(request, cancellationToken);
static ScalarHandlerDelegate<TResponse> Chain(ScalarHandlerDelegate<TResponse> next, IScalarRequestBehavior<TRequest, TResponse> filter)
{
return (req, ct) => filter.HandleAsync((TRequest)req, next, ct);
}
Task<TResponse> Head(IScalarRequest<TResponse> r, CancellationToken ct)
{
return options.GetRequestHandlers<IScalarRequestHandler<TRequest, TResponse>>(serviceProvider).First().HandleAsync((TRequest)r, ct);
}
}
}

View file

@ -0,0 +1,50 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Provides extension methods for configuring and registering request dispatchers and their dependencies.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds the request dispatcher services to the specified <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The service collection to which the request dispatcher services will be added.</param>
/// <returns>An instance of <see cref="IRequestDispatcherBuilder"/> to configure the request dispatcher.</returns>
public static IRequestDispatcherBuilder AddRequestDispatcher(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.AddOptions<RequestDispatcherOptions>();
services.TryAddTransient<IRequestDispatcher, RequestDispatcher>();
return new RequestDispatcherBuilder(services);
}
/// <summary>
/// Adds the request dispatcher services to the specified <see cref="IServiceCollection"/>
/// and configures it using the provided <see cref="Action{T}"/>.
/// </summary>
/// <param name="services">The service collection to which the request dispatcher services will be added.</param>
/// <param name="configure">A delegate to configure the request dispatcher builder.</param>
/// <returns>The service collection with the request dispatcher services added.</returns>
public static IServiceCollection AddRequestDispatcher(this IServiceCollection services, Action<IRequestDispatcherBuilder> configure)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);
configure(services.AddRequestDispatcher());
return services;
}
private sealed class RequestDispatcherBuilder(IServiceCollection services) : IRequestDispatcherBuilder
{
public IServiceCollection Services { get; } = services;
}
}

View file

@ -0,0 +1,52 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Geekeey.Request.Dispatcher;
internal abstract class StreamRequestInvoker
{
public abstract IAsyncEnumerable<object?> HandleAsync(object request, IServiceProvider serviceProvider, CancellationToken cancellationToken);
}
internal abstract class StreamRequestInvoker<TResponse> : StreamRequestInvoker
{
public abstract IAsyncEnumerable<TResponse> HandleAsync(IStreamRequest<TResponse> request, IServiceProvider serviceProvider, CancellationToken cancellationToken);
}
internal sealed class StreamRequestInvoker<TRequest, TResponse> : StreamRequestInvoker<TResponse>
where TRequest : IStreamRequest<TResponse>
{
public override async IAsyncEnumerable<object?> HandleAsync(object request, IServiceProvider serviceProvider, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var item in HandleAsync((IStreamRequest<TResponse>)request, serviceProvider, cancellationToken))
{
yield return item;
}
}
public override IAsyncEnumerable<TResponse> HandleAsync(IStreamRequest<TResponse> request, IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var options = serviceProvider.GetRequiredService<IOptions<RequestDispatcherOptions>>().Value;
var pipeline = options.GetRequestBehaviors<IStreamRequestBehavior<TRequest, TResponse>>(serviceProvider)
.Reverse()
.Aggregate((StreamHandlerDelegate<TResponse>)Head, Chain);
return pipeline(request, cancellationToken);
static StreamHandlerDelegate<TResponse> Chain(StreamHandlerDelegate<TResponse> next, IStreamRequestBehavior<TRequest, TResponse> filter)
{
return (req, ct) => filter.HandleAsync((TRequest)req, next, ct);
}
IAsyncEnumerable<TResponse> Head(IStreamRequest<TResponse> r, CancellationToken ct)
{
return options.GetRequestHandlers<IStreamRequestHandler<TRequest, TResponse>>(serviceProvider).First().HandleAsync((TRequest)r, ct);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,68 @@
## Features
- **Simple interfaces:** no complex constraints, just marker interfaces that work.
- **Minmal dependencies:** only depends on `Microsoft.Extensions.DependencyInjection.Abstractions` and the
`Microsoft.Extensions.Options` package.
## Getting Started
### Install the NuGet package:
```shell
dotnet add package Geekeey.Request.Dispatcher
```
You may need to add our NuGet feed to your nuget.config this can be done by running the following command:
```shell
dotnet nuget add source -n geekeey https://code.geekeey.de/api/packages/geekeey/nuget/index.json
```
### Usage
```csharp
public static Task<int> Main()
{
var collection = new ServiceCollection();
collection.AddRequestDispatcher(builder => builder
.SearchHandlerInAssembly(typeof(ScalarHandler).Assembly)
.Add(typeof(ScalarBehavior)));
await using var provider = collection.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new ScalarRequest { Value = "Hello" };
var result = await dispatcher.DispatchAsync(request);
Console.WriteLine(result);
return 0;
}
public class ScalarRequest : IScalarRequest<string>
{
public string Value { get; set; } = string.Empty;
}
public class ScalarHandler : IScalarRequestHandler<ScalarRequest, string>
{
public Task<string> HandleAsync(ScalarRequest request, CancellationToken cancellationToken)
{
return Task.FromResult($"{request.Value} World");
}
}
public class ScalarBehavior : IScalarRequestBehavior<ScalarRequest, string>
{
public async Task<string> HandleAsync(ScalarRequest request, ScalarHandlerDelegate<string> next, CancellationToken cancellationToken)
{
Console.WriteLine("Before");
var result = await next(request, cancellationToken);
Console.WriteLine("After");
return result;
}
}
```
## Behaviour of the Handlers
Handlers are resolved from either the DI container or are created on the fly but can receive arguments from the DI
container when being constructed. The same also applied for the request pipeline behaviours.