// 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; /// /// Provides extension methods for configuring /// with additional capabilities such as searching and registering request handlers in assemblies or adding types directly. /// public static class RequestDispatcherBuilderExtensions { /// /// Searches for request handler types within the specified assembly and adds them to the request dispatcher /// configuration. /// /// /// Prefer the generated Add<Name>(builder) extensions for assemblies that directly reference /// Geekeey.Request.Dispatcher. This runtime scanning API remains supported, but nested request handlers are rejected /// during registration and generated registration should not be mixed with /// for the same assembly. /// /// The to configure. /// The assembly to search for request handler types. /// The instance for further configuration. 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; } /// /// Searches for request handler types within the specified assembly and adds them to the request dispatcher /// configuration with the given service lifetime. /// /// /// Prefer the generated Add<Name>(builder, lifetime) extensions for assemblies that directly reference /// Geekeey.Request.Dispatcher. This runtime scanning API remains supported, but nested request handlers are rejected /// during registration and generated registration should not be mixed with /// for the same assembly. /// /// The to configure. /// The assembly to search for request handler types. /// The lifetime with which the request handlers are registered in the dependency injection container. /// The instance for further configuration. 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; } /// /// Adds the specified type to the request dispatcher configuration for inspection. /// /// The to configure. /// The type to be added to the request dispatcher configuration. /// The instance for further configuration. public static IRequestDispatcherBuilder Add(this IRequestDispatcherBuilder builder, Type type) { ArgumentNullException.ThrowIfNull(builder); ValidateNoNestedRequestHandlers([type]); builder.Services.AddOptions() .Configure(options => options.Inspect([type])); return builder; } /// /// 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. /// /// The used to configure the request dispatcher. /// The type to be added to the request dispatcher configuration. /// The lifetime scope of the type in the service container. /// The instance for further configuration. public static IRequestDispatcherBuilder Add(this IRequestDispatcherBuilder builder, Type type, ServiceLifetime lifetime) { ArgumentNullException.ThrowIfNull(builder); ValidateNoNestedRequestHandlers([type]); builder.Services.AddOptions() .Configure(options => options.Inspect([type])); builder.Services.Add(new ServiceDescriptor(type, type, lifetime)); return builder; } /// /// Adds the specified collection of types to the request dispatcher configuration for inspection. /// /// The to configure. /// The collection of types to be added to the request dispatcher configuration. /// The instance for further configuration. public static IRequestDispatcherBuilder Add(this IRequestDispatcherBuilder builder, IEnumerable type) { ArgumentNullException.ThrowIfNull(builder); var types = ValidateNoNestedRequestHandlers([.. type]); builder.Services.AddOptions() .Configure(options => options.Inspect(types)); return builder; } /// /// 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. /// /// The to configure. /// The collection of types to be added to the request dispatcher configuration. /// The lifetime scope of the types in the service container. /// The instance for further configuration. public static IRequestDispatcherBuilder Add(this IRequestDispatcherBuilder builder, IEnumerable type, ServiceLifetime lifetime) { ArgumentNullException.ThrowIfNull(builder); var types = ValidateNoNestedRequestHandlers([.. type]); builder.Services.AddOptions() .Configure(options => options.Inspect(types)); builder.Services.Add(types.Select(export => new ServiceDescriptor(export, export, lifetime))); return builder; } private static IReadOnlyCollection ValidateNoNestedRequestHandlers(IReadOnlyCollection 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); } }