diff --git a/src/Http/HttpServerConfiguration.cs b/src/Http/HttpServerConfiguration.cs index fe3d10d..5ee6bf0 100644 --- a/src/Http/HttpServerConfiguration.cs +++ b/src/Http/HttpServerConfiguration.cs @@ -8,7 +8,6 @@ // Repository: https://github.com/sisk-http/core using System.Globalization; -using System.Text; namespace Sisk.Core.Http { @@ -61,12 +60,6 @@ public sealed class HttpServerConfiguration : IDisposable /// public ForwardingResolver? ForwardingResolver { get; set; } - /// - /// Gets or sets the default encoding for sending and decoding messages. - /// - [Obsolete("This property is deprecated and will be removed in later Sisk versions.")] - public Encoding DefaultEncoding { get; set; } = Encoding.UTF8; - /// /// Gets or sets the maximum size of a request body before it is closed by the socket. /// diff --git a/src/Http/HttpServerFlags.cs b/src/Http/HttpServerFlags.cs index ae9f9fb..921cc3c 100644 --- a/src/Http/HttpServerFlags.cs +++ b/src/Http/HttpServerFlags.cs @@ -128,14 +128,22 @@ public sealed class HttpServerFlags public TimeSpan IdleConnectionTimeout = TimeSpan.FromSeconds(120); /// - /// 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. /// /// Default value: true /// /// public bool EnableNewMultipartFormReader = true; + /// + /// Determines if the HTTP server should convert object responses into + /// an blocking . + /// + /// Default value: true + /// + /// + public bool ConvertIAsyncEnumerableIntoEnumerable = true; + /// /// Creates an new instance with default flags values. /// diff --git a/src/Http/HttpServer__Core.cs b/src/Http/HttpServer__Core.cs index 3c9bcab..f22c171 100644 --- a/src/Http/HttpServer__Core.cs +++ b/src/Http/HttpServer__Core.cs @@ -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; diff --git a/src/Http/LogStream.cs b/src/Http/LogStream.cs index c28e319..724e7a0 100644 --- a/src/Http/LogStream.cs +++ b/src/Http/LogStream.cs @@ -258,10 +258,17 @@ private async void ProcessQueue() /// Writes an exception description in the log. /// /// The exception which will be written. - public virtual void WriteException(Exception exp) + public virtual void WriteException(Exception exp) => this.WriteException(exp, null); + + /// + /// Writes an exception description in the log. + /// + /// The exception which will be written. + /// Extra context message to append to the exception message. + 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()); } @@ -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 { diff --git a/src/Routing/Route.cs b/src/Routing/Route.cs index 090db38..ae27144 100644 --- a/src/Routing/Route.cs +++ b/src/Routing/Route.cs @@ -22,7 +22,8 @@ public class Route : IEquatable internal RouteAction? _singleParamCallback; internal ParameterlessRouteAction? _parameterlessRouteAction; - internal bool _isAsync; + internal bool _isAsyncEnumerable; + internal bool _isAsyncTask; internal Regex? routeRegex; private string path; @@ -40,7 +41,7 @@ public class Route : IEquatable /// /// Gets an boolean indicating if this action return is an asynchronous . /// - public bool IsAsync { get => this._isAsync; } + public bool IsAsync { get => this._isAsyncTask; } /// /// Gets or sets how this route can write messages to log files on the server. @@ -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)) @@ -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) { @@ -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; @@ -223,7 +243,7 @@ public Route() /// 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 ?? ""}"; } /// diff --git a/src/Routing/Router.cs b/src/Routing/Router.cs index 55bdb49..c739f99 100644 --- a/src/Routing/Router.cs +++ b/src/Routing/Router.cs @@ -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 Handler { get; set; } + + public ActionHandler(Type matchingType, Func handler) + { + this.MatchingType = matchingType; + this.Handler = handler; + } +} namespace Sisk.Core.Routing @@ -28,7 +38,7 @@ internal record RouterExecutionResult(HttpResponse? Response, Route? Route, Rout internal HttpServer? parentServer; internal List _routesList = new(); - internal List _actionHandlersList = new(); + internal List _actionHandlersList = new(); [MethodImpl(MethodImplOptions.AggressiveOptimization)] internal void BindServer(HttpServer server) @@ -76,13 +86,29 @@ public Router() { } + /// + /// Creates an new instance with given route collection. + /// + /// The route collection to import in this router. +#if NET9_0_OR_GREATER + public Router(params IEnumerable routes) +#else + public Router(IEnumerable routes) +#endif + { + foreach (var route in routes) + this.SetRoute(route); + } + /// /// Gets or sets the global requests handlers that will be executed in all matched routes. /// public IRequestHandler[] GlobalRequestHandlers { get; set; } = Array.Empty(); /// - /// 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 is disabled. /// public ExceptionErrorCallback? CallbackErrorHandler { get; set; } @@ -174,17 +200,18 @@ public bool TryResolveActionResult(object? result, [NotNullWhen(true)] out HttpR { Type actionType = result.GetType(); - Span hspan = CollectionsMarshal.AsSpan(this._actionHandlersList); - ref RouteDictItem pointer = ref MemoryMarshal.GetReference(hspan); + Span 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; @@ -220,13 +247,13 @@ public void RegisterValueHandler(RouterActionHandlerCallback 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) diff --git a/src/Routing/Router__CoreInvoker.cs b/src/Routing/Router__CoreInvoker.cs index 6fbdcee..5adc957 100644 --- a/src/Routing/Router__CoreInvoker.cs +++ b/src/Routing/Router__CoreInvoker.cs @@ -56,7 +56,7 @@ private Internal.HttpStringInternals.PathMatchResult TestRouteMatchUsingRegex(Ro } else { - return new HttpStringInternals.PathMatchResult(false, new NameValueCollection()); + return new HttpStringInternals.PathMatchResult(false, null); } } @@ -99,13 +99,12 @@ internal bool InvokeRequestHandlerGroup(RequestHandlerExecutionMode mode, Span actionTask = ref Unsafe.As>(ref actionResult); actionResult = actionTask.GetAwaiter().GetResult(); } + else if (matchedRoute._isAsyncEnumerable) + { + if (flag.ConvertIAsyncEnumerableIntoEnumerable) + { + ref IAsyncEnumerable asyncEnumerable = ref Unsafe.As>(ref actionResult); + actionResult = asyncEnumerable.ToBlockingEnumerable(); + } + } result = this.ResolveAction(actionResult); } @@ -282,6 +290,7 @@ internal RouterExecutionResult Execute(HttpContext context) { if (this.CallbackErrorHandler is not null) { + handledException = ex; result = this.CallbackErrorHandler(ex, context); } else @@ -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); } } diff --git a/src/Sisk.Core.csproj b/src/Sisk.Core.csproj index a78320b..0feafb2 100644 --- a/src/Sisk.Core.csproj +++ b/src/Sisk.Core.csproj @@ -45,9 +45,9 @@ - 1.3.0 - 1.3.0 - 1.3.0 + 1.3.1 + 1.3.1 + 1.3.1