request/src/request.dispatcher/RequestDispatcherBuilderExtensions.cs

174 lines
7.8 KiB
C#
Raw Normal View History

// 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);
}
}