feat: create request projects for basic CQRS
This commit is contained in:
commit
d614788e06
190 changed files with 12236 additions and 0 deletions
36
src/request.dispatcher/Geekeey.Request.Dispatcher.csproj
Normal file
36
src/request.dispatcher/Geekeey.Request.Dispatcher.csproj
Normal 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>
|
||||
28
src/request.dispatcher/IRequestDispatcher.cs
Normal file
28
src/request.dispatcher/IRequestDispatcher.cs
Normal 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);
|
||||
}
|
||||
19
src/request.dispatcher/IRequestDispatcherBuilder.cs
Normal file
19
src/request.dispatcher/IRequestDispatcherBuilder.cs
Normal 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; }
|
||||
}
|
||||
35
src/request.dispatcher/IScalarPipelineBehavior.cs
Normal file
35
src/request.dispatcher/IScalarPipelineBehavior.cs
Normal 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);
|
||||
14
src/request.dispatcher/IScalarRequest.cs
Normal file
14
src/request.dispatcher/IScalarRequest.cs
Normal 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>
|
||||
{
|
||||
}
|
||||
20
src/request.dispatcher/IScalarRequestHandler.cs
Normal file
20
src/request.dispatcher/IScalarRequestHandler.cs
Normal 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);
|
||||
}
|
||||
35
src/request.dispatcher/IStreamPipelineBehavior.cs
Normal file
35
src/request.dispatcher/IStreamPipelineBehavior.cs
Normal 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);
|
||||
14
src/request.dispatcher/IStreamRequest.cs
Normal file
14
src/request.dispatcher/IStreamRequest.cs
Normal 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>
|
||||
{
|
||||
}
|
||||
21
src/request.dispatcher/IStreamRequestHandler.cs
Normal file
21
src/request.dispatcher/IStreamRequestHandler.cs
Normal 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);
|
||||
}
|
||||
48
src/request.dispatcher/RequestDispatcher.cs
Normal file
48
src/request.dispatcher/RequestDispatcher.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
174
src/request.dispatcher/RequestDispatcherBuilderExtensions.cs
Normal file
174
src/request.dispatcher/RequestDispatcherBuilderExtensions.cs
Normal 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<Name>(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<Name>(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);
|
||||
}
|
||||
}
|
||||
207
src/request.dispatcher/RequestDispatcherOptions.cs
Normal file
207
src/request.dispatcher/RequestDispatcherOptions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/request.dispatcher/ScalarRequestInvoker.cs
Normal file
47
src/request.dispatcher/ScalarRequestInvoker.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/request.dispatcher/ServiceCollectionExtensions.cs
Normal file
50
src/request.dispatcher/ServiceCollectionExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
52
src/request.dispatcher/StreamRequestInvoker.cs
Normal file
52
src/request.dispatcher/StreamRequestInvoker.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/request.dispatcher/package-icon.png
Normal file
BIN
src/request.dispatcher/package-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
68
src/request.dispatcher/package-readme.md
Normal file
68
src/request.dispatcher/package-readme.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue