diff --git a/GenHTTP.slnx b/GenHTTP.slnx index d22941ea4..a1d3af5d7 100644 --- a/GenHTTP.slnx +++ b/GenHTTP.slnx @@ -1,19 +1,15 @@  - - - - @@ -23,6 +19,7 @@ + @@ -46,29 +43,23 @@ - - - - - - - - - - + + + + \ No newline at end of file diff --git a/Modules/Controllers/Provider/ControllerBuilder.cs b/Modules/Controllers/Provider/ControllerBuilder.cs index 7ede317e8..471afdcd9 100644 --- a/Modules/Controllers/Provider/ControllerBuilder.cs +++ b/Modules/Controllers/Provider/ControllerBuilder.cs @@ -1,6 +1,9 @@ using System.Diagnostics.CodeAnalysis; + using GenHTTP.Api.Content; using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Conversion; using GenHTTP.Modules.Conversion.Formatters; using GenHTTP.Modules.Conversion.Serializers; @@ -9,16 +12,18 @@ namespace GenHTTP.Modules.Controllers.Provider; -public sealed class ControllerBuilder : IHandlerBuilder +public sealed class ControllerBuilder : IHandlerBuilder, IRegistryBuilder { private readonly List _Concerns = []; + private Type? _Type; + + private Func>? _InstanceProvider; + private IBuilder? _Formatters; private IBuilder? _Injection; - private object? _Instance; - private IBuilder? _Serializers; #region Functionality @@ -42,14 +47,25 @@ public ControllerBuilder Formatters(IBuilder registry) } public ControllerBuilder Type<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : new() + => Instance(new T()); + + public ControllerBuilder Type([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) { - _Instance = new T(); + _Type = type; return this; } public ControllerBuilder Instance(object instance) { - _Instance = instance; + _Type = instance.GetType(); + _InstanceProvider = (_) => ValueTask.FromResult(instance); + + return this; + } + + public ControllerBuilder InstanceProvider(Func> provider) + { + _InstanceProvider = provider; return this; } @@ -67,11 +83,13 @@ public IHandler Build() var formatters = (_Formatters ?? Formatting.Default()).Build(); - var instance = _Instance ?? throw new BuilderMissingPropertyException("Instance or Type"); + var instanceProvider = _InstanceProvider ?? throw new BuilderMissingPropertyException("Instance provider has not been set"); + + var type = _Type ?? throw new BuilderMissingPropertyException("Type has not been set"); var extensions = new MethodRegistry(serializers, injectors, formatters); - return Concerns.Chain(_Concerns, new ControllerHandler(instance, extensions)); + return Concerns.Chain(_Concerns, new ControllerHandler(type, instanceProvider, extensions)); } #endregion diff --git a/Modules/Controllers/Provider/ControllerHandler.cs b/Modules/Controllers/Provider/ControllerHandler.cs index b463f2be2..5b100b26f 100644 --- a/Modules/Controllers/Provider/ControllerHandler.cs +++ b/Modules/Controllers/Provider/ControllerHandler.cs @@ -13,59 +13,74 @@ public sealed partial class ControllerHandler : IHandler, IServiceMethodProvider { private static readonly Regex HyphenMatcher = CreateHyphenMatcher(); + private MethodCollection? _Methods; + #region Get-/Setters - public MethodCollection Methods { get; } + private Type Type { get; } - private ResponseProvider ResponseProvider { get; } + private Func> InstanceProvider { get; } private MethodRegistry Registry { get; } - private object Instance { get; } - #endregion #region Initialization - public ControllerHandler(object instance, MethodRegistry registry) + public ControllerHandler(Type type, Func> instanceProvider, MethodRegistry registry) { + Type = type; + InstanceProvider = instanceProvider; Registry = registry; + } + + #endregion - Instance = instance; + #region Functionality - ResponseProvider = new ResponseProvider(registry); + public ValueTask PrepareAsync() => ValueTask.CompletedTask; - Methods = new MethodCollection(AnalyzeMethods(instance.GetType(), registry)); - } + public async ValueTask HandleAsync(IRequest request) => await (await GetMethodsAsync(request)).HandleAsync(request); - private IEnumerable AnalyzeMethods(Type type, MethodRegistry registry) + public async ValueTask GetMethodsAsync(IRequest request) { - foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + if (_Methods != null) return _Methods; + + var found = new List(); + + + foreach (var method in Type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) { var annotation = method.GetCustomAttribute(true) ?? new MethodAttribute(); var arguments = FindPathArguments(method); - var operation = CreateOperation(method, arguments); + var operation = CreateOperation(request, method, arguments, Registry); - yield return new MethodHandler(operation, Instance, annotation, registry); + found.Add(new MethodHandler(operation, InstanceProvider, annotation, Registry)); } + + var result = new MethodCollection(found); + + await result.PrepareAsync(); + + return _Methods = result; } - private Operation CreateOperation(MethodInfo method, List arguments) + private static Operation CreateOperation(IRequest request, MethodInfo method, List arguments, MethodRegistry registry) { var pathArguments = string.Join('/', arguments.Select(a => $":{a}")); if (method.Name == "Index") { - return OperationBuilder.Create(pathArguments.Length > 0 ? $"/{pathArguments}/" : null, method, Registry, true); + return OperationBuilder.Create(request, pathArguments.Length > 0 ? $"/{pathArguments}/" : null, method, registry, true); } var name = HypenCase(method.Name); var path = $"/{name}"; - return OperationBuilder.Create(pathArguments.Length > 0 ? $"{path}/{pathArguments}/" : $"{path}/", method, Registry, true); + return OperationBuilder.Create(request, pathArguments.Length > 0 ? $"{path}/{pathArguments}/" : $"{path}/", method, registry, true); } private static List FindPathArguments(MethodInfo method) @@ -93,12 +108,4 @@ private static List FindPathArguments(MethodInfo method) #endregion - #region Functionality - - public ValueTask PrepareAsync() => Methods.PrepareAsync(); - - public ValueTask HandleAsync(IRequest request) => Methods.HandleAsync(request); - - #endregion - } diff --git a/Modules/DependencyInjection/Basics/ConcernIntegration.cs b/Modules/DependencyInjection/Basics/ConcernIntegration.cs new file mode 100644 index 000000000..1c96ff8f4 --- /dev/null +++ b/Modules/DependencyInjection/Basics/ConcernIntegration.cs @@ -0,0 +1,38 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.DependencyInjection.Infrastructure; + +namespace GenHTTP.Modules.DependencyInjection.Basics; + +internal class ConcernIntegration : IConcern where T : class, IDependentConcern +{ + + #region Getters/Setters + + public IHandler Content { get; } + + #endregion + + #region Initialization + + internal ConcernIntegration(IHandler content) + { + Content = content; + } + + #endregion + + #region Functionality + + public ValueTask PrepareAsync() => Content.PrepareAsync(); + + public async ValueTask HandleAsync(IRequest request) + { + var instance = await InstanceProvider.ProvideAsync(request); + + return await instance.HandleAsync(Content, request); + } + + #endregion + +} diff --git a/Modules/DependencyInjection/Basics/ConcernIntegrationBuilder.cs b/Modules/DependencyInjection/Basics/ConcernIntegrationBuilder.cs new file mode 100644 index 000000000..2947cbbcb --- /dev/null +++ b/Modules/DependencyInjection/Basics/ConcernIntegrationBuilder.cs @@ -0,0 +1,10 @@ +using GenHTTP.Api.Content; + +namespace GenHTTP.Modules.DependencyInjection.Basics; + +internal class ConcernIntegrationBuilder : IConcernBuilder where T : class, IDependentConcern +{ + + public IConcern Build(IHandler content) => new ConcernIntegration(content); + +} diff --git a/Modules/DependencyInjection/Basics/HandlerIntegration.cs b/Modules/DependencyInjection/Basics/HandlerIntegration.cs new file mode 100644 index 000000000..df3e87104 --- /dev/null +++ b/Modules/DependencyInjection/Basics/HandlerIntegration.cs @@ -0,0 +1,20 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.DependencyInjection.Infrastructure; + +namespace GenHTTP.Modules.DependencyInjection.Basics; + +internal class HandlerIntegration : IHandler where T: class, IDependentHandler +{ + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public async ValueTask HandleAsync(IRequest request) + { + var instance = await InstanceProvider.ProvideAsync(request); + + return await instance.HandleAsync(request); + } + +} diff --git a/Modules/DependencyInjection/Basics/HandlerIntegrationBuilder.cs b/Modules/DependencyInjection/Basics/HandlerIntegrationBuilder.cs new file mode 100644 index 000000000..172402db8 --- /dev/null +++ b/Modules/DependencyInjection/Basics/HandlerIntegrationBuilder.cs @@ -0,0 +1,10 @@ +using GenHTTP.Api.Content; + +namespace GenHTTP.Modules.DependencyInjection.Basics; + +internal class HandlerIntegrationBuilder : IHandlerBuilder where T : class, IDependentHandler +{ + + public IHandler Build() => new HandlerIntegration(); + +} diff --git a/Modules/DependencyInjection/Dependent.cs b/Modules/DependencyInjection/Dependent.cs new file mode 100644 index 000000000..fac38d366 --- /dev/null +++ b/Modules/DependencyInjection/Dependent.cs @@ -0,0 +1,25 @@ +using GenHTTP.Api.Content; + +using GenHTTP.Modules.DependencyInjection.Basics; + +namespace GenHTTP.Modules.DependencyInjection; + +/// +/// Allows to enable dependency injection for concerns and handlers. +/// +public static class Dependent +{ + + /// + /// Allows the given class to be used as a regular concern with dependency injection enabled. + /// + /// The class implementing the concern + public static IConcernBuilder Concern() where T : class, IDependentConcern => new ConcernIntegrationBuilder(); + + /// + /// Allows the given class to be used as a regular handler with dependency injection enabled. + /// + /// The class implementing the handler + public static IHandlerBuilder Handler() where T : class, IDependentHandler => new HandlerIntegrationBuilder(); + +} diff --git a/Modules/DependencyInjection/DependentController.cs b/Modules/DependencyInjection/DependentController.cs new file mode 100644 index 000000000..5dd0c1800 --- /dev/null +++ b/Modules/DependencyInjection/DependentController.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; + +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Modules.Controllers.Provider; +using GenHTTP.Modules.Conversion.Formatters; +using GenHTTP.Modules.Conversion.Serializers; +using GenHTTP.Modules.DependencyInjection.Infrastructure; +using GenHTTP.Modules.Layouting.Provider; +using GenHTTP.Modules.Reflection.Injectors; + +namespace GenHTTP.Modules.DependencyInjection; + +public static class DependentController +{ + + /// + /// Adds the given controller to the layout with dependency injection enabled. + /// + /// The layout to add the controller to + /// The path to register the controller at + /// Additional parameter injections to be used by the controller + /// Additional serializers to be used by the controller + /// Additional formatters to be used by the controller + /// The class implementing the controller + /// The given layout with the specified controller attached + public static LayoutBuilder AddDependentController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder layout, string path, InjectionRegistryBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) where T : class + { + var builder = new ControllerBuilder(); + + builder.Type(typeof(T)); + builder.InstanceProvider(async (r) => await InstanceProvider.ProvideAsync(r)); + + builder.Configure(injectors, serializers, formatters); + + return layout.Add(path, builder); + } + +} diff --git a/Modules/DependencyInjection/DependentInline.cs b/Modules/DependencyInjection/DependentInline.cs new file mode 100644 index 000000000..07282a48f --- /dev/null +++ b/Modules/DependencyInjection/DependentInline.cs @@ -0,0 +1,25 @@ +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Modules.Conversion.Formatters; +using GenHTTP.Modules.Conversion.Serializers; +using GenHTTP.Modules.DependencyInjection.Infrastructure; +using GenHTTP.Modules.Functional; +using GenHTTP.Modules.Functional.Provider; +using GenHTTP.Modules.Reflection.Injectors; + +namespace GenHTTP.Modules.DependencyInjection; + +public static class DependentInline +{ + + /// + /// Creates a new line handler with dependency injection enabled on the declared methods. + /// + /// Additional parameter injections to be used by the inline handler + /// Additional serializers to be used by the inline handler + /// Additional formatters to be used by the inline handler + /// The newly created inline handler + public static InlineBuilder Create(InjectionRegistryBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) + => Inline.Create().Configure(injectors, serializers, formatters); + +} diff --git a/Modules/DependencyInjection/DependentWebservice.cs b/Modules/DependencyInjection/DependentWebservice.cs new file mode 100644 index 000000000..94e058a5d --- /dev/null +++ b/Modules/DependencyInjection/DependentWebservice.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; + +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Modules.Conversion.Formatters; +using GenHTTP.Modules.Conversion.Serializers; +using GenHTTP.Modules.DependencyInjection.Infrastructure; +using GenHTTP.Modules.Layouting.Provider; +using GenHTTP.Modules.Reflection.Injectors; +using GenHTTP.Modules.Webservices.Provider; + +namespace GenHTTP.Modules.DependencyInjection; + +public static class DependentWebservice +{ + + /// + /// Adds the given webservice to the layout with dependency injection enabled. + /// + /// The layout to add the webservice to + /// The path to register the webservice at + /// Additional parameter injections to be used by the webservice + /// Additional serializers to be used by the webservice + /// Additional formatters to be used by the webservice + /// The class implementing the webservice + /// The given layout with the specified webservice attached + public static LayoutBuilder AddDependentService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder layout, string path, InjectionRegistryBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) where T : class + { + var builder = new ServiceResourceBuilder(); + + builder.Type(typeof(T)); + builder.InstanceProvider(async (r) => await InstanceProvider.ProvideAsync(r)); + + builder.Configure(injectors, serializers, formatters); + + return layout.Add(path, builder); + } + +} diff --git a/Modules/DependencyInjection/GenHTTP.Modules.DependencyInjection.csproj b/Modules/DependencyInjection/GenHTTP.Modules.DependencyInjection.csproj new file mode 100644 index 000000000..34d4aa418 --- /dev/null +++ b/Modules/DependencyInjection/GenHTTP.Modules.DependencyInjection.csproj @@ -0,0 +1,34 @@ + + + + + Enables dependency injection for all framework services. + HTTP Webserver C# Module DI Dependency Injection + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/DependencyInjection/IDependentConcern.cs b/Modules/DependencyInjection/IDependentConcern.cs new file mode 100644 index 000000000..14b2bcc10 --- /dev/null +++ b/Modules/DependencyInjection/IDependentConcern.cs @@ -0,0 +1,30 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Modules.DependencyInjection; + +/// +/// Represents a dependency injection enabled concern that can be used to +/// serve requests. +/// +/// +/// In contrast to regular concerns there is no concern or content preparation as +/// the lifecycle of the handler is managed by the dependency injection +/// container. +/// +public interface IDependentConcern +{ + + /// + /// Handles the given request and returns a response, if applicable. + /// + /// + /// Not returning a response causes the server to respond with a not found + /// response code. + /// + /// The request to be handled + /// The (initialized) content to be served by the concern + /// The response to be sent to the requesting client + ValueTask HandleAsync(IHandler content, IRequest request); + +} diff --git a/Modules/DependencyInjection/IDependentHandler.cs b/Modules/DependencyInjection/IDependentHandler.cs new file mode 100644 index 000000000..fec2ccbac --- /dev/null +++ b/Modules/DependencyInjection/IDependentHandler.cs @@ -0,0 +1,28 @@ +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Modules.DependencyInjection; + +/// +/// Represents a dependency injection enabled handler that can be used to +/// serve requests. +/// +/// +/// In contrast to regular handlers there is no handler preparation as +/// the lifecycle of the handler is managed by the dependency injection +/// container. +/// +public interface IDependentHandler +{ + + /// + /// Handles the given request and returns a response, if applicable. + /// + /// + /// Not returning a response causes the server to respond with a not found + /// response code. + /// + /// The request to be handled + /// The response to be sent to the requesting client + ValueTask HandleAsync(IRequest request); + +} diff --git a/Modules/DependencyInjection/Infrastructure/DependencyInjector.cs b/Modules/DependencyInjection/Infrastructure/DependencyInjector.cs new file mode 100644 index 000000000..4c80bd97d --- /dev/null +++ b/Modules/DependencyInjection/Infrastructure/DependencyInjector.cs @@ -0,0 +1,34 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Reflection.Injectors; + +using Microsoft.Extensions.DependencyInjection; + +namespace GenHTTP.Modules.DependencyInjection.Infrastructure; + +public class DependencyInjector : IParameterInjector +{ + + public bool Supports(IRequest request, Type type) + { + var scope = request.GetServiceScope(); + + var supportService = scope.ServiceProvider.GetService(); + + if (supportService != null) + { + return supportService.IsService(type); + } + + return (scope.ServiceProvider.GetService(type) != null); + } + + public object? GetValue(IHandler handler, IRequest request, Type targetType) + { + var scope = request.GetServiceScope(); + + return scope.ServiceProvider.GetService(targetType); + } + +} diff --git a/Modules/DependencyInjection/Infrastructure/FrameworkConfiguration.cs b/Modules/DependencyInjection/Infrastructure/FrameworkConfiguration.cs new file mode 100644 index 000000000..bbaafb1de --- /dev/null +++ b/Modules/DependencyInjection/Infrastructure/FrameworkConfiguration.cs @@ -0,0 +1,33 @@ +using GenHTTP.Api.Infrastructure; +using GenHTTP.Modules.Conversion.Formatters; +using GenHTTP.Modules.Conversion.Serializers; +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Reflection.Injectors; + +namespace GenHTTP.Modules.DependencyInjection.Infrastructure; + +internal static class FrameworkConfiguration +{ + + internal static T Configure(this T builder, InjectionRegistryBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) where T : IRegistryBuilder + { + var injectionOverlay = injectors ?? Injection.Default(); + + injectionOverlay.Add(new DependencyInjector()); + + builder.Injectors(injectionOverlay); + + if (serializers != null) + { + builder.Serializers(serializers); + } + + if (formatters != null) + { + builder.Formatters(formatters); + } + + return builder; + } + +} diff --git a/Modules/DependencyInjection/Infrastructure/InjectionConcern.cs b/Modules/DependencyInjection/Infrastructure/InjectionConcern.cs new file mode 100644 index 000000000..93f16f635 --- /dev/null +++ b/Modules/DependencyInjection/Infrastructure/InjectionConcern.cs @@ -0,0 +1,43 @@ +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using Microsoft.Extensions.DependencyInjection; + +namespace GenHTTP.Modules.DependencyInjection.Infrastructure; + +internal class InjectionConcern : IConcern +{ + + #region Get-/Setters + + public IHandler Content { get; } + + internal IServiceProvider Services { get; } + + #endregion + + #region Initialization + + internal InjectionConcern(IHandler content, IServiceProvider services) + { + Content = content; + Services = services; + } + + #endregion + + #region Functionality + + public ValueTask PrepareAsync() => Content.PrepareAsync(); + + public ValueTask HandleAsync(IRequest request) + { + using var scope = Services.CreateScope(); + + request.Configure(Services, scope); + + return Content.HandleAsync(request); + } + + #endregion + +} diff --git a/Modules/DependencyInjection/Infrastructure/InjectionConcernBuilder.cs b/Modules/DependencyInjection/Infrastructure/InjectionConcernBuilder.cs new file mode 100644 index 000000000..4530ed3f5 --- /dev/null +++ b/Modules/DependencyInjection/Infrastructure/InjectionConcernBuilder.cs @@ -0,0 +1,20 @@ +using GenHTTP.Api.Content; + +namespace GenHTTP.Modules.DependencyInjection.Infrastructure; + +internal class InjectionConcernBuilder : IConcernBuilder +{ + private readonly IServiceProvider _Services; + + #region Initialization + + internal InjectionConcernBuilder(IServiceProvider services) + { + _Services = services; + } + + #endregion + + public IConcern Build(IHandler content) => new InjectionConcern(content, _Services); + +} diff --git a/Modules/DependencyInjection/Infrastructure/InstanceProvider.cs b/Modules/DependencyInjection/Infrastructure/InstanceProvider.cs new file mode 100644 index 000000000..105816e51 --- /dev/null +++ b/Modules/DependencyInjection/Infrastructure/InstanceProvider.cs @@ -0,0 +1,24 @@ +using GenHTTP.Api.Protocol; +using Microsoft.Extensions.DependencyInjection; + +namespace GenHTTP.Modules.DependencyInjection.Infrastructure; + +public static class InstanceProvider +{ + + public static ValueTask ProvideAsync(IRequest request) where T : class + { + var scope = request.GetServiceScope(); + + var instance = scope.ServiceProvider.GetService(typeof(T)) + ?? ActivatorUtilities.CreateInstance(scope.ServiceProvider); + + if (instance == null) + { + throw new InvalidOperationException($"Unable to resolve or construct instance of type '{typeof(T)}'"); + } + + return ValueTask.FromResult((T)instance); + } + +} diff --git a/Modules/DependencyInjection/Integration.cs b/Modules/DependencyInjection/Integration.cs new file mode 100644 index 000000000..d46bad06f --- /dev/null +++ b/Modules/DependencyInjection/Integration.cs @@ -0,0 +1,62 @@ +using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.DependencyInjection.Infrastructure; + +using Microsoft.Extensions.DependencyInjection; + +namespace GenHTTP.Modules.DependencyInjection; + +public static class Integration +{ + private const string ProviderVar = "__DI_SERVICE_PROVIDER"; + + private const string ScopeVar = "__DI_SERVICE_SCOPE"; + + /// + /// Uses the given provider to injected dependencies within the server instance. + /// + /// The host to add dependency injection to + /// The service collection to be used for injection + /// The updated host + public static IServerHost AddDependencyInjection(this IServerHost host, IServiceProvider services) => host.Add(new InjectionConcernBuilder(services)); + + internal static void Configure(this IRequest request, IServiceProvider provider, IServiceScope scope) + { + request.Properties[ProviderVar] = provider; + request.Properties[ScopeVar] = scope; + } + + /// + /// Retrieves the service provider used to resolve dependencies from the given request. + /// + /// The request to obtain the service provider from + /// The service provider retrieved from the request + /// Thrown if dependency injection is not enabled on the host + public static IServiceProvider GetServiceProvider(this IRequest request) + { + if (!request.Properties.TryGet(ScopeVar, out IServiceProvider? provider)) + { + throw new InvalidOperationException("Unable to retrieve service provider from the request. Ensure dependency injection has been configured."); + } + + return provider!; + } + + /// + /// Retrieves the service scope used to resolve dependencies from the given request. + /// + /// The request to obtain the service scope from + /// The service scope retrieved from the request + /// Thrown if dependency injection is not enabled on the host + public static IServiceScope GetServiceScope(this IRequest request) + { + if (!request.Properties.TryGet(ScopeVar, out IServiceScope? scope)) + { + throw new InvalidOperationException("Unable to retrieve service scope from the request. Ensure dependency injection has been configured."); + } + + return scope!; + } + +} diff --git a/Modules/Functional/Provider/InlineBuilder.cs b/Modules/Functional/Provider/InlineBuilder.cs index 7f1d1e8be..ed553d652 100644 --- a/Modules/Functional/Provider/InlineBuilder.cs +++ b/Modules/Functional/Provider/InlineBuilder.cs @@ -10,7 +10,7 @@ namespace GenHTTP.Modules.Functional.Provider; -public class InlineBuilder : IHandlerBuilder +public class InlineBuilder : IHandlerBuilder, IRegistryBuilder { private static readonly HashSet AllMethods = [..Enum.GetValues().Select(FlexibleRequestMethod.Get)]; @@ -174,7 +174,7 @@ public IHandler Build() var extensions = new MethodRegistry(serializers, injectors, formatters); - return Concerns.Chain(_Concerns, new InlineHandler( _Functions, extensions)); + return Concerns.Chain(_Concerns, new InlineHandler(_Functions, extensions)); } #endregion diff --git a/Modules/Functional/Provider/InlineHandler.cs b/Modules/Functional/Provider/InlineHandler.cs index eb0d99c7c..d6518c7d8 100644 --- a/Modules/Functional/Provider/InlineHandler.cs +++ b/Modules/Functional/Provider/InlineHandler.cs @@ -8,12 +8,13 @@ namespace GenHTTP.Modules.Functional.Provider; public class InlineHandler : IHandler, IServiceMethodProvider { + private MethodCollection? _Methods; #region Get-/Setters - public MethodCollection Methods { get; } + private List Functions { get; } - private ResponseProvider ResponseProvider { get; } + private MethodRegistry Registry { get; } #endregion @@ -21,32 +22,43 @@ public class InlineHandler : IHandler, IServiceMethodProvider public InlineHandler(List functions, MethodRegistry registry) { - ResponseProvider = new ResponseProvider(registry); - - Methods = new MethodCollection(AnalyzeMethods(functions, registry)); + Functions = functions; + Registry = registry; } - private static IEnumerable AnalyzeMethods(List functions, MethodRegistry registry) + #endregion + + #region Functionality + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; + + public async ValueTask HandleAsync(IRequest request) => await (await GetMethodsAsync(request)).HandleAsync(request); + + public async ValueTask GetMethodsAsync(IRequest request) { - foreach (var function in functions) + if (_Methods != null) return _Methods; + + var found = new List(); + + foreach (var function in Functions) { var method = function.Delegate.Method; - var operation = OperationBuilder.Create(function.Path, method, registry); + var operation = OperationBuilder.Create(request, function.Path, method, Registry); var target = function.Delegate.Target ?? throw new InvalidOperationException("Delegate target must not be null"); - yield return new MethodHandler(operation, target, function.Configuration, registry); - } - } + var instanceProvider = (IRequest _) => ValueTask.FromResult(target); - #endregion + found.Add(new MethodHandler(operation, instanceProvider, function.Configuration, Registry)); + } - #region Functionality + var result = new MethodCollection(found); - public ValueTask PrepareAsync() => Methods.PrepareAsync(); + await result.PrepareAsync(); - public ValueTask HandleAsync(IRequest request) => Methods.HandleAsync(request); + return _Methods = result; + } #endregion diff --git a/Modules/OpenApi/Discovery/ApiDiscoveryRegistry.cs b/Modules/OpenApi/Discovery/ApiDiscoveryRegistry.cs index 1ab1a71c7..08260884a 100644 --- a/Modules/OpenApi/Discovery/ApiDiscoveryRegistry.cs +++ b/Modules/OpenApi/Discovery/ApiDiscoveryRegistry.cs @@ -1,4 +1,6 @@ using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + using NSwag; namespace GenHTTP.Modules.OpenApi.Discovery; @@ -27,17 +29,18 @@ public ApiDiscoveryRegistry(List explorers) /// Iterates through the registered explorers to find a responsible one to analyze /// the given handler instance. /// + /// The currently executed request /// The handler to get analyzed /// The current stack of path segments that have already been analyzed, relative to the location of the OpenAPI concern /// The document to be adjusted and enriched /// The manager to generate JSON schemas with - public void Explore(IHandler handler, List path, OpenApiDocument document, SchemaManager schemata) + public async ValueTask ExploreAsync(IRequest request, IHandler handler, List path, OpenApiDocument document, SchemaManager schemata) { foreach (var explorer in Explorers) { if (explorer.CanExplore(handler)) { - explorer.Explore(handler, path, document, schemata, this); + await explorer.ExploreAsync(request, handler, path, document, schemata, this); break; } } diff --git a/Modules/OpenApi/Discovery/ConcernExplorer.cs b/Modules/OpenApi/Discovery/ConcernExplorer.cs index 84e2bcf91..639e4d8ce 100644 --- a/Modules/OpenApi/Discovery/ConcernExplorer.cs +++ b/Modules/OpenApi/Discovery/ConcernExplorer.cs @@ -1,4 +1,6 @@ using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + using NSwag; namespace GenHTTP.Modules.OpenApi.Discovery; @@ -8,11 +10,11 @@ public sealed class ConcernExplorer : IApiExplorer public bool CanExplore(IHandler handler) => handler is IConcern; - public void Explore(IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) + public async ValueTask ExploreAsync(IRequest request, IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) { if (handler is IConcern concern) { - registry.Explore(concern.Content, path, document, schemata); + await registry.ExploreAsync(request, concern.Content, path, document, schemata); } } diff --git a/Modules/OpenApi/Discovery/LayoutExplorer.cs b/Modules/OpenApi/Discovery/LayoutExplorer.cs index 6f14217a6..5c06e4f68 100644 --- a/Modules/OpenApi/Discovery/LayoutExplorer.cs +++ b/Modules/OpenApi/Discovery/LayoutExplorer.cs @@ -1,5 +1,8 @@ using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Layouting.Provider; + using NSwag; namespace GenHTTP.Modules.OpenApi.Discovery; @@ -9,18 +12,18 @@ public sealed class LayoutExplorer : IApiExplorer public bool CanExplore(IHandler handler) => handler is LayoutRouter; - public void Explore(IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) + public async ValueTask ExploreAsync(IRequest request, IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) { if (handler is LayoutRouter layout) { foreach (var root in layout.RootHandlers) { - registry.Explore(root, path, document, schemata); + await registry.ExploreAsync(request, root, path, document, schemata); } foreach (var (route, routeHandler) in layout.RoutedHandlers) { - registry.Explore(routeHandler, [..path, route], document, schemata); + await registry.ExploreAsync(request, routeHandler, [..path, route], document, schemata); } } } diff --git a/Modules/OpenApi/Discovery/MethodCollectionExplorer.cs b/Modules/OpenApi/Discovery/MethodCollectionExplorer.cs index 9b53ebb36..2937c0efb 100644 --- a/Modules/OpenApi/Discovery/MethodCollectionExplorer.cs +++ b/Modules/OpenApi/Discovery/MethodCollectionExplorer.cs @@ -1,5 +1,8 @@ using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Reflection; + using NSwag; namespace GenHTTP.Modules.OpenApi.Discovery; @@ -9,13 +12,13 @@ public sealed class MethodCollectionExplorer : IApiExplorer public bool CanExplore(IHandler handler) => handler is MethodCollection; - public void Explore(IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) + public async ValueTask ExploreAsync(IRequest request, IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) { if (handler is MethodCollection collection) { foreach (var method in collection.Methods) { - registry.Explore(method, path, document, schemata); + await registry.ExploreAsync(request, method, path, document, schemata); } } } diff --git a/Modules/OpenApi/Discovery/MethodHandlerExplorer.cs b/Modules/OpenApi/Discovery/MethodHandlerExplorer.cs index e6fa5026b..96e833a1d 100644 --- a/Modules/OpenApi/Discovery/MethodHandlerExplorer.cs +++ b/Modules/OpenApi/Discovery/MethodHandlerExplorer.cs @@ -12,7 +12,7 @@ public sealed class MethodHandlerExplorer : IApiExplorer public bool CanExplore(IHandler handler) => handler is MethodHandler; - public void Explore(IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) + public ValueTask ExploreAsync(IRequest request, IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) { if (handler is MethodHandler methodHandler) { @@ -123,6 +123,8 @@ public void Explore(IHandler handler, List path, OpenApiDocument documen pathItem.Add(method.RawMethod, operation); } } + + return ValueTask.CompletedTask; } private static OpenApiParameterKind MapArgumentType(OperationArgumentSource source) => source switch diff --git a/Modules/OpenApi/Discovery/ServiceExplorer.cs b/Modules/OpenApi/Discovery/ServiceExplorer.cs index 0205d0826..a1091b957 100644 --- a/Modules/OpenApi/Discovery/ServiceExplorer.cs +++ b/Modules/OpenApi/Discovery/ServiceExplorer.cs @@ -1,5 +1,8 @@ using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Reflection; + using NSwag; namespace GenHTTP.Modules.OpenApi.Discovery; @@ -9,11 +12,11 @@ public sealed class ServiceExplorer : IApiExplorer public bool CanExplore(IHandler handler) => handler is IServiceMethodProvider; - public void Explore(IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) + public async ValueTask ExploreAsync(IRequest request, IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) { if (handler is IServiceMethodProvider serviceProvider) { - registry.Explore(serviceProvider.Methods, path, document, schemata); + await registry.ExploreAsync(request, await serviceProvider.GetMethodsAsync(request), path, document, schemata); } } diff --git a/Modules/OpenApi/Handler/OpenApiConcern.cs b/Modules/OpenApi/Handler/OpenApiConcern.cs index c6c501e87..f38032b99 100644 --- a/Modules/OpenApi/Handler/OpenApiConcern.cs +++ b/Modules/OpenApi/Handler/OpenApiConcern.cs @@ -55,14 +55,14 @@ public OpenApiConcern(IHandler content, ApiDiscoveryRegistry discovery, bool ena { response = accept.ToLowerInvariant() switch { - "application/json" or "application/application/vnd.oai.openapi+json" => GetDocument(request, OpenApiFormat.Json), - "application/yaml" or "application/application/vnd.oai.openapi+yaml" => GetDocument(request, OpenApiFormat.Yaml), + "application/json" or "application/application/vnd.oai.openapi+json" => await GetDocumentAsync(request, OpenApiFormat.Json), + "application/yaml" or "application/application/vnd.oai.openapi+yaml" => await GetDocumentAsync(request, OpenApiFormat.Yaml), _ => throw new ProviderException(ResponseStatus.BadRequest, $"Generating API specifications of format '{accept}' is not supported") }; } else { - response = GetDocument(request, OpenApiFormat.Json); + response = await GetDocumentAsync(request, OpenApiFormat.Json); } response.Headers.Add("Vary", "Accept"); @@ -71,20 +71,20 @@ public OpenApiConcern(IHandler content, ApiDiscoveryRegistry discovery, bool ena } if (string.Compare(path, "openapi.json", StringComparison.OrdinalIgnoreCase) == 0) { - return GetDocument(request, OpenApiFormat.Json); + return await GetDocumentAsync(request, OpenApiFormat.Json); } if (string.Compare(path, "openapi.yaml", StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(path, "openapi.yml", StringComparison.OrdinalIgnoreCase) == 0) { - return GetDocument(request, OpenApiFormat.Yaml); + return await GetDocumentAsync(request, OpenApiFormat.Yaml); } } return await Content.HandleAsync(request); } - private IResponse GetDocument(IRequest request, OpenApiFormat format) + private async ValueTask GetDocumentAsync(IRequest request, OpenApiFormat format) { - var document = Discover(request, Discovery); + var document = await DiscoverAsync(request, Discovery); var content = new OpenApiContent(document, format); @@ -96,7 +96,7 @@ private IResponse GetDocument(IRequest request, OpenApiFormat format) .Build(); } - private ReturnDocument Discover(IRequest request, ApiDiscoveryRegistry registry) + private async ValueTask DiscoverAsync(IRequest request, ApiDiscoveryRegistry registry) { if (EnableCaching && _Cached != null) { @@ -119,7 +119,7 @@ private ReturnDocument Discover(IRequest request, ApiDiscoveryRegistry registry) }); } - registry.Explore(Content, [], document, schemata); + await registry.ExploreAsync(request, Content, [], document, schemata); PostProcessor.Invoke(request, document); diff --git a/Modules/OpenApi/IApiExplorer.cs b/Modules/OpenApi/IApiExplorer.cs index f5560c987..67657f7de 100644 --- a/Modules/OpenApi/IApiExplorer.cs +++ b/Modules/OpenApi/IApiExplorer.cs @@ -1,4 +1,5 @@ using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; using GenHTTP.Modules.OpenApi.Discovery; using NSwag; @@ -27,11 +28,12 @@ public interface IApiExplorer /// Analyzes the given handler and adds the endpoints defined by this handler to the resulting /// OpenAPI document. /// + /// The currently executed request /// The handler to be analyzed /// The current stack of path segments that have already been analyzed, relative to the location of the OpenAPI concern /// The document to be adjusted and enriched /// The manager to generate JSON schemas with /// The registry containing all active explorers which can be used to further analyze any child handler of the given handler instance - void Explore(IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry); + ValueTask ExploreAsync(IRequest request, IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry); } diff --git a/Modules/Reflection/IRegistryBuilder.cs b/Modules/Reflection/IRegistryBuilder.cs new file mode 100644 index 000000000..62ee3990e --- /dev/null +++ b/Modules/Reflection/IRegistryBuilder.cs @@ -0,0 +1,40 @@ +using GenHTTP.Api.Infrastructure; + +using GenHTTP.Modules.Conversion.Formatters; +using GenHTTP.Modules.Conversion.Serializers; +using GenHTTP.Modules.Reflection.Injectors; + +namespace GenHTTP.Modules.Reflection; + +/// +/// A protocol for builders that will internally create a +/// instance. +/// +/// The builder type to be returned +public interface IRegistryBuilder +{ + + /// + /// Configures the serialization formats (such as JSON or XML) that are + /// supported by this service to read or generate bodies. + /// + /// The serialization registry to be used + T Serializers(IBuilder registry); + + /// + /// Configures the parameter injectors that are available within this + /// service. This allows you to inject custom values into the methods + /// of your service, such as the currently logged-in user or session. + /// + /// The injection registry to be used + T Injectors(IBuilder registry); + + /// + /// Configures the formatters that will be used by this service to + /// format primitive types (such as numbers or dates). This allows + /// you to use custom formats within paths, queries or bodies. + /// + /// The formatter registry to be used + T Formatters(IBuilder registry); + +} diff --git a/Modules/Reflection/IServiceMethodProvider.cs b/Modules/Reflection/IServiceMethodProvider.cs index 3798415d7..b43afce82 100644 --- a/Modules/Reflection/IServiceMethodProvider.cs +++ b/Modules/Reflection/IServiceMethodProvider.cs @@ -1,4 +1,6 @@ -namespace GenHTTP.Modules.Reflection; +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Modules.Reflection; /// /// Implemented by handlers that use the handler @@ -9,8 +11,8 @@ public interface IServiceMethodProvider { /// - /// The method collection actually serving requests. + /// Retrieves the methods that are provided by this handler. /// - MethodCollection Methods { get; } + ValueTask GetMethodsAsync(IRequest request); } diff --git a/Modules/Reflection/Injectors/HandlerInjector.cs b/Modules/Reflection/Injectors/HandlerInjector.cs index a37e16038..f990e2a1a 100644 --- a/Modules/Reflection/Injectors/HandlerInjector.cs +++ b/Modules/Reflection/Injectors/HandlerInjector.cs @@ -6,7 +6,7 @@ namespace GenHTTP.Modules.Reflection.Injectors; public class HandlerInjector : IParameterInjector { - public bool Supports(Type type) => type == typeof(IHandler); + public bool Supports(IRequest request, Type type) => type == typeof(IHandler); public object GetValue(IHandler handler, IRequest request, Type targetType) => handler; } diff --git a/Modules/Reflection/Injectors/IParameterInjector.cs b/Modules/Reflection/Injectors/IParameterInjector.cs index 4656a0223..7c33e2c6c 100644 --- a/Modules/Reflection/Injectors/IParameterInjector.cs +++ b/Modules/Reflection/Injectors/IParameterInjector.cs @@ -6,7 +6,7 @@ namespace GenHTTP.Modules.Reflection.Injectors; public interface IParameterInjector { - bool Supports(Type type); + bool Supports(IRequest request, Type type); object? GetValue(IHandler handler, IRequest request, Type targetType); diff --git a/Modules/Reflection/Injectors/RequestInjector.cs b/Modules/Reflection/Injectors/RequestInjector.cs index ade6a8399..350496e8d 100644 --- a/Modules/Reflection/Injectors/RequestInjector.cs +++ b/Modules/Reflection/Injectors/RequestInjector.cs @@ -6,7 +6,7 @@ namespace GenHTTP.Modules.Reflection.Injectors; public class RequestInjector : IParameterInjector { - public bool Supports(Type type) => type == typeof(IRequest); + public bool Supports(IRequest request, Type type) => type == typeof(IRequest); public object GetValue(IHandler handler, IRequest request, Type targetType) => request; } diff --git a/Modules/Reflection/Injectors/UserInjector.cs b/Modules/Reflection/Injectors/UserInjector.cs index 7367091b8..e9a13e426 100644 --- a/Modules/Reflection/Injectors/UserInjector.cs +++ b/Modules/Reflection/Injectors/UserInjector.cs @@ -9,7 +9,7 @@ public class UserInjector : IParameterInjector where T : IUser #region Get-/Setters - public bool Supports(Type type) => type == typeof(T); + public bool Supports(IRequest request, Type type) => type == typeof(T); #endregion diff --git a/Modules/Reflection/MethodHandler.cs b/Modules/Reflection/MethodHandler.cs index 858b0183d..dd9580bda 100644 --- a/Modules/Reflection/MethodHandler.cs +++ b/Modules/Reflection/MethodHandler.cs @@ -29,7 +29,7 @@ public sealed class MethodHandler : IHandler public IMethodConfiguration Configuration { get; } - private object Instance { get; } + private Func> InstanceProvider { get; } public MethodRegistry Registry { get; } @@ -43,13 +43,13 @@ public sealed class MethodHandler : IHandler /// Creates a new handler to serve a single API operation. /// /// The operation to be executed and provided (use to create an operation) - /// The object to execute the operation on + /// A factory that will provide an instance to actually execute the operation on /// Additional, use-specified information about the operation /// The customized registry to be used to read and write data - public MethodHandler(Operation operation, object instance, IMethodConfiguration metaData, MethodRegistry registry) + public MethodHandler(Operation operation, Func> instanceProvider, IMethodConfiguration metaData, MethodRegistry registry) { Configuration = metaData; - Instance = instance; + InstanceProvider = instanceProvider; Operation = operation; Registry = registry; @@ -72,7 +72,7 @@ public MethodHandler(Operation operation, object instance, IMethodConfiguration return interception; } - var result = Invoke(arguments.Values.ToArray()); + var result = await InvokeAsync(request, arguments.Values.ToArray()); return await ResponseProvider.GetResponseAsync(request, Operation, await UnwrapAsync(result), null); } @@ -144,11 +144,13 @@ public MethodHandler(Operation operation, object instance, IMethodConfiguration return null; } - private object? Invoke(object?[] arguments) + private async ValueTask InvokeAsync(IRequest request, object?[] arguments) { try { - return Operation.Method.Invoke(Instance, arguments); + var instance = await InstanceProvider(request); + + return Operation.Method.Invoke(instance, arguments); } catch (TargetInvocationException e) { diff --git a/Modules/Reflection/Operations/ArgumentProvider.cs b/Modules/Reflection/Operations/ArgumentProvider.cs index 614248ace..1e72ed896 100644 --- a/Modules/Reflection/Operations/ArgumentProvider.cs +++ b/Modules/Reflection/Operations/ArgumentProvider.cs @@ -12,7 +12,7 @@ public static class ArgumentProvider { foreach (var injector in registry.Injection) { - if (injector.Supports(argument.Type)) + if (injector.Supports(request, argument.Type)) { return injector.GetValue(handler, request, argument.Type); } diff --git a/Modules/Reflection/Operations/OperationBuilder.cs b/Modules/Reflection/Operations/OperationBuilder.cs index 99159f7af..d3878cdcf 100644 --- a/Modules/Reflection/Operations/OperationBuilder.cs +++ b/Modules/Reflection/Operations/OperationBuilder.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; namespace GenHTTP.Modules.Reflection.Operations; @@ -27,7 +28,7 @@ public static partial class OperationBuilder /// The customizable registry used to read and write data /// If set to true, the operation requires the client to append a trailing slash to the path /// The newly created operation - public static Operation Create(string? definition, MethodInfo method, MethodRegistry registry, bool forceTrailingSlash = false) + public static Operation Create(IRequest request, string? definition, MethodInfo method, MethodRegistry registry, bool forceTrailingSlash = false) { var isWildcard = CheckWildcardRoute(method.ReturnType); @@ -94,7 +95,7 @@ public static Operation Create(string? definition, MethodInfo method, MethodRegi path = new OperationPath(nameBuilder.ToString(), matcher, false, isWildcard); } - var arguments = SignatureAnalyzer.GetArguments(method, pathArguments, registry); + var arguments = SignatureAnalyzer.GetArguments(request, method, pathArguments, registry); var result = SignatureAnalyzer.GetResult(method, registry); diff --git a/Modules/Reflection/Operations/SignatureAnalyzer.cs b/Modules/Reflection/Operations/SignatureAnalyzer.cs index 798a255cd..fc7141508 100644 --- a/Modules/Reflection/Operations/SignatureAnalyzer.cs +++ b/Modules/Reflection/Operations/SignatureAnalyzer.cs @@ -8,7 +8,7 @@ namespace GenHTTP.Modules.Reflection.Operations; public static class SignatureAnalyzer { - public static Dictionary GetArguments(MethodInfo method, HashSet pathArguments, MethodRegistry registry) + public static Dictionary GetArguments(IRequest request, MethodInfo method, HashSet pathArguments, MethodRegistry registry) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -25,7 +25,7 @@ public static Dictionary GetArguments(MethodInfo meth continue; } - if (TryInject(param, registry, out var injectedArg)) + if (TryInject(request, param, registry, out var injectedArg)) { result.Add(param.Name, injectedArg); continue; @@ -69,11 +69,11 @@ private static bool TryStream(ParameterInfo param, [NotNullWhen(true)] out Opera return false; } - private static bool TryInject(ParameterInfo param, MethodRegistry registry, [NotNullWhen(true)] out OperationArgument? argument) + private static bool TryInject(IRequest request, ParameterInfo param, MethodRegistry registry, [NotNullWhen(true)] out OperationArgument? argument) { foreach (var injector in registry.Injection) { - if (injector.Supports(param.ParameterType)) + if (injector.Supports(request, param.ParameterType)) { argument = new OperationArgument(param.Name!, param.ParameterType, OperationArgumentSource.Injected); return true; diff --git a/Modules/Webservices/Provider/ServiceResourceBuilder.cs b/Modules/Webservices/Provider/ServiceResourceBuilder.cs index 7c294b0c4..998dc8651 100644 --- a/Modules/Webservices/Provider/ServiceResourceBuilder.cs +++ b/Modules/Webservices/Provider/ServiceResourceBuilder.cs @@ -1,6 +1,9 @@ using System.Diagnostics.CodeAnalysis; + using GenHTTP.Api.Content; using GenHTTP.Api.Infrastructure; +using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Conversion; using GenHTTP.Modules.Conversion.Formatters; using GenHTTP.Modules.Conversion.Serializers; @@ -9,25 +12,41 @@ namespace GenHTTP.Modules.Webservices.Provider; -public sealed class ServiceResourceBuilder : IHandlerBuilder +public sealed class ServiceResourceBuilder : IHandlerBuilder, IRegistryBuilder { private readonly List _Concerns = []; + private Type? _Type; + + private Func>? _InstanceProvider; + private IBuilder? _Formatters; private IBuilder? _Injectors; - private object? _Instance; - private IBuilder? _Serializers; #region Functionality public ServiceResourceBuilder Type<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : new() => Instance(new T()); + public ServiceResourceBuilder Type([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) + { + _Type = type; + return this; + } + public ServiceResourceBuilder Instance(object instance) { - _Instance = instance; + _Type = instance.GetType(); + _InstanceProvider = (_) => ValueTask.FromResult(instance); + + return this; + } + + public ServiceResourceBuilder InstanceProvider(Func> provider) + { + _InstanceProvider = provider; return this; } @@ -63,11 +82,13 @@ public IHandler Build() var formatters = (_Formatters ?? Formatting.Default()).Build(); - var instance = _Instance ?? throw new BuilderMissingPropertyException("instance"); + var instanceProvider = _InstanceProvider ?? throw new BuilderMissingPropertyException("Instance provider has not been set"); + + var type = _Type ?? throw new BuilderMissingPropertyException("Type has not been set"); var extensions = new MethodRegistry(serializers, injectors, formatters); - return Concerns.Chain(_Concerns, new ServiceResourceRouter( instance, extensions)); + return Concerns.Chain(_Concerns, new ServiceResourceRouter(type, instanceProvider, extensions)); } #endregion diff --git a/Modules/Webservices/Provider/ServiceResourceRouter.cs b/Modules/Webservices/Provider/ServiceResourceRouter.cs index 625e73c65..c1cdc448a 100644 --- a/Modules/Webservices/Provider/ServiceResourceRouter.cs +++ b/Modules/Webservices/Provider/ServiceResourceRouter.cs @@ -1,6 +1,8 @@ using System.Reflection; + using GenHTTP.Api.Content; using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Reflection; using GenHTTP.Modules.Reflection.Operations; @@ -8,50 +10,59 @@ namespace GenHTTP.Modules.Webservices.Provider; public sealed class ServiceResourceRouter : IHandler, IServiceMethodProvider { + private MethodCollection? _Methods; #region Get-/Setters - public MethodCollection Methods { get; } + private Type Type { get; } - public ResponseProvider ResponseProvider { get; } + private Func> InstanceProvider { get; } - public object Instance { get; } + private MethodRegistry Registry { get; } - #endregion + #endregion #region Initialization - public ServiceResourceRouter(object instance, MethodRegistry registry) + public ServiceResourceRouter(Type type, Func> instanceProvider, MethodRegistry registry) { - Instance = instance; + Type = type; + InstanceProvider = instanceProvider; + Registry = registry; + } - ResponseProvider = new ResponseProvider(registry); + #endregion - Methods = new MethodCollection(AnalyzeMethods(instance.GetType(), registry)); - } + #region Functionality + + public ValueTask PrepareAsync() => ValueTask.CompletedTask; - private IEnumerable AnalyzeMethods(Type type, MethodRegistry registry) + public async ValueTask HandleAsync(IRequest request) => await (await GetMethodsAsync(request)).HandleAsync(request); + + public async ValueTask GetMethodsAsync(IRequest request) { - foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) + if (_Methods != null) return _Methods; + + var found = new List(); + + foreach (var method in Type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { var attribute = method.GetCustomAttribute(true); if (attribute is not null) { - var operation = OperationBuilder.Create(attribute.Path, method, registry); + var operation = OperationBuilder.Create(request, attribute.Path, method, Registry); - yield return new MethodHandler(operation, Instance, attribute, registry); + found.Add(new MethodHandler(operation, InstanceProvider, attribute, Registry)); } } - } - #endregion - - #region Functionality + var result = new MethodCollection(found); - public ValueTask PrepareAsync() => Methods.PrepareAsync(); + await result.PrepareAsync(); - public ValueTask HandleAsync(IRequest request) => Methods.HandleAsync(request); + return _Methods = result; + } #endregion diff --git a/Playground/GenHTTP.Playground.csproj b/Playground/GenHTTP.Playground.csproj index bc2e296a4..f7ee935fc 100644 --- a/Playground/GenHTTP.Playground.csproj +++ b/Playground/GenHTTP.Playground.csproj @@ -22,6 +22,7 @@ + @@ -31,6 +32,7 @@ + diff --git a/Playground/Program.cs b/Playground/Program.cs index a3046b669..5808f9974 100644 --- a/Playground/Program.cs +++ b/Playground/Program.cs @@ -1,4 +1,5 @@ using GenHTTP.Engine.Kestrel; + using GenHTTP.Modules.IO; using GenHTTP.Modules.Practices; diff --git a/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj index 861699f80..7ad70517d 100644 --- a/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj +++ b/Testing/Acceptance/GenHTTP.Testing.Acceptance.csproj @@ -70,6 +70,7 @@ + diff --git a/Testing/Acceptance/Modules/DependencyInjection/BasicTests.cs b/Testing/Acceptance/Modules/DependencyInjection/BasicTests.cs new file mode 100644 index 000000000..f4097ff6b --- /dev/null +++ b/Testing/Acceptance/Modules/DependencyInjection/BasicTests.cs @@ -0,0 +1,90 @@ +using System.Net; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.DependencyInjection; +using GenHTTP.Modules.Functional; +using GenHTTP.Modules.IO; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.DependencyInjection; + +[TestClass] +public class BasicTests +{ + + #region Supporting data structures + + public class MyConcern(AwesomeService service) : IDependentConcern + { + + public async ValueTask HandleAsync(IHandler content, IRequest request) + { + var response = await content.HandleAsync(request); + + if (response != null) + { + response.Headers.Add("X-Custom", service.DoWork()); + } + + return response; + } + } + + public class MyHandler(AwesomeService service) : IDependentHandler + { + + public ValueTask HandleAsync(IRequest request) + { + var response = request.Respond() + .Content(service.DoWork()) + .Build(); + + return ValueTask.FromResult(response); + } + + } + + #endregion + + #region Tests + + [TestMethod] + [MultiEngineTest] + public async Task TestDependentConcern(TestEngine engine) + { + var concern = Dependent.Concern(); + + var app = Inline.Create() + .Get(() => "Yay") + .Add(concern); + + await using var host = await DependentHost.RunAsync(app, engine: engine); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + Assert.AreEqual("42", response.GetHeader("X-Custom")); + } + + [TestMethod] + [MultiEngineTest] + public async Task TestDependentHandler(TestEngine engine) + { + var handler = Dependent.Handler(); + + await using var host = await DependentHost.RunAsync(handler, engine: engine); + + using var response = await host.GetResponseAsync(); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + Assert.AreEqual("42", await response.GetContentAsync()); + } + + #endregion + +} diff --git a/Testing/Acceptance/Modules/DependencyInjection/ControllerTests.cs b/Testing/Acceptance/Modules/DependencyInjection/ControllerTests.cs new file mode 100644 index 000000000..c1018f16a --- /dev/null +++ b/Testing/Acceptance/Modules/DependencyInjection/ControllerTests.cs @@ -0,0 +1,41 @@ +using GenHTTP.Modules.DependencyInjection; +using GenHTTP.Modules.Layouting; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.DependencyInjection; + +[TestClass] +public class ControllerTests +{ + + #region Supporting Structures + + public class TestController(AwesomeService first) + { + + public string DoWork(AnotherAwesomeService second) => $"{first.DoWork()}-{second.DoWork()}"; + + } + + #endregion + + #region Functionality + + [TestMethod] + [MultiEngineTest] + public async Task TestServiceDependencyInjection(TestEngine engine) + { + var app = Layout.Create() + .AddDependentController("t"); + + await using var runner = await DependentHost.RunAsync(app, engine: engine); + + using var response = await runner.GetResponseAsync("/t/do-work/"); + + Assert.AreEqual("42-24", await response.GetContentAsync()); + } + + #endregion + +} diff --git a/Testing/Acceptance/Modules/DependencyInjection/DependentHost.cs b/Testing/Acceptance/Modules/DependencyInjection/DependentHost.cs new file mode 100644 index 000000000..cfe4be123 --- /dev/null +++ b/Testing/Acceptance/Modules/DependencyInjection/DependentHost.cs @@ -0,0 +1,43 @@ +using GenHTTP.Api.Content; + +using GenHTTP.Modules.DependencyInjection; + +using Microsoft.Extensions.DependencyInjection; + +namespace GenHTTP.Testing.Acceptance.Modules.DependencyInjection; + +public static class DependentHost +{ + + public static async Task RunAsync(IHandlerBuilder app, TestEngine engine = TestEngine.Internal) + { + var host = new TestHost(app.Build(), engine: engine); + + var services = new ServiceCollection(); + + services.AddSingleton() + .AddSingleton(); + + host.Host.AddDependencyInjection(services.BuildServiceProvider()); + + await host.StartAsync(); + + return host; + } + +} + + +public class AwesomeService +{ + + public string DoWork() => "42"; + +} + +public class AnotherAwesomeService +{ + + public string DoWork() => "24"; + +} diff --git a/Testing/Acceptance/Modules/DependencyInjection/InfrastructureTests.cs b/Testing/Acceptance/Modules/DependencyInjection/InfrastructureTests.cs new file mode 100644 index 000000000..e828fd45e --- /dev/null +++ b/Testing/Acceptance/Modules/DependencyInjection/InfrastructureTests.cs @@ -0,0 +1,51 @@ +using System.Net; + +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.DependencyInjection; +using GenHTTP.Modules.Functional; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.DependencyInjection; + +[TestClass] +public class InfrastructureTests +{ + + [TestMethod] + [MultiEngineTest] + public async Task TestServiceAvailability(TestEngine engine) + { + var app = Inline.Create() + .Get((IRequest r) => + { + Assert.IsNotNull(r.GetServiceProvider()); + Assert.IsNotNull(r.GetServiceScope()); + }); + + await using var runner = await DependentHost.RunAsync(app, engine: engine); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode); + } + + [TestMethod] + public async Task TestMeaningfulErrorIfNotConfigured() + { + var app = Inline.Create() + .Get((IRequest r) => + { + Assert.Throws(r.GetServiceProvider); + Assert.Throws(r.GetServiceScope); + }); + + await using var runner = await TestHost.RunAsync(app); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode); + } + +} diff --git a/Testing/Acceptance/Modules/DependencyInjection/InlineTests.cs b/Testing/Acceptance/Modules/DependencyInjection/InlineTests.cs new file mode 100644 index 000000000..fdda6add5 --- /dev/null +++ b/Testing/Acceptance/Modules/DependencyInjection/InlineTests.cs @@ -0,0 +1,25 @@ +using GenHTTP.Modules.DependencyInjection; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.DependencyInjection; + +[TestClass] +public class InlineTests +{ + + [TestMethod] + [MultiEngineTest] + public async Task TestParameterInjection(TestEngine engine) + { + var app = DependentInline.Create() + .Get((AwesomeService s) => s.DoWork()); + + await using var runner = await DependentHost.RunAsync(app, engine: engine); + + using var response = await runner.GetResponseAsync(); + + Assert.AreEqual("42", await response.GetContentAsync()); + } + +} diff --git a/Testing/Acceptance/Modules/DependencyInjection/WebserviceTests.cs b/Testing/Acceptance/Modules/DependencyInjection/WebserviceTests.cs new file mode 100644 index 000000000..cc8850b20 --- /dev/null +++ b/Testing/Acceptance/Modules/DependencyInjection/WebserviceTests.cs @@ -0,0 +1,58 @@ +using GenHTTP.Modules.Conversion; +using GenHTTP.Modules.DependencyInjection; +using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Reflection; +using GenHTTP.Modules.Webservices; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GenHTTP.Testing.Acceptance.Modules.DependencyInjection; + +[TestClass] +public class WebserviceTests +{ + + #region Supporting Structures + + public class TestService(AwesomeService first) + { + + [ResourceMethod] + public string DoWork(AnotherAwesomeService second) => $"{first.DoWork()}-{second.DoWork()}"; + + } + + #endregion + + #region Functionality + + [TestMethod] + [MultiEngineTest] + public async Task TestServiceDependencyInjection(TestEngine engine) + { + var app = Layout.Create() + .AddDependentService("service"); + + await using var runner = await DependentHost.RunAsync(app, engine: engine); + + using var response = await runner.GetResponseAsync("/service"); + + Assert.AreEqual("42-24", await response.GetContentAsync()); + } + + [TestMethod] + public async Task TestCustomization() + { + var app = Layout.Create() + .AddDependentService("s", Injection.Default(), Serialization.Default(), Formatting.Default()); + + await using var runner = await DependentHost.RunAsync(app); + + using var response = await runner.GetResponseAsync("/s"); + + response.EnsureSuccessStatusCode(); + } + + #endregion + +} diff --git a/Testing/Acceptance/Modules/OpenApi/DiscoveryTests.cs b/Testing/Acceptance/Modules/OpenApi/DiscoveryTests.cs index ecef6d370..8143a9b59 100644 --- a/Testing/Acceptance/Modules/OpenApi/DiscoveryTests.cs +++ b/Testing/Acceptance/Modules/OpenApi/DiscoveryTests.cs @@ -1,5 +1,5 @@ using GenHTTP.Api.Content; - +using GenHTTP.Api.Protocol; using GenHTTP.Modules.Functional; using GenHTTP.Modules.IO; using GenHTTP.Modules.Layouting; @@ -73,9 +73,11 @@ public class CustomExplorer : IApiExplorer public bool CanExplore(IHandler handler) => true; - public void Explore(IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) + public ValueTask ExploreAsync(IRequest request, IHandler handler, List path, OpenApiDocument document, SchemaManager schemata, ApiDiscoveryRegistry registry) { document.Servers.First().Description = "Added by explorer"; + + return ValueTask.CompletedTask; } }