< Summary

Information
Class: NanoRoute.Json.NanoRouteJsonExtensions
Assembly: NanoRoute.dll
File(s): /home/runner/work/nanoroute/nanoroute/Src/NanoRoute/Public/NanoRouteJsonExtensions.cs
Line coverage
100%
Covered lines: 146
Uncovered lines: 0
Coverable lines: 146
Total lines: 434
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

File(s)

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

#LineLine coverage
 1/********************************************************************************
 2* RouterBuilderJsonExtensions.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;
 16using System.Text.Json.Serialization.Metadata;
 17
 18namespace NanoRoute.Json
 19{
 20    using Internals;
 21    using Properties;
 22
 23    [JsonSerializable(typeof(ErrorDetails))]
 24    [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCo
 25    internal partial class JsonContext : JsonSerializerContext  // cannot be nested =(
 26    {
 27    }
 28
 29    /// <summary>
 30    /// Adds JSON-focused helpers for request body binding, structured error responses, and JSON responses.
 31    /// </summary>
 32    /// <remarks>
 33    /// These helpers are optional conveniences on top of the core routing pipeline. They are implemented as
 34    /// extension methods on <see cref="RouteBuilder"/> and <see cref="HttpResponseMessage"/>.
 35    /// </remarks>
 36    public static class NanoRouteJsonExtensions
 37    {
 38        private const string JSON_MEDIA_TYPE =
 39#if NETSTANDARD2_1_OR_GREATER
 40            MediaTypeNames.Application.Json;
 41#else
 42            "application/json";
 43#endif
 44
 45        extension<TBuilder>(TBuilder routeBuilder) where TBuilder: RouteBuilder
 46        {
 47            /// <summary>
 48            /// Deserializes JSON request bodies into a route parameter for the selected HTTP methods.
 49            /// </summary>
 50            /// <param name="verbs">The HTTP methods that should require a JSON body.</param>
 51            /// <param name="pattern">
 52            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 53            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 54            /// </param>
 55            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 56            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 57            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 58            /// <remarks>
 59            /// Requests without content, requests with a non-JSON content type, and requests with invalid JSON throw
 60            /// <see cref="HttpRequestException"/>. Add <see cref="AddJsonErrorDetails"/> to translate those into
 61            /// structured HTTP error responses. The deserialized body is written into
 62            /// <see cref="RequestContext.Parameters"/>, and an existing value with the same key is overwritten.
 63            /// </remarks>
 64            /// <example>
 65            /// <code>
 66            /// routerBuilder
 67            ///     .AddJsonErrorDetails()
 68            ///     .AddJsonBody("POST", "/users/", MyJsonContext.Default.CreateUserRequest, "body")
 69            ///     .AddHandler("POST", "/users", (context, _) =&gt;
 70            ///     {
 71            ///         CreateUserRequest body = (CreateUserRequest) context.Parameters["body"]!;
 72            ///         return Task.FromResult(HttpResponseMessage.Json(HttpStatusCode.Created, body));
 73            ///     });
 74            /// </code>
 75            /// </example>
 76            public TBuilder AddJsonBody(IEnumerable<string> verbs, string pattern, JsonTypeInfo typeInfo, string paramNa
 177            {
 178                Ensure.NotNull(routeBuilder);
 179                Ensure.NotNull(verbs);
 180                Ensure.NotNull(pattern);
 181                Ensure.NotNull(typeInfo);
 182                Ensure.NotNull(paramName);
 83
 184                routeBuilder.AddHandler(verbs, pattern, async (RequestContext context, CallNextHandlerDelegate next) =>
 185                {
 186                    context.Cancellation.ThrowIfCancellationRequested();
 187
 188                    if (context.Request.Content is not { } content)
 189                    {
 190                        BadRequest(Resources.ERR_MISSING_BODY);
 191                        return null!;
 192                    }
 193
 194                    if (!JSON_MEDIA_TYPE.Equals(content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCa
 195                        BadRequest(Resources.ERR_BAD_CONTENT_TYPE);
 196
 197                    Stream contentStream = await content.ReadAsStreamAsync();
 198
 199                    object? body = null;
 1100
 1101                    try
 1102                    {
 1103                        body = await JsonSerializer.DeserializeAsync(contentStream, typeInfo, context.Cancellation);
 1104                    }
 1105                    catch (JsonException ex)
 1106                    {
 1107                        BadRequest(ex.Message);
 1108                    }
 1109
 1110                    context.Parameters[paramName] = body;
 1111
 1112                    return await next();
 1113                });
 114
 1115                return routeBuilder;
 116
 117                [DoesNotReturn]
 118                static void BadRequest(string error) => HttpRequestException.Throw(HttpStatusCode.BadRequest, Resources.
 119            }
 120
 121            /// <summary>
 122            /// Deserializes JSON request bodies into a route parameter for a single HTTP method.
 123            /// </summary>
 124            /// <param name="verb">The HTTP method that should require a JSON body.</param>
 125            /// <param name="pattern">
 126            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 127            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 128            /// </param>
 129            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 130            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 131            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 132            public TBuilder AddJsonBody(string verb, string pattern, JsonTypeInfo typeInfo, string paramName)
 1133            {
 1134                Ensure.NotNull(routeBuilder);
 1135                Ensure.NotNull(verb);
 1136                Ensure.NotNull(pattern);
 1137                Ensure.NotNull(typeInfo);
 1138                Ensure.NotNull(paramName);
 139
 1140                return routeBuilder.AddJsonBody([verb], pattern, typeInfo, paramName);
 141            }
 142
 143            /// <summary>
 144            /// Deserializes JSON request bodies into a route parameter for the selected HTTP methods.
 145            /// </summary>
 146            /// <param name="verbs">The HTTP methods that should require a JSON body.</param>
 147            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 148            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 149            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 150            public TBuilder AddJsonBody(IEnumerable<string> verbs, JsonTypeInfo typeInfo, string paramName)
 1151            {
 1152                Ensure.NotNull(routeBuilder);
 1153                Ensure.NotNull(verbs);
 1154                Ensure.NotNull(typeInfo);
 1155                Ensure.NotNull(paramName);
 156
 1157                return routeBuilder.AddJsonBody(verbs, "/", typeInfo, paramName);
 158            }
 159
 160            /// <summary>
 161            /// Deserializes JSON request bodies into a route parameter for <c>POST</c> and <c>PUT</c>.
 162            /// </summary>
 163            /// <param name="pattern">
 164            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 165            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 166            /// </param>
 167            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 168            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 169            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 170            public TBuilder AddJsonBody(string pattern, JsonTypeInfo typeInfo, string paramName)
 1171            {
 1172                Ensure.NotNull(routeBuilder);
 1173                Ensure.NotNull(pattern);
 1174                Ensure.NotNull(typeInfo);
 1175                Ensure.NotNull(paramName);
 176
 1177                return routeBuilder.AddJsonBody(["POST", "PUT"], pattern, typeInfo, paramName);
 178            }
 179
 180            /// <summary>
 181            /// Deserializes JSON request bodies into a route parameter for <c>POST</c> and <c>PUT</c>.
 182            /// </summary>
 183            /// <param name="typeInfo">The metadata used to deserialize the request body.</param>
 184            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 185            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 186            public TBuilder AddJsonBody(JsonTypeInfo typeInfo, string paramName)
 1187            {
 1188                Ensure.NotNull(routeBuilder);
 1189                Ensure.NotNull(typeInfo);
 1190                Ensure.NotNull(paramName);
 191
 1192                return routeBuilder.AddJsonBody(["POST", "PUT"], "/", typeInfo, paramName);
 193            }
 194
 195            /// <summary>
 196            /// Deserializes JSON request bodies into a route parameter using runtime type metadata.
 197            /// </summary>
 198            /// <param name="verbs">The HTTP methods that should require a JSON body.</param>
 199            /// <param name="pattern">
 200            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 201            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 202            /// </param>
 203            /// <param name="type">The CLR type expected in the request body.</param>
 204            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 205            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 206            public TBuilder AddJsonBody(IEnumerable<string> verbs, string pattern, Type type, string paramName)
 1207            {
 1208                Ensure.NotNull(routeBuilder);
 1209                Ensure.NotNull(verbs);
 1210                Ensure.NotNull(pattern);
 1211                Ensure.NotNull(type);
 1212                Ensure.NotNull(paramName);
 213
 1214                return routeBuilder.AddJsonBody
 1215                (
 1216                    verbs,
 1217                    pattern,
 1218                    JsonSerializerOptions.Web.GetTypeInfo(type),
 1219                    paramName
 1220                );
 221            }
 222
 223            /// <summary>
 224            /// Deserializes JSON request bodies into a route parameter using runtime type metadata.
 225            /// </summary>
 226            /// <param name="verb">The HTTP method that should require a JSON body.</param>
 227            /// <param name="pattern">
 228            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 229            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 230            /// </param>
 231            /// <param name="type">The CLR type expected in the request body.</param>
 232            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 233            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 234            public TBuilder AddJsonBody(string verb, string pattern, Type type, string paramName)
 1235            {
 1236                Ensure.NotNull(routeBuilder);
 1237                Ensure.NotNull(verb);
 1238                Ensure.NotNull(pattern);
 1239                Ensure.NotNull(type);
 1240                Ensure.NotNull(paramName);
 241
 1242                return routeBuilder.AddJsonBody([verb], pattern, type, paramName);
 243            }
 244
 245            /// <summary>
 246            /// Deserializes JSON request bodies into a route parameter using runtime type metadata.
 247            /// </summary>
 248            /// <param name="verbs">The HTTP methods that should require a JSON body.</param>
 249            /// <param name="type">The CLR type expected in the request body.</param>
 250            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 251            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 252            public TBuilder AddJsonBody(IEnumerable<string> verbs, Type type, string paramName)
 1253            {
 1254                Ensure.NotNull(routeBuilder);
 1255                Ensure.NotNull(verbs);
 1256                Ensure.NotNull(type);
 1257                Ensure.NotNull(paramName);
 258
 1259                return routeBuilder.AddJsonBody(verbs, "/", type, paramName);
 260            }
 261
 262            /// <summary>
 263            /// Deserializes JSON request bodies into a route parameter using runtime type metadata for <c>POST</c> and 
 264            /// </summary>
 265            /// <param name="pattern">
 266            /// The route pattern where the JSON-binding middleware should be inserted. Use <c>/</c> to apply it
 267            /// to the whole pipeline, or a narrower prefix/exact pattern to scope body binding to selected routes.
 268            /// </param>
 269            /// <param name="type">The CLR type expected in the request body.</param>
 270            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 271            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 272            public TBuilder AddJsonBody(string pattern, Type type, string paramName)
 1273            {
 1274                Ensure.NotNull(routeBuilder);
 1275                Ensure.NotNull(pattern);
 1276                Ensure.NotNull(type);
 1277                Ensure.NotNull(paramName);
 278
 1279                return routeBuilder.AddJsonBody(["POST", "PUT"], pattern, type, paramName);
 280            }
 281
 282            /// <summary>
 283            /// Deserializes JSON request bodies into a route parameter using runtime type metadata for <c>POST</c> and 
 284            /// </summary>
 285            /// <param name="type">The CLR type expected in the request body.</param>
 286            /// <param name="paramName">The parameter name under which the deserialized body will be stored.</param>
 287            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 288            public TBuilder AddJsonBody(Type type, string paramName)
 1289            {
 1290                Ensure.NotNull(routeBuilder);
 1291                Ensure.NotNull(type);
 1292                Ensure.NotNull(paramName);
 293
 1294                return routeBuilder.AddJsonBody(["POST", "PUT"], "/", type, paramName);
 295            }
 296
 297            /// <summary>
 298            /// Adds middleware that converts router exceptions into JSON <see cref="ErrorDetails"/> responses.
 299            /// </summary>
 300            /// <param name="populateErrorInfo">
 301            /// <see langword="true"/> to include developer-facing diagnostic details when they are attached to the
 302            /// underlying exception; otherwise <see langword="false"/>.
 303            /// </param>
 304            /// <returns>The current <paramref name="routeBuilder"/> instance.</returns>
 305            /// <remarks>
 306            /// This helper wraps <see cref="HttpRequestException"/> values into JSON responses and also installs
 307            /// <see cref="NanoRouteExceptionExtensions.AddExceptionHandler{TBuilder}(TBuilder)"/> so unexpected
 308            /// exceptions are normalized before they reach the client. <see cref="OperationCanceledException"/> is
 309            /// not translated into JSON and continues to propagate to the caller unchanged.
 310            /// </remarks>
 311            /// <example>
 312            /// <code>
 313            /// routerBuilder
 314            ///     .AddJsonErrorDetails()
 315            ///     .AddHandler("GET", "/items/{id:int}", (context, _) =&gt;
 316            ///         throw new InvalidOperationException("Unexpected state"));
 317            /// </code>
 318            /// </example>
 319            public TBuilder AddJsonErrorDetails(bool populateErrorInfo = false)
 1320            {
 1321                Ensure.NotNull(routeBuilder);
 322
 1323                routeBuilder
 1324                    .AddHandler("/", async (RequestContext context, CallNextHandlerDelegate next) =>
 1325                    {
 1326                        try
 1327                        {
 1328                            return await next();
 1329                        }
 1330                        catch (HttpRequestException ex)
 1331                        {
 1332                            ErrorDetails errorDetails = ex.GetErrorDetails(populateErrorInfo, context.Request.Properties
 1333
 1334                            return HttpResponseMessage.Json
 1335                            (
 1336                                errorDetails.Status,
 1337                                errorDetails,
 1338                                ErrorDetails.JsonTypeInfo
 1339                            );
 1340                        }
 1341                    })
 1342                    .AddExceptionHandler();
 343
 1344                return routeBuilder;
 345            }
 346        }
 347
 348        extension(HttpResponseMessage)
 349        {
 350            /// <summary>
 351            /// Creates a JSON response using the supplied type metadata.
 352            /// </summary>
 353            /// <param name="statusCode">The HTTP status code to assign to the response.</param>
 354            /// <param name="body">The value to serialize.</param>
 355            /// <param name="typeInfo">The metadata used to serialize <paramref name="body"/>.</param>
 356            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 357            public static HttpResponseMessage Json(HttpStatusCode statusCode, object? body, JsonTypeInfo typeInfo)
 1358            {
 1359                Ensure.NotNull(typeInfo);
 360
 1361                return new HttpResponseMessage(statusCode)
 1362                {
 1363                    Content = new StringContent
 1364                    (
 1365                        JsonSerializer.Serialize(body, typeInfo),
 1366                        Encoding.UTF8,
 1367                        JSON_MEDIA_TYPE
 1368                    )
 1369                };
 370            }
 371
 372            /// <summary>
 373            /// Creates a JSON response using the supplied type metadata.
 374            /// </summary>
 375            /// <param name="statusCode">The HTTP status code to assign to the response.</param>
 376            /// <param name="body">The value to serialize.</param>
 377            /// <param name="typeInfo">The metadata used to serialize <paramref name="body"/>.</param>
 378            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 379            public static HttpResponseMessage Json<T>(HttpStatusCode statusCode, T? body, JsonTypeInfo<T> typeInfo)
 1380            {
 1381                Ensure.NotNull(typeInfo);
 382
 1383                return Json(statusCode, (object?) body, typeInfo);
 384            }
 385
 386            /// <summary>
 387            /// Creates a JSON response using serializer <paramref name="options"/> to resolve metadata for <typeparamre
 388            /// </summary>
 389            /// <typeparam name="T">The type of the response body.</typeparam>
 390            /// <param name="statusCode">The HTTP status code to assign to the response.</param>
 391            /// <param name="body">The value to serialize.</param>
 392            /// <param name="options">The serializer options used to resolve metadata and serialization behavior.</param
 393            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 394            public static HttpResponseMessage Json<T>(HttpStatusCode statusCode, T? body, JsonSerializerOptions options)
 1395            {
 1396                Ensure.NotNull(options);
 397
 1398                if (options.TypeInfoResolver is null)
 399                    // do not change the original options
 1400                    options = new JsonSerializerOptions(options)
 1401                    {
 1402                        TypeInfoResolver = new DefaultJsonTypeInfoResolver()
 1403                    };
 404
 1405                return Json(statusCode, body, options.GetTypeInfo(typeof(T)));
 406            }
 407
 408            /// <summary>
 409            /// Creates a JSON response using <see cref="JsonSerializerOptions.Web"/>.
 410            /// </summary>
 411            /// <typeparam name="T">The type of the response body.</typeparam>
 412            /// <param name="statusCode">The HTTP status code to assign to the response.</param>
 413            /// <param name="body">The value to serialize.</param>
 414            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 1415            public static HttpResponseMessage Json<T>(HttpStatusCode statusCode, T? body) => Json(statusCode, body, Json
 416
 417            /// <summary>
 418            /// Creates a JSON response with <see cref="HttpStatusCode.OK"/>. This method uses <see cref="JsonSerializer
 419            /// </summary>
 420            /// <typeparam name="T">The type of the response body.</typeparam>
 421            /// <param name="body">The value to serialize.</param>
 422            /// <returns>A new <see cref="HttpResponseMessage"/> with JSON content.</returns>
 1423            public static HttpResponseMessage Json<T>(T? body) => Json(HttpStatusCode.OK, body);
 424        }
 425
 426        extension(ErrorDetails)
 427        {
 428            /// <summary>
 429            /// Provides the JSON serialization meta-data.
 430            /// </summary>
 2431            public static JsonTypeInfo<ErrorDetails> JsonTypeInfo => JsonContext.Default.ErrorDetails;
 432        }
 433    }
 434}