Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
de94424
Add support for compiled service methods
Kaliumhexacyanoferrat Dec 13, 2025
0d36332
WIP
Kaliumhexacyanoferrat Dec 14, 2025
475be72
Make simple inline handlers work
Kaliumhexacyanoferrat Dec 14, 2025
165eed2
Restructure extensions
Kaliumhexacyanoferrat Dec 14, 2025
d078770
Code gen improvements
Kaliumhexacyanoferrat Dec 14, 2025
ef64dcf
Fix some sonar issues
Kaliumhexacyanoferrat Dec 14, 2025
f46be67
Create new execution settings type to hold settings
Kaliumhexacyanoferrat Dec 15, 2025
428459c
Avoid state machine in the hot path of the framework handlers
Kaliumhexacyanoferrat Dec 15, 2025
9ae0fa0
Cleanup method handler
Kaliumhexacyanoferrat Dec 15, 2025
99b6214
Remove redundancies by introducing SynchronizedMethodCollection
Kaliumhexacyanoferrat Dec 15, 2025
dacec53
Switch to release mode in codegen
Kaliumhexacyanoferrat Dec 15, 2025
ba893a9
Add first argument handling
Kaliumhexacyanoferrat Dec 17, 2025
852b681
Escape argument names as we generate code here ...
Kaliumhexacyanoferrat Dec 17, 2025
341f968
Extend acceptance tests for code generation (yet to do: rest of contr…
Kaliumhexacyanoferrat Dec 17, 2025
cb2689b
Add code generation to all relevant tests
Kaliumhexacyanoferrat Dec 18, 2025
351311d
Add some compilation error handling flow. Add void support.
Kaliumhexacyanoferrat Dec 18, 2025
d8ae5c3
Further progress
Kaliumhexacyanoferrat Dec 23, 2025
cb1a939
Add support for nested type names
Kaliumhexacyanoferrat Dec 24, 2025
2e48e4d
Add most sinks and argument sources. Path yet missing, 72 tests to go.
Kaliumhexacyanoferrat Dec 24, 2025
8fdc56d
Some fixes, down to 50
Kaliumhexacyanoferrat Dec 24, 2025
fab63bd
Add support for path arguments
Kaliumhexacyanoferrat Dec 27, 2025
330414e
Improve null handling
Kaliumhexacyanoferrat Dec 27, 2025
56b4c75
Allow empty query/body arguments
Kaliumhexacyanoferrat Dec 27, 2025
6316709
Fix async delegate invocations. Down to 22
Kaliumhexacyanoferrat Dec 27, 2025
f4dc135
Error free
Kaliumhexacyanoferrat Dec 29, 2025
396966d
Error free 2
Kaliumhexacyanoferrat Dec 29, 2025
8147ba8
Add error page for code generation issues
Kaliumhexacyanoferrat Dec 29, 2025
083256b
Optimize parsing and serialization of primitive types
Kaliumhexacyanoferrat Dec 30, 2025
c91b1ae
WIP: New method collection implementation
Kaliumhexacyanoferrat Dec 30, 2025
85e1348
Add and fix tests
Kaliumhexacyanoferrat Dec 30, 2025
c0eda88
Optimize allocations
Kaliumhexacyanoferrat Dec 30, 2025
01ae3a4
Commit benchmark
Kaliumhexacyanoferrat Dec 30, 2025
6d633cf
Add documentation
Kaliumhexacyanoferrat Dec 30, 2025
35cab1e
Merge branch 'main' into feature/compiled-delegates
Kaliumhexacyanoferrat Dec 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion API/Routing/RoutingTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public RoutingTarget(WebPath path)
/// <summary>
/// The segment to be currently handled by the responsible handler.
/// </summary>
public WebPathPart? Current => _index < Path.Parts.Count ? Path.Parts[_index] : null;
public WebPathPart? Current => Next(0);

/// <summary>
/// Specifies, whether the end of the path has been reached.
Expand Down Expand Up @@ -65,6 +65,24 @@ public void Advance()

_index++;
}

/// <summary>
/// Acknowledges the number of segments passed as a parameter.
/// </summary>
/// <param name="byOffset">The number of segments to advance by</param>
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;
}

/// <summary>
/// Retrieves the part of the path that still needs to be routed.
Expand All @@ -84,6 +102,19 @@ public WebPath GetRemaining()
return new WebPath(resultList, Path.TrailingSlash);
}

/// <summary>
/// Peeks at the next segment identified by the offset index,
/// beginning from the current position.
/// </summary>
/// <param name="offset">The offset to be applied</param>
/// <returns>The segment at the given position or null, if there are no more segments</returns>
public WebPathPart? Next(int offset)
{
var index = _index + offset;

return index < Path.Parts.Count ? Path.Parts[index] : null;
}

#endregion

}
16 changes: 11 additions & 5 deletions Modules/Controllers/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,9 +22,9 @@ public static class Extensions
/// <param name="injectors">Optionally the injectors to be used by this controller</param>
/// <param name="serializers">Optionally the serializers to be used by this controller</param>
/// <param name="formatters">Optionally the formatters to be used by this controller</param>
public static LayoutBuilder AddController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, string path, IBuilder<InjectionRegistry>? injectors = null, IBuilder<SerializationRegistry>? serializers = null, IBuilder<FormatterRegistry>? formatters = null) where T : new()
public static LayoutBuilder AddController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, string path, IBuilder<InjectionRegistry>? injectors = null, IBuilder<SerializationRegistry>? serializers = null, IBuilder<FormatterRegistry>? formatters = null, ExecutionMode? mode = null) where T : new()
{
builder.Add(path, Controller.From<T>().Configured(injectors, serializers, formatters));
builder.Add(path, Controller.From<T>().Configured(injectors, serializers, formatters, mode));
return builder;
}

Expand All @@ -36,13 +37,13 @@ public static class Extensions
/// <param name="injectors">Optionally the injectors to be used by this controller</param>
/// <param name="serializers">Optionally the serializers to be used by this controller</param>
/// <param name="formatters">Optionally the formatters to be used by this controller</param>
public static LayoutBuilder IndexController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, IBuilder<InjectionRegistry>? injectors = null, IBuilder<SerializationRegistry>? serializers = null, IBuilder<FormatterRegistry>? formatters = null) where T : new()
public static LayoutBuilder IndexController<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this LayoutBuilder builder, IBuilder<InjectionRegistry>? injectors = null, IBuilder<SerializationRegistry>? serializers = null, IBuilder<FormatterRegistry>? formatters = null, ExecutionMode? mode = null) where T : new()
{
builder.Add(Controller.From<T>().Configured(injectors, serializers, formatters));
builder.Add(Controller.From<T>().Configured(injectors, serializers, formatters, mode));
return builder;
}

private static ControllerBuilder Configured(this ControllerBuilder builder, IBuilder<InjectionRegistry>? injectors = null, IBuilder<SerializationRegistry>? serializers = null, IBuilder<FormatterRegistry>? formatters = null)
private static ControllerBuilder Configured(this ControllerBuilder builder, IBuilder<InjectionRegistry>? injectors = null, IBuilder<SerializationRegistry>? serializers = null, IBuilder<FormatterRegistry>? formatters = null, ExecutionMode? mode = null)
{
if (injectors != null)
{
Expand All @@ -59,6 +60,11 @@ private static ControllerBuilder Configured(this ControllerBuilder builder, IBui
builder.Formatters(formatters);
}

if (mode != null)
{
builder.ExecutionMode(mode.Value);
}

return builder;
}
}
18 changes: 16 additions & 2 deletions Modules/Controllers/Provider/ControllerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace GenHTTP.Modules.Controllers.Provider;

public sealed class ControllerBuilder : IHandlerBuilder<ControllerBuilder>, IRegistryBuilder<ControllerBuilder>
public sealed class ControllerBuilder : IReflectionFrameworkBuilder<ControllerBuilder>
{
private readonly List<IConcernBuilder> _concerns = [];

Expand All @@ -26,6 +26,8 @@ public sealed class ControllerBuilder : IHandlerBuilder<ControllerBuilder>, IReg

private IBuilder<SerializationRegistry>? _serializers;

private ExecutionMode? _executionMode;

#region Functionality

public ControllerBuilder Serializers(IBuilder<SerializationRegistry> registry)
Expand Down Expand Up @@ -69,6 +71,16 @@ public ControllerBuilder InstanceProvider(Func<IRequest, ValueTask<object>> prov
return this;
}

/// <summary>
/// Sets the execution mode to be used to run functions.
/// </summary>
/// <param name="mode">The mode to be used for execution</param>
public ControllerBuilder ExecutionMode(ExecutionMode mode)
{
_executionMode = mode;
return this;
}

public ControllerBuilder Add(IConcernBuilder concern)
{
_concerns.Add(concern);
Expand All @@ -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
Expand Down
30 changes: 16 additions & 14 deletions Modules/Controllers/Provider/ControllerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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<IRequest, ValueTask<object>> instanceProvider, MethodRegistry registry)
public ControllerHandler(Type type, Func<IRequest, ValueTask<object>> instanceProvider, ExecutionSettings executionSettings, MethodRegistry registry)
{
Type = type;
InstanceProvider = instanceProvider;
ExecutionSettings = executionSettings;
Registry = registry;

Methods = new SynchronizedMethodCollection(GetMethodsAsync);
}

#endregion
Expand All @@ -40,47 +45,44 @@ public ControllerHandler(Type type, Func<IRequest, ValueTask<object>> instancePr

public ValueTask PrepareAsync() => ValueTask.CompletedTask;

public async ValueTask<IResponse?> HandleAsync(IRequest request) => await (await GetMethodsAsync(request)).HandleAsync(request);
public ValueTask<IResponse?> HandleAsync(IRequest request) => Methods.HandleAsync(request);

public async ValueTask<MethodCollection> GetMethodsAsync(IRequest request)
private async Task<MethodCollection> GetMethodsAsync(IRequest request)
{
if (_methods != null) return _methods;

var found = new List<MethodHandler>();


foreach (var method in Type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
var annotation = method.GetCustomAttribute<ControllerActionAttribute>(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<string> arguments, MethodRegistry registry)
private static Operation CreateOperation(IRequest request, MethodInfo method, ExecutionSettings executionSettings, IMethodConfiguration configuration, List<string> 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<string> FindPathArguments(MethodInfo method)
Expand Down
5 changes: 4 additions & 1 deletion Modules/Conversion/Formatters/DateOnlyFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Text.RegularExpressions;
using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;

namespace GenHTTP.Modules.Conversion.Formatters;

Expand All @@ -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();

}
18 changes: 16 additions & 2 deletions Modules/Functional/Provider/InlineBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace GenHTTP.Modules.Functional.Provider;

public class InlineBuilder : IHandlerBuilder<InlineBuilder>, IRegistryBuilder<InlineBuilder>
public class InlineBuilder : IReflectionFrameworkBuilder<InlineBuilder>
{
private static readonly HashSet<FlexibleRequestMethod> AllMethods = [..Enum.GetValues<RequestMethod>().Select(FlexibleRequestMethod.Get)];

Expand All @@ -24,6 +24,8 @@ public class InlineBuilder : IHandlerBuilder<InlineBuilder>, IRegistryBuilder<In

private IBuilder<SerializationRegistry>? _serializers;

private ExecutionMode? _executionMode;

#region Functionality

/// <summary>
Expand Down Expand Up @@ -57,6 +59,16 @@ public InlineBuilder Formatters(IBuilder<FormatterRegistry> registry)
return this;
}

/// <summary>
/// Sets the execution mode to be used to run functions.
/// </summary>
/// <param name="mode">The mode to be used for execution</param>
public InlineBuilder ExecutionMode(ExecutionMode mode)
{
_executionMode = mode;
return this;
}

/// <summary>
/// Adds a route for a request of any type to the root of the handler.
/// </summary>
Expand Down Expand Up @@ -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
Expand Down
24 changes: 14 additions & 10 deletions Modules/Functional/Provider/InlineHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@

namespace GenHTTP.Modules.Functional.Provider;

public class InlineHandler : IHandler, IServiceMethodProvider
public sealed class InlineHandler : IHandler, IServiceMethodProvider
{
private MethodCollection? _methods;

#region Get-/Setters

private List<InlineFunction> Functions { get; }

private MethodRegistry Registry { get; }

private ExecutionSettings ExecutionSettings { get; }

public SynchronizedMethodCollection Methods { get; }

#endregion

#region Initialization

public InlineHandler(List<InlineFunction> functions, MethodRegistry registry)
public InlineHandler(List<InlineFunction> functions, MethodRegistry registry, ExecutionSettings executionSettings)
{
Functions = functions;
Registry = registry;
ExecutionSettings = executionSettings;

Methods = new SynchronizedMethodCollection(GetMethodsAsync);
}

#endregion
Expand All @@ -32,32 +38,30 @@ public InlineHandler(List<InlineFunction> functions, MethodRegistry registry)

public ValueTask PrepareAsync() => ValueTask.CompletedTask;

public async ValueTask<IResponse?> HandleAsync(IRequest request) => await (await GetMethodsAsync(request)).HandleAsync(request);
public ValueTask<IResponse?> HandleAsync(IRequest request) => Methods.HandleAsync(request);

public async ValueTask<MethodCollection> GetMethodsAsync(IRequest request)
private async Task<MethodCollection> GetMethodsAsync(IRequest request)
{
if (_methods != null) return _methods;

var found = new List<MethodHandler>();

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
Expand Down
Loading
Loading