Skip to content

Commit

Permalink
fixes, added experimental IAsyncEnumerable<> support
Browse files Browse the repository at this point in the history
  • Loading branch information
CypherPotato committed Dec 3, 2024
1 parent 94f45bb commit 0c7e36e
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 49 deletions.
7 changes: 0 additions & 7 deletions src/Http/HttpServerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
// Repository: https://github.com/sisk-http/core

using System.Globalization;
using System.Text;

namespace Sisk.Core.Http
{
Expand Down Expand Up @@ -61,12 +60,6 @@ public sealed class HttpServerConfiguration : IDisposable
/// </summary>
public ForwardingResolver? ForwardingResolver { get; set; }

/// <summary>
/// Gets or sets the default encoding for sending and decoding messages.
/// </summary>
[Obsolete("This property is deprecated and will be removed in later Sisk versions.")]
public Encoding DefaultEncoding { get; set; } = Encoding.UTF8;

/// <summary>
/// Gets or sets the maximum size of a request body before it is closed by the socket.
/// </summary>
Expand Down
12 changes: 10 additions & 2 deletions src/Http/HttpServerFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,22 @@ public sealed class HttpServerFlags
public TimeSpan IdleConnectionTimeout = TimeSpan.FromSeconds(120);

/// <summary>
/// Determines if the new span-based multipart form reader should be used. This is an experimental
/// feature and may not be stable for production usage.
/// Determines if the new span-based multipart form reader should be used.
/// <para>
/// Default value: <c>true</c>
/// </para>
/// </summary>
public bool EnableNewMultipartFormReader = true;

/// <summary>
/// Determines if the HTTP server should convert <see cref="IAsyncEnumerable{T}"/> object responses into
/// an blocking <see cref="IEnumerable{T}"/>.
/// <para>
/// Default value: <c>true</c>
/// </para>
/// </summary>
public bool ConvertIAsyncEnumerableIntoEnumerable = true;

/// <summary>
/// Creates an new <see cref="HttpServerFlags"/> instance with default flags values.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Http/HttpServer__Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ private void ProcessRequest(HttpListenerContext context)

// get response
routerResult = matchedListeningHost.Router.Execute(srContext);
executionResult.ServerException = routerResult.Exception;

response = routerResult.Response;
bool routeAllowCors = routerResult.Route?.UseCors ?? true;
Expand Down
20 changes: 15 additions & 5 deletions src/Http/LogStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,17 @@ private async void ProcessQueue()
/// Writes an exception description in the log.
/// </summary>
/// <param name="exp">The exception which will be written.</param>
public virtual void WriteException(Exception exp)
public virtual void WriteException(Exception exp) => this.WriteException(exp, null);

/// <summary>
/// Writes an exception description in the log.
/// </summary>
/// <param name="exp">The exception which will be written.</param>
/// <param name="extraContext">Extra context message to append to the exception message.</param>
public virtual void WriteException(Exception exp, string? extraContext = null)
{
StringBuilder excpStr = new StringBuilder();
this.WriteExceptionInternal(excpStr, exp, 0);
this.WriteExceptionInternal(excpStr, exp, extraContext, 0);
this.WriteLineInternal(excpStr.ToString());
}

Expand Down Expand Up @@ -339,17 +346,20 @@ void EnqueueMessageLine(string message)
_ = this.channel.Writer.WriteAsync(message);
}

void WriteExceptionInternal(StringBuilder exceptionSbuilder, Exception exp, int currentDepth = 0)
void WriteExceptionInternal(StringBuilder exceptionSbuilder, Exception exp, string? context = null, int currentDepth = 0)
{
if (currentDepth == 0)
exceptionSbuilder.AppendLine(string.Format(SR.LogStream_ExceptionDump_Header, DateTime.Now.ToString("R")));
exceptionSbuilder.AppendLine(string.Format(SR.LogStream_ExceptionDump_Header,
context is null ? DateTime.Now.ToString("R") : $"{context}, {DateTime.Now:R}"));

exceptionSbuilder.AppendLine(exp.ToString());

if (exp.InnerException != null)
{
if (currentDepth <= 3)
{
this.WriteExceptionInternal(exceptionSbuilder, exp.InnerException, currentDepth + 1);
exceptionSbuilder.AppendLine("+++ inner exception +++");
this.WriteExceptionInternal(exceptionSbuilder, exp.InnerException, null, currentDepth + 1);
}
else
{
Expand Down
50 changes: 35 additions & 15 deletions src/Routing/Route.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class Route : IEquatable<Route>
internal RouteAction? _singleParamCallback;
internal ParameterlessRouteAction? _parameterlessRouteAction;

internal bool _isAsync;
internal bool _isAsyncEnumerable;
internal bool _isAsyncTask;
internal Regex? routeRegex;
private string path;

Expand All @@ -40,7 +41,7 @@ public class Route : IEquatable<Route>
/// <summary>
/// Gets an boolean indicating if this <see cref="Route"/> action return is an asynchronous <see cref="Task"/>.
/// </summary>
public bool IsAsync { get => this._isAsync; }
public bool IsAsync { get => this._isAsyncTask; }

/// <summary>
/// Gets or sets how this route can write messages to log files on the server.
Expand Down Expand Up @@ -109,7 +110,8 @@ public Delegate? Action
{
this._parameterlessRouteAction = null;
this._singleParamCallback = null;
this._isAsync = false;
this._isAsyncTask = false;
this._isAsyncEnumerable = false;
return;
}
else if (!this.TrySetRouteAction(value.Method, value.Target, out Exception? ex))
Expand Down Expand Up @@ -137,6 +139,23 @@ internal bool TrySetRouteAction(MethodInfo method, object? target, [NotNullWhen(
return false;
}

Exception? CheckAsyncReturnParameters(Type asyncOutType)
{
if (asyncOutType.GenericTypeArguments.Length == 0)
{
return new InvalidOperationException(string.Format(SR.Route_Action_AsyncMissingGenericType, this));
}
else
{
Type genericAssignType = asyncOutType.GenericTypeArguments[0];
if (genericAssignType.IsValueType)
{
return new NotSupportedException(SR.Route_Action_ValueTypeSet);
}
}
return null;
}

var retType = method.ReturnType;
if (retType.IsValueType)
{
Expand All @@ -145,25 +164,26 @@ internal bool TrySetRouteAction(MethodInfo method, object? target, [NotNullWhen(
}
else if (retType.IsAssignableTo(typeof(Task)))
{
this._isAsync = true;
if (retType.GenericTypeArguments.Length == 0)
this._isAsyncTask = true;
if (CheckAsyncReturnParameters(retType) is Exception rex)
{
ex = new InvalidOperationException(string.Format(SR.Route_Action_AsyncMissingGenericType, this));
ex = rex;
return false;
}
else
}
else if (retType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))
{
this._isAsyncEnumerable = true;
if (CheckAsyncReturnParameters(retType) is Exception rex)
{
Type genericAssignType = retType.GenericTypeArguments[0];
if (genericAssignType.IsValueType)
{
ex = new NotSupportedException(SR.Route_Action_ValueTypeSet);
return false;
}
ex = rex;
return false;
}
}
else
{
this._isAsync = false;
this._isAsyncTask = false;
this._isAsyncEnumerable = false;
}

ex = null;
Expand Down Expand Up @@ -223,7 +243,7 @@ public Route()
/// </summary>
public override string ToString()
{
return $"[{this.Method.ToString().ToUpper()} {this.path}] {this.Name ?? this.Action?.Method.Name}";
return $"[{this.Method.ToString().ToUpper()} {this.path}] {this.Name ?? this.Action?.Method.Name ?? "<no action>"}";
}

/// <inheritdoc/>
Expand Down
51 changes: 39 additions & 12 deletions src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

record struct RouteDictItem(System.Type type, Delegate lambda);
class ActionHandler
{
public Type MatchingType { get; set; }
public Func<object, HttpResponse> Handler { get; set; }

public ActionHandler(Type matchingType, Func<object, HttpResponse> handler)
{
this.MatchingType = matchingType;
this.Handler = handler;
}
}


namespace Sisk.Core.Routing
Expand All @@ -28,7 +38,7 @@ internal record RouterExecutionResult(HttpResponse? Response, Route? Route, Rout

internal HttpServer? parentServer;
internal List<Route> _routesList = new();
internal List<RouteDictItem> _actionHandlersList = new();
internal List<ActionHandler> _actionHandlersList = new();

[MethodImpl(MethodImplOptions.AggressiveOptimization)]
internal void BindServer(HttpServer server)
Expand Down Expand Up @@ -76,13 +86,29 @@ public Router()
{
}

/// <summary>
/// Creates an new <see cref="Router"/> instance with given route collection.
/// </summary>
/// <param name="routes">The route collection to import in this router.</param>
#if NET9_0_OR_GREATER
public Router(params IEnumerable<Route> routes)
#else
public Router(IEnumerable<Route> routes)
#endif
{
foreach (var route in routes)
this.SetRoute(route);
}

/// <summary>
/// Gets or sets the global requests handlers that will be executed in all matched routes.
/// </summary>
public IRequestHandler[] GlobalRequestHandlers { get; set; } = Array.Empty<IRequestHandler>();

/// <summary>
/// Gets or sets the Router action exception handler.
/// Gets or sets the Router action exception handler. The response handler for this property will
/// send an HTTP response to the client when an exception is caught during execution. This property
/// is only called when <see cref="HttpServerConfiguration.ThrowExceptions"/> is disabled.
/// </summary>
public ExceptionErrorCallback? CallbackErrorHandler { get; set; }

Expand Down Expand Up @@ -174,17 +200,18 @@ public bool TryResolveActionResult(object? result, [NotNullWhen(true)] out HttpR
{
Type actionType = result.GetType();

Span<RouteDictItem> hspan = CollectionsMarshal.AsSpan(this._actionHandlersList);
ref RouteDictItem pointer = ref MemoryMarshal.GetReference(hspan);
Span<ActionHandler> hspan = CollectionsMarshal.AsSpan(this._actionHandlersList);
ref ActionHandler pointer = ref MemoryMarshal.GetReference(hspan);
for (int i = 0; i < hspan.Length; i++)
{
ref RouteDictItem current = ref Unsafe.Add(ref pointer, i);
if (actionType.IsAssignableTo(current.type))
ref ActionHandler current = ref Unsafe.Add(ref pointer, i);

if (actionType.IsAssignableTo(current.MatchingType))
{
var resultObj = current.lambda.DynamicInvoke(result) as HttpResponse;
var resultObj = current.Handler(result);
if (resultObj is null)
{
throw new InvalidOperationException(SR.Format(SR.Router_Handler_HandlerNotHttpResponse, current.type.Name));
throw new InvalidOperationException(SR.Format(SR.Router_Handler_HandlerNotHttpResponse, current.MatchingType.Name));
}
response = resultObj;
return true;
Expand Down Expand Up @@ -220,13 +247,13 @@ public void RegisterValueHandler<T>(RouterActionHandlerCallback<T> actionHandler
}
for (int i = 0; i < this._actionHandlersList!.Count; i++)
{
RouteDictItem item = this._actionHandlersList[i];
if (item.type.Equals(type))
ActionHandler item = this._actionHandlersList[i];
if (item.MatchingType.Equals(type))
{
throw new ArgumentException(SR.Router_Handler_Duplicate);
}
}
this._actionHandlersList.Add(new RouteDictItem(type, actionHandler));
this._actionHandlersList.Add(new ActionHandler(type, (obj) => actionHandler((T)obj)));
}

HttpResponse ResolveAction(object? routeResult)
Expand Down
19 changes: 14 additions & 5 deletions src/Routing/Router__CoreInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private Internal.HttpStringInternals.PathMatchResult TestRouteMatchUsingRegex(Ro
}
else
{
return new HttpStringInternals.PathMatchResult(false, new NameValueCollection());
return new HttpStringInternals.PathMatchResult(false, null);
}
}

Expand Down Expand Up @@ -99,13 +99,12 @@ internal bool InvokeRequestHandlerGroup(RequestHandlerExecutionMode mode, Span<I
catch (Exception ex)
{
exception = ex;
if (!this.parentServer!.ServerConfiguration.ThrowExceptions)
if (this.parentServer!.ServerConfiguration.ThrowExceptions == false)
{
if (this.CallbackErrorHandler is not null)
{
result = this.CallbackErrorHandler(ex, context);
}
else { /* do nothing */ };
}
else throw;
}
Expand All @@ -125,6 +124,7 @@ internal RouterExecutionResult Execute(HttpContext context)

Route? matchedRoute = null;
RouteMatchResult matchResult = RouteMatchResult.NotMatched;
Exception? handledException = null;

// IsReadOnly ensures that no route will be added or removed from the list during the
// span iteration
Expand Down Expand Up @@ -263,7 +263,7 @@ internal RouterExecutionResult Execute(HttpContext context)
throw new ArgumentException(string.Format(SR.Router_NoRouteActionDefined, matchedRoute));
}

if (matchedRoute._isAsync)
if (matchedRoute._isAsyncTask)
{
if (actionResult is null)
{
Expand All @@ -273,6 +273,14 @@ internal RouterExecutionResult Execute(HttpContext context)
ref Task<object> actionTask = ref Unsafe.As<object, Task<object>>(ref actionResult);
actionResult = actionTask.GetAwaiter().GetResult();
}
else if (matchedRoute._isAsyncEnumerable)
{
if (flag.ConvertIAsyncEnumerableIntoEnumerable)
{
ref IAsyncEnumerable<object> asyncEnumerable = ref Unsafe.As<object, IAsyncEnumerable<object>>(ref actionResult);
actionResult = asyncEnumerable.ToBlockingEnumerable();
}
}

result = this.ResolveAction(actionResult);
}
Expand All @@ -282,6 +290,7 @@ internal RouterExecutionResult Execute(HttpContext context)
{
if (this.CallbackErrorHandler is not null)
{
handledException = ex;
result = this.CallbackErrorHandler(ex, context);
}
else
Expand Down Expand Up @@ -310,6 +319,6 @@ internal RouterExecutionResult Execute(HttpContext context)
#endregion
}

return new RouterExecutionResult(context.RouterResponse, matchedRoute, matchResult, null);
return new RouterExecutionResult(context.RouterResponse, matchedRoute, matchResult, handledException);
}
}
6 changes: 3 additions & 3 deletions src/Sisk.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@

<!-- version info -->
<PropertyGroup>
<AssemblyVersion>1.3.0</AssemblyVersion>
<FileVersion>1.3.0</FileVersion>
<Version>1.3.0</Version>
<AssemblyVersion>1.3.1</AssemblyVersion>
<FileVersion>1.3.1</FileVersion>
<Version>1.3.1</Version>
</PropertyGroup>

<!-- licensing, readme, signing -->
Expand Down

0 comments on commit 0c7e36e

Please sign in to comment.