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