< Summary

Information
Class: NanoRoute.JsonErrorDetailsConfig
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/Extensions/NanoRouteJsonExtensions.cs
Line coverage
100%
Covered lines: 6
Uncovered lines: 0
Coverable lines: 6
Total lines: 834
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBlocks covered Blocks not covered
JsonErrorDetailsConfig()30
JsonErrorDetailsConfig()20

File(s)

/home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/Extensions/NanoRouteJsonExtensions.cs

#LineLine coverage
 1/********************************************************************************
 2* NanoRouteJsonExtensions.cs                                                    *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Collections.Generic;
 8using System.Diagnostics.CodeAnalysis;
 9using System.IO;
 10using System.Net;
 11using System.Net.Http;
 12using System.Net.Mime;
 13using System.Text;
 14using System.Text.Json;
 15using System.Text.Json.Serialization.Metadata;
 16
 17namespace NanoRoute
 18{
 19    using Internals;
 20    using Properties;
 21
 22    /// <summary>
 23    /// Configures how <see cref="NanoRouteJsonExtensions.AddJsonErrorDetails{TBuilder}(TBuilder)"/> creates JSON
 24    /// <see cref="ErrorDetails"/> responses.
 25    /// </summary>
 26    /// <remarks>
 27    /// Instances are stored in <see cref="RouteScopeBuilder.Metadata"/> by
 28    /// <see cref="NanoRouteJsonExtensions.ConfigureJsonErrorDetails{TBuilder}(TBuilder, ConfigureBuilderDelegate{JsonEr
 29    /// The configuration visible from the builder scope is captured when JSON error-detail middleware is registered.
 30    /// </remarks>
 31    /// <example>
 32    /// <code>
 33    /// builder.ConfigureJsonErrorDetails(config =&gt; config with
 34    /// {
 35    ///     PopulateErrorInfo = true,
 36    ///     ErrorDetailsTypeInfo = MyJsonContext.Default.ErrorDetails
 37    /// });
 38    /// </code>
 39    /// </example>
 40    public sealed record JsonErrorDetailsConfig
 41    {
 42        /// <summary>
 43        /// Gets a value indicating whether developer-facing diagnostic details should be included in JSON error respons
 44        /// </summary>
 45        /// <remarks>
 46        /// Diagnostic details may contain exception messages or stack traces. Keep this value <see langword="false"/>
 47        /// for production responses unless the caller is trusted to see those details.
 48        /// </remarks>
 49        /// <example>
 50        /// <code>
 51        /// builder.ConfigureJsonErrorDetails(config =&gt; config with
 52        /// {
 53        ///     PopulateErrorInfo = true
 54        /// });
 55        /// </code>
 56        /// </example>
 57        public bool PopulateErrorInfo { get; init; }
 58
 59        /// <summary>
 60        /// Gets the JSON serialization metadata used for <see cref="ErrorDetails"/> responses.
 61        /// </summary>
 62        /// <remarks>
 63        /// Replace this value to use custom source-generated metadata, property naming, converters, or other
 64        /// serializer behavior for the error payload.
 65        /// </remarks>
 66        /// <exception cref="ArgumentNullException">Thrown when the assigned value is <see langword="null"/>.</exception
 67        /// <example>
 68        /// <code>
 69        /// builder.ConfigureJsonErrorDetails(config =&gt; config with
 70        /// {
 71        ///     ErrorDetailsTypeInfo = MyJsonContext.Default.ErrorDetails
 72        /// });
 73        /// </code>
 74        /// </example>
 75        public JsonTypeInfo<ErrorDetails> ErrorDetailsTypeInfo
 76        {
 77            get;
 78            init
 179            {
 180                Ensure.NotNull(value);
 181                field = value;
 182            }
 183        } = ErrorDetails.JsonTypeInfo;
 84
 85        /// <summary>
 86        /// Gets the default JSON error-detail configuration.
 87        /// </summary>
 88        /// <example>
 89        /// <code>
 90        /// JsonErrorDetailsConfig config = JsonErrorDetailsConfig.Default;
 91        /// </code>
 92        /// </example>
 193        public static JsonErrorDetailsConfig Default { get; } = new();
 94    }
 95
 96
 97    /// <summary>
 98    /// Adds JSON-focused helpers for request body binding, structured error responses, and JSON responses.
 99    /// </summary>
 100    /// <remarks>
 101    /// These helpers are optional conveniences on top of the core routing pipeline. They are implemented as
 102    /// extension methods on <see cref="RouteScopeBuilder"/> and <see cref="HttpResponseMessage"/>.
 103    /// </remarks>
 104    /// <example>
 105    /// <code>
 106    /// builder
 107    ///     .AddJsonErrorDetails()
 108    ///     .AddJsonBody(typeof(CreateUserRequest), "body")
 109    ///     .AddHandler("POST", "/users/", (context, _) =&gt; Results.Ok(context.Parameters["body"]));
 110    /// </code>
 111    /// </example>
 112    public static class NanoRouteJsonExtensions
 113    {
 114        private const string JSON_MEDIA_TYPE =
 115#if NETSTANDARD2_1_OR_GREATER
 116            MediaTypeNames.Application.Json;
 117#else
 118            "application/json";
 119#endif
 120        private static RequestHandlerDelegate CreateHandler(JsonTypeInfo typeInfo, string paramName)
 121        {
 122            Ensure.NotNull(typeInfo);
 123            Ensure.NotNull(paramName);
 124
 125            return async (RequestContext context, CallNextHandlerDelegate next) =>
 126            {
 127                context.Cancellation.ThrowIfCancellationRequested();
 128
 129                if (context.Request.Content is not { } content)
 130                {
 131                    BadRequest(Resources.ERR_MISSING_BODY);
 132                    return null!;
 133                }
 134
 135                if (!JSON_MEDIA_TYPE.Equals(content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase))
 136                    BadRequest(Resources.ERR_BAD_CONTENT_TYPE);
 137
 138                using Stream contentStream = await content.ReadAsStreamAsync().ConfigureAwait(false);
 139
 140                object? body = null;
 141
 142                try
 143                {
 144                    body = await JsonSerializer.DeserializeAsync(contentStream, typeInfo, context.Cancellation).Configur
 145                }
 146                catch (JsonException ex)
 147                {
 148                    BadRequest(ex.Message);
 149                }
 150
 151                context.Parameters[paramName] = body;
 152
 153                return await next().ConfigureAwait(false);
 154            };
 155
 156            [DoesNotReturn]
 157            static void BadRequest(string error) => HttpRequestException.Throw(HttpStatusCode.BadRequest, Resources.ERR_
 158        }
 159
 160        extension<TBuilder>(TBuilder routeScopeBuilder) where TBuilder : RouteScopeBuilder
 161        {
 162            /// <summary>
 163            /// Deserializes JSON request bodies into a route parameter for the selected HTTP methods.
 164            /// </summary>
 165            /// <param name="verbs">The HTTP methods that should require a JSON body.</param>
 166            /// <param name="pattern">
 167            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 168            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 169            /// </param>
 170            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 171            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 172            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 173            /// <remarks>
 174            /// Requests without content, requests with a non-JSON content type, and requests with invalid JSON throw
 175            /// <see cref="HttpRequestException"/>. Add AddJsonErrorDetails to translate those into
 176            /// structured HTTP error responses. The deserialized body is written into
 177            /// <see cref="RequestContext.Parameters"/>, and an existing value with the same key is overwritten.
 178            /// </remarks>
 179            /// <example>
 180            /// <code>
 181            /// routerBuilder
 182            ///     .AddJsonErrorDetails()
 183            ///     .AddJsonBody("POST", "/users/", MyJsonContext.Default.CreateUserRequest, "body")
 184            ///     .AddHandler("POST", "/users/", (context, _) =&gt;
 185            ///     {
 186            ///         CreateUserRequest body = (CreateUserRequest) context.Parameters["body"]!;
 187            ///         return Task.FromResult(HttpResponseMessage.Json(HttpStatusCode.Created, body));
 188            ///     });
 189            /// </code>
 190            /// </example>
 191            /// <exception cref="ArgumentNullException">
 192            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="verbs"/>,
 193            /// <paramref name="pattern"/>, <paramref name="typeInfo"/>, or <paramref name="paramName"/> is
 194            /// <see langword="null"/>.
 195            /// </exception>
 196            /// <exception cref="ArgumentException">
 197            /// Thrown when an entry in <paramref name="verbs"/> is not supported or <paramref name="pattern"/> has
 198            /// invalid route-template syntax.
 199            /// </exception>
 200            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 201            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 202            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 203            public TBuilder AddJsonBody(IEnumerable<string> verbs, string pattern, JsonTypeInfo typeInfo, string paramNa
 204                routeScopeBuilder.AddHandler(verbs, pattern, CreateHandler(typeInfo, paramName));
 205
 206            /// <summary>
 207            /// Deserializes JSON request bodies into a route parameter for a single HTTP method.
 208            /// </summary>
 209            /// <param name="verb">The HTTP method that should require a JSON body.</param>
 210            /// <param name="pattern">
 211            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 212            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 213            /// </param>
 214            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 215            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 216            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 217            /// <exception cref="ArgumentNullException">
 218            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="verb"/>,
 219            /// <paramref name="pattern"/>, <paramref name="typeInfo"/>, or <paramref name="paramName"/> is
 220            /// <see langword="null"/>.
 221            /// </exception>
 222            /// <exception cref="ArgumentException">Thrown when <paramref name="verb"/> is not supported or <paramref na
 223            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 224            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 225            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 226            /// <example>
 227            /// <code>
 228            /// builder.AddJsonBody("POST", "/users/", MyJsonContext.Default.CreateUserRequest, "body");
 229            /// </code>
 230            /// </example>
 231            public TBuilder AddJsonBody(string verb, string pattern, JsonTypeInfo typeInfo, string paramName) =>
 232                routeScopeBuilder.AddJsonBody([verb /*will be null checked*/], pattern, typeInfo, paramName);
 233
 234            /// <summary>
 235            /// Deserializes JSON request bodies into a route parameter for the selected HTTP methods.
 236            /// </summary>
 237            /// <param name="verbs">The HTTP methods that should require a JSON body.</param>
 238            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 239            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 240            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 241            /// <remarks>
 242            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the JSON-bindi
 243            /// middleware is bound to the whole current builder scope for the selected HTTP methods.
 244            /// </remarks>
 245            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 246            /// <exception cref="ArgumentException">Thrown when an entry in <paramref name="verbs"/> is not a supported 
 247            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 248            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 249            /// <example>
 250            /// <code>
 251            /// builder.AddJsonBody(["POST", "PUT"], MyJsonContext.Default.CreateUserRequest, "body");
 252            /// </code>
 253            /// </example>
 254            public TBuilder AddJsonBody(IEnumerable<string> verbs, JsonTypeInfo typeInfo, string paramName) =>
 255                routeScopeBuilder.AddJsonBody(verbs, RouteScopeBuilder.CurrentPrefix, typeInfo, paramName);
 256
 257            /// <summary>
 258            /// Deserializes JSON request bodies into a route parameter for <c>POST</c>, <c>PUT</c>, and <c>PATCH</c>.
 259            /// </summary>
 260            /// <param name="pattern">
 261            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 262            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 263            /// </param>
 264            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 265            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 266            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 267            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 268            /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> has invalid route-template sy
 269            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 270            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 271            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 272            /// <example>
 273            /// <code>
 274            /// builder.AddJsonBody("/users/", MyJsonContext.Default.CreateUserRequest, "body");
 275            /// </code>
 276            /// </example>
 277            public TBuilder AddJsonBody(string pattern, JsonTypeInfo typeInfo, string paramName) =>
 278                routeScopeBuilder.AddJsonBody(HttpVerb.HavingBody, pattern, typeInfo, paramName);
 279
 280            /// <summary>
 281            /// Deserializes JSON request bodies into a route parameter for <c>POST</c>, <c>PUT</c>, and <c>PATCH</c>.
 282            /// </summary>
 283            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 284            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 285            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 286            /// <remarks>
 287            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the JSON-bindi
 288            /// middleware is bound to the whole current builder scope for <c>POST</c>, <c>PUT</c>, and <c>PATCH</c>.
 289            /// </remarks>
 290            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 291            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 292            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 293            /// <example>
 294            /// <code>
 295            /// builder.AddJsonBody(MyJsonContext.Default.CreateUserRequest, "body");
 296            /// </code>
 297            /// </example>
 298            public TBuilder AddJsonBody(JsonTypeInfo typeInfo, string paramName) =>
 299                routeScopeBuilder.AddJsonBody(HttpVerb.HavingBody, RouteScopeBuilder.CurrentPrefix, typeInfo, paramName)
 300
 301            /// <summary>
 302            /// Deserializes JSON request bodies into a route parameter using runtime type metadata.
 303            /// </summary>
 304            /// <param name="verbs">The HTTP methods that should require a JSON body.</param>
 305            /// <param name="pattern">
 306            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 307            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 308            /// </param>
 309            /// <param name="type">The CLR type expected in the request body.</param>
 310            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 311            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 312            /// <exception cref="ArgumentNullException">
 313            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="verbs"/>,
 314            /// <paramref name="pattern"/>, <paramref name="type"/>, or <paramref name="paramName"/> is
 315            /// <see langword="null"/>.
 316            /// </exception>
 317            /// <exception cref="ArgumentException">
 318            /// Thrown when an entry in <paramref name="verbs"/> is not supported or <paramref name="pattern"/> has
 319            /// invalid route-template syntax.
 320            /// </exception>
 321            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 322            /// <exception cref="NotSupportedException">Thrown when JSON metadata cannot be resolved for <paramref name=
 323            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 324            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 325            /// <example>
 326            /// <code>
 327            /// builder.AddJsonBody(["POST", "PUT"], "/users/", typeof(CreateUserRequest), "body");
 328            /// </code>
 329            /// </example>
 330            public TBuilder AddJsonBody(IEnumerable<string> verbs, string pattern, Type type, string paramName)
 331            {
 332                Ensure.NotNull(type);
 333
 334                return routeScopeBuilder.AddJsonBody
 335                (
 336                    verbs,
 337                    pattern,
 338                    JsonSerializerOptions.Web.GetTypeInfo(type),
 339                    paramName
 340                );
 341            }
 342
 343            /// <summary>
 344            /// Deserializes JSON request bodies into a route parameter using runtime type metadata.
 345            /// </summary>
 346            /// <param name="verb">The HTTP method that should require a JSON body.</param>
 347            /// <param name="pattern">
 348            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 349            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 350            /// </param>
 351            /// <param name="type">The CLR type expected in the request body.</param>
 352            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 353            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 354            /// <exception cref="ArgumentNullException">
 355            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="verb"/>,
 356            /// <paramref name="pattern"/>, <paramref name="type"/>, or <paramref name="paramName"/> is
 357            /// <see langword="null"/>.
 358            /// </exception>
 359            /// <exception cref="ArgumentException">Thrown when <paramref name="verb"/> is not supported or <paramref na
 360            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 361            /// <exception cref="NotSupportedException">Thrown when JSON metadata cannot be resolved for <paramref name=
 362            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 363            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 364            /// <example>
 365            /// <code>
 366            /// builder.AddJsonBody("POST", "/users/", typeof(CreateUserRequest), "body");
 367            /// </code>
 368            /// </example>
 369            public TBuilder AddJsonBody(string verb, string pattern, Type type, string paramName) =>
 370                routeScopeBuilder.AddJsonBody([verb /*will be null checked*/], pattern, type, paramName);
 371
 372            /// <summary>
 373            /// Deserializes JSON request bodies into a route parameter using runtime type metadata.
 374            /// </summary>
 375            /// <param name="verbs">The HTTP methods that should require a JSON body.</param>
 376            /// <param name="type">The CLR type expected in the request body.</param>
 377            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 378            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 379            /// <remarks>
 380            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the JSON-bindi
 381            /// middleware is bound to the whole current builder scope for the selected HTTP methods.
 382            /// </remarks>
 383            /// <exception cref="ArgumentNullException">
 384            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="verbs"/>,
 385            /// <paramref name="type"/>, or <paramref name="paramName"/> is <see langword="null"/>.
 386            /// </exception>
 387            /// <exception cref="ArgumentException">Thrown when an entry in <paramref name="verbs"/> is not a supported 
 388            /// <exception cref="NotSupportedException">Thrown when JSON metadata cannot be resolved for <paramref name=
 389            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 390            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 391            /// <example>
 392            /// <code>
 393            /// builder.AddJsonBody(["POST", "PUT"], typeof(CreateUserRequest), "body");
 394            /// </code>
 395            /// </example>
 396            public TBuilder AddJsonBody(IEnumerable<string> verbs, Type type, string paramName) =>
 397                routeScopeBuilder.AddJsonBody(verbs, RouteScopeBuilder.CurrentPrefix, type, paramName);
 398
 399            /// <summary>
 400            /// Deserializes JSON request bodies into a route parameter using runtime type metadata for <c>POST</c>, <c>
 401            /// </summary>
 402            /// <param name="pattern">
 403            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 404            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 405            /// </param>
 406            /// <param name="type">The CLR type expected in the request body.</param>
 407            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 408            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 409            /// <exception cref="ArgumentNullException">
 410            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="pattern"/>,
 411            /// <paramref name="type"/>, or <paramref name="paramName"/> is <see langword="null"/>.
 412            /// </exception>
 413            /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> has invalid route-template sy
 414            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 415            /// <exception cref="NotSupportedException">Thrown when JSON metadata cannot be resolved for <paramref name=
 416            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 417            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 418            /// <example>
 419            /// <code>
 420            /// builder.AddJsonBody("/users/", typeof(CreateUserRequest), "body");
 421            /// </code>
 422            /// </example>
 423            public TBuilder AddJsonBody(string pattern, Type type, string paramName) =>
 424                routeScopeBuilder.AddJsonBody(HttpVerb.HavingBody, pattern, type, paramName);
 425
 426            /// <summary>
 427            /// Deserializes JSON request bodies into a route parameter using runtime type metadata for <c>POST</c>, <c>
 428            /// </summary>
 429            /// <param name="type">The CLR type expected in the request body.</param>
 430            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 431            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 432            /// <remarks>
 433            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the JSON-bindi
 434            /// middleware is bound to the whole current builder scope for <c>POST</c>, <c>PUT</c>, and <c>PATCH</c>.
 435            /// </remarks>
 436            /// <exception cref="ArgumentNullException">
 437            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="type"/>, or
 438            /// <paramref name="paramName"/> is <see langword="null"/>.
 439            /// </exception>
 440            /// <exception cref="NotSupportedException">Thrown when JSON metadata cannot be resolved for <paramref name=
 441            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 442            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 443            /// <example>
 444            /// <code>
 445            /// builder.AddJsonBody(typeof(CreateUserRequest), "body");
 446            /// </code>
 447            /// </example>
 448            public TBuilder AddJsonBody(Type type, string paramName) =>
 449                routeScopeBuilder.AddJsonBody(HttpVerb.HavingBody, RouteScopeBuilder.CurrentPrefix, type, paramName);
 450
 451            /// <summary>
 452            /// Updates the JSON error-detail configuration visible from the current builder scope.
 453            /// </summary>
 454            /// <param name="configure">
 455            /// A callback that receives the current configuration and returns the replacement configuration.
 456            /// </param>
 457            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 458            /// <remarks>
 459            /// The configuration is stored in <see cref="RouteScopeBuilder.Metadata"/>. Child builders created after th
 460            /// method is called inherit the updated configuration; existing child builders keep their own scoped copy.
 461            /// Registered JSON error-detail middleware snapshots the configuration that is current at registration time
 462            /// </remarks>
 463            /// <exception cref="ArgumentNullException">
 464            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="configure"/>, or the value returned
 465            /// by <paramref name="configure"/> is <see langword="null"/>.
 466            /// </exception>
 467            /// <example>
 468            /// <code>
 469            /// builder.ConfigureJsonErrorDetails(config =&gt; config with
 470            /// {
 471            ///     PopulateErrorInfo = true
 472            /// });
 473            /// </code>
 474            /// </example>
 475            public TBuilder ConfigureJsonErrorDetails(ConfigureBuilderDelegate<JsonErrorDetailsConfig> configure)
 476            {
 477                Ensure.NotNull(routeScopeBuilder);
 478                Ensure.NotNull(configure);
 479
 480                JsonErrorDetailsConfig config = configure(routeScopeBuilder.Metadata.GetOrDefault(JsonErrorDetailsConfig
 481                Ensure.NotNull(config);
 482
 483                routeScopeBuilder.Metadata.Set(config);
 484
 485                return routeScopeBuilder;
 486            }
 487
 488            /// <summary>
 489            /// Adds middleware that converts router exceptions into JSON <see cref="ErrorDetails"/> responses for all s
 490            /// </summary>
 491            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 492            /// <remarks>
 493            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the error-deta
 494            /// middleware is bound to the whole current builder scope for all supported HTTP methods.
 495            /// </remarks>
 496            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> is <see langwor
 497            /// <example>
 498            /// <code>
 499            /// builder.AddJsonErrorDetails();
 500            /// </code>
 501            /// </example>
 502            public TBuilder AddJsonErrorDetails() =>
 503                routeScopeBuilder.AddJsonErrorDetails(RouteScopeBuilder.CurrentPrefix);
 504
 505            /// <summary>
 506            /// Adds middleware that converts router exceptions into JSON <see cref="ErrorDetails"/> responses for all s
 507            /// </summary>
 508            /// <param name="pattern">
 509            /// The route pattern where the error-detail middleware should be inserted. Use <c>/</c> to apply it to
 510            /// the whole pipeline, or a narrower prefix/exact pattern to scope JSON error responses to selected routes.
 511            /// </param>
 512            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 513            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> or <paramref na
 514            /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> has invalid route-template sy
 515            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 516            /// <example>
 517            /// <code>
 518            /// builder.AddJsonErrorDetails("/api/*");
 519            /// </code>
 520            /// </example>
 521            public TBuilder AddJsonErrorDetails(string pattern) =>
 522                routeScopeBuilder.AddJsonErrorDetails(HttpVerb.Names, pattern);
 523
 524            /// <summary>
 525            /// Adds middleware that converts router exceptions into JSON <see cref="ErrorDetails"/> responses for a sin
 526            /// </summary>
 527            /// <param name="verb">The HTTP method that should use the error-detail middleware.</param>
 528            /// <param name="pattern">
 529            /// The route pattern where the error-detail middleware should be inserted. Use <c>/</c> to apply it to
 530            /// the whole pipeline, or a narrower prefix/exact pattern to scope JSON error responses to selected routes.
 531            /// </param>
 532            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 533            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 534            /// <exception cref="ArgumentException">Thrown when <paramref name="verb"/> is not supported or <paramref na
 535            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 536            /// <example>
 537            /// <code>
 538            /// builder.AddJsonErrorDetails("GET", "/api/*");
 539            /// </code>
 540            /// </example>
 541            public TBuilder AddJsonErrorDetails(string verb, string pattern) =>
 542                routeScopeBuilder.AddJsonErrorDetails([verb /*will be null checked*/], pattern);
 543
 544            /// <summary>
 545            /// Adds middleware that converts router exceptions into JSON <see cref="ErrorDetails"/> responses for the s
 546            /// </summary>
 547            /// <param name="verbs">The HTTP methods that should use the error-detail middleware.</param>
 548            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 549            /// <remarks>
 550            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the error-deta
 551            /// middleware is bound to the whole current builder scope for the selected HTTP methods.
 552            /// </remarks>
 553            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> or <paramref na
 554            /// <exception cref="ArgumentException">Thrown when an entry in <paramref name="verbs"/> is not a supported 
 555            /// <example>
 556            /// <code>
 557            /// builder.AddJsonErrorDetails(["GET", "POST"]);
 558            /// </code>
 559            /// </example>
 560            public TBuilder AddJsonErrorDetails(IEnumerable<string> verbs) =>
 561                routeScopeBuilder.AddJsonErrorDetails(verbs, RouteScopeBuilder.CurrentPrefix);
 562
 563            /// <summary>
 564            /// Adds middleware that converts router exceptions into JSON <see cref="ErrorDetails"/> responses.
 565            /// </summary>
 566            /// <param name="verbs">The HTTP methods that should use the error-detail middleware.</param>
 567            /// <param name="pattern">
 568            /// The route pattern where the error-detail middleware should be inserted. Use <c>/</c> to apply it to
 569            /// the whole pipeline, or a narrower prefix/exact pattern to scope JSON error responses to selected routes.
 570            /// </param>
 571            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 572            /// <remarks>
 573            /// This helper wraps <see cref="HttpRequestException"/> values into JSON responses and also installs
 574            /// <see cref="NanoRouteExceptionExtensions.AddExceptionHandler{TBuilder}(TBuilder)"/> so unexpected
 575            /// exceptions are normalized before they reach the client. <see cref="OperationCanceledException"/> is
 576            /// not translated into JSON and continues to propagate to the caller unchanged. Use
 577            /// <see cref="ConfigureJsonErrorDetails{TBuilder}(TBuilder, ConfigureBuilderDelegate{JsonErrorDetailsConfig
 578            /// calling this method to include developer diagnostics or replace the <see cref="ErrorDetails"/>
 579            /// serialization metadata.
 580            /// </remarks>
 581            /// <example>
 582            /// <code>
 583            /// routerBuilder
 584            ///     .AddJsonErrorDetails()
 585            ///     .AddHandler("GET", "/items/{id:int}/", (context, _) =&gt;
 586            ///         throw new InvalidOperationException("Unexpected state"));
 587            /// </code>
 588            /// </example>
 589            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 590            /// <exception cref="ArgumentException">Thrown when an entry in <paramref name="verbs"/> is not supported or
 591            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 592            public TBuilder AddJsonErrorDetails(IEnumerable<string> verbs, string pattern)
 593            {
 594                Ensure.NotNull(routeScopeBuilder);
 595                Ensure.NotNull(verbs);
 596                Ensure.NotNull(pattern);
 597
 598                JsonErrorDetailsConfig config = routeScopeBuilder.Metadata.GetOrDefault(JsonErrorDetailsConfig.Default);
 599
 600                routeScopeBuilder
 601                    .AddHandler(verbs, pattern, async (RequestContext context, CallNextHandlerDelegate next) =>
 602                    {
 603                        try
 604                        {
 605                            return await next().ConfigureAwait(false);
 606                        }
 607                        catch (HttpRequestException ex)
 608                        {
 609                            context.Request.Properties.TryGetValue(Router.TraceIdName, out object? traceId);
 610
 611                            ErrorDetails errorDetails = ex.GetErrorDetails(config.PopulateErrorInfo, traceId as string);
 612
 613                            return HttpResponseMessage.Json
 614                            (
 615                                errorDetails.Status,
 616                                errorDetails,
 617                                config.ErrorDetailsTypeInfo
 618                            );
 619                        }
 620                    })
 621                    .AddExceptionHandler(verbs, pattern);
 622
 623                return routeScopeBuilder;
 624            }
 625        }
 626
 627        extension(EndpointBuilder endpointBuilder)
 628        {
 629            /// <summary>
 630            /// Deserializes JSON request bodies into an endpoint parameter using source-generated or custom JSON metada
 631            /// </summary>
 632            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 633            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 634            /// <returns>The current <paramref name="endpointBuilder"/> instance.</returns>
 635            /// <remarks>
 636            /// The JSON-binding middleware is registered for the endpoint's captured HTTP methods and route match
 637            /// kind. The deserialized body is written into <see cref="RequestContext.Parameters"/>, and an existing
 638            /// value with the same key is overwritten.
 639            /// </remarks>
 640            /// <exception cref="ArgumentNullException">Thrown when <paramref name="endpointBuilder"/>, <paramref name="
 641            /// <exception cref="ArgumentException">Thrown when the endpoint's captured HTTP method is not supported.</e
 642            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 643            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 644            /// <example>
 645            /// <code>
 646            /// endpoint.WithJsonBody(MyJsonContext.Default.CreateUserRequest, "body");
 647            /// </code>
 648            /// </example>
 649            public EndpointBuilder WithJsonBody(JsonTypeInfo typeInfo, string paramName)
 650            {
 651                Ensure.NotNull(endpointBuilder);
 652
 653                return endpointBuilder.WithHandler
 654                (
 655                    CreateHandler(typeInfo, paramName)
 656                );
 657            }
 658
 659            /// <summary>
 660            /// Deserializes JSON request bodies into an endpoint parameter using runtime type metadata.
 661            /// </summary>
 662            /// <param name="type">The CLR type expected in the request body.</param>
 663            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 664            /// <returns>The current <paramref name="endpointBuilder"/> instance.</returns>
 665            /// <exception cref="ArgumentNullException">Thrown when <paramref name="endpointBuilder"/>, <paramref name="
 666            /// <exception cref="ArgumentException">Thrown when the endpoint's captured HTTP method is not supported.</e
 667            /// <exception cref="NotSupportedException">Thrown when JSON metadata cannot be resolved for <paramref name=
 668            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 669            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 670            /// <example>
 671            /// <code>
 672            /// endpoint.WithJsonBody(typeof(CreateUserRequest), "body");
 673            /// </code>
 674            /// </example>
 675            public EndpointBuilder WithJsonBody(Type type, string paramName)
 676            {
 677                Ensure.NotNull(type);
 678
 679                return endpointBuilder.WithJsonBody
 680                (
 681                    JsonSerializerOptions.Web.GetTypeInfo(type),
 682                    paramName
 683                );
 684            }
 685
 686            /// <summary>
 687            /// Deserializes JSON request bodies into an endpoint parameter using runtime type metadata.
 688            /// </summary>
 689            /// <typeparam name="T">The CLR type expected in the request body.</typeparam>
 690            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 691            /// <returns>The current <paramref name="endpointBuilder"/> instance.</returns>
 692            /// <exception cref="ArgumentNullException">Thrown when <paramref name="endpointBuilder"/> or <paramref name
 693            /// <exception cref="ArgumentException">Thrown when the endpoint's captured HTTP method is not supported.</e
 694            /// <exception cref="NotSupportedException">Thrown when JSON metadata cannot be resolved for <typeparamref n
 695            /// <exception cref="HttpRequestException">Thrown during request processing when the body is missing, the co
 696            /// <exception cref="OperationCanceledException">Thrown during request processing when the request cancellat
 697            /// <example>
 698            /// <code>
 699            /// endpoint.WithJsonBody&lt;CreateUserRequest&gt;("body");
 700            /// </code>
 701            /// </example>
 702            public EndpointBuilder WithJsonBody<T>(string paramName) => endpointBuilder.WithJsonBody(typeof(T), paramNam
 703        }
 704
 705        extension(HttpResponseMessage)
 706        {
 707            /// <summary>
 708            /// Creates a JSON response using the supplied type metadata.
 709            /// </summary>
 710            /// <param name="statusCode">The HTTP status code to assign to the response.</param>
 711            /// <param name="body">The value to serialize.</param>
 712            /// <param name="typeInfo">The metadata used to serialize <paramref name="body"/>.</param>
 713            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 714            /// <exception cref="ArgumentNullException">Thrown when <paramref name="typeInfo"/> is <see langword="null"/
 715            /// <exception cref="InvalidOperationException">Thrown when the supplied JSON metadata is not compatible wit
 716            /// <exception cref="NotSupportedException">Thrown when the value cannot be serialized with the supplied JSO
 717            /// <example>
 718            /// <code>
 719            /// return HttpResponseMessage.Json(HttpStatusCode.Created, body, MyJsonContext.Default.CreateUserResponse);
 720            /// </code>
 721            /// </example>
 722            public static HttpResponseMessage Json(HttpStatusCode statusCode, object? body, JsonTypeInfo typeInfo)
 723            {
 724                Ensure.NotNull(typeInfo);
 725
 726                return new HttpResponseMessage(statusCode)
 727                {
 728                    Content = new StringContent
 729                    (
 730                        JsonSerializer.Serialize(body, typeInfo),
 731                        Encoding.UTF8,
 732                        JSON_MEDIA_TYPE
 733                    )
 734                };
 735            }
 736
 737            /// <summary>
 738            /// Creates a JSON response using the supplied type metadata.
 739            /// </summary>
 740            /// <param name="statusCode">The HTTP status code to assign to the response.</param>
 741            /// <param name="body">The value to serialize.</param>
 742            /// <param name="typeInfo">The metadata used to serialize <paramref name="body"/>.</param>
 743            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 744            /// <exception cref="ArgumentNullException">Thrown when <paramref name="typeInfo"/> is <see langword="null"/
 745            /// <exception cref="InvalidOperationException">Thrown when the supplied JSON metadata is not compatible wit
 746            /// <exception cref="NotSupportedException">Thrown when the value cannot be serialized with the supplied JSO
 747            /// <example>
 748            /// <code>
 749            /// return HttpResponseMessage.Json(HttpStatusCode.OK, body, MyJsonContext.Default.CreateUserResponse);
 750            /// </code>
 751            /// </example>
 752            public static HttpResponseMessage Json<T>(HttpStatusCode statusCode, T? body, JsonTypeInfo<T> typeInfo)
 753            {
 754                Ensure.NotNull(typeInfo);
 755
 756                return Json(statusCode, (object?) body, typeInfo);
 757            }
 758
 759            /// <summary>
 760            /// Creates a JSON response using serializer <paramref name="options"/> to resolve metadata for <typeparamre
 761            /// </summary>
 762            /// <typeparam name="T">The type of the response body.</typeparam>
 763            /// <param name="statusCode">The HTTP status code to assign to the response.</param>
 764            /// <param name="body">The value to serialize.</param>
 765            /// <param name="options">The serializer options used to resolve metadata and serialization behavior.</param
 766            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 767            /// <exception cref="ArgumentNullException">Thrown when <paramref name="options"/> is <see langword="null"/>
 768            /// <exception cref="InvalidOperationException">Thrown when JSON metadata cannot be resolved or is not compa
 769            /// <exception cref="NotSupportedException">Thrown when the value cannot be serialized with the resolved JSO
 770            /// <example>
 771            /// <code>
 772            /// return HttpResponseMessage.Json(HttpStatusCode.OK, body, JsonSerializerOptions.Web);
 773            /// </code>
 774            /// </example>
 775            public static HttpResponseMessage Json<T>(HttpStatusCode statusCode, T? body, JsonSerializerOptions options)
 776            {
 777                Ensure.NotNull(options);
 778
 779                if (options.TypeInfoResolver is null)
 780                    // do not change the original options
 781                    options = new JsonSerializerOptions(options)
 782                    {
 783                        TypeInfoResolver = new DefaultJsonTypeInfoResolver()
 784                    };
 785
 786                return Json(statusCode, body, options.GetTypeInfo(typeof(T)));
 787            }
 788
 789            /// <summary>
 790            /// Creates a JSON response using <see cref="JsonSerializerOptions.Web"/>.
 791            /// </summary>
 792            /// <typeparam name="T">The type of the response body.</typeparam>
 793            /// <param name="statusCode">The HTTP status code to assign to the response.</param>
 794            /// <param name="body">The value to serialize.</param>
 795            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 796            /// <exception cref="InvalidOperationException">Thrown when JSON metadata cannot be resolved or is not compa
 797            /// <exception cref="NotSupportedException">Thrown when the value cannot be serialized with the resolved JSO
 798            /// <example>
 799            /// <code>
 800            /// return HttpResponseMessage.Json(HttpStatusCode.Created, body);
 801            /// </code>
 802            /// </example>
 803            public static HttpResponseMessage Json<T>(HttpStatusCode statusCode, T? body) => Json(statusCode, body, Json
 804
 805            /// <summary>
 806            /// Creates a JSON response with <see cref="HttpStatusCode.OK"/>. This method uses <see cref="JsonSerializer
 807            /// </summary>
 808            /// <typeparam name="T">The type of the response body.</typeparam>
 809            /// <param name="body">The value to serialize.</param>
 810            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 811            /// <exception cref="InvalidOperationException">Thrown when JSON metadata cannot be resolved or is not compa
 812            /// <exception cref="NotSupportedException">Thrown when the value cannot be serialized with the resolved JSO
 813            /// <example>
 814            /// <code>
 815            /// return HttpResponseMessage.Json(body);
 816            /// </code>
 817            /// </example>
 818            public static HttpResponseMessage Json<T>(T? body) => Json(HttpStatusCode.OK, body);
 819        }
 820
 821        extension(ErrorDetails)
 822        {
 823            /// <summary>
 824            /// Provides the default JSON serialization meta-data.
 825            /// </summary>
 826            /// <example>
 827            /// <code>
 828            /// JsonTypeInfo&lt;ErrorDetails&gt; typeInfo = ErrorDetails.JsonTypeInfo;
 829            /// </code>
 830            /// </example>
 831            public static JsonTypeInfo<ErrorDetails> JsonTypeInfo => JsonContext.Default.ErrorDetails;
 832        }
 833    }
 834}