diff --git a/API/Routing/RoutingTarget.cs b/API/Routing/RoutingTarget.cs index 60dc50348..49c30c2ac 100644 --- a/API/Routing/RoutingTarget.cs +++ b/API/Routing/RoutingTarget.cs @@ -36,7 +36,7 @@ public RoutingTarget(WebPath path) /// /// The segment to be currently handled by the responsible handler. /// - public WebPathPart? Current => _index < Path.Parts.Count ? Path.Parts[_index] : null; + public WebPathPart? Current => Next(0); /// /// Specifies, whether the end of the path has been reached. @@ -65,6 +65,24 @@ public void Advance() _index++; } + + /// + /// Acknowledges the number of segments passed as a parameter. + /// + /// The number of segments to advance by + public void Advance(int byOffset) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(byOffset); + + var newIndex = _index + byOffset; + + if (newIndex > Path.Parts.Count) + { + throw new InvalidOperationException($"Cannot advance {byOffset} segments from position {_index} with {Path.Parts.Count} segments in total"); + } + + _index = newIndex; + } /// /// Retrieves the part of the path that still needs to be routed. @@ -84,6 +102,19 @@ public WebPath GetRemaining() return new WebPath(resultList, Path.TrailingSlash); } + /// + /// Peeks at the next segment identified by the offset index, + /// beginning from the current position. + /// + /// The offset to be applied + /// The segment at the given position or null, if there are no more segments + public WebPathPart? Next(int offset) + { + var index = _index + offset; + + return index < Path.Parts.Count ? Path.Parts[index] : null; + } + #endregion } diff --git a/Modules/Controllers/Extensions.cs b/Modules/Controllers/Extensions.cs index e8bb659b6..9b850d98e 100644 --- a/Modules/Controllers/Extensions.cs +++ b/Modules/Controllers/Extensions.cs @@ -4,6 +4,7 @@ using GenHTTP.Modules.Conversion.Formatters; using GenHTTP.Modules.Conversion.Serializers; using GenHTTP.Modules.Layouting.Provider; +using GenHTTP.Modules.Reflection; using GenHTTP.Modules.Reflection.Injectors; namespace GenHTTP.Modules.Controllers; @@ -21,9 +22,9 @@ public static class Extensions /// Optionally the injectors to be used by this controller /// Optionally the serializers to be used by this controller /// Optionally the formatters to be used by this controller - public static LayoutBuilder AddController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, string path, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) where T : new() + public static LayoutBuilder AddController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, string path, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null, ExecutionMode? mode = null) where T : new() { - builder.Add(path, Controller.From().Configured(injectors, serializers, formatters)); + builder.Add(path, Controller.From().Configured(injectors, serializers, formatters, mode)); return builder; } @@ -36,13 +37,13 @@ public static class Extensions /// Optionally the injectors to be used by this controller /// Optionally the serializers to be used by this controller /// Optionally the formatters to be used by this controller - public static LayoutBuilder IndexController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) where T : new() + public static LayoutBuilder IndexController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null, ExecutionMode? mode = null) where T : new() { - builder.Add(Controller.From().Configured(injectors, serializers, formatters)); + builder.Add(Controller.From().Configured(injectors, serializers, formatters, mode)); return builder; } - private static ControllerBuilder Configured(this ControllerBuilder builder, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) + private static ControllerBuilder Configured(this ControllerBuilder builder, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null, ExecutionMode? mode = null) { if (injectors != null) { @@ -59,6 +60,11 @@ private static ControllerBuilder Configured(this ControllerBuilder builder, IBui builder.Formatters(formatters); } + if (mode != null) + { + builder.ExecutionMode(mode.Value); + } + return builder; } } diff --git a/Modules/Controllers/Provider/ControllerBuilder.cs b/Modules/Controllers/Provider/ControllerBuilder.cs index c1db41fa7..9841a670e 100644 --- a/Modules/Controllers/Provider/ControllerBuilder.cs +++ b/Modules/Controllers/Provider/ControllerBuilder.cs @@ -12,7 +12,7 @@ namespace GenHTTP.Modules.Controllers.Provider; -public sealed class ControllerBuilder : IHandlerBuilder, IRegistryBuilder +public sealed class ControllerBuilder : IReflectionFrameworkBuilder { private readonly List _concerns = []; @@ -26,6 +26,8 @@ public sealed class ControllerBuilder : IHandlerBuilder, IReg private IBuilder? _serializers; + private ExecutionMode? _executionMode; + #region Functionality public ControllerBuilder Serializers(IBuilder registry) @@ -69,6 +71,16 @@ public ControllerBuilder InstanceProvider(Func> prov return this; } + /// + /// Sets the execution mode to be used to run functions. + /// + /// The mode to be used for execution + public ControllerBuilder ExecutionMode(ExecutionMode mode) + { + _executionMode = mode; + return this; + } + public ControllerBuilder Add(IConcernBuilder concern) { _concerns.Add(concern); @@ -89,7 +101,9 @@ public IHandler Build() var extensions = new MethodRegistry(serializers, injectors, formatters); - return Concerns.Chain(_concerns, new ControllerHandler(type, instanceProvider, extensions)); + var executionSettings = new ExecutionSettings(_executionMode); + + return Concerns.Chain(_concerns, new ControllerHandler(type, instanceProvider, executionSettings, extensions)); } #endregion diff --git a/Modules/Controllers/Provider/ControllerHandler.cs b/Modules/Controllers/Provider/ControllerHandler.cs index 99de4722e..01dca495c 100644 --- a/Modules/Controllers/Provider/ControllerHandler.cs +++ b/Modules/Controllers/Provider/ControllerHandler.cs @@ -13,8 +13,6 @@ public sealed partial class ControllerHandler : IHandler, IServiceMethodProvider { private static readonly Regex HyphenMatcher = CreateHyphenMatcher(); - private MethodCollection? _methods; - #region Get-/Setters private Type Type { get; } @@ -23,15 +21,22 @@ public sealed partial class ControllerHandler : IHandler, IServiceMethodProvider private MethodRegistry Registry { get; } + private ExecutionSettings ExecutionSettings { get; } + + public SynchronizedMethodCollection Methods { get; } + #endregion #region Initialization - public ControllerHandler(Type type, Func> instanceProvider, MethodRegistry registry) + public ControllerHandler(Type type, Func> instanceProvider, ExecutionSettings executionSettings, MethodRegistry registry) { Type = type; InstanceProvider = instanceProvider; + ExecutionSettings = executionSettings; Registry = registry; + + Methods = new SynchronizedMethodCollection(GetMethodsAsync); } #endregion @@ -40,47 +45,44 @@ public ControllerHandler(Type type, Func> instancePr public ValueTask PrepareAsync() => ValueTask.CompletedTask; - public async ValueTask HandleAsync(IRequest request) => await (await GetMethodsAsync(request)).HandleAsync(request); + public ValueTask HandleAsync(IRequest request) => Methods.HandleAsync(request); - public async ValueTask GetMethodsAsync(IRequest request) + private async Task GetMethodsAsync(IRequest request) { - 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(request, method, arguments, Registry); + var operation = CreateOperation(request, method, ExecutionSettings, annotation, arguments, Registry); - found.Add(new MethodHandler(operation, InstanceProvider, annotation, Registry)); + found.Add(new MethodHandler(operation, InstanceProvider, Registry)); } var result = new MethodCollection(found); await result.PrepareAsync(); - return _methods = result; + return result; } - private static Operation CreateOperation(IRequest request, MethodInfo method, List arguments, MethodRegistry registry) + private static Operation CreateOperation(IRequest request, MethodInfo method, ExecutionSettings executionSettings, IMethodConfiguration configuration, List arguments, MethodRegistry registry) { var pathArguments = string.Join('/', arguments.Select(a => $":{a}")); if (method.Name == "Index") { - return OperationBuilder.Create(request, pathArguments.Length > 0 ? $"/{pathArguments}/" : null, method, registry, true); + return OperationBuilder.Create(request, pathArguments.Length > 0 ? $"/{pathArguments}/" : null, method, null, executionSettings, configuration, registry, true); } var name = HypenCase(method.Name); var path = $"/{name}"; - return OperationBuilder.Create(request, pathArguments.Length > 0 ? $"{path}/{pathArguments}/" : $"{path}/", method, registry, true); + return OperationBuilder.Create(request, pathArguments.Length > 0 ? $"{path}/{pathArguments}/" : $"{path}/", method, null, executionSettings, configuration, registry, true); } private static List FindPathArguments(MethodInfo method) diff --git a/Modules/Conversion/Formatters/DateOnlyFormatter.cs b/Modules/Conversion/Formatters/DateOnlyFormatter.cs index b0aebc16c..ddc926956 100644 --- a/Modules/Conversion/Formatters/DateOnlyFormatter.cs +++ b/Modules/Conversion/Formatters/DateOnlyFormatter.cs @@ -1,4 +1,6 @@ using System.Text.RegularExpressions; +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; namespace GenHTTP.Modules.Conversion.Formatters; @@ -21,11 +23,12 @@ public object Read(string value, Type type) return new DateOnly(year, month, day); } - throw new ArgumentException($"Input does not match the requested format (yyyy-mm-dd): {value}"); + throw new ProviderException(ResponseStatus.BadRequest, $"Input does not match the requested format (yyyy-mm-dd): {value}"); } public string Write(object value, Type type) => ((DateOnly)value).ToString("yyyy-MM-dd"); [GeneratedRegex(@"^([0-9]{4})\-([0-9]{2})\-([0-9]{2})$", RegexOptions.Compiled)] private static partial Regex CreateDateOnlyPattern(); + } diff --git a/Modules/Functional/Provider/InlineBuilder.cs b/Modules/Functional/Provider/InlineBuilder.cs index b2cf9d92b..8613b633e 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, IRegistryBuilder +public class InlineBuilder : IReflectionFrameworkBuilder { private static readonly HashSet AllMethods = [..Enum.GetValues().Select(FlexibleRequestMethod.Get)]; @@ -24,6 +24,8 @@ public class InlineBuilder : IHandlerBuilder, IRegistryBuilder? _serializers; + private ExecutionMode? _executionMode; + #region Functionality /// @@ -57,6 +59,16 @@ public InlineBuilder Formatters(IBuilder registry) return this; } + /// + /// Sets the execution mode to be used to run functions. + /// + /// The mode to be used for execution + public InlineBuilder ExecutionMode(ExecutionMode mode) + { + _executionMode = mode; + return this; + } + /// /// Adds a route for a request of any type to the root of the handler. /// @@ -174,7 +186,9 @@ public IHandler Build() var extensions = new MethodRegistry(serializers, injectors, formatters); - return Concerns.Chain(_concerns, new InlineHandler(_functions, extensions)); + var executionSettings = new ExecutionSettings(_executionMode); + + return Concerns.Chain(_concerns, new InlineHandler(_functions, extensions, executionSettings)); } #endregion diff --git a/Modules/Functional/Provider/InlineHandler.cs b/Modules/Functional/Provider/InlineHandler.cs index a9c0e18cb..7fa4ecca9 100644 --- a/Modules/Functional/Provider/InlineHandler.cs +++ b/Modules/Functional/Provider/InlineHandler.cs @@ -6,9 +6,8 @@ namespace GenHTTP.Modules.Functional.Provider; -public class InlineHandler : IHandler, IServiceMethodProvider +public sealed class InlineHandler : IHandler, IServiceMethodProvider { - private MethodCollection? _methods; #region Get-/Setters @@ -16,14 +15,21 @@ public class InlineHandler : IHandler, IServiceMethodProvider private MethodRegistry Registry { get; } + private ExecutionSettings ExecutionSettings { get; } + + public SynchronizedMethodCollection Methods { get; } + #endregion #region Initialization - public InlineHandler(List functions, MethodRegistry registry) + public InlineHandler(List functions, MethodRegistry registry, ExecutionSettings executionSettings) { Functions = functions; Registry = registry; + ExecutionSettings = executionSettings; + + Methods = new SynchronizedMethodCollection(GetMethodsAsync); } #endregion @@ -32,32 +38,30 @@ public InlineHandler(List functions, MethodRegistry registry) public ValueTask PrepareAsync() => ValueTask.CompletedTask; - public async ValueTask HandleAsync(IRequest request) => await (await GetMethodsAsync(request)).HandleAsync(request); + public ValueTask HandleAsync(IRequest request) => Methods.HandleAsync(request); - public async ValueTask GetMethodsAsync(IRequest request) + private async Task GetMethodsAsync(IRequest request) { - if (_methods != null) return _methods; - var found = new List(); foreach (var function in Functions) { var method = function.Delegate.Method; - var operation = OperationBuilder.Create(request, function.Path, method, Registry); + var operation = OperationBuilder.Create(request, function.Path, method, function.Delegate, ExecutionSettings, function.Configuration, Registry); var target = function.Delegate.Target ?? throw new InvalidOperationException("Delegate target must not be null"); var instanceProvider = (IRequest _) => ValueTask.FromResult(target); - found.Add(new MethodHandler(operation, instanceProvider, function.Configuration, Registry)); + found.Add(new MethodHandler(operation, instanceProvider, Registry)); } var result = new MethodCollection(found); await result.PrepareAsync(); - return _methods = result; + return result; } #endregion diff --git a/Modules/OpenApi/Discovery/MethodHandlerExplorer.cs b/Modules/OpenApi/Discovery/MethodHandlerExplorer.cs index f49e38526..469da5ecf 100644 --- a/Modules/OpenApi/Discovery/MethodHandlerExplorer.cs +++ b/Modules/OpenApi/Discovery/MethodHandlerExplorer.cs @@ -2,6 +2,7 @@ using GenHTTP.Api.Protocol; using GenHTTP.Modules.Reflection; using GenHTTP.Modules.Reflection.Operations; + using NJsonSchema; using NSwag; @@ -31,9 +32,9 @@ public ValueTask ExploreAsync(IRequest request, IHandler handler, List p var pathItem = OpenApiExtensions.GetPathItem(document, path, methodHandler.Operation); - foreach (var method in methodHandler.Configuration.SupportedMethods) + foreach (var method in methodHandler.Operation.Configuration.SupportedMethods) { - if (method == RequestMethod.Head && methodHandler.Configuration.SupportedMethods.Count > 1) + if (method == RequestMethod.Head && methodHandler.Operation.Configuration.SupportedMethods.Count > 1) { continue; } @@ -101,7 +102,7 @@ public ValueTask ExploreAsync(IRequest request, IHandler handler, List p } } - if (methodHandler.Operation.Path.IsWildcard) + if (methodHandler.Operation.Route.IsWildcard) { var param = new OpenApiParameter { diff --git a/Modules/OpenApi/Discovery/OpenApiExtensions.cs b/Modules/OpenApi/Discovery/OpenApiExtensions.cs index 3a6687f9e..1dcd3b4d1 100644 --- a/Modules/OpenApi/Discovery/OpenApiExtensions.cs +++ b/Modules/OpenApi/Discovery/OpenApiExtensions.cs @@ -19,7 +19,7 @@ public static bool MightBeNull(this Type type) public static OpenApiPathItem GetPathItem(OpenApiDocument document, List path, Operation operation) { - var stringPath = BuildPath(operation.Path.Name, path, operation.Path.IsWildcard); + var stringPath = BuildPath(operation.Route.Name, path, operation.Route.IsWildcard); if (document.Paths.TryGetValue(stringPath, out var existing)) { diff --git a/Modules/OpenApi/Discovery/ServiceExplorer.cs b/Modules/OpenApi/Discovery/ServiceExplorer.cs index a1091b957..235532ec9 100644 --- a/Modules/OpenApi/Discovery/ServiceExplorer.cs +++ b/Modules/OpenApi/Discovery/ServiceExplorer.cs @@ -16,7 +16,9 @@ public async ValueTask ExploreAsync(IRequest request, IHandler handler, List +/// Specifies how the service should be executed by +/// the internal engine. +/// +public enum ExecutionMode +{ + + /// + /// The server will attempt to use code generation + /// and fall back to reflection, if codegen is not + /// available in the current environment. + /// + Auto, + + /// + /// The server executes methods using reflection. + /// + Reflection + +} diff --git a/Modules/Reflection/ExecutionSettings.cs b/Modules/Reflection/ExecutionSettings.cs new file mode 100644 index 000000000..0f3a61aa5 --- /dev/null +++ b/Modules/Reflection/ExecutionSettings.cs @@ -0,0 +1,7 @@ +namespace GenHTTP.Modules.Reflection; + +public record ExecutionSettings( + + ExecutionMode? Mode + +); diff --git a/Modules/Reflection/Extensions.cs b/Modules/Reflection/Extensions.cs index 56cb22624..5c107c89c 100644 --- a/Modules/Reflection/Extensions.cs +++ b/Modules/Reflection/Extensions.cs @@ -29,6 +29,10 @@ public static class Extensions /// The newly created expression public static string ToParameter(this string name) => $"(?<{name}>[^/]+)"; + public static bool IsAsync(this Type resultType) => resultType.IsAsyncGeneric() || resultType.IsAsyncVoid(); + + public static bool IsAsyncVoid(this Type resultType) => resultType == typeof(ValueTask) || resultType == typeof(Task); + public static bool IsAsyncGeneric(this Type resultType) => resultType.IsAssignableToGenericType(typeof(ValueTask<>)) || resultType.IsAssignableToGenericType(typeof(Task<>)); public static bool IsGenericallyVoid(this Type type) => type.GenericTypeArguments.Length == 1 && type.GenericTypeArguments[0] == VoidTaskResult; diff --git a/Modules/Reflection/GenHTTP.Modules.Reflection.csproj b/Modules/Reflection/GenHTTP.Modules.Reflection.csproj index 9612b7600..781d49a4b 100644 --- a/Modules/Reflection/GenHTTP.Modules.Reflection.csproj +++ b/Modules/Reflection/GenHTTP.Modules.Reflection.csproj @@ -16,6 +16,7 @@ + @@ -23,4 +24,13 @@ + + + + + + + + + diff --git a/Modules/Reflection/Generation/CodeGenerationException.cs b/Modules/Reflection/Generation/CodeGenerationException.cs new file mode 100644 index 000000000..2ceaab18a --- /dev/null +++ b/Modules/Reflection/Generation/CodeGenerationException.cs @@ -0,0 +1,16 @@ +namespace GenHTTP.Modules.Reflection.Generation; + +/// +/// Thrown if the server failed to compile generated code into a delegate. +/// +/// The code that caused the issue +/// Information about the actual cause +public class CodeGenerationException(string? code, Exception inner) : Exception("Failed to compile code for generated handler", inner) +{ + + /// + /// The code that caused the issue. + /// + public string? Code => code; + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Arguments.cs b/Modules/Reflection/Generation/CodeProvider.Arguments.cs new file mode 100644 index 000000000..fb73d5e60 --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Arguments.cs @@ -0,0 +1,224 @@ +using System.Text; +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProviderArgumentExtensions +{ + + public static void AppendArguments(this StringBuilder sb, Operation operation) + { + if (operation.Arguments.Count > 0) + { + var hasQueryArgs = operation.Arguments.Any(a => a.Value.Source == OperationArgumentSource.Query); + + var mayHasBody = operation.Configuration.SupportedMethods.Any(m => m != RequestMethod.Get && m != RequestMethod.Head); + + var supportBodyArguments = hasQueryArgs && mayHasBody; + + if (supportBodyArguments) + { + sb.AppendLine(" Dictionary? bodyArgs = null;"); + sb.AppendLine(); + + sb.AppendLine(" if (request.ContentType?.KnownType == ContentType.ApplicationWwwFormUrlEncoded)"); + sb.AppendLine(" {"); + sb.AppendLine(" bodyArgs = FormFormat.GetContent(request);"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + var index = 0; + + foreach (var arg in operation.Arguments) + { + sb.AppendArgument(arg.Value, ++index, supportBodyArguments); + sb.AppendLine(); + } + } + } + + private static void AppendArgument(this StringBuilder sb, OperationArgument argument, int index, bool supportBodyArguments) + { + switch (argument.Source) + { + case OperationArgumentSource.Path: + { + sb.AppendPathArgument(argument, index); + break; + } + case OperationArgumentSource.Query: + { + sb.AppendQueryArgument(argument, index, supportBodyArguments); + break; + } + case OperationArgumentSource.Streamed: + { + sb.AppendStreamArgument(index); + break; + } + case OperationArgumentSource.Content: + { + sb.AppendContentArgument(argument, index); + break; + } + case OperationArgumentSource.Injected: + { + sb.AppendInjectedArgument(argument, index); + break; + } + case OperationArgumentSource.Body: + { + sb.AppendBodyArgument(argument, index); + break; + } + default: + throw new NotSupportedException(); + } + } + + private static void AppendQueryArgument(this StringBuilder sb, OperationArgument argument, int index, bool supportBodyArguments) + { + var safeType = CompilationUtil.GetQualifiedName(argument.Type, false); + + sb.AppendLine($" {safeType}? arg{index} = null;"); + sb.AppendLine(); + + sb.AppendLine($" if (request.Query.TryGetValue({CompilationUtil.GetSafeString(argument.Name)}, out var queryArg{index}))"); + sb.AppendLine(" {"); + sb.AppendLine($" if (!string.IsNullOrEmpty(queryArg{index}))"); + sb.AppendLine(" {"); + sb.AppendArgumentAssignment(argument, index, "query"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + + if (supportBodyArguments) + { + sb.AppendLine($" else if (bodyArgs?.TryGetValue({CompilationUtil.GetSafeString(argument.Name)}, out var bodyArg{index}) == true)"); + sb.AppendLine(" {"); + sb.AppendLine($" if (!string.IsNullOrEmpty(bodyArg{index}))"); + sb.AppendLine(" {"); + sb.AppendArgumentAssignment(argument, index, "body"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + } + } + + private static void AppendStreamArgument(this StringBuilder sb, int index) + { + sb.AppendLine($" var arg{index} = ArgumentProvider.GetStream(request);"); + sb.AppendLine(); + } + + private static void AppendContentArgument(this StringBuilder sb, OperationArgument argument, int index) + { + var safeType = CompilationUtil.GetQualifiedName(argument.Type, false); + + sb.AppendLine(" var deserializer = registry.Serialization.GetDeserialization(request) ?? throw new ProviderException(ResponseStatus.UnsupportedMediaType, \"Requested format is not supported\");"); + sb.AppendLine(); + sb.AppendLine(" var content = request.Content ?? throw new ProviderException(ResponseStatus.BadRequest, \"Request body expected\");"); + sb.AppendLine(); + sb.AppendLine($" {safeType}? arg{index} = null;"); + sb.AppendLine(); + sb.AppendLine(" try"); + sb.AppendLine(" {"); + sb.AppendLine($" arg{index} = ({safeType}?)await deserializer.DeserializeAsync(content, typeof({safeType}));"); + sb.AppendLine(" }"); + sb.AppendLine(" catch (Exception e)"); + sb.AppendLine(" {"); + sb.AppendLine(" throw new ProviderException(ResponseStatus.BadRequest, \"Failed to deserialize request body\", e);"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + private static void AppendInjectedArgument(this StringBuilder sb, OperationArgument argument, int index) + { + var safeType = CompilationUtil.GetQualifiedName(argument.Type, false); + + if (argument.Type == typeof(IRequest)) + { + sb.AppendLine($" var arg{index} = request;"); + } + else if (argument.Type == typeof(IHandler)) + { + sb.AppendLine($" var arg{index} = handler;"); + } + else + { + sb.AppendLine($" {safeType}? arg{index} = null;"); + sb.AppendLine(); + + sb.AppendLine(" foreach (var injector in registry.Injection)"); + sb.AppendLine(" {"); + sb.AppendLine($" if (injector.Supports(request, typeof({safeType})))"); + sb.AppendLine(" {"); + sb.AppendLine($" arg{index} = ({safeType})injector.GetValue(handler, request, typeof({safeType}));"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + } + } + + private static void AppendBodyArgument(this StringBuilder sb, OperationArgument argument, int index) + { + var safeType = CompilationUtil.GetQualifiedName(argument.Type, false); + var safeName = CompilationUtil.GetSafeString(argument.Name); + + sb.AppendLine($" {safeType}? arg{index} = ({safeType}?)await ArgumentProvider.GetBodyArgumentAsync(request, {safeName}, typeof({safeType}), registry);"); + sb.AppendLine(); + } + + private static void AppendPathArgument(this StringBuilder sb, OperationArgument argument, int index) + { + var safeType = CompilationUtil.GetQualifiedName(argument.Type, false); + + sb.AppendLine($" {safeType}? arg{index} = null;"); + sb.AppendLine(); + + sb.AppendLine($" if (routingMatch.PathArguments?.TryGetValue({CompilationUtil.GetSafeString(argument.Name)}, out var pathArg{index}) ?? false)"); + sb.AppendLine(" {"); + sb.AppendLine($" if (!string.IsNullOrEmpty(pathArg{index}))"); + sb.AppendLine(" {"); + sb.AppendArgumentAssignment(argument, index, "path"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + } + + private static void AppendArgumentAssignment(this StringBuilder sb, OperationArgument argument, int index, string readFrom) + { + var sourceName = $"{readFrom}Arg{index}"; + + var safeType = CompilationUtil.GetQualifiedName(argument.Type, false); + + if (argument.Type == typeof(string)) + { + sb.AppendLine($" arg{index} = {sourceName};"); + } + else if (argument.Type.IsPrimitive && argument.Type != typeof(bool)) + { + sb.AppendTryParse(argument, $"{safeType}.TryParse({sourceName}, out var {sourceName}Typed)", sourceName, index); + } + else if (argument.Type.IsEnum) + { + sb.AppendTryParse(argument, $"Enum.TryParse({sourceName}, out {safeType} {sourceName}Typed)", sourceName, index); + } + else + { + sb.AppendLine($" arg{index} = ({safeType}?)registry.Formatting.Read({sourceName}, typeof({safeType}));"); + } + } + + private static void AppendTryParse(this StringBuilder sb, OperationArgument argument, string condition, string sourceName, int index) + { + sb.AppendLine($" if ({condition})"); + sb.AppendLine(" {"); + sb.AppendLine($" arg{index} = {sourceName}Typed;"); + sb.AppendLine(" }"); + sb.AppendLine(" else"); + sb.AppendLine(" {"); + sb.AppendLine($" throw new ProviderException(ResponseStatus.BadRequest, \"Invalid format for input parameter '{argument.Name}'\");"); + sb.AppendLine(" }"); + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Interception.cs b/Modules/Reflection/Generation/CodeProvider.Interception.cs new file mode 100644 index 000000000..ed261dfc3 --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Interception.cs @@ -0,0 +1,44 @@ +using System.Text; +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProviderInterceptionExtensions +{ + + public static void AppendInterception(this StringBuilder sb, Operation operation) + { + if (operation.Interceptors.Count > 0) + { + sb.AppendLine($" var interceptionArgs = new Dictionary({operation.Arguments.Count})"); + sb.AppendLine(" {"); + + int i = 1; + + foreach (var arg in operation.Arguments) + { + sb.Append($" {{ {CompilationUtil.GetSafeString(arg.Key)}, arg{i++} }}"); + + if (i < operation.Arguments.Count - 1) + { + sb.Append(','); + } + + sb.AppendLine(); + } + + sb.AppendLine(" };"); + sb.AppendLine(); + + sb.AppendLine(" var interceptionResult = await interception(request, interceptionArgs);"); + sb.AppendLine(); + + sb.AppendLine(" if (interceptionResult != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" return interceptionResult;"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Invocation.cs b/Modules/Reflection/Generation/CodeProvider.Invocation.cs new file mode 100644 index 000000000..a5e731a68 --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Invocation.cs @@ -0,0 +1,135 @@ +using System.Text; +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProviderInvocationExtensions +{ + + public static void AppendInvocation(this StringBuilder sb, Operation operation) + { + if (operation.Delegate != null) + { + sb.AppendDelegateInvocation(operation); + } + else + { + sb.AppendMethodInvocation(operation); + } + + sb.AppendLine(); + } + + public static void AppendDelegateInvocation(this StringBuilder sb, Operation operation) + { + var type = (operation.Result.Type == typeof(void)) ? "Action" : "Func"; + + var argumentTypes = new List(operation.Arguments.Select(x => x.Value.Type)); + + if (operation.Result.Type != typeof(void)) + { + argumentTypes.Add(operation.Method.ReturnType); + } + + var stringTypes = string.Join(", ", argumentTypes.Select(a => CompilationUtil.GetQualifiedName(a, true))); + + if (stringTypes.Length != 0) + { + sb.AppendLine($" var typedLogic = ({type}<{stringTypes}>)logic;"); + } + else + { + sb.AppendLine($" var typedLogic = ({type})logic;"); + } + + sb.AppendLine(); + + sb.AppendInvocation(operation, "typedLogic"); + } + + public static void AppendMethodInvocation(this StringBuilder sb, Operation operation) + { + var methodName = operation.Method.Name; + + var typeName = CompilationUtil.GetQualifiedName(operation.Method.DeclaringType!, false); + + sb.AppendLine($" var typedInstance = ({typeName})instance;"); + sb.AppendLine(); + + sb.AppendInvocation(operation, $"typedInstance.{methodName}"); + } + + private static void AppendInvocation(this StringBuilder sb, Operation operation, string invoker) + { + var resultType = operation.Method.ReturnType; + + var isAsyncGeneric = resultType.IsAsyncGeneric(); + + var isVoid = (isAsyncGeneric) ? resultType.IsGenericallyVoid() : resultType.IsAsyncVoid() || resultType == typeof(void); + + var isAsync = resultType.IsAsync(); + + var wrapped = CompilationUtil.HasWrappedResult(operation); + + if (isVoid) + { + sb.Append(" "); + } + else + { + if (wrapped) + { + sb.Append(" var wrapped = "); + } + else + { + sb.Append(" var result = "); + } + } + + + if (isAsync) + { + sb.Append("await "); + } + + sb.AppendLine($"{invoker}("); + sb.AppendArgumentList(operation); + sb.AppendLine(" );"); + + if (wrapped) + { + sb.AppendLine(); + sb.AppendLine($" var result = wrapped.Payload;"); + } + } + + private static void AppendArgumentList(this StringBuilder sb, Operation operation) + { + var i = 0; + + foreach (var argument in operation.Arguments) + { + sb.Append($" arg{i + 1}"); + + var defaultIsNull = !argument.Value.Type.IsValueType || Nullable.GetUnderlyingType(argument.Value.Type) != null; + + if (!defaultIsNull) + { + sb.Append(" ?? default"); + } + + var last = (i++ == operation.Arguments.Count - 1); + + if (last) + { + sb.AppendLine(); + } + else + { + sb.AppendLine(","); + } + } + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Result.Dynamic.cs b/Modules/Reflection/Generation/CodeProvider.Result.Dynamic.cs new file mode 100644 index 000000000..d1503032f --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Result.Dynamic.cs @@ -0,0 +1,37 @@ +using System.Text; +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProviderResultDynamicExtensions +{ + + public static void AppendDynamicResult(this StringBuilder sb, Operation operation) + { + var resultType = operation.Result.Type; + + if (typeof(IResponse).IsAssignableFrom(resultType)) + { + sb.AppendLine(" var response = result;"); + } + else if (typeof(IResponseBuilder).IsAssignableFrom(resultType)) + { + sb.AppendLine(" var response = result.Build();"); + } + else if (typeof(IHandler).IsAssignableFrom(resultType)) + { + sb.AppendLine(" var response = await result.HandleAsync(request);"); + } + else if (typeof(IHandlerBuilder).IsAssignableFrom(resultType)) + { + sb.AppendLine(" var response = await result.Build().HandleAsync(request);"); + } + else + { + throw new NotSupportedException($"Dynamic result of type '{resultType}' is not supported"); + } + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Result.Formatting.cs b/Modules/Reflection/Generation/CodeProvider.Result.Formatting.cs new file mode 100644 index 000000000..2c35ed92f --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Result.Formatting.cs @@ -0,0 +1,56 @@ +using System.Text; +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProviderResultFormattingExtensions +{ + + public static void AppendFormattedResult(this StringBuilder sb, Operation operation) + { + var type = operation.Result.Type; + + if (type == typeof(string)) + { + sb.AppendIOSupportedResult(operation); + } + else if (type.IsEnum || type == typeof(Guid) || IsSimpleNumber(type)) + { + sb.AppendLine(" var formattedResult = result.ToString() ?? string.Empty;"); + sb.AppendLine(); + + sb.AppendFormattedString(operation); + } + else if (type.IsPrimitive && type != typeof(bool)) + { + sb.AppendLine(" var formattedResult = Convert.ChangeType(result, typeof(string), CultureInfo.InvariantCulture) as string ?? string.Empty;"); + sb.AppendLine(); + + sb.AppendFormattedString(operation); + } + else + { + sb.AppendLine(" var formattedResult = registry.Formatting.Write(result, result.GetType()) ?? string.Empty;"); + sb.AppendLine(); + + sb.AppendFormattedString(operation); + } + } + + private static void AppendFormattedString(this StringBuilder sb, Operation operation) + { + sb.AppendLine(" var response = request.Respond()"); + sb.AppendLine(" .Content(formattedResult)"); + sb.AppendResultModifications(operation, " "); + sb.AppendLine(" .Build();"); + sb.AppendLine(); + } + + private static bool IsSimpleNumber(Type type) + { + return type == typeof(int) || type == typeof(byte) || type == typeof(long) + || type == typeof(uint) || type == typeof(ulong) + || type == typeof(short) || type == typeof(ushort); + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Result.IO.cs b/Modules/Reflection/Generation/CodeProvider.Result.IO.cs new file mode 100644 index 000000000..da5c1ea6d --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Result.IO.cs @@ -0,0 +1,18 @@ +using System.Text; +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProviderResultIOExtensions +{ + + public static void AppendIOSupportedResult(this StringBuilder sb, Operation operation) + { + sb.AppendLine(" var response = request.Respond()"); + sb.AppendLine(" .Content(result)"); + sb.AppendResultModifications(operation, " "); + sb.AppendLine(" .Build();"); + sb.AppendLine(); + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Result.Serialization.cs b/Modules/Reflection/Generation/CodeProvider.Result.Serialization.cs new file mode 100644 index 000000000..3928ee6fb --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Result.Serialization.cs @@ -0,0 +1,28 @@ +using System.Text; + +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProviderResultSerializationExtensions +{ + + public static void AppendSerializedResult(this StringBuilder sb, Operation operation) + { + sb.AppendLine(" var serializer = registry.Serialization.GetSerialization(request);"); + sb.AppendLine(); + sb.AppendLine(" if (serializer is null)"); + sb.AppendLine(" {"); + sb.AppendLine(" throw new ProviderException(ResponseStatus.UnsupportedMediaType, \"Requested format is not supported\");"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" var serializedResult = await serializer.SerializeAsync(request, result);"); + sb.AppendLine(); + sb.AppendLine(" var response = serializedResult"); + sb.AppendResultModifications(operation, " "); + sb.AppendLine(" .Build();"); + sb.AppendLine(); + + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Result.Void.cs b/Modules/Reflection/Generation/CodeProvider.Result.Void.cs new file mode 100644 index 000000000..b401130f6 --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Result.Void.cs @@ -0,0 +1,15 @@ +using System.Text; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProviderResultVoidExtensions +{ + + public static void AppendVoidResult(this StringBuilder sb) + { + sb.AppendLine(" var response = request.Respond()"); + sb.AppendLine(" .Status(ResponseStatus.NoContent)"); + sb.AppendLine(" .Build();"); + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Result.Wrapping.cs b/Modules/Reflection/Generation/CodeProvider.Result.Wrapping.cs new file mode 100644 index 000000000..96d65659a --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Result.Wrapping.cs @@ -0,0 +1,17 @@ +using System.Text; +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeBuilderResultWrappingExtensions +{ + + public static void AppendResultModifications(this StringBuilder sb, Operation operation, string prefix) + { + if (CompilationUtil.HasWrappedResult(operation)) + { + sb.AppendLine($"{prefix}.Apply(b => wrapped.Apply(b))"); + } + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.Result.cs b/Modules/Reflection/Generation/CodeProvider.Result.cs new file mode 100644 index 000000000..0bb1ef74e --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.Result.cs @@ -0,0 +1,82 @@ +using System.Text; +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProviderResultExtensions +{ + + public static void AppendResultConversion(this StringBuilder sb, Operation operation, bool isAsync) + { + sb.AppendNullReturn(operation, isAsync); + + switch (operation.Result.Sink) + { + case OperationResultSink.Formatter: + { + sb.AppendFormattedResult(operation); + break; + } + case OperationResultSink.Serializer: + { + sb.AppendSerializedResult(operation); + break; + } + case OperationResultSink.Binary: + { + sb.AppendIOSupportedResult(operation); + break; + } + case OperationResultSink.Dynamic: + { + sb.AppendDynamicResult(operation); + break; + } + case OperationResultSink.None: + { + sb.AppendVoidResult(); + break; + } + default: throw new NotSupportedException(); + } + + if (isAsync) + { + sb.AppendLine(" return response;"); + } + else + { + sb.AppendLine(" return new(response);"); + } + } + + private static void AppendNullReturn(this StringBuilder sb, Operation operation, bool isAsync) + { + if (CompilationUtil.CanHoldNull(operation.Result.Type)) + { + sb.AppendLine(" if (result == null)"); + sb.AppendLine(" {"); + sb.AppendLine(" var noContent = request.Respond().Status(ResponseStatus.NoContent);"); + sb.AppendLine(); + + if (CompilationUtil.HasWrappedResult(operation)) + { + sb.AppendLine(" wrapped.Apply(noContent);"); + sb.AppendLine(); + } + + if (isAsync) + { + sb.AppendLine(" return noContent.Build();"); + } + else + { + sb.AppendLine(" return new(noContent.Build());"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } + } + +} diff --git a/Modules/Reflection/Generation/CodeProvider.cs b/Modules/Reflection/Generation/CodeProvider.cs new file mode 100644 index 000000000..08438c5ca --- /dev/null +++ b/Modules/Reflection/Generation/CodeProvider.cs @@ -0,0 +1,97 @@ +using System.Text; + +using GenHTTP.Api.Content; + +using GenHTTP.Modules.Reflection.Operations; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CodeProvider +{ + + /// + /// Generates code that fetches the arguments expected by the operation + /// from the incoming request, executes it and maps the result into + /// a HTTP response. + /// + /// The operation to generate code for + /// The source code to be compiled to execute the operation + public static string Generate(Operation operation) + { + var isAsync = CheckAsync(operation); + + var sb = new StringBuilder(); + + sb.AppendLine("using System;"); + sb.AppendLine("using System.Collections;"); + sb.AppendLine("using System.Collections.Generic;"); + sb.AppendLine("using System.Globalization;"); + sb.AppendLine("using System.Threading.Tasks;"); + sb.AppendLine(); + sb.AppendLine("using GenHTTP.Api.Protocol;"); + sb.AppendLine("using GenHTTP.Api.Content;"); + sb.AppendLine(); + sb.AppendLine("using GenHTTP.Modules.Conversion.Serializers.Forms;"); + sb.AppendLine("using GenHTTP.Modules.Reflection;"); + sb.AppendLine("using GenHTTP.Modules.Reflection.Operations;"); + sb.AppendLine("using GenHTTP.Modules.Reflection.Routing;"); + sb.AppendLine("using GenHTTP.Modules.IO;"); + sb.AppendLine(); + + sb.AppendLine("public static class Invoker"); + sb.AppendLine("{"); + + if (operation.Delegate != null) + { + sb.AppendLine($" public static {(isAsync ? "async" : string.Empty)} ValueTask Invoke(Delegate logic, Operation operation, IRequest request, IHandler handler, MethodRegistry registry, RoutingMatch routingMatch, RequestInterception interception)"); + } + else + { + sb.AppendLine($" public static {(isAsync ? "async" : string.Empty)} ValueTask Invoke(object instance, Operation operation, IRequest request, IHandler handler, MethodRegistry registry, RoutingMatch routingMatch, RequestInterception interception)"); + } + + sb.AppendLine(" {"); + + sb.AppendArguments(operation); + + sb.AppendInterception(operation); + + sb.AppendInvocation(operation); + + sb.AppendResultConversion(operation, isAsync); + + sb.AppendLine(" }"); + + sb.AppendLine("}"); + + var code = sb.ToString(); + + return code; + } + + private static bool CheckAsync(Operation operation) + { + if (operation.Result.Sink == OperationResultSink.Serializer) + return true; + + if (operation.Method.ReturnType.IsAsync()) + return true; + + if (operation.Arguments.Any(a => a.Value.Source == OperationArgumentSource.Body || a.Value.Source == OperationArgumentSource.Content)) + return true; + + if (operation.Result.Sink == OperationResultSink.Dynamic) + { + var resultType = operation.Result.Type; + + if (typeof(IHandler).IsAssignableFrom(resultType) || typeof(IHandlerBuilder).IsAssignableFrom(resultType)) + return true; + } + + if (operation.Interceptors.Count > 0) + return true; + + return false; + } + +} diff --git a/Modules/Reflection/Generation/CompilationUtil.cs b/Modules/Reflection/Generation/CompilationUtil.cs new file mode 100644 index 000000000..3fd778d49 --- /dev/null +++ b/Modules/Reflection/Generation/CompilationUtil.cs @@ -0,0 +1,77 @@ +using GenHTTP.Modules.Reflection.Operations; + +using Microsoft.CodeAnalysis.CSharp; + +namespace GenHTTP.Modules.Reflection.Generation; + +public static class CompilationUtil +{ + private static readonly Dictionary BuiltInTypes = new() + { + { typeof(int), "int" }, + { typeof(string), "string" }, + { typeof(bool), "bool" }, + { typeof(void), "void" }, + { typeof(object), "object" }, + { typeof(long), "long" }, + { typeof(short), "short" }, + { typeof(byte), "byte" }, + { typeof(double), "double" }, + { typeof(float), "float" }, + { typeof(decimal), "decimal" }, + { typeof(char), "char" } + }; + + internal static string GetSafeString(string input) + => SyntaxFactory.Literal(input).ToFullString(); + + internal static string GetQualifiedName(Type type, bool allowNullable) + { + if (BuiltInTypes.TryGetValue(type, out var keyword)) + return keyword; + + if (Nullable.GetUnderlyingType(type) is Type underlyingType) + return $"{GetQualifiedName(underlyingType, false)}" + (allowNullable ? "?" : string.Empty); + + if (type.IsGenericType) + { + var genericType = type.GetGenericTypeDefinition(); + var args = type.GetGenericArguments(); + + var name = genericType.FullName!; + name = name[..name.IndexOf('`')].Replace('+', '.'); + + return $"{name}<{string.Join(", ", args.Select(a => GetQualifiedName(a, allowNullable)))}>"; + } + + if (type.IsArray) + return $"{GetQualifiedName(type.GetElementType()!, allowNullable)}[]"; + + return type.FullName!.Replace('+', '.'); + } + + internal static bool CanHoldNull(Type type) + { + if (type == typeof(void)) + return false; + + if (type.IsAsyncVoid()) + return false; + + if (type.IsGenericallyVoid()) + return false; + + if (!type.IsValueType) + return true; + + return Nullable.GetUnderlyingType(type) != null; + } + + internal static bool HasWrappedResult(Operation operation) + { + var returnType = operation.Method.ReturnType; + + return typeof(IResultWrapper).IsAssignableFrom(returnType); + } + +} diff --git a/Modules/Reflection/Generation/DelegateProvider.cs b/Modules/Reflection/Generation/DelegateProvider.cs new file mode 100644 index 000000000..c8a6b2e3c --- /dev/null +++ b/Modules/Reflection/Generation/DelegateProvider.cs @@ -0,0 +1,71 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Reflection.Operations; +using GenHTTP.Modules.Reflection.Routing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace GenHTTP.Modules.Reflection.Generation; + +internal static class DelegateProvider +{ + + /// + /// Compiles the given source code into an invocable delegate. + /// + /// The source code to be compiled + /// Either object for a webservice instance or a delegate for functional invocations + /// The compiled delegate + /// Thrown if the compilation failed for some reason + internal static Func> Compile(string code) + { + var syntaxTree = CSharpSyntaxTree.ParseText(code); + + var assemblyName = Path.GetRandomFileName(); + + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) + .Select(a => MetadataReference.CreateFromFile(a.Location)) + .Cast(); + + var compilation = CSharpCompilation.Create(assemblyName, [syntaxTree], references, + new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release, + concurrentBuild: true, + deterministic: false + ) + ); + + using var ms = new MemoryStream(); + + var result = compilation.Emit(ms); + + if (!result.Success) + { + var errors = string.Join(Environment.NewLine, result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)); + + throw new InvalidOperationException("Failed to compile handler:" + Environment.NewLine + Environment.NewLine + errors); + } + + ms.Seek(0, SeekOrigin.Begin); + + var assembly = Assembly.Load(ms.ToArray()); + + var type = assembly.GetType("Invoker"); + + var method = type!.GetMethod("Invoke")!; + + // warm up the JIT + RuntimeHelpers.PrepareMethod(method.MethodHandle); + + return (Func>)Delegate.CreateDelegate( + typeof(Func>), method + ); + } + +} diff --git a/Modules/Reflection/Generation/OptimizedDelegate.cs b/Modules/Reflection/Generation/OptimizedDelegate.cs new file mode 100644 index 000000000..d8203bc5e --- /dev/null +++ b/Modules/Reflection/Generation/OptimizedDelegate.cs @@ -0,0 +1,85 @@ +using System.Runtime.InteropServices; + +using GenHTTP.Api.Content; +using GenHTTP.Api.Protocol; + +using GenHTTP.Modules.Reflection.Operations; +using GenHTTP.Modules.Reflection.Routing; + +namespace GenHTTP.Modules.Reflection.Generation; + +internal static class OptimizedDelegate +{ + + /// + /// Specifies, whether code generation is supported on this system. + /// + internal static bool Supported { get; } = IsRuntimeCompilationSupported(); + + /// + /// Compiles the given operation into an invokable delegate. + /// + /// The operation to be compiled + /// Either an object for instance based services or a delegate for functional services + /// The delegate or null, if code generation is not supported on this system + /// Thrown, if the compilation failed for some reason + internal static Func>? Compile(Operation operation) + { + if (!Supported) return null; + + String? code = null; + + try + { + code = CodeProvider.Generate(operation); + + return DelegateProvider.Compile(code); + } + catch (Exception e) + { + throw new CodeGenerationException(code, e); + } + } + + private static bool IsRuntimeCompilationSupported() + { + if (RuntimeInformation.ProcessArchitecture is Architecture.Arm or Architecture.Arm64) + { + return false; + } + + var roslynType = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpCompilation, Microsoft.CodeAnalysis.CSharp"); + + if (roslynType == null) + { + return false; + } + + var asmBuilderType = Type.GetType("System.Reflection.Emit.AssemblyBuilder"); + + if (asmBuilderType == null) + { + return false; + } + + var defineMethod = asmBuilderType.GetMethod("DefineDynamicAssembly", + [ + typeof(System.Reflection.AssemblyName), typeof(System.Reflection.Emit.AssemblyBuilderAccess) + ]); + + if (defineMethod == null) + { + return false; + } + + var assemblyLoadMethod = typeof(System.Reflection.Assembly).GetMethod("Load", new[] { typeof(byte[]) }); + + if (assemblyLoadMethod == null) + { + return false; + } + + return true; + } + +} diff --git a/Modules/Reflection/IExecutionSettingsBuilder.cs b/Modules/Reflection/IExecutionSettingsBuilder.cs new file mode 100644 index 000000000..02d85de27 --- /dev/null +++ b/Modules/Reflection/IExecutionSettingsBuilder.cs @@ -0,0 +1,17 @@ +namespace GenHTTP.Modules.Reflection; + +/// +/// A protocol for builders that will internally create a +/// instance. +/// +/// The builder type to be returned +public interface IExecutionSettingsBuilder +{ + + /// + /// Sets the execution mode to be used to run functions. + /// + /// The mode to be used for execution + T ExecutionMode(ExecutionMode mode); + +} diff --git a/Modules/Reflection/IReflectionFrameworkBuilder.cs b/Modules/Reflection/IReflectionFrameworkBuilder.cs new file mode 100644 index 000000000..611a46ce7 --- /dev/null +++ b/Modules/Reflection/IReflectionFrameworkBuilder.cs @@ -0,0 +1,11 @@ +using GenHTTP.Api.Content; + +namespace GenHTTP.Modules.Reflection; + +/// +/// Basic protocol for all reflection based frameworks. +/// +/// The actual builder class (e.g. "InlineBuilder") +public interface IReflectionFrameworkBuilder + : IHandlerBuilder, IRegistryBuilder, IExecutionSettingsBuilder + where T : IHandlerBuilder; diff --git a/Modules/Reflection/IResultWrapper.cs b/Modules/Reflection/IResultWrapper.cs index c5eca7dc5..ff0f3569e 100644 --- a/Modules/Reflection/IResultWrapper.cs +++ b/Modules/Reflection/IResultWrapper.cs @@ -6,7 +6,7 @@ namespace GenHTTP.Modules.Reflection; /// Allows the framework to unwrap /// instances. /// -internal interface IResultWrapper +public interface IResultWrapper { /// diff --git a/Modules/Reflection/IServiceMethodProvider.cs b/Modules/Reflection/IServiceMethodProvider.cs index b43afce82..f3bcafd94 100644 --- a/Modules/Reflection/IServiceMethodProvider.cs +++ b/Modules/Reflection/IServiceMethodProvider.cs @@ -1,6 +1,4 @@ -using GenHTTP.Api.Protocol; - -namespace GenHTTP.Modules.Reflection; +namespace GenHTTP.Modules.Reflection; /// /// Implemented by handlers that use the handler @@ -11,8 +9,8 @@ public interface IServiceMethodProvider { /// - /// Retrieves the methods that are provided by this handler. + /// Allows to read or initialize a new method collection. /// - ValueTask GetMethodsAsync(IRequest request); + SynchronizedMethodCollection Methods { get; } } diff --git a/Modules/Reflection/MethodCollection.cs b/Modules/Reflection/MethodCollection.cs index f69840234..19f5b2923 100644 --- a/Modules/Reflection/MethodCollection.cs +++ b/Modules/Reflection/MethodCollection.cs @@ -1,5 +1,7 @@ -using GenHTTP.Api.Content; +using System.Runtime.CompilerServices; +using GenHTTP.Api.Content; using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Reflection.Routing; namespace GenHTTP.Modules.Reflection; @@ -25,33 +27,61 @@ public MethodCollection(IEnumerable methods) public ValueTask HandleAsync(IRequest request) { - var methods = FindProviders(request.Target.GetRemaining().ToString(), request.Method, out var others); + var foundOthers = false; - if (methods.Count == 1) - { - return methods[0].HandleAsync(request); - } - if (methods.Count > 1) + (MethodHandler, RoutingMatch)? directMatch = null; + (MethodHandler, RoutingMatch)? wildcardMatch = null; + + var requestTarget = request.Target; + + for (var i = 0; i < Methods.Count; i++) { - // if there is only one non-wildcard, use this one - var nonWildcards = methods.Where(m => !m.Operation.Path.IsWildcard).ToList(); + var method = Methods[i]; + + var operation = method.Operation; + + var route = operation.Route; + + var match = OperationRouter.TryMatch(requestTarget, route); + + if (match == null) + { + continue; + } - if (nonWildcards.Count == 1) + if (!operation.Configuration.SupportedMethods.Contains(request.Method)) + { + foundOthers = true; + continue; + } + + if (route.IsWildcard) + { + wildcardMatch = (method, match); + } + else if (directMatch != null) + { + throw new ProviderException(ResponseStatus.BadRequest, $"There are multiple methods matching '{requestTarget.Path}'"); + } + else { - return nonWildcards[0].HandleAsync(request); + directMatch = (method, match); } + } - throw new ProviderException(ResponseStatus.BadRequest, $"There are multiple methods matching '{request.Target.Path}'"); + if (directMatch != null) + { + return Execute(request, directMatch.Value); } - if (others.Count > 0) + if (wildcardMatch != null) { - throw new ProviderException(ResponseStatus.MethodNotAllowed, "There is no method of a matching request type", AddAllowHeader); + return Execute(request, wildcardMatch.Value); + } - void AddAllowHeader(IResponseBuilder b) - { - b.Header("Allow", string.Join(", ", others.Select(o => o.RawMethod.ToUpper()))); - } + if (foundOthers) + { + throw new ProviderException(ResponseStatus.MethodNotAllowed, "There is no method of a matching request type"); } return new ValueTask(); @@ -65,43 +95,9 @@ public async ValueTask PrepareAsync() } } - private List FindProviders(string path, FlexibleRequestMethod requestedMethod, out HashSet otherMethods) - { - otherMethods = []; - - var result = new List(2); - - foreach (var method in Methods) - { - if (method.Operation.Path.IsIndex && path == "/") - { - if (method.Configuration.SupportedMethods.Contains(requestedMethod)) - { - result.Add(method); - } - else - { - otherMethods.UnionWith(method.Configuration.SupportedMethods); - } - } - else - { - if (method.Operation.Path.Matcher.IsMatch(path)) - { - if (method.Configuration.SupportedMethods.Contains(requestedMethod)) - { - result.Add(method); - } - else - { - otherMethods.UnionWith(method.Configuration.SupportedMethods); - } - } - } - } - - return result; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ValueTask Execute(IRequest request, (MethodHandler, RoutingMatch) match) + => match.Item1.HandleAsync(request, match.Item2); #endregion diff --git a/Modules/Reflection/MethodHandler.cs b/Modules/Reflection/MethodHandler.cs index dd9580bda..3a483f31e 100644 --- a/Modules/Reflection/MethodHandler.cs +++ b/Modules/Reflection/MethodHandler.cs @@ -1,16 +1,23 @@ using System.Reflection; using System.Runtime.ExceptionServices; -using System.Text.RegularExpressions; + +using Cottle; using GenHTTP.Api.Content; using GenHTTP.Api.Protocol; -using GenHTTP.Api.Routing; using GenHTTP.Modules.Conversion.Serializers.Forms; +using GenHTTP.Modules.IO; +using GenHTTP.Modules.Pages; +using GenHTTP.Modules.Pages.Rendering; +using GenHTTP.Modules.Reflection.Generation; using GenHTTP.Modules.Reflection.Operations; +using GenHTTP.Modules.Reflection.Routing; namespace GenHTTP.Modules.Reflection; +public delegate ValueTask RequestInterception(IRequest request, IReadOnlyDictionary arguments); + /// /// Allows to invoke a function on a service oriented resource. /// @@ -22,19 +29,29 @@ namespace GenHTTP.Modules.Reflection; public sealed class MethodHandler : IHandler { private static readonly Dictionary NoArguments = []; + + private static readonly TemplateRenderer ErrorRenderer = Renderer.From(Resource.FromAssembly("CodeGenerationError.html").Build()); + + private Func>? _compiledMethod; + + private Func>? _compiledDelegate; + + private CodeGenerationException? _compilationError; + + private readonly RequestInterception _interceptor; #region Get-/Setters public Operation Operation { get; } - public IMethodConfiguration Configuration { get; } - private Func> InstanceProvider { get; } public MethodRegistry Registry { get; } private ResponseProvider ResponseProvider { get; } + private bool UseCodeGeneration { get; } + #endregion #region Initialization @@ -44,54 +61,111 @@ public sealed class MethodHandler : IHandler /// /// The operation to be executed and provided (use to create an operation) /// 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, Func> instanceProvider, IMethodConfiguration metaData, MethodRegistry registry) + public MethodHandler(Operation operation, Func> instanceProvider, MethodRegistry registry) { - Configuration = metaData; InstanceProvider = instanceProvider; Operation = operation; Registry = registry; ResponseProvider = new(registry); + + var effectiveMode = Operation.ExecutionSettings.Mode ?? ExecutionMode.Reflection; + + UseCodeGeneration = OptimizedDelegate.Supported && effectiveMode == ExecutionMode.Auto; + + _interceptor = InterceptAsync; } #endregion #region Functionality - public async ValueTask HandleAsync(IRequest request) + public ValueTask PrepareAsync() { - var arguments = await GetArguments(request); + if (UseCodeGeneration) + { + try + { + if (Operation.Delegate != null) + { + _compiledDelegate = OptimizedDelegate.Compile(Operation); + } + else + { + _compiledMethod = OptimizedDelegate.Compile(Operation); + } + } + catch (CodeGenerationException e) + { + _compilationError = e; + } + } - var interception = await InterceptAsync(request, arguments); + return ValueTask.CompletedTask; + } - if (interception is not null) + public ValueTask HandleAsync(IRequest request) => HandleAsync(request, new(0, null)); + + public ValueTask HandleAsync(IRequest request, RoutingMatch match) + { + if (match.Offset > 0) { - return interception; + request.Target.Advance(match.Offset); + } + + if (UseCodeGeneration) + { + if (_compilationError != null) + { + return RenderCompilationErrorAsync(request, _compilationError); + } + + return Operation.Delegate != null ? RunAsDelegate(request, match) : RunAsMethod(request, match); } - var result = await InvokeAsync(request, arguments.Values.ToArray()); + return RunViaReflection(request, match); + } + + private ValueTask RunAsDelegate(IRequest request, RoutingMatch match) + { + if (_compiledDelegate == null || Operation.Delegate == null) + throw new InvalidOperationException("Compiled delegate is not initialized"); - return await ResponseProvider.GetResponseAsync(request, Operation, await UnwrapAsync(result), null); + return _compiledDelegate(Operation.Delegate, Operation, request, this, Registry, match, _interceptor); } - private async ValueTask> GetArguments(IRequest request) + private async ValueTask RunAsMethod(IRequest request, RoutingMatch match) { - var targetParameters = Operation.Method.GetParameters(); + if (_compiledMethod == null) + throw new InvalidOperationException("Compiled method is not initialized"); - Match? sourceParameters = null; + var instance = await InstanceProvider(request); - if (!Operation.Path.IsIndex) - { - sourceParameters = Operation.Path.Matcher.Match(request.Target.GetRemaining().ToString()); + return await _compiledMethod(instance, Operation, request, this, Registry, match, _interceptor); + } - var matchedPath = WebPath.FromString(sourceParameters.Value); + private async ValueTask RunViaReflection(IRequest request, RoutingMatch match) + { + var arguments = await GetArguments(request, match); - foreach (var _ in matchedPath.Parts) request.Target.Advance(); + var interception = await InterceptAsync(request, arguments); + + if (interception is not null) + { + return interception; } + var result = await InvokeAsync(request, arguments.Values.ToArray()); + + return await ResponseProvider.GetResponseAsync(request, Operation, await UnwrapAsync(result)); + } + + private async ValueTask> GetArguments(IRequest request, RoutingMatch match) + { + var targetParameters = Operation.Method.GetParameters(); + if (targetParameters.Length > 0) { var targetArguments = new Dictionary(targetParameters.Length); @@ -109,8 +183,8 @@ public MethodHandler(Operation operation, Func> inst targetArguments[arg.Name] = arg.Source switch { OperationArgumentSource.Injected => ArgumentProvider.GetInjectedArgument(request, this, arg, Registry), - OperationArgumentSource.Path => ArgumentProvider.GetPathArgument(arg, sourceParameters, Registry), - OperationArgumentSource.Body => await ArgumentProvider.GetBodyArgumentAsync(request, arg, Registry), + OperationArgumentSource.Path => ArgumentProvider.GetPathArgument(arg.Name, arg.Type, match, Registry), + OperationArgumentSource.Body => await ArgumentProvider.GetBodyArgumentAsync(request, arg.Name, arg.Type, Registry), OperationArgumentSource.Query => ArgumentProvider.GetQueryArgument(request, bodyArguments, arg, Registry), OperationArgumentSource.Content => await ArgumentProvider.GetContentAsync(request, arg, Registry), OperationArgumentSource.Streamed => ArgumentProvider.GetStream(request), @@ -126,8 +200,6 @@ public MethodHandler(Operation operation, Func> inst return NoArguments; } - public ValueTask PrepareAsync() => ValueTask.CompletedTask; - private async ValueTask InterceptAsync(IRequest request, IReadOnlyDictionary arguments) { if (Operation.Interceptors.Count > 0) @@ -194,6 +266,20 @@ public MethodHandler(Operation operation, Func> inst return result; } + private static async ValueTask RenderCompilationErrorAsync(IRequest request, CodeGenerationException error) + { + var template = new Dictionary + { + ["exception"] = error.InnerException?.ToString() ?? string.Empty, + ["code"] = error.Code ?? string.Empty + }; + + var content = await ErrorRenderer.RenderAsync(template); + + return request.GetPage(content) + .Build(); + } + #endregion } diff --git a/Modules/Reflection/Operations/ArgumentProvider.cs b/Modules/Reflection/Operations/ArgumentProvider.cs index 1e72ed896..094d21009 100644 --- a/Modules/Reflection/Operations/ArgumentProvider.cs +++ b/Modules/Reflection/Operations/ArgumentProvider.cs @@ -1,7 +1,10 @@ -using System.Text.RegularExpressions; +using System.Runtime.CompilerServices; + using GenHTTP.Api.Content; using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Conversion; +using GenHTTP.Modules.Reflection.Routing; namespace GenHTTP.Modules.Reflection.Operations; @@ -21,26 +24,21 @@ public static class ArgumentProvider return null; } - public static object? GetPathArgument(OperationArgument argument, Match? matchedPath, MethodRegistry registry) + public static object? GetPathArgument(string name, Type type, RoutingMatch match, MethodRegistry registry) { - if (matchedPath != null) + if (match.PathArguments?.TryGetValue(name, out var pathArgument) ?? false) { - var sourceArgument = matchedPath.Groups[argument.Name]; - - if (sourceArgument.Success) - { - return sourceArgument.Value.ConvertTo(argument.Type, registry.Formatting); - } + return pathArgument.ConvertTo(type, registry.Formatting); } return null; } - public static async ValueTask GetBodyArgumentAsync(IRequest request, OperationArgument argument, MethodRegistry registry) + public static async ValueTask GetBodyArgumentAsync(IRequest request, string name, Type type, MethodRegistry registry) { if (request.Content == null) { - throw new ProviderException(ResponseStatus.BadRequest, $"Argument '{argument.Name}' is expected to be read from the request body but the request does not contain any payload"); + throw new ProviderException(ResponseStatus.BadRequest, $"Argument '{name}' is expected to be read from the request body but the request does not contain any payload"); } object? result = null; @@ -51,7 +49,7 @@ public static class ArgumentProvider if (!string.IsNullOrWhiteSpace(body)) { - result = body.ConvertTo(argument.Type, registry.Formatting); + result = body.ConvertTo(type, registry.Formatting); } if (request.Content.CanSeek) @@ -104,7 +102,8 @@ public static class ArgumentProvider } } - public static object? GetStream(IRequest request) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream GetStream(IRequest request) { if (request.Content == null) { @@ -113,4 +112,5 @@ public static class ArgumentProvider return request.Content; } + } diff --git a/Modules/Reflection/Operations/Operation.cs b/Modules/Reflection/Operations/Operation.cs index 18c0c710e..ba6351116 100644 --- a/Modules/Reflection/Operations/Operation.cs +++ b/Modules/Reflection/Operations/Operation.cs @@ -1,4 +1,5 @@ using System.Reflection; +using GenHTTP.Modules.Reflection.Routing; namespace GenHTTP.Modules.Reflection.Operations; @@ -12,10 +13,26 @@ public sealed class Operation /// public MethodInfo Method { get; } + /// + /// If available, the delegate to be executed at runtime + /// to retrieve a result. + /// + public Delegate? Delegate { get; } + + /// + /// Specifies the way the method should be executed. + /// + public ExecutionSettings ExecutionSettings { get; } + + /// + /// The configuration of this method. + /// + public IMethodConfiguration Configuration { get; } + /// /// Information about the endpoint provided by this operation. /// - public OperationPath Path { get; } + public OperationRoute Route { get; } /// /// The arguments expected by this operation. @@ -36,10 +53,13 @@ public sealed class Operation #region Initialization - public Operation(MethodInfo method, OperationPath path, OperationResult result, IReadOnlyDictionary arguments, IReadOnlyList interceptors) + public Operation(MethodInfo method, Delegate? del, ExecutionSettings executionSettings, IMethodConfiguration configuration, OperationRoute route, OperationResult result, IReadOnlyDictionary arguments, IReadOnlyList interceptors) { Method = method; - Path = path; + Delegate = del; + ExecutionSettings = executionSettings; + Configuration = configuration; + Route = route; Result = result; Arguments = arguments; Interceptors = interceptors; diff --git a/Modules/Reflection/Operations/OperationBuilder.cs b/Modules/Reflection/Operations/OperationBuilder.cs index d3878cdcf..a01b812f7 100644 --- a/Modules/Reflection/Operations/OperationBuilder.cs +++ b/Modules/Reflection/Operations/OperationBuilder.cs @@ -1,21 +1,18 @@ using System.Reflection; using System.Text; using System.Text.RegularExpressions; - using GenHTTP.Api.Content; using GenHTTP.Api.Protocol; +using GenHTTP.Modules.Reflection.Routing; +using GenHTTP.Modules.Reflection.Routing.Segments; namespace GenHTTP.Modules.Reflection.Operations; -public static partial class OperationBuilder +public static class OperationBuilder { - private static readonly Regex VarPattern = CreateVarPattern(); - - private static readonly Regex RegexPattern = CreateRegexPattern(); + private static readonly OperationRoute IndexRoute = new("/", [new ClosingSegment(false, false)], false); - private static readonly Regex EmptyWildcardRoute = CreateEmptyWildcardRoute(); - - private static readonly Regex EmptyRoute = CreateEmptyRoute(); + private static readonly OperationRoute WildcardIndexRoute = new("/", [new ClosingSegment(false, true)], true); #region Functionality @@ -25,74 +22,61 @@ public static partial class OperationBuilder /// /// The path definition of the endpoint, e.g. "/users/:id" /// The actual .NET method to be executed to retrieve a result + /// If the method is defined by a delegate and not as an instance method, pass it here /// 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(IRequest request, string? definition, MethodInfo method, MethodRegistry registry, bool forceTrailingSlash = false) + public static Operation Create(IRequest request, string? definition, MethodInfo method, Delegate? del, ExecutionSettings executionSettings, IMethodConfiguration configuration, MethodRegistry registry, bool forceTrailingSlash = false) { var isWildcard = CheckWildcardRoute(method.ReturnType); - OperationPath path; - - var pathArguments = new HashSet(StringComparer.OrdinalIgnoreCase); + OperationRoute route; if (string.IsNullOrWhiteSpace(definition)) { - if (isWildcard) - { - path = new OperationPath("/", EmptyWildcardRoute, true, true); - } - else - { - path = new OperationPath("/", EmptyRoute, true, false); - } + route = isWildcard ? WildcardIndexRoute : IndexRoute; } else { var normalized = Normalize(definition); - var matchBuilder = new StringBuilder(normalized); - var nameBuilder = new StringBuilder(WithPrefix(normalized)); - - // convert parameters of the format ":var" into appropriate groups - foreach (Match match in VarPattern.Matches(definition)) - { - var name = match.Groups[1].Value; - - matchBuilder.Replace(match.Value, name.ToParameter()); - nameBuilder.Replace(match.Value, "{" + name + "}"); + var parts = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries); - pathArguments.Add(name); - } + var segments = new List(parts.Length + 1); - // convert advanced regex params as well - foreach (Match match in RegexPattern.Matches(definition)) + foreach (var part in parts) { - var name = match.Groups[1].Value; + var colonCount = part.Count(c => c == ':'); + + if (part.StartsWith(':') && IsValidVariable(part.AsSpan()[1..]) && colonCount == 1) + { + segments.Add(new SimpleVariableSegment(part[1..])); + } + else if (part.Contains("?<") || colonCount > 0) + { + segments.Add(new RegexSegment(part)); + } + else + { + segments.Add(new StringSegment(part)); + } + } - nameBuilder.Replace(match.Value, "{" + name + "}"); + segments.Add(new ClosingSegment(forceTrailingSlash || definition.EndsWith('/'), isWildcard)); - pathArguments.Add(name); - } + var displayName = GetDisplayName(definition, forceTrailingSlash); - if (forceTrailingSlash || definition.EndsWith('/')) - { - matchBuilder.Append('/'); - nameBuilder.Append('/'); - } - else - { - matchBuilder.Append("(/|)"); - } + route = new OperationRoute(displayName, segments, isWildcard); + } + + var pathArguments = new HashSet(StringComparer.OrdinalIgnoreCase); - if (!isWildcard) + foreach (var segment in route.Segments) + { + foreach (var pathArg in segment.ProvidedArguments) { - matchBuilder.Append('$'); + pathArguments.Add(pathArg); } - - var matcher = new Regex($"^/{matchBuilder}", RegexOptions.Compiled); - - path = new OperationPath(nameBuilder.ToString(), matcher, false, isWildcard); } var arguments = SignatureAnalyzer.GetArguments(request, method, pathArguments, registry); @@ -101,7 +85,7 @@ public static Operation Create(IRequest request, string? definition, MethodInfo var interceptors = InterceptorAnalyzer.GetInterceptors(method); - return new Operation(method, path, result, arguments, interceptors); + return new Operation(method, del, executionSettings, configuration, route, result, arguments, interceptors); } private static bool CheckWildcardRoute(Type returnType) @@ -124,6 +108,39 @@ private static bool CheckWildcardRoute(Type returnType) private static bool IsHandlerType(Type returnType) => typeof(IHandlerBuilder).IsAssignableFrom(returnType) || typeof(IHandler).IsAssignableFrom(returnType); + private static string GetDisplayName(string definition, bool forceTrailingSlash) + { + var nameBuilder = new StringBuilder(Normalize(definition)); + + ReplaceMatches(nameBuilder, definition, ":([A-Za-z0-9]+)"); // :var + + ReplaceMatches(nameBuilder, definition, @"\(\?\<([a-z]+)\>([^)]+)\)"); // (?[0-9]{12,13}) + + if (forceTrailingSlash || definition.EndsWith('/')) + { + nameBuilder.Append('/'); + } + + if (nameBuilder.Length > 1) + { + nameBuilder.Insert(0, '/'); + } + + return nameBuilder.ToString(); + } + + private static void ReplaceMatches(StringBuilder sb, string definition, string regex) + { + var pattern = new Regex(regex); + + var matches = pattern.Matches(definition); + + foreach (Match match in matches) + { + sb.Replace(match.Value, $"{{{match.Groups[1].Value}}}"); + } + } + private static string Normalize(string definition) { int trimStart = 0, trimEnd = 0; @@ -144,35 +161,19 @@ private static string Normalize(string definition) return definition.Substring(trimStart, definition.Length - trimStart - trimEnd); } - private static string WithPrefix(string path) + private static bool IsValidVariable(ReadOnlySpan value) { - if (path.Length > 0) + foreach (var c in value) { - if (path[0] != '/') + if (c is (< 'A' or > 'Z') and (< 'a' or > 'z') and (< '0' or > '9')) { - return $"{path}"; + return false; } } - return path; + return true; } #endregion - #region Regular Expressions - - [GeneratedRegex(@"\:([a-z]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")] - private static partial Regex CreateVarPattern(); - - [GeneratedRegex(@"\(\?\<([a-z]+)\>([^)]+)\)", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")] - private static partial Regex CreateRegexPattern(); - - [GeneratedRegex("^.*", RegexOptions.Compiled)] - private static partial Regex CreateEmptyWildcardRoute(); - - [GeneratedRegex("^(/|)$", RegexOptions.Compiled)] - private static partial Regex CreateEmptyRoute(); - - #endregion - } diff --git a/Modules/Reflection/Operations/OperationPath.cs b/Modules/Reflection/Operations/OperationPath.cs deleted file mode 100644 index dcfd96107..000000000 --- a/Modules/Reflection/Operations/OperationPath.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Text.RegularExpressions; - -namespace GenHTTP.Modules.Reflection.Operations; - -public sealed class OperationPath -{ - - #region Get-/Setters - - /// - /// An user-friendly string to display this path. - /// - public string Name { get; } - - /// - /// The path of the method, converted into a regular - /// expression to be evaluated at runtime. - /// - public Regex Matcher { get; } - - /// - /// True, if this route matches the index of the - /// scoped segment. - /// - public bool IsIndex { get; } - - /// - /// True, if this is a wildcard route that is created - /// when returning a handler or handler builder from - /// a method. - /// - /// - /// Wildcard routes have a lower priority compared to - /// non-wildcard routes and will not be considered - /// ambiguous. - /// - public bool IsWildcard { get; } - - #endregion - - #region Initialization - - public OperationPath(string name, Regex matcher, bool isIndex, bool isWildcard) - { - Matcher = matcher; - Name = name; - IsIndex = isIndex; - IsWildcard = isWildcard; - } - - #endregion - -} diff --git a/Modules/Reflection/Operations/SignatureAnalyzer.cs b/Modules/Reflection/Operations/SignatureAnalyzer.cs index e357bfa43..00e8459e9 100644 --- a/Modules/Reflection/Operations/SignatureAnalyzer.cs +++ b/Modules/Reflection/Operations/SignatureAnalyzer.cs @@ -133,7 +133,7 @@ public static OperationResult GetResult(MethodInfo method, MethodRegistry regist { return type.IsGenericallyVoid() ? null : type.GenericTypeArguments[0]; } - if (type == typeof(ValueTask) || type == typeof(Task)) + if (type.IsAsync()) { return null; } @@ -145,4 +145,5 @@ public static OperationResult GetResult(MethodInfo method, MethodRegistry regist return type; } + } diff --git a/Modules/Reflection/Resources/CodeGenerationError.html b/Modules/Reflection/Resources/CodeGenerationError.html new file mode 100644 index 000000000..3477a1125 --- /dev/null +++ b/Modules/Reflection/Resources/CodeGenerationError.html @@ -0,0 +1,22 @@ + + + + + Failed to generate handler + + + + + +

Code Generation Failed

+ +

The server failed to compile the code generated for this endpoint. Please submit an error report to our GitHub repository.

+ +

Exception

+
{exception}
+ +

Generated Code

+
{code}
+ + + diff --git a/Modules/Reflection/ResponseProvider.cs b/Modules/Reflection/ResponseProvider.cs index ce084b56c..77bfb7047 100644 --- a/Modules/Reflection/ResponseProvider.cs +++ b/Modules/Reflection/ResponseProvider.cs @@ -116,7 +116,6 @@ private static IResponse GetBinaryResponse(IRequest request, object data, Action private IResponse GetFormattedResponse(IRequest request, object result, Type type, Action? adjustments) => request.Respond() .Content(Registry.Formatting.Write(result, type) ?? string.Empty) - .Type(ContentType.TextPlain) .Adjust(adjustments) .Build(); diff --git a/Modules/Reflection/Result.cs b/Modules/Reflection/Result.cs index 567bee73f..8944ce1c3 100644 --- a/Modules/Reflection/Result.cs +++ b/Modules/Reflection/Result.cs @@ -123,7 +123,7 @@ public Result Encoding(string encoding) return this; } - void IResultWrapper.Apply(IResponseBuilder builder) + public void Apply(IResponseBuilder builder) { if (_status != null) { diff --git a/Modules/Reflection/Routing/IRoutingSegment.cs b/Modules/Reflection/Routing/IRoutingSegment.cs new file mode 100644 index 000000000..b7b7b3d30 --- /dev/null +++ b/Modules/Reflection/Routing/IRoutingSegment.cs @@ -0,0 +1,28 @@ +using GenHTTP.Api.Routing; + +namespace GenHTTP.Modules.Reflection.Routing; + +/// +/// A piece of logic that knows how to check the current segment +/// for a given route and extracts the path arguments from the +/// incoming request. +/// +public interface IRoutingSegment +{ + + /// + /// The path arguments that are provided by this segment, if any. + /// + string[] ProvidedArguments { get; } + + /// + /// Checks whether the segment is responsible for handling the incoming + /// request. + /// + /// The route of the incoming request + /// The offset from the current routing position to be applied + /// The sink to write argument values to + /// Whether the segment matched and what offset to be applied for this segment + (bool matched, int offsetBy) TryMatch(RoutingTarget target, int offset, ref PathArgumentSink argumentSink); + +} diff --git a/Modules/Reflection/Routing/OperationRoute.cs b/Modules/Reflection/Routing/OperationRoute.cs new file mode 100644 index 000000000..db5bcf7f2 --- /dev/null +++ b/Modules/Reflection/Routing/OperationRoute.cs @@ -0,0 +1,29 @@ +namespace GenHTTP.Modules.Reflection.Routing; + +public sealed class OperationRoute(string name, IReadOnlyList segments, bool isWildcard) +{ + + /// + /// An user-friendly string to display this path. + /// + public string Name { get; } = name; + + /// + /// True, if this is a wildcard route that is created + /// when returning a handler or handler builder from + /// a method. + /// + /// + /// Wildcard routes have a lower priority compared to + /// non-wildcard routes and will not be considered + /// ambiguous. + /// + public bool IsWildcard { get; } = isWildcard; + + /// + /// The segments to be evaluated to check whether the route + /// has been matched. + /// + public IReadOnlyList Segments { get; } = segments; + +} diff --git a/Modules/Reflection/Routing/OperationRouter.cs b/Modules/Reflection/Routing/OperationRouter.cs new file mode 100644 index 000000000..9647a5546 --- /dev/null +++ b/Modules/Reflection/Routing/OperationRouter.cs @@ -0,0 +1,39 @@ +using GenHTTP.Api.Routing; + +namespace GenHTTP.Modules.Reflection.Routing; + +internal static class OperationRouter +{ + + /// + /// Checks whether the requested path matches the given route. + /// + /// The requested path + /// The route to check for + /// A match if the route is capable of handling the incoming request + internal static RoutingMatch? TryMatch(RoutingTarget target, OperationRoute route) + { + var segments = route.Segments; + + var sink = new PathArgumentSink(); + + var offset = 0; + + for (var i = 0; i < segments.Count; i++) + { + var current = segments[i]; + + var (matched, offsetBy) = current.TryMatch(target, i, ref sink); + + if (!matched) + { + return null; + } + + offset += offsetBy; + } + + return new(offset, sink.Arguments); + } + +} diff --git a/Modules/Reflection/Routing/PathArgumentSink.cs b/Modules/Reflection/Routing/PathArgumentSink.cs new file mode 100644 index 000000000..b18a96696 --- /dev/null +++ b/Modules/Reflection/Routing/PathArgumentSink.cs @@ -0,0 +1,15 @@ +namespace GenHTTP.Modules.Reflection.Routing; + +/// +/// Passed as ref struct to routing segments to allow them +/// to add path argument values during matching. +/// +public struct PathArgumentSink +{ + + internal Dictionary? Arguments; + + public void Add(string key, string value) + => (Arguments ??= new()).Add(key, value); + +} diff --git a/Modules/Reflection/Routing/RoutingMatch.cs b/Modules/Reflection/Routing/RoutingMatch.cs new file mode 100644 index 000000000..b33b57e67 --- /dev/null +++ b/Modules/Reflection/Routing/RoutingMatch.cs @@ -0,0 +1,9 @@ +namespace GenHTTP.Modules.Reflection.Routing; + +/// +/// Returned by the operation router if a web service method matched +/// the incoming request. +/// +/// The offset to advance the request target by +/// The arguments read from the request path, if any +public record RoutingMatch(int Offset, IReadOnlyDictionary? PathArguments); diff --git a/Modules/Reflection/Routing/Segments/ClosingSegment.cs b/Modules/Reflection/Routing/Segments/ClosingSegment.cs new file mode 100644 index 000000000..ef91a7583 --- /dev/null +++ b/Modules/Reflection/Routing/Segments/ClosingSegment.cs @@ -0,0 +1,40 @@ +using GenHTTP.Api.Routing; + +namespace GenHTTP.Modules.Reflection.Routing.Segments; + +/// +/// Appended to every operation to check, whether a trailing slash +/// has been requested and enables wildcards for routes. +/// +/// If set to true, the segment will only match if a trailing slash has been passed with the request +/// If set to false, the segment will not match if additional path segments have been passed +public sealed class ClosingSegment(bool forceTrailingSlash, bool wildcard) : IRoutingSegment +{ + + public string[] ProvidedArguments { get; } = []; + + public (bool matched, int offsetBy) TryMatch(RoutingTarget target, int offset, ref PathArgumentSink argumentSink) + { + var ended = target.Next(offset) is null; + + if (!ended) + { + return wildcard ? (true, 0) : (false, 0); + } + + var endedWithSlash = target.Path.TrailingSlash; + + if (endedWithSlash) + { + return (true, 0); + } + + if (forceTrailingSlash) + { + return (false, 0); + } + + return (true, offset > 0 ? -1 : 0); + } + +} diff --git a/Modules/Reflection/Routing/Segments/RegexSegment.cs b/Modules/Reflection/Routing/Segments/RegexSegment.cs new file mode 100644 index 000000000..56b1324e7 --- /dev/null +++ b/Modules/Reflection/Routing/Segments/RegexSegment.cs @@ -0,0 +1,83 @@ +using System.Text; +using System.Text.RegularExpressions; + +using GenHTTP.Api.Routing; + +namespace GenHTTP.Modules.Reflection.Routing.Segments; + +/// +/// Checks for multiple variables in one segment or advanced +/// regex variable definitions. +/// +internal sealed partial class RegexSegment : IRoutingSegment +{ + private static readonly Regex VarPattern = CreateVarPattern(); + + private static readonly Regex RegexPattern = CreateRegexPattern(); + + private readonly Regex _matcher; + + public string[] ProvidedArguments { get; } + + public RegexSegment(string definition) + { + var providedArguments = new List(); + + var matchBuilder = new StringBuilder(definition); + + // convert parameters of the format ":var" into appropriate groups + foreach (Match match in VarPattern.Matches(definition)) + { + var name = match.Groups[1].Value; + + matchBuilder.Replace(match.Value, name.ToParameter()); + + providedArguments.Add(name); + } + + // convert advanced regex params as well + foreach (Match match in RegexPattern.Matches(definition)) + { + var name = match.Groups[1].Value; + + providedArguments.Add(name); + } + + _matcher = new Regex($"^{matchBuilder}$", RegexOptions.Compiled); + + ProvidedArguments = providedArguments.ToArray(); + } + + public (bool matched, int offsetBy) TryMatch(RoutingTarget target, int offset, ref PathArgumentSink argumentSink) + { + var part = target.Next(offset); + + if (part is null) + { + return (false, 0); + } + + var match = _matcher.Match(part.Value); + + if (!match.Success) + { + return (false, 0); + } + + var groups = match.Groups; + + for (var i = 1; i < groups.Count; i++) + { + argumentSink.Add(groups[i].Name, groups[i].Value); + } + + return (true, 1); + } + + [GeneratedRegex(@"\:([a-z]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")] + private static partial Regex CreateVarPattern(); + + [GeneratedRegex(@"\(\?\<([a-z]+)\>([^)]+)\)", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")] + private static partial Regex CreateRegexPattern(); + +} diff --git a/Modules/Reflection/Routing/Segments/SimpleVariableSegment.cs b/Modules/Reflection/Routing/Segments/SimpleVariableSegment.cs new file mode 100644 index 000000000..4b11af734 --- /dev/null +++ b/Modules/Reflection/Routing/Segments/SimpleVariableSegment.cs @@ -0,0 +1,31 @@ +using GenHTTP.Api.Routing; + +namespace GenHTTP.Modules.Reflection.Routing.Segments; + +/// +/// Matches a single variable, such as "/:var/". +/// +/// The name of the variable to look for (without the colon) +/// +/// This implementation allows a simple path variable (which is the common use case) +/// without the need of creating a regex for parsing. +/// +internal sealed class SimpleVariableSegment(string variableName) : IRoutingSegment +{ + + public string[] ProvidedArguments { get; } = [variableName]; + + public (bool matched, int offsetBy) TryMatch(RoutingTarget target, int offset, ref PathArgumentSink argumentSink) + { + var part = target.Next(offset); + + if (part is null) + { + return (false, 0); + } + + argumentSink.Add(variableName, part.Value); + return (true, 1); + } + +} diff --git a/Modules/Reflection/Routing/Segments/StringSegment.cs b/Modules/Reflection/Routing/Segments/StringSegment.cs new file mode 100644 index 000000000..837d6f03c --- /dev/null +++ b/Modules/Reflection/Routing/Segments/StringSegment.cs @@ -0,0 +1,17 @@ +using GenHTTP.Api.Routing; + +namespace GenHTTP.Modules.Reflection.Routing.Segments; + +/// +/// Matches a single segment within a requested path, such as "/segment/". +/// +/// The segment to match +internal sealed class StringSegment(string segment) : IRoutingSegment +{ + + public string[] ProvidedArguments { get; } = []; + + public (bool matched, int offsetBy) TryMatch(RoutingTarget target, int offset, ref PathArgumentSink argumentSink) + => (target.Next(offset)?.Value == segment, 1); + +} diff --git a/Modules/Reflection/SynchronizedMethodCollection.cs b/Modules/Reflection/SynchronizedMethodCollection.cs new file mode 100644 index 000000000..dd877367a --- /dev/null +++ b/Modules/Reflection/SynchronizedMethodCollection.cs @@ -0,0 +1,68 @@ +using GenHTTP.Api.Protocol; + +namespace GenHTTP.Modules.Reflection; + +/// +/// Ensures that a method collection is initialized exactly once, as this +/// might be a heavy task involving code generation. Also provides an +/// execution path that will not generate a state machine. +/// +public class SynchronizedMethodCollection(Func> factory) +{ + private MethodCollection? _instance; + + private Task? _initialization; + + #region State Handling + + /// + /// Fetches the method collection to be used. + /// + /// The currently handled request + /// The method collection to be used + public Task GetAsync(IRequest request) + { + if (_instance != null) + { + return Task.FromResult(_instance); + } + + var init = _initialization; + + if (init != null) + { + return init; + } + + init = InitializeAsync(request); + + var original = Interlocked.CompareExchange(ref _initialization, init, null); + + return original ?? init; + } + + private async Task InitializeAsync(IRequest request) + { + var instance = await factory(request).ConfigureAwait(false); + _instance = instance; + return instance; + } + + #endregion + + #region Request Handling + + /// + /// Instructs the cached method collection to handle the given request. + /// + /// The request to be handled + /// The response generated by the method collection + public ValueTask HandleAsync(IRequest request) + => _instance?.HandleAsync(request) ?? HandleWithInitialization(request); + + private async ValueTask HandleWithInitialization(IRequest request) + => await (await GetAsync(request)).HandleAsync(request); + + #endregion + +} diff --git a/Modules/Webservices/Extensions.cs b/Modules/Webservices/Extensions.cs index 3b9b570de..4449f1de4 100644 --- a/Modules/Webservices/Extensions.cs +++ b/Modules/Webservices/Extensions.cs @@ -3,6 +3,7 @@ using GenHTTP.Modules.Conversion.Formatters; using GenHTTP.Modules.Conversion.Serializers; using GenHTTP.Modules.Layouting.Provider; +using GenHTTP.Modules.Reflection; using GenHTTP.Modules.Reflection.Injectors; using GenHTTP.Modules.Webservices.Provider; @@ -23,8 +24,8 @@ public static class Extensions /// Optionally the injectors to be used by this service /// Optionally the formats to be used by this service /// Optionally the formatters to be used by this service - public static LayoutBuilder AddService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder layout, string path, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) where T : new() => - layout.Add(path, ServiceResource.From().Configured(injectors, serializers, formatters)); + public static LayoutBuilder AddService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder layout, string path, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null, ExecutionMode? mode = null) where T : new() => + layout.Add(path, ServiceResource.From().Configured(injectors, serializers, formatters, mode)); /// /// Adds the given webservice resource to the layout, accessible using @@ -35,10 +36,10 @@ public static class Extensions /// Optionally the injectors to be used by this service /// Optionally the formats to be used by this service /// Optionally the formatters to be used by this service - public static LayoutBuilder AddService(this LayoutBuilder layout, string path, object instance, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) => - layout.Add(path, ServiceResource.From(instance).Configured(injectors, serializers, formatters)); + public static LayoutBuilder AddService(this LayoutBuilder layout, string path, object instance, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null, ExecutionMode? mode = null) => + layout.Add(path, ServiceResource.From(instance).Configured(injectors, serializers, formatters, mode)); - private static ServiceResourceBuilder Configured(this ServiceResourceBuilder builder, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null) + private static ServiceResourceBuilder Configured(this ServiceResourceBuilder builder, IBuilder? injectors = null, IBuilder? serializers = null, IBuilder? formatters = null, ExecutionMode? mode = null) { if (injectors != null) { @@ -55,6 +56,12 @@ private static ServiceResourceBuilder Configured(this ServiceResourceBuilder bui builder.Formatters(formatters); } + if (mode != null) + { + builder.ExecutionMode(mode.Value); + } + return builder; } + } diff --git a/Modules/Webservices/Provider/ServiceResourceBuilder.cs b/Modules/Webservices/Provider/ServiceResourceBuilder.cs index ceac8ed6e..a5730ef23 100644 --- a/Modules/Webservices/Provider/ServiceResourceBuilder.cs +++ b/Modules/Webservices/Provider/ServiceResourceBuilder.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; - using GenHTTP.Api.Content; using GenHTTP.Api.Infrastructure; using GenHTTP.Api.Protocol; @@ -7,12 +6,13 @@ using GenHTTP.Modules.Conversion; using GenHTTP.Modules.Conversion.Formatters; using GenHTTP.Modules.Conversion.Serializers; + using GenHTTP.Modules.Reflection; using GenHTTP.Modules.Reflection.Injectors; namespace GenHTTP.Modules.Webservices.Provider; -public sealed class ServiceResourceBuilder : IHandlerBuilder, IRegistryBuilder +public sealed class ServiceResourceBuilder : IReflectionFrameworkBuilder { private readonly List _concerns = []; @@ -26,6 +26,8 @@ public sealed class ServiceResourceBuilder : IHandlerBuilder? _serializers; + private ExecutionMode? _executionMode; + #region Functionality public ServiceResourceBuilder Type<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : new() => Instance(new T()); @@ -68,6 +70,16 @@ public ServiceResourceBuilder Formatters(IBuilder registry) return this; } + /// + /// Sets the execution mode to be used to run functions. + /// + /// The mode to be used for execution + public ServiceResourceBuilder ExecutionMode(ExecutionMode mode) + { + _executionMode = mode; + return this; + } + public ServiceResourceBuilder Add(IConcernBuilder concern) { _concerns.Add(concern); @@ -88,7 +100,9 @@ public IHandler Build() var extensions = new MethodRegistry(serializers, injectors, formatters); - return Concerns.Chain(_concerns, new ServiceResourceRouter(type, instanceProvider, extensions)); + var executionSettings = new ExecutionSettings(_executionMode); + + return Concerns.Chain(_concerns, new ServiceResourceRouter(type, instanceProvider, executionSettings, extensions)); } #endregion diff --git a/Modules/Webservices/Provider/ServiceResourceRouter.cs b/Modules/Webservices/Provider/ServiceResourceRouter.cs index b891dba82..7e6959e76 100644 --- a/Modules/Webservices/Provider/ServiceResourceRouter.cs +++ b/Modules/Webservices/Provider/ServiceResourceRouter.cs @@ -10,7 +10,6 @@ namespace GenHTTP.Modules.Webservices.Provider; public sealed class ServiceResourceRouter : IHandler, IServiceMethodProvider { - private MethodCollection? _methods; #region Get-/Setters @@ -20,15 +19,22 @@ public sealed class ServiceResourceRouter : IHandler, IServiceMethodProvider private MethodRegistry Registry { get; } - #endregion + private ExecutionSettings ExecutionSettings { get; } + + public SynchronizedMethodCollection Methods { get; } + + #endregion #region Initialization - public ServiceResourceRouter(Type type, Func> instanceProvider, MethodRegistry registry) + public ServiceResourceRouter(Type type, Func> instanceProvider, ExecutionSettings executionSettings, MethodRegistry registry) { Type = type; InstanceProvider = instanceProvider; + ExecutionSettings = executionSettings; Registry = registry; + + Methods = new SynchronizedMethodCollection(GetMethodsAsync); } #endregion @@ -37,12 +43,10 @@ public ServiceResourceRouter(Type type, Func> instan public ValueTask PrepareAsync() => ValueTask.CompletedTask; - public async ValueTask HandleAsync(IRequest request) => await (await GetMethodsAsync(request)).HandleAsync(request); + public ValueTask HandleAsync(IRequest request) => Methods.HandleAsync(request); - public async ValueTask GetMethodsAsync(IRequest request) + private async Task GetMethodsAsync(IRequest request) { - if (_methods != null) return _methods; - var found = new List(); foreach (var method in Type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) @@ -51,9 +55,9 @@ public async ValueTask GetMethodsAsync(IRequest request) if (attribute is not null) { - var operation = OperationBuilder.Create(request, attribute.Path, method, Registry); + var operation = OperationBuilder.Create(request, attribute.Path, method, null, ExecutionSettings, attribute, Registry); - found.Add(new MethodHandler(operation, InstanceProvider, attribute, Registry)); + found.Add(new MethodHandler(operation, InstanceProvider, Registry)); } } @@ -61,7 +65,7 @@ public async ValueTask GetMethodsAsync(IRequest request) await result.PrepareAsync(); - return _methods = result; + return result; } #endregion diff --git a/Playground/GenHTTP.Playground.csproj b/Playground/GenHTTP.Playground.csproj index 0735ef23d..a267f8ca5 100644 --- a/Playground/GenHTTP.Playground.csproj +++ b/Playground/GenHTTP.Playground.csproj @@ -46,6 +46,7 @@ + diff --git a/Testing/Acceptance/Modules/Controllers/ActionTests.cs b/Testing/Acceptance/Modules/Controllers/ActionTests.cs index ad90205ed..7898aee0b 100644 --- a/Testing/Acceptance/Modules/Controllers/ActionTests.cs +++ b/Testing/Acceptance/Modules/Controllers/ActionTests.cs @@ -1,10 +1,13 @@ using System.Net; using System.Net.Http.Headers; + using GenHTTP.Api.Content; using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Controllers; using GenHTTP.Modules.IO; using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Reflection; namespace GenHTTP.Testing.Acceptance.Modules.Controllers; @@ -14,7 +17,7 @@ public sealed class ActionTests #region Helpers - private async Task GetRunnerAsync(TestEngine engine) => await TestHost.RunAsync(Layout.Create().AddController("t"), engine: engine); + private async Task GetRunnerAsync(TestEngine engine, ExecutionMode mode) => await TestHost.RunAsync(Layout.Create().AddController("t", mode: mode), engine: engine); #endregion @@ -53,10 +56,10 @@ public void Void() { } #region Tests [TestMethod] - [MultiEngineTest] - public async Task TestIndex(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestIndex(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/"); @@ -65,10 +68,10 @@ public async Task TestIndex(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestAction(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestAction(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/action/"); @@ -77,10 +80,10 @@ public async Task TestAction(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestActionWithQuery(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestActionWithQuery(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/action/?query=0815"); @@ -89,10 +92,10 @@ public async Task TestActionWithQuery(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestActionWithQueryFromBody(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestActionWithQueryFromBody(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); var dict = new Dictionary { @@ -113,10 +116,10 @@ public async Task TestActionWithQueryFromBody(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestActionWithBody(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestActionWithBody(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); var request = runner.GetRequest("/t/action/"); @@ -132,10 +135,10 @@ public async Task TestActionWithBody(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestActionWithParameter(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestActionWithParameter(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/simple-action/4711/"); @@ -144,10 +147,10 @@ public async Task TestActionWithParameter(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestActionWithBadParameter(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestActionWithBadParameter(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/simple-action/string/"); @@ -155,10 +158,10 @@ public async Task TestActionWithBadParameter(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestActionWithMixedParameters(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestActionWithMixedParameters(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/complex-action/1/2/?three=3"); @@ -167,10 +170,10 @@ public async Task TestActionWithMixedParameters(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestActionWithNoResult(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestActionWithNoResult(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/void/"); @@ -178,10 +181,10 @@ public async Task TestActionWithNoResult(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestNonExistingAction(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestNonExistingAction(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/nope/"); @@ -189,10 +192,10 @@ public async Task TestNonExistingAction(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestHypenCasing(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestHypenCasing(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/hypen-casing-99/"); @@ -201,10 +204,10 @@ public async Task TestHypenCasing(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestIndexController(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestIndexController(TestEngine engine, ExecutionMode mode) { - await using var runner = await TestHost.RunAsync(Layout.Create().IndexController(), engine: engine); + await using var runner = await TestHost.RunAsync(Layout.Create().IndexController(mode: mode), engine: engine); using var response = await runner.GetResponseAsync("/simple-action/4711/"); diff --git a/Testing/Acceptance/Modules/Controllers/DataTests.cs b/Testing/Acceptance/Modules/Controllers/DataTests.cs index 8a4dcc9a4..1c8df2161 100644 --- a/Testing/Acceptance/Modules/Controllers/DataTests.cs +++ b/Testing/Acceptance/Modules/Controllers/DataTests.cs @@ -13,12 +13,13 @@ public sealed class DataTests #region Helpers - private static async Task GetHostAsync(TestEngine engine) + private static async Task GetHostAsync(TestEngine engine, ExecutionMode mode) { var app = Layout.Create() .AddController("t", serializers: Serialization.Default(), injectors: Injection.Default(), - formatters: Formatting.Default()); + formatters: Formatting.Default(), + mode: mode); return await TestHost.RunAsync(app, engine: engine); } @@ -39,10 +40,10 @@ public class TestController #region Tests [TestMethod] - [MultiEngineTest] - public async Task TestDateOnly(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestDateOnly(TestEngine engine, ExecutionMode mode) { - await using var host = await GetHostAsync(engine); + await using var host = await GetHostAsync(engine, mode); var request = host.GetRequest("/t/date/", HttpMethod.Post); @@ -63,10 +64,10 @@ public async Task TestDateOnly(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestInvalidDateOnly(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestInvalidDateOnly(TestEngine engine, ExecutionMode mode) { - await using var host = await GetHostAsync(engine); + await using var host = await GetHostAsync(engine, mode); var request = host.GetRequest("/t/date/", HttpMethod.Post); diff --git a/Testing/Acceptance/Modules/Controllers/IntegrationTests.cs b/Testing/Acceptance/Modules/Controllers/IntegrationTests.cs index 274a727f8..3422402f5 100644 --- a/Testing/Acceptance/Modules/Controllers/IntegrationTests.cs +++ b/Testing/Acceptance/Modules/Controllers/IntegrationTests.cs @@ -1,7 +1,11 @@ using System.Net; + using GenHTTP.Api.Protocol; + using GenHTTP.Modules.Controllers; using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Reflection; + using GenHTTP.Testing.Acceptance.Utilities; namespace GenHTTP.Testing.Acceptance.Modules.Controllers; @@ -23,11 +27,14 @@ public class TestController #endregion [TestMethod] - [MultiEngineTest] - public async Task TestInstance(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestInstance(TestEngine engine, ExecutionMode mode) { + var controller = Controller.From(new TestController()) + .ExecutionMode(mode); + var app = Layout.Create() - .Add(Controller.From(new TestController())); + .Add(controller); await using var host = await TestHost.RunAsync(app, engine: engine); diff --git a/Testing/Acceptance/Modules/Controllers/ResultTypeTests.cs b/Testing/Acceptance/Modules/Controllers/ResultTypeTests.cs index dcab8b8f5..cfd393ccf 100644 --- a/Testing/Acceptance/Modules/Controllers/ResultTypeTests.cs +++ b/Testing/Acceptance/Modules/Controllers/ResultTypeTests.cs @@ -15,11 +15,12 @@ public sealed class ResultTypeTests #region Helpers - private static async Task GetRunnerAsync(TestEngine engine) + private static async Task GetRunnerAsync(TestEngine engine, ExecutionMode mode) { var controller = Controller.From() .Serializers(Serialization.Default()) - .Injectors(Injection.Default()); + .Injectors(Injection.Default()) + .ExecutionMode(mode); return await TestHost.RunAsync(Layout.Create().Add("t", controller), engine: engine); } @@ -45,10 +46,10 @@ public sealed class TestController #region Tests [TestMethod] - [MultiEngineTest] - public async Task ControllerMayReturnHandlerBuilder(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task ControllerMayReturnHandlerBuilder(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/handler-builder/"); @@ -57,10 +58,10 @@ public async Task ControllerMayReturnHandlerBuilder(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task ControllerMayReturnHandler(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task ControllerMayReturnHandler(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/handler/"); @@ -69,10 +70,10 @@ public async Task ControllerMayReturnHandler(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task ControllerMayReturnResponseBuilder(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task ControllerMayReturnResponseBuilder(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/response-builder/"); @@ -81,10 +82,10 @@ public async Task ControllerMayReturnResponseBuilder(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task ControllerMayReturnResponse(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task ControllerMayReturnResponse(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/response/"); diff --git a/Testing/Acceptance/Modules/Controllers/SeoTests.cs b/Testing/Acceptance/Modules/Controllers/SeoTests.cs index 89eb458b9..7cc0478df 100644 --- a/Testing/Acceptance/Modules/Controllers/SeoTests.cs +++ b/Testing/Acceptance/Modules/Controllers/SeoTests.cs @@ -4,6 +4,7 @@ using GenHTTP.Modules.Controllers; using GenHTTP.Modules.IO; using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Reflection; namespace GenHTTP.Testing.Acceptance.Modules.Controllers; @@ -18,10 +19,10 @@ public sealed class SeoTests /// by accepting upper case letters in action names. /// [TestMethod] - [MultiEngineTest] - public async Task TestActionCasingMatters(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestActionCasingMatters(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/Action/"); @@ -32,7 +33,7 @@ public async Task TestActionCasingMatters(TestEngine engine) #region Helpers - private async Task GetRunnerAsync(TestEngine engine) => await TestHost.RunAsync(Layout.Create().AddController("t"), engine: engine); + private async Task GetRunnerAsync(TestEngine engine, ExecutionMode mode) => await TestHost.RunAsync(Layout.Create().AddController("t", mode: mode), engine: engine); #endregion diff --git a/Testing/Acceptance/Modules/Functional/InlineTests.cs b/Testing/Acceptance/Modules/Functional/InlineTests.cs index 379d158bd..a9dc51d0d 100644 --- a/Testing/Acceptance/Modules/Functional/InlineTests.cs +++ b/Testing/Acceptance/Modules/Functional/InlineTests.cs @@ -7,6 +7,7 @@ using GenHTTP.Modules.IO; using GenHTTP.Modules.Functional; using GenHTTP.Modules.Redirects; +using GenHTTP.Modules.Reflection; namespace GenHTTP.Testing.Acceptance.Modules.Functional; @@ -15,10 +16,10 @@ public sealed class InlineTests { [TestMethod] - [MultiEngineTest] - public async Task TestGetRoot(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestGetRoot(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get(() => 42), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get(() => 42).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync(); @@ -26,10 +27,10 @@ public async Task TestGetRoot(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestGetPath(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestGetPath(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get("/blubb", () => 42), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get("/blubb", () => 42).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync("/blubb"); @@ -37,10 +38,10 @@ public async Task TestGetPath(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestGetQueryParam(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestGetQueryParam(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get((int param) => param + 1), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get((int param) => param + 1).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync("/?param=41"); @@ -48,10 +49,10 @@ public async Task TestGetQueryParam(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestGetEmptyBooleanQueryParam(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestGetEmptyBooleanQueryParam(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get((bool param) => param), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get((bool param) => param).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync("/?param="); @@ -59,10 +60,10 @@ public async Task TestGetEmptyBooleanQueryParam(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestGetEmptyDoubleQueryParam(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestGetEmptyDoubleQueryParam(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get((double param) => param), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get((double param) => param).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync("/?param="); @@ -70,10 +71,10 @@ public async Task TestGetEmptyDoubleQueryParam(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestGetEmptyStringQueryParam(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestGetEmptyStringQueryParam(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get((string param) => param), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get((string param) => param).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync("/?param="); @@ -81,10 +82,10 @@ public async Task TestGetEmptyStringQueryParam(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestGetEmptyEnumQueryParam(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestGetEmptyEnumQueryParam(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get((EnumData param) => param), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get((EnumData param) => param).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync("/?param="); @@ -92,10 +93,10 @@ public async Task TestGetEmptyEnumQueryParam(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestGetPathParam(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestGetPathParam(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get(":param", (int param) => param + 1), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get(":param", (int param) => param + 1).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync("/41"); @@ -103,10 +104,10 @@ public async Task TestGetPathParam(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestNotFound(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestNotFound(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get(() => 42), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get(() => 42).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync("/nope"); @@ -114,15 +115,15 @@ public async Task TestNotFound(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestRaw(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestRaw(TestEngine engine, ExecutionMode mode) { await using var host = await TestHost.RunAsync(Inline.Create().Get((IRequest request) => { return request.Respond() .Status(ResponseStatus.Ok) .Content("42"); - }), engine: engine); + }).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync(); @@ -130,10 +131,10 @@ public async Task TestRaw(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestStream(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestStream(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get(() => new MemoryStream("42"u8.ToArray())), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get(() => new MemoryStream("42"u8.ToArray())).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync(); @@ -141,10 +142,10 @@ public async Task TestStream(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestJson(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestJson(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Get(() => new MyClass("42", 42, 42.0)), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get(() => new MyClass("42", 42, 42.0)).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync(); @@ -152,10 +153,10 @@ public async Task TestJson(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestPostJson(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestPostJson(TestEngine engine, ExecutionMode mode) { - await using var host = await TestHost.RunAsync(Inline.Create().Post((MyClass input) => input), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Post((MyClass input) => input).ExecutionMode(mode), engine: engine); var request = host.GetRequest(); @@ -169,8 +170,8 @@ public async Task TestPostJson(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestAsync(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestAsync(TestEngine engine, ExecutionMode mode) { await using var host = await TestHost.RunAsync(Inline.Create().Get(async () => { @@ -181,7 +182,7 @@ public async Task TestAsync(TestEngine engine) stream.Seek(0, SeekOrigin.Begin); return stream; - }), engine: engine); + }).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync(); @@ -189,12 +190,12 @@ public async Task TestAsync(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestHandlerBuilder(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestHandlerBuilder(TestEngine engine, ExecutionMode mode) { var target = "https://www.google.de/"; - await using var host = await TestHost.RunAsync(Inline.Create().Get(() => Redirect.To(target)), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get(() => Redirect.To(target)).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync(); @@ -202,12 +203,12 @@ public async Task TestHandlerBuilder(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestHandler(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestHandler(TestEngine engine, ExecutionMode mode) { var target = "https://www.google.de/"; - await using var host = await TestHost.RunAsync(Inline.Create().Get((IHandler parent) => Redirect.To(target).Build()), engine: engine); + await using var host = await TestHost.RunAsync(Inline.Create().Get((IHandler parent) => Redirect.To(target).Build()).ExecutionMode(mode), engine: engine); using var response = await host.GetResponseAsync(); @@ -218,7 +219,7 @@ public async Task TestHandler(TestEngine engine) public record MyClass(string String, int Int, double Double); - private enum EnumData { One, Two } + public enum EnumData { One, Two } #endregion diff --git a/Testing/Acceptance/Modules/Functional/IntegrationTest.cs b/Testing/Acceptance/Modules/Functional/IntegrationTest.cs index ef137bcc8..cf256c596 100644 --- a/Testing/Acceptance/Modules/Functional/IntegrationTest.cs +++ b/Testing/Acceptance/Modules/Functional/IntegrationTest.cs @@ -1,7 +1,9 @@ using System.Net; + using GenHTTP.Modules.Conversion; using GenHTTP.Modules.Conversion.Formatters; using GenHTTP.Modules.Functional; +using GenHTTP.Modules.Reflection; namespace GenHTTP.Testing.Acceptance.Modules.Functional; @@ -10,15 +12,16 @@ public class IntegrationTest { [TestMethod] - [MultiEngineTest] - public async Task TestFormatters(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestFormatters(TestEngine engine, ExecutionMode mode) { var formatting = Formatting.Empty() .Add(); var api = Inline.Create() .Any("get-bool", (bool value) => value) - .Formatters(formatting); + .Formatters(formatting) + .ExecutionMode(mode); await using var host = await TestHost.RunAsync(api, engine: engine); diff --git a/Testing/Acceptance/Modules/Functional/MethodTest.cs b/Testing/Acceptance/Modules/Functional/MethodTest.cs index 036d801d8..f26f80682 100644 --- a/Testing/Acceptance/Modules/Functional/MethodTest.cs +++ b/Testing/Acceptance/Modules/Functional/MethodTest.cs @@ -1,6 +1,8 @@ using System.Net; using System.Net.Http.Headers; + using GenHTTP.Modules.Functional; +using GenHTTP.Modules.Reflection; namespace GenHTTP.Testing.Acceptance.Modules.Functional; @@ -9,11 +11,12 @@ public class MethodTest { [TestMethod] - [MultiEngineTest] - public async Task TestAnyMethod(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestAnyMethod(TestEngine engine, ExecutionMode mode) { var app = Inline.Create() - .Any((List data) => data.Count); + .Any((List data) => data.Count) + .ExecutionMode(mode); await using var host = await TestHost.RunAsync(app, engine: engine); @@ -36,11 +39,12 @@ public async Task TestAnyMethod(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestDelete(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestDelete(TestEngine engine, ExecutionMode mode) { var app = Inline.Create() - .Delete(() => { }); + .Delete(() => { }) + .ExecutionMode(mode); await using var host = await TestHost.RunAsync(app, engine: engine); @@ -52,11 +56,12 @@ public async Task TestDelete(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestHead(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestHead(TestEngine engine, ExecutionMode mode) { var app = Inline.Create() - .Head(() => "42"); + .Head(() => "42") + .ExecutionMode(mode); await using var host = await TestHost.RunAsync(app, engine: engine); diff --git a/Testing/Acceptance/Modules/Reflection/ContentTests.cs b/Testing/Acceptance/Modules/Reflection/ContentTests.cs index 57d90be8a..991671651 100644 --- a/Testing/Acceptance/Modules/Reflection/ContentTests.cs +++ b/Testing/Acceptance/Modules/Reflection/ContentTests.cs @@ -9,13 +9,14 @@ public sealed class ContentTests { [TestMethod] - [MultiEngineTest] - public async Task TestDeserialization(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestDeserialization(TestEngine engine, ExecutionMode mode) { var expectation = new MyType(42); var handler = Inline.Create() - .Get(() => expectation); + .Get(() => expectation) + .ExecutionMode(mode); await using var host = await TestHost.RunAsync(handler, engine: engine); @@ -25,11 +26,12 @@ public async Task TestDeserialization(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestNull(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestNull(TestEngine engine, ExecutionMode mode) { var handler = Inline.Create() - .Get(() => (MyType?)null); + .Get(() => (MyType?)null) + .ExecutionMode(mode); await using var host = await TestHost.RunAsync(handler, engine: engine); @@ -39,11 +41,12 @@ public async Task TestNull(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestUnsupported(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestUnsupported(TestEngine engine, ExecutionMode mode) { var handler = Inline.Create() - .Get(() => new Result("Nah").Type(FlexibleContentType.Get("text/html"))); + .Get(() => new Result("Nah").Type(FlexibleContentType.Get("text/html"))) + .ExecutionMode(mode); await using var host = await TestHost.RunAsync(handler, engine: engine); diff --git a/Testing/Acceptance/Modules/Reflection/ErrorHandlingTests.cs b/Testing/Acceptance/Modules/Reflection/ErrorHandlingTests.cs index 07645285c..01a6a5dbc 100644 --- a/Testing/Acceptance/Modules/Reflection/ErrorHandlingTests.cs +++ b/Testing/Acceptance/Modules/Reflection/ErrorHandlingTests.cs @@ -2,6 +2,7 @@ using GenHTTP.Api.Protocol; using GenHTTP.Modules.Conversion; using GenHTTP.Modules.Functional; +using GenHTTP.Modules.Reflection; namespace GenHTTP.Testing.Acceptance.Modules.Reflection; @@ -10,15 +11,16 @@ public class ErrorHandlingTests { [TestMethod] - [MultiEngineTest] - public async Task TestSerializationNotPossible(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestSerializationNotPossible(TestEngine engine, ExecutionMode mode) { var serialization = Serialization.Empty() .Default(ContentType.AudioMp4); var api = Inline.Create() .Get(() => new HashSet()) - .Serializers(serialization); + .Serializers(serialization) + .ExecutionMode(mode); await using var host = await TestHost.RunAsync(api, engine: engine); diff --git a/Testing/Acceptance/Modules/Reflection/InterceptionTests.cs b/Testing/Acceptance/Modules/Reflection/InterceptionTests.cs index ddd1b5491..e1c3fa684 100644 --- a/Testing/Acceptance/Modules/Reflection/InterceptionTests.cs +++ b/Testing/Acceptance/Modules/Reflection/InterceptionTests.cs @@ -60,9 +60,10 @@ public void Configure(object attribute) #region Tests [TestMethod] - public async Task TestInterception() + [MultiEngineFrameworkTest] + public async Task TestInterception(TestEngine engine, ExecutionMode mode) { - var app = Inline.Create().Get([My("intercept")] () => 42); + var app = Inline.Create().Get([My("intercept")] (int? q) => 42).ExecutionMode(mode); await using var host = await TestHost.RunAsync(app); @@ -74,9 +75,10 @@ public async Task TestInterception() } [TestMethod] - public async Task TestPassThrough() + [MultiEngineFrameworkTest] + public async Task TestPassThrough(TestEngine engine, ExecutionMode mode) { - var app = Inline.Create().Get([My("pass")] () => 42); + var app = Inline.Create().Get([My("pass")] () => 42).ExecutionMode(mode); await using var host = await TestHost.RunAsync(app); @@ -88,9 +90,10 @@ public async Task TestPassThrough() } [TestMethod] - public async Task TestException() + [MultiEngineFrameworkTest] + public async Task TestException(TestEngine engine, ExecutionMode mode) { - var app = Inline.Create().Get([My("throw")] () => 42); + var app = Inline.Create().Get([My("throw")] () => 42).ExecutionMode(mode); await using var host = await TestHost.RunAsync(app); diff --git a/Testing/Acceptance/Modules/Reflection/ParameterTests.cs b/Testing/Acceptance/Modules/Reflection/ParameterTests.cs index c0e395a56..9028ac486 100644 --- a/Testing/Acceptance/Modules/Reflection/ParameterTests.cs +++ b/Testing/Acceptance/Modules/Reflection/ParameterTests.cs @@ -26,11 +26,12 @@ public async Task TestCanReadSimpleTypesFromBody() } [TestMethod] - [MultiEngineTest] - public async Task TestCanPassEmptyString(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestCanPassEmptyString(TestEngine engine, ExecutionMode mode) { var inline = Inline.Create() - .Post(([FromBody] int number) => number); + .Post(([FromBody] int number) => number) + .ExecutionMode(mode); await using var runner = await TestHost.RunAsync(inline, engine: engine); @@ -61,11 +62,12 @@ public async Task TestCanAccessBothBodyAndStream() } [TestMethod] - [MultiEngineTest] - public async Task TestConversionError(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestConversionError(TestEngine engine, ExecutionMode mode) { var inline = Inline.Create() - .Post(([FromBody] int number) => number); + .Post(([FromBody] int number) => number) + .ExecutionMode(mode); await using var runner = await TestHost.RunAsync(inline, engine: engine); diff --git a/Testing/Acceptance/Modules/Reflection/ResultTests.cs b/Testing/Acceptance/Modules/Reflection/ResultTests.cs index bb1cd1013..c06e677a8 100644 --- a/Testing/Acceptance/Modules/Reflection/ResultTests.cs +++ b/Testing/Acceptance/Modules/Reflection/ResultTests.cs @@ -22,8 +22,8 @@ public record MyPayload(string Message); #region Tests [TestMethod] - [MultiEngineTest] - public async Task TestResponseCanBeModified(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestResponseCanBeModified(TestEngine engine, ExecutionMode mode) { var result = new Result(new MyPayload("Hello World!")) .Status(ResponseStatus.Accepted) @@ -37,7 +37,8 @@ public async Task TestResponseCanBeModified(TestEngine engine) .Encoding("my-encoding"); var inline = Inline.Create() - .Get(() => result); + .Get(() => result) + .ExecutionMode(mode); await using var runner = await TestHost.RunAsync(inline, engine: engine); @@ -51,13 +52,14 @@ public async Task TestResponseCanBeModified(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestStreamsCanBeWrapped(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestStreamsCanBeWrapped(TestEngine engine, ExecutionMode mode) { var stream = new MemoryStream("Hello World!"u8.ToArray()); var inline = Inline.Create() - .Get(() => new Result(stream).Status(ResponseStatus.Created)); + .Get(() => new Result(stream).Status(ResponseStatus.Created)) + .ExecutionMode(mode); await using var runner = await TestHost.RunAsync(inline, engine: engine); diff --git a/Testing/Acceptance/Modules/Reflection/RoutingTests.cs b/Testing/Acceptance/Modules/Reflection/RoutingTests.cs new file mode 100644 index 000000000..a8f7eadfe --- /dev/null +++ b/Testing/Acceptance/Modules/Reflection/RoutingTests.cs @@ -0,0 +1,123 @@ +using GenHTTP.Api.Routing; + +using GenHTTP.Modules.Reflection.Routing; +using GenHTTP.Modules.Reflection.Routing.Segments; + +namespace GenHTTP.Testing.Acceptance.Modules.Reflection; + +[TestClass] +public class RoutingTests +{ + + [TestMethod] + public void TestSimpleSegment() + { + var route = CreateRoute(false, new StringSegment("segment"), new ClosingSegment(false, false)); + + var target = new RoutingTarget(WebPath.FromString("/segment")); + + var match = OperationRouter.TryMatch(target, route); + + Assert.IsNotNull(match); + + Assert.AreEqual(0, match.Offset); + Assert.IsNull(match.PathArguments); + } + + [TestMethod] + public void TestSimpleSegmentWithTrailingSlash() + { + var route = CreateRoute(false, new StringSegment("segment"), new ClosingSegment(false, false)); + + var target = new RoutingTarget(WebPath.FromString("/segment/")); + + var match = OperationRouter.TryMatch(target, route); + + Assert.IsNotNull(match); + + Assert.AreEqual(1, match.Offset); + Assert.IsNull(match.PathArguments); + } + + [TestMethod] + public void TestMissingSlashFailsRouting() + { + var route = CreateRoute(false, new StringSegment("segment"), new ClosingSegment(true, false)); + + var target = new RoutingTarget(WebPath.FromString("/segment")); + + var match = OperationRouter.TryMatch(target, route); + + Assert.IsNull(match); + } + + [TestMethod] + public void TestNonWildcardDoesNotAllowAdditionalSegments() + { + var route = CreateRoute(false, new StringSegment("segment"), new ClosingSegment(false, false)); + + var target = new RoutingTarget(WebPath.FromString("/segment/another")); + + var match = OperationRouter.TryMatch(target, route); + + Assert.IsNull(match); + } + + [TestMethod] + public void TestWildcardAllowsAdditionalSegments() + { + var route = CreateRoute(true, new StringSegment("segment"), new ClosingSegment(false, true)); + + var target = new RoutingTarget(WebPath.FromString("/segment/another")); + + var match = OperationRouter.TryMatch(target, route); + + Assert.IsNotNull(match); + + Assert.AreEqual(1, match.Offset); + Assert.IsNull(match.PathArguments); + } + + [TestMethod] + public void TestSimpleVariable() + { + var route = CreateRoute(false, new SimpleVariableSegment("var")); + + var target = new RoutingTarget(WebPath.FromString("/99/")); + + var match = OperationRouter.TryMatch(target, route); + + Assert.IsNotNull(match); + + Assert.AreEqual(1, match.Offset); + + Assert.IsNotNull(match.PathArguments); + Assert.HasCount(1, match.PathArguments); + + Assert.AreEqual("99", match.PathArguments["var"]); + } + + [TestMethod] + public void TestComplexVariables() + { + var route = CreateRoute(false, new RegexSegment("(?[0-9]{2})-(?[0-9]{2})-:three")); + + var target = new RoutingTarget(WebPath.FromString("/99-88-77")); + + var match = OperationRouter.TryMatch(target, route); + + Assert.IsNotNull(match); + + Assert.AreEqual(1, match.Offset); + + Assert.IsNotNull(match.PathArguments); + Assert.HasCount(3, match.PathArguments); + + Assert.AreEqual("99", match.PathArguments["one"]); + Assert.AreEqual("88", match.PathArguments["two"]); + Assert.AreEqual("77", match.PathArguments["three"]); + } + + private static OperationRoute CreateRoute(bool wildcard, params IRoutingSegment[] segments) => new("name", segments, wildcard); + +} diff --git a/Testing/Acceptance/Modules/Reflection/WildcardTests.cs b/Testing/Acceptance/Modules/Reflection/WildcardTests.cs index 688702233..aa92ae311 100644 --- a/Testing/Acceptance/Modules/Reflection/WildcardTests.cs +++ b/Testing/Acceptance/Modules/Reflection/WildcardTests.cs @@ -2,6 +2,7 @@ using GenHTTP.Modules.Functional; using GenHTTP.Modules.IO; +using GenHTTP.Modules.Reflection; namespace GenHTTP.Testing.Acceptance.Modules.Reflection; @@ -10,8 +11,8 @@ public class WildcardTests { [TestMethod] - [MultiEngineTest] - public async Task TestRouting(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestRouting(TestEngine engine, ExecutionMode mode) { var tree = ResourceTree.FromAssembly("Resources"); @@ -22,7 +23,8 @@ public async Task TestRouting(TestEngine engine) { Assert.IsGreaterThan(0, tenantId); return resources; - }); + }) + .ExecutionMode(mode); await using var host = await TestHost.RunAsync(app); diff --git a/Testing/Acceptance/Modules/Webservices/AmbiguityTests.cs b/Testing/Acceptance/Modules/Webservices/AmbiguityTests.cs index f291f8d76..298b3f5dd 100644 --- a/Testing/Acceptance/Modules/Webservices/AmbiguityTests.cs +++ b/Testing/Acceptance/Modules/Webservices/AmbiguityTests.cs @@ -2,6 +2,7 @@ using GenHTTP.Api.Content; using GenHTTP.Modules.IO; using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Reflection; using GenHTTP.Modules.Webservices; namespace GenHTTP.Testing.Acceptance.Modules.Webservices; @@ -13,11 +14,11 @@ public sealed class AmbiguityTests #region Tests [TestMethod] - [MultiEngineTest] - public async Task TestSpecificPreferred(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestSpecificPreferred(TestEngine engine, ExecutionMode mode) { var app = Layout.Create() - .AddService("c"); + .AddService("c", mode: mode); await using var host = await TestHost.RunAsync(app, engine: engine); diff --git a/Testing/Acceptance/Modules/Webservices/ExtensionTests.cs b/Testing/Acceptance/Modules/Webservices/ExtensionTests.cs index 69150602b..759c0eb45 100644 --- a/Testing/Acceptance/Modules/Webservices/ExtensionTests.cs +++ b/Testing/Acceptance/Modules/Webservices/ExtensionTests.cs @@ -13,16 +13,16 @@ public sealed class ExtensionTests #region Tests [TestMethod] - [MultiEngineTest] - public async Task TestConfiguration(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestConfiguration(TestEngine engine, ExecutionMode mode) { var injectors = Injection.Default(); var formats = Serialization.Default(); var app = Layout.Create() - .AddService("by-type", injectors, formats) - .AddService("by-instance", new TestService(), injectors, formats); + .AddService("by-type", injectors, formats, mode: mode) + .AddService("by-instance", new TestService(), injectors, formats, mode: mode); await using var host = await TestHost.RunAsync(app, engine: engine); diff --git a/Testing/Acceptance/Modules/Webservices/HandlerResultTests.cs b/Testing/Acceptance/Modules/Webservices/HandlerResultTests.cs index cf4c9680d..23d8d4f3b 100644 --- a/Testing/Acceptance/Modules/Webservices/HandlerResultTests.cs +++ b/Testing/Acceptance/Modules/Webservices/HandlerResultTests.cs @@ -4,6 +4,7 @@ using GenHTTP.Api.Infrastructure; using GenHTTP.Modules.IO; using GenHTTP.Modules.Layouting; +using GenHTTP.Modules.Reflection; using GenHTTP.Modules.StaticWebsites; using GenHTTP.Modules.Webservices; @@ -71,11 +72,11 @@ public Task Pathed(string pathParam) #region Tests [TestMethod] - [MultiEngineTest] - public async Task TestRoot(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestRoot(TestEngine engine, ExecutionMode mode) { var app = Layout.Create() - .AddService("c"); + .AddService("c", mode: mode); await using var host = await TestHost.RunAsync(app, engine: engine); @@ -87,11 +88,11 @@ public async Task TestRoot(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestPathed(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestPathed(TestEngine engine, ExecutionMode mode) { var app = Layout.Create() - .AddService("c"); + .AddService("c", mode: mode); await using var host = await TestHost.RunAsync(app, engine: engine); @@ -103,11 +104,11 @@ public async Task TestPathed(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task TestPathedAsync(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestPathedAsync(TestEngine engine, ExecutionMode mode) { var app = Layout.Create() - .AddService("c"); + .AddService("c", mode: mode); await using var host = await TestHost.RunAsync(app, engine: engine); diff --git a/Testing/Acceptance/Modules/Webservices/ResultTypeTests.cs b/Testing/Acceptance/Modules/Webservices/ResultTypeTests.cs index 1be89fda7..4f5e434be 100644 --- a/Testing/Acceptance/Modules/Webservices/ResultTypeTests.cs +++ b/Testing/Acceptance/Modules/Webservices/ResultTypeTests.cs @@ -32,19 +32,20 @@ public class ResultTypeTests #region Helpers - private async Task GetRunnerAsync(TestEngine engine) => await TestHost.RunAsync(Layout.Create().AddService("t", serializers: Serialization.Default(), + private async Task GetRunnerAsync(TestEngine engine, ExecutionMode mode) => await TestHost.RunAsync(Layout.Create().AddService("t", serializers: Serialization.Default(), injectors: Injection.Default(), - formatters: Formatting.Default()), engine: engine); + formatters: Formatting.Default(), + mode: mode), engine: engine); #endregion #region Tests [TestMethod] - [MultiEngineTest] - public async Task ControllerMayReturnTask(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task ControllerMayReturnTask(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/task"); @@ -52,10 +53,10 @@ public async Task ControllerMayReturnTask(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task ControllerMayReturnValueTask(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task ControllerMayReturnValueTask(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/value-task"); @@ -63,10 +64,10 @@ public async Task ControllerMayReturnValueTask(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task ControllerMayReturnGenericTask(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task ControllerMayReturnGenericTask(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/generic-task"); @@ -75,10 +76,10 @@ public async Task ControllerMayReturnGenericTask(TestEngine engine) } [TestMethod] - [MultiEngineTest] - public async Task ControllerMayReturnGenericValueTask(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task ControllerMayReturnGenericValueTask(TestEngine engine, ExecutionMode mode) { - await using var runner = await GetRunnerAsync(engine); + await using var runner = await GetRunnerAsync(engine, mode); using var response = await runner.GetResponseAsync("/t/generic-value-task"); diff --git a/Testing/Acceptance/Modules/Webservices/WebserviceTests.cs b/Testing/Acceptance/Modules/Webservices/WebserviceTests.cs index 150c611db..58993a9f2 100644 --- a/Testing/Acceptance/Modules/Webservices/WebserviceTests.cs +++ b/Testing/Acceptance/Modules/Webservices/WebserviceTests.cs @@ -2,18 +2,14 @@ using System.Net.Http.Headers; using System.Text; using System.Xml.Serialization; - using GenHTTP.Api.Content; using GenHTTP.Api.Protocol; - using GenHTTP.Modules.Conversion; using GenHTTP.Modules.IO; using GenHTTP.Modules.Layouting; using GenHTTP.Modules.Reflection; using GenHTTP.Modules.Webservices; - using GenHTTP.Testing.Acceptance.Utilities; - using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -103,133 +99,129 @@ public void Empty() { } #region Tests [TestMethod] - [MultiEngineTest] - public async Task TestEmpty(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestEmpty(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); }); + await WithResponse(engine, mode, "", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); }); } [TestMethod] - [MultiEngineTest] - public async Task TestVoidReturn(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestVoidReturn(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "nothing", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); }); + await WithResponse(engine, mode, "nothing", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); }); } [TestMethod] - [MultiEngineTest] - public async Task TestPrimitives(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestPrimitives(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "primitive?input=42", async r => Assert.AreEqual("42", await r.GetContentAsync())); + await WithResponse(engine, mode, "primitive?input=42", async r => Assert.AreEqual("42", await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestEnums(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestEnums(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "enum?input=One", async r => Assert.AreEqual("One", await r.GetContentAsync())); + await WithResponse(engine, mode, "enum?input=One", async r => Assert.AreEqual("One", await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestNullableSet(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestNullableSet(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "nullable?input=1", async r => Assert.AreEqual("1", await r.GetContentAsync())); + await WithResponse(engine, mode, "nullable?input=1", async r => Assert.AreEqual("1", await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestNullableNotSet(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestNullableNotSet(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "nullable", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); }); + await WithResponse(engine, mode, "nullable", async r => { await r.AssertStatusAsync(HttpStatusCode.NoContent); }); } [TestMethod] - [MultiEngineTest] - public async Task TestGuid(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestGuid(TestEngine engine, ExecutionMode mode) { var id = Guid.NewGuid().ToString(); - await WithResponse(engine, $"guid?id={id}", async r => Assert.AreEqual(id, await r.GetContentAsync())); + await WithResponse(engine, mode, $"guid?id={id}", async r => Assert.AreEqual(id, await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestParam(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestParam(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "param/42", async r => Assert.AreEqual("42", await r.GetContentAsync())); + await WithResponse(engine, mode, "param/42", async r => Assert.AreEqual("42", await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestConversionFailure(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestConversionFailure(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "param/abc", async r => { await r.AssertStatusAsync(HttpStatusCode.BadRequest); }); + await WithResponse(engine, mode, "param/abc", async r => { await r.AssertStatusAsync(HttpStatusCode.BadRequest); }); } [TestMethod] - [MultiEngineTest] - public async Task TestRegex(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestRegex(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "regex/42", async r => Assert.AreEqual("42", await r.GetContentAsync())); + await WithResponse(engine, mode, "regex/42", async r => Assert.AreEqual("42", await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestEntityWithNulls(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestEntityWithNulls(TestEngine engine, ExecutionMode mode) { const string entity = "{\"id\":42}"; - await WithResponse(engine, "entity", HttpMethod.Post, entity, null, null, async r => Assert.AreEqual(entity, await r.GetContentAsync())); + await WithResponse(engine, mode, "entity", HttpMethod.Post, entity, null, null, async r => Assert.AreEqual(entity, await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestEntityWithNoNulls(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestEntityWithNoNulls(TestEngine engine, ExecutionMode mode) { const string entity = "{\"id\":42,\"nullable\":123.456}"; - await WithResponse(engine, "entity", HttpMethod.Post, entity, null, null, async r => Assert.AreEqual(entity, await r.GetContentAsync())); + await WithResponse(engine, mode, "entity", HttpMethod.Post, entity, null, null, async r => Assert.AreEqual(entity, await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestNotSupportedUpload(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestNotSupportedUpload(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "entity", HttpMethod.Post, "123", "bla/blubb", null, async r => { await r.AssertStatusAsync(HttpStatusCode.UnsupportedMediaType); }); + await WithResponse(engine, mode, "entity", HttpMethod.Post, "123", "bla/blubb", null, async r => { await r.AssertStatusAsync(HttpStatusCode.UnsupportedMediaType); }); } [TestMethod] - [MultiEngineTest] - public async Task TestUnsupportedDownloadEnforcesDefault(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestUnsupportedDownloadEnforcesDefault(TestEngine engine, ExecutionMode mode) { const string entity = "{\"id\":42,\"nullable\":123.456}"; - await WithResponse(engine, "entity", HttpMethod.Post, entity, null, "bla/blubb", async r => Assert.AreEqual(entity, await r.GetContentAsync())); - } - - [TestMethod] - [MultiEngineTest] - public async Task TestWrongMethod(TestEngine engine) - { - await WithResponse(engine, "entity", HttpMethod.Put, "123", null, null, async r => { await r.AssertStatusAsync(HttpStatusCode.MethodNotAllowed); Assert.AreEqual("POST", r.GetContentHeader("Allow")); }); + await WithResponse(engine, mode, "entity", HttpMethod.Post, entity, null, "bla/blubb", async r => Assert.AreEqual(entity, await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestNoMethod(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestNoMethod(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "idonotexist", async r => { await r.AssertStatusAsync(HttpStatusCode.NotFound); }); + await WithResponse(engine, mode, "idonotexist", async r => { await r.AssertStatusAsync(HttpStatusCode.NotFound); }); } [TestMethod] public async Task TestStream() { - await WithResponse(TestEngine.Internal, "stream", HttpMethod.Put, "123456", null, null, async r => Assert.AreEqual("6", await r.GetContentAsync())); + foreach (var mode in new[] { ExecutionMode.Reflection, ExecutionMode.Auto }) + { + await WithResponse(TestEngine.Internal, mode, "stream", HttpMethod.Put, "123456", null, null, async r => Assert.AreEqual("6", await r.GetContentAsync())); + } } [TestMethod] - [MultiEngineTest] - public async Task TestByteArrayReturn(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestByteArrayReturn(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "bytes", async r => + await WithResponse(engine, mode, "bytes", async r => { await r.AssertStatusAsync(HttpStatusCode.OK); Assert.AreEqual("Hello Bytes", await r.GetContentAsync()); @@ -237,10 +229,10 @@ await WithResponse(engine, "bytes", async r => } [TestMethod] - [MultiEngineTest] - public async Task TestReadOnlyMemoryReturn(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestReadOnlyMemoryReturn(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "memory", async r => + await WithResponse(engine, mode, "memory", async r => { await r.AssertStatusAsync(HttpStatusCode.OK); Assert.AreEqual("Hello Memory", await r.GetContentAsync()); @@ -248,26 +240,26 @@ await WithResponse(engine, "memory", async r => } [TestMethod] - [MultiEngineTest] - public async Task TestRequestResponse(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestRequestResponse(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "requestResponse", async r => Assert.AreEqual("Hello World", await r.GetContentAsync())); + await WithResponse(engine, mode, "requestResponse", async r => Assert.AreEqual("Hello World", await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestRouting(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestRouting(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "request", async r => Assert.AreEqual("yes", await r.GetContentAsync())); + await WithResponse(engine, mode, "request", async r => Assert.AreEqual("yes", await r.GetContentAsync())); } [TestMethod] - [MultiEngineTest] - public async Task TestEntityAsXml(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestEntityAsXml(TestEngine engine, ExecutionMode mode) { const string entity = "11234.56"; - await WithResponse(engine, "entity", HttpMethod.Post, entity, "text/xml", "text/xml", async r => + await WithResponse(engine, mode, "entity", HttpMethod.Post, entity, "text/xml", "text/xml", async r => { var result = new XmlSerializer(typeof(TestEntity)).Deserialize(await r.Content.ReadAsStreamAsync()) as TestEntity; @@ -279,15 +271,15 @@ await WithResponse(engine, "entity", HttpMethod.Post, entity, "text/xml", "text/ } [TestMethod] - [MultiEngineTest] - public async Task TestEntityAsYaml(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestEntityAsYaml(TestEngine engine, ExecutionMode mode) { const string entity = """ id: 1 nullable: 1234.56 """; - await WithResponse(engine, "entity", HttpMethod.Post, entity, "application/yaml", "application/yaml", async r => + await WithResponse(engine, mode, "entity", HttpMethod.Post, entity, "application/yaml", "application/yaml", async r => { await r.AssertStatusAsync(HttpStatusCode.OK); @@ -306,24 +298,24 @@ await WithResponse(engine, "entity", HttpMethod.Post, entity, "application/yaml" } [TestMethod] - [MultiEngineTest] - public async Task TestException(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestException(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "exception", async r => { await r.AssertStatusAsync(HttpStatusCode.AlreadyReported); }); + await WithResponse(engine, mode, "exception", async r => { await r.AssertStatusAsync(HttpStatusCode.AlreadyReported); }); } [TestMethod] - [MultiEngineTest] - public async Task TestDuplicate(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestDuplicate(TestEngine engine, ExecutionMode mode) { - await WithResponse(engine, "duplicate", async r => { await r.AssertStatusAsync(HttpStatusCode.BadRequest); }); + await WithResponse(engine, mode, "duplicate", async r => { await r.AssertStatusAsync(HttpStatusCode.BadRequest); }); } [TestMethod] - [MultiEngineTest] - public async Task TestWithInstance(TestEngine engine) + [MultiEngineFrameworkTest] + public async Task TestWithInstance(TestEngine engine, ExecutionMode mode) { - var layout = Layout.Create().AddService("t", new TestResource()); + var layout = Layout.Create().AddService("t", new TestResource(), mode: mode); await using var runner = await TestHost.RunAsync(layout); @@ -344,11 +336,11 @@ public void TestConcernChaining() #region Helpers - private Task WithResponse(TestEngine engine, string uri, Func logic) => WithResponse(engine, uri, HttpMethod.Get, null, null, null, logic); + private Task WithResponse(TestEngine engine, ExecutionMode mode, string uri, Func logic) => WithResponse(engine, mode, uri, HttpMethod.Get, null, null, null, logic); - private async Task WithResponse(TestEngine engine, string uri, HttpMethod method, string? body, string? contentType, string? accept, Func logic) + private async Task WithResponse(TestEngine engine, ExecutionMode mode, string uri, HttpMethod method, string? body, string? contentType, string? accept, Func logic) { - await using var service = await GetServiceAsync(engine); + await using var service = await GetServiceAsync(engine, mode); var request = service.GetRequest($"/t/{uri}"); @@ -378,11 +370,12 @@ private async Task WithResponse(TestEngine engine, string uri, HttpMethod method await logic(response); } - private static async Task GetServiceAsync(TestEngine engine) + private static async Task GetServiceAsync(TestEngine engine, ExecutionMode mode) { var service = ServiceResource.From() .Serializers(Serialization.Default()) - .Injectors(Injection.Default()); + .Injectors(Injection.Default()) + .ExecutionMode(mode); return await TestHost.RunAsync(Layout.Create().Add("t", service), engine: engine); } diff --git a/Testing/Acceptance/MultiEngineFrameworkTest.cs b/Testing/Acceptance/MultiEngineFrameworkTest.cs new file mode 100644 index 000000000..69a441644 --- /dev/null +++ b/Testing/Acceptance/MultiEngineFrameworkTest.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using GenHTTP.Modules.Reflection; + +namespace GenHTTP.Testing.Acceptance; + +/// +/// When attributed on a test method, this attribute +/// injects the both the engine as well the execution +/// mode to be used. +/// +[AttributeUsage(AttributeTargets.Method)] +public class MultiEngineFrameworkTestAttribute : Attribute, ITestDataSource +{ + + public IEnumerable GetData(MethodInfo methodInfo) + { + var engines = new MultiEngineTestAttribute().GetData(methodInfo); // todo: ugly + + var result = new List(); + + foreach (var engine in engines) + { + result.Add([engine[0], ExecutionMode.Reflection]); + result.Add([engine[0], ExecutionMode.Auto]); + } + + return result; + } + + public string GetDisplayName(MethodInfo methodInfo, object?[]? data) + { + if (data?.Length == 2) + { + return $"{methodInfo.Name} ({data[0]}, {data[1]})"; + } + + return methodInfo.Name; + } + +}