< Summary

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

File(s)

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

#LineLine coverage
 1/********************************************************************************
 2* NanoRouteQueryExtensions.cs                                                   *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Collections.Generic;
 8using System.Collections.Frozen;
 9using System.Net.Http;
 10
 11namespace NanoRoute
 12{
 13    using Internals;
 14    using Properties;
 15
 16    /// <summary>
 17    /// Defines how query-binding middleware handles query-string parameters that are not declared in the binding descri
 18    /// </summary>
 19    /// <example>
 20    /// <code>
 21    /// builder.ConfigureQueryParsing(config =&gt; config with
 22    /// {
 23    ///     UnexpectedParameterBehavior = UnexpectedParameterBehavior.Reject
 24    /// });
 25    /// </code>
 26    /// </example>
 27    public enum UnexpectedParameterBehavior
 28    {
 29        /// <summary>
 30        /// Ignore undeclared query-string parameters.
 31        /// </summary>
 32        /// <example>
 33        /// <code>
 34        /// builder.ConfigureQueryParsing(config =&gt; config with
 35        /// {
 36        ///     UnexpectedParameterBehavior = UnexpectedParameterBehavior.Ignore
 37        /// });
 38        /// </code>
 39        /// </example>
 40        Ignore,
 41
 42        /// <summary>
 43        /// Reject undeclared query-string parameters with a <c>400 Bad Request</c> error.
 44        /// </summary>
 45        /// <example>
 46        /// <code>
 47        /// builder.ConfigureQueryParsing(config =&gt; config with
 48        /// {
 49        ///     UnexpectedParameterBehavior = UnexpectedParameterBehavior.Reject
 50        /// });
 51        /// </code>
 52        /// </example>
 53        Reject,
 54
 55        // Dangerous since it lets the caller override parameter values provided by earlier middlewares
 56
 57        //AcceptAsString
 58    }
 59
 60    /// <summary>
 61    /// Configures how query-binding middleware parses query strings.
 62    /// </summary>
 63    /// <remarks>
 64    /// Query-binding middleware snapshots the configuration that is current when it is registered.
 65    /// </remarks>
 66    /// <example>
 67    /// <code>
 68    /// builder.ConfigureQueryParsing(config =&gt; config with
 69    /// {
 70    ///     UnexpectedParameterBehavior = UnexpectedParameterBehavior.Reject
 71    /// });
 72    /// </code>
 73    /// </example>
 74    public sealed record QueryParsingConfig
 75    {
 76        /// <summary>
 77        /// Gets how undeclared query-string parameters are handled.
 78        /// </summary>
 79        /// <remarks>
 80        /// The default is <see cref="UnexpectedParameterBehavior.Ignore"/>, so additional query-string keys that are
 81        /// not present in the binding descriptor do not affect request processing.
 82        /// </remarks>
 83        /// <exception cref="ArgumentOutOfRangeException">Thrown when the assigned value is not a defined <see cref="Une
 84        /// <example>
 85        /// <code>
 86        /// builder.ConfigureQueryParsing(config =&gt; config with
 87        /// {
 88        ///     UnexpectedParameterBehavior = UnexpectedParameterBehavior.Reject
 89        /// });
 90        /// </code>
 91        /// </example>
 92        public UnexpectedParameterBehavior UnexpectedParameterBehavior
 93        {
 94            get;
 95            init
 196            {
 197                if (!Enum.IsDefined(typeof(UnexpectedParameterBehavior), value))
 198                    throw new ArgumentOutOfRangeException(nameof(value));
 199                field = value;
 1100            }
 1101        } = UnexpectedParameterBehavior.Ignore;
 102
 103        /// <summary>
 104        /// Gets the default query-parsing configuration.
 105        /// </summary>
 106        /// <example>
 107        /// <code>
 108        /// QueryParsingConfig config = QueryParsingConfig.Default;
 109        /// </code>
 110        /// </example>
 1111        public static QueryParsingConfig Default { get; } = new();
 112    }
 113
 114    /// <summary>
 115    /// Adds query-parameter binding helpers to NanoRoute.
 116    /// </summary>
 117    /// <example>
 118    /// <code>
 119    /// builder
 120    ///     .AddDefaultValueParsers()
 121    ///     .AddQueryBindings("{page?:int(min=1)}")
 122    ///     .AddHandler("GET", "/items/", (context, _) =&gt; Results.Ok(context.Parameters));
 123    /// </code>
 124    /// </example>
 125    public static class NanoRouteQueryExtensions
 126    {
 127        #region Private
 128        private static RequestHandlerDelegate CreateHandler(RouteScopeBuilder routeScopeBuilder, string bindings)
 129        {
 130            Ensure.NotNull(bindings);
 131
 132            Dictionary<ReadOnlyMemory<char>, ParameterParser> parsedBindingsTmp = new(ReadOnlyMemoryCharComparer.Instanc
 133
 134            foreach (ParameterDefinition parameterDefinition in DslParser.ParseQueryPattern(bindings))
 135            {
 136                if (!routeScopeBuilder.ValueParsers.TryGetValue(parameterDefinition.ValueParser.Name, out ValueParserReg
 137                    throw new InvalidOperationException
 138                    (
 139                        string.Format(Resources.Culture, Resources.ERR_NO_SUCH_PARSER, parameterDefinition.ValueParser.N
 140                    );
 141
 142                parsedBindingsTmp.Add
 143                (
 144                    parameterDefinition.ParameterName!.AsMemory(),
 145                    new ParameterParser
 146                    (
 147                        parameterDefinition,
 148                        parserRegistration.Parse,
 149                        parserRegistration.BindArguments(parameterDefinition.ValueParser.RawArguments)
 150                    )
 151                );
 152            }
 153
 154            FrozenDictionary<ReadOnlyMemory<char>, ParameterParser> parsedBindings = parsedBindingsTmp.ToFrozenDictionar
 155
 156            QueryParsingConfig config = routeScopeBuilder.Metadata.GetOrDefault(QueryParsingConfig.Default);
 157
 158            return async (RequestContext context, CallNextHandlerDelegate next) =>
 159            {
 160                using QueryStringParser queryStringParser = new(context, parsedBindings, config);
 161
 162                await queryStringParser.Parse().ConfigureAwait(false);
 163
 164                return await next().ConfigureAwait(false);
 165            };
 166        }
 167        #endregion
 168
 169        extension<TBuilder>(TBuilder routeScopeBuilder) where TBuilder : RouteScopeBuilder
 170        {
 171            /// <summary>
 172            /// Updates the query-parsing configuration visible from the current builder scope.
 173            /// </summary>
 174            /// <param name="configure">
 175            /// A callback that receives the current configuration and returns the replacement configuration.
 176            /// </param>
 177            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 178            /// <remarks>
 179            /// The configuration is stored in <see cref="RouteScopeBuilder.Metadata"/>. Child builders created after th
 180            /// method is called inherit the updated configuration; existing child builders keep their own scoped copy.
 181            /// Registered query-binding middleware snapshots the configuration that is current at registration time.
 182            /// </remarks>
 183            /// <exception cref="ArgumentNullException">
 184            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="configure"/>, or the value returned
 185            /// by <paramref name="configure"/> is <see langword="null"/>.
 186            /// </exception>
 187            /// <example>
 188            /// <code>
 189            /// builder.ConfigureQueryParsing(config =&gt; config with
 190            /// {
 191            ///     UnexpectedParameterBehavior = UnexpectedParameterBehavior.Reject
 192            /// });
 193            /// </code>
 194            /// </example>
 195            public TBuilder ConfigureQueryParsing(ConfigureBuilderDelegate<QueryParsingConfig> configure)
 196            {
 197                Ensure.NotNull(routeScopeBuilder);
 198                Ensure.NotNull(configure);
 199
 200                QueryParsingConfig config = configure(routeScopeBuilder.Metadata.GetOrDefault(QueryParsingConfig.Default
 201                Ensure.NotNull(config);
 202
 203                routeScopeBuilder.Metadata.Set(config);
 204
 205                return routeScopeBuilder;
 206            }
 207
 208            /// <summary>
 209            /// Parses configured query parameters and stores their values in <see cref="RequestContext.Parameters"/>.
 210            /// </summary>
 211            /// <param name="bindings">
 212            /// A query-parameter descriptor such as <c>{filter:str(min=3)}&amp;{page?:int(min=1)}</c>.
 213            /// </param>
 214            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 215            /// <remarks>
 216            /// Parsed query values are written into <see cref="RequestContext.Parameters"/>. If that dictionary
 217            /// already contains the same key because of route binding, JSON binding, or earlier middleware, the
 218            /// query binding overwrites the existing value.
 219            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the query-bind
 220            /// middleware is bound to the whole current builder scope for all supported HTTP methods.
 221            /// </remarks>
 222            /// <exception cref="InvalidOperationException">
 223            /// Thrown when <paramref name="bindings"/> references a value parser that is not registered.
 224            /// </exception>
 225            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> or <paramref na
 226            /// <exception cref="ArgumentException">Thrown when <paramref name="bindings"/> has invalid query-binding sy
 227            /// <exception cref="HttpRequestException">Thrown during request processing when the query string is invalid
 228            /// <example>
 229            /// <code>
 230            /// builder.AddQueryBindings("{filter?:str}&amp;{page?:int(min=1)}");
 231            /// </code>
 232            /// </example>
 233            public TBuilder AddQueryBindings(string bindings) => routeScopeBuilder.AddQueryBindings(HttpVerb.Names, Rout
 234
 235            /// <summary>
 236            /// Parses configured query parameters and stores their values in <see cref="RequestContext.Parameters"/>.
 237            /// </summary>
 238            /// <param name="verbs">The HTTP methods that activate the query-binding middleware.</param>
 239            /// <param name="bindings">
 240            /// A query-parameter descriptor such as <c>{filter:str(min=3)}&amp;{page?:int(min=1)}</c>.
 241            /// </param>
 242            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 243            /// <remarks>
 244            /// Parsed query values are written into <see cref="RequestContext.Parameters"/>. If that dictionary
 245            /// already contains the same key because of route binding, JSON binding, or earlier middleware, the
 246            /// query binding overwrites the existing value.
 247            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the query-bind
 248            /// middleware is bound to the whole current builder scope for the selected HTTP methods.
 249            /// </remarks>
 250            /// <exception cref="InvalidOperationException">
 251            /// Thrown when <paramref name="bindings"/> references a value parser that is not registered.
 252            /// </exception>
 253            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 254            /// <exception cref="ArgumentException">Thrown when an entry in <paramref name="verbs"/> is not supported or
 255            /// <exception cref="HttpRequestException">Thrown during request processing when the query string is invalid
 256            /// <example>
 257            /// <code>
 258            /// builder.AddQueryBindings(["GET", "HEAD"], "{filter?:str}&amp;{page?:int(min=1)}");
 259            /// </code>
 260            /// </example>
 261            public TBuilder AddQueryBindings(IEnumerable<string> verbs, string bindings) => routeScopeBuilder.AddQueryBi
 262
 263            /// <summary>
 264            /// Parses configured query parameters and stores their values in <see cref="RequestContext.Parameters"/>.
 265            /// </summary>
 266            /// <param name="pattern">
 267            /// The route pattern where the query-binding middleware should be inserted. Use <c>/</c> to apply it to
 268            /// the whole pipeline, or a narrower prefix/exact pattern to scope query binding to selected routes.
 269            /// </param>
 270            /// <param name="bindings">
 271            /// A query-parameter descriptor such as <c>{filter:str(min=3)}&amp;{page?:int(min=1)}</c>.
 272            /// </param>
 273            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 274            /// <remarks>
 275            /// Parsed query values are written into <see cref="RequestContext.Parameters"/>. If that dictionary
 276            /// already contains the same key because of route binding, JSON binding, or earlier middleware, the
 277            /// query binding overwrites the existing value.
 278            /// </remarks>
 279            /// <exception cref="InvalidOperationException">
 280            /// Thrown when <paramref name="bindings"/> references a value parser that is not registered.
 281            /// </exception>
 282            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 283            /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> has invalid route-template sy
 284            /// <exception cref="HttpRequestException">Thrown during request processing when the query string is invalid
 285            /// <example>
 286            /// <code>
 287            /// builder.AddQueryBindings("/items/*", "{filter?:str}&amp;{page?:int(min=1)}");
 288            /// </code>
 289            /// </example>
 290            public TBuilder AddQueryBindings(string pattern, string bindings) => routeScopeBuilder.AddQueryBindings(Http
 291
 292            /// <summary>
 293            /// Parses configured query parameters and stores their values in <see cref="RequestContext.Parameters"/>.
 294            /// </summary>
 295            /// <param name="verb">The HTTP method that activates the query-binding middleware.</param>
 296            /// <param name="pattern">
 297            /// The route pattern where the query-binding middleware should be inserted. Use <c>/</c> to apply it to
 298            /// the whole pipeline, or a narrower prefix/exact pattern to scope query binding to selected routes.
 299            /// </param>
 300            /// <param name="bindings">
 301            /// A query-parameter descriptor such as <c>{filter:str(min=3)}&amp;{page?:int(min=1)}</c>.
 302            /// </param>
 303            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 304            /// <remarks>
 305            /// Parsed query values are written into <see cref="RequestContext.Parameters"/>. If that dictionary
 306            /// already contains the same key because of route binding, JSON binding, or earlier middleware, the
 307            /// query binding overwrites the existing value.
 308            /// </remarks>
 309            /// <exception cref="InvalidOperationException">
 310            /// Thrown when <paramref name="bindings"/> references a value parser that is not registered.
 311            /// </exception>
 312            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 313            /// <exception cref="ArgumentException">Thrown when <paramref name="verb"/> is not supported, <paramref name
 314            /// <exception cref="HttpRequestException">Thrown during request processing when the query string is invalid
 315            /// <example>
 316            /// <code>
 317            /// builder.AddQueryBindings("GET", "/items/*", "{filter?:str}&amp;{page?:int(min=1)}");
 318            /// </code>
 319            /// </example>
 320            public TBuilder AddQueryBindings(string verb, string pattern, string bindings) => routeScopeBuilder.AddQuery
 321
 322            /// <summary>
 323            /// Parses configured query parameters and stores their values in <see cref="RequestContext.Parameters"/>.
 324            /// </summary>
 325            /// <param name="verbs">The HTTP methods that activate the query-binding middleware.</param>
 326            /// <param name="pattern">
 327            /// The route pattern where the query-binding middleware should be inserted. Use <c>/</c> to apply it to
 328            /// the whole pipeline, or a narrower prefix/exact pattern to scope query binding to selected routes.
 329            /// </param>
 330            /// <param name="bindings">
 331            /// A query-parameter descriptor such as <c>{filter:str(min=3)}&amp;{page?:int(min=1)}</c>.
 332            /// </param>
 333            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 334            /// <remarks>
 335            /// Parsed query values are written into <see cref="RequestContext.Parameters"/>. If that dictionary
 336            /// already contains the same key because of route binding, JSON binding, or earlier middleware, the
 337            /// query binding overwrites the existing value.
 338            /// </remarks>
 339            /// <exception cref="InvalidOperationException">
 340            /// Thrown when <paramref name="bindings"/> references a value parser that is not registered.
 341            /// </exception>
 342            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 343            /// <exception cref="ArgumentException">Thrown when an entry in <paramref name="verbs"/> is not supported, <
 344            /// <exception cref="HttpRequestException">Thrown during request processing when the query string is invalid
 345            /// <example>
 346            /// <code>
 347            /// builder.AddQueryBindings(["GET", "HEAD"], "/items/*", "{filter?:str}&amp;{page?:int(min=1)}");
 348            /// </code>
 349            /// </example>
 350            public TBuilder AddQueryBindings(IEnumerable<string> verbs, string pattern, string bindings)
 351            {
 352                Ensure.NotNull(routeScopeBuilder);
 353                Ensure.NotNull(verbs);
 354                Ensure.NotNull(pattern);
 355
 356                routeScopeBuilder.AddHandler(verbs, pattern, CreateHandler(routeScopeBuilder, bindings));
 357
 358                return routeScopeBuilder;
 359            }
 360        }
 361
 362        extension(EndpointBuilder endpointBuilder)
 363        {
 364            /// <summary>
 365            /// Parses configured query parameters and stores their values in <see cref="RequestContext.Parameters"/>
 366            /// for the current endpoint.
 367            /// </summary>
 368            /// <param name="bindings">
 369            /// A query-parameter descriptor such as <c>{filter:str(min=3)}&amp;{page?:int(min=1)}</c>.
 370            /// </param>
 371            /// <returns>The current <paramref name="endpointBuilder"/> instance.</returns>
 372            /// <remarks>
 373            /// The query-binding middleware is registered for the endpoint's captured HTTP methods and route match
 374            /// kind. Parsed query values are written into <see cref="RequestContext.Parameters"/>. If that dictionary
 375            /// already contains the same key because of route binding, JSON binding, or earlier middleware, the
 376            /// query binding overwrites the existing value.
 377            /// </remarks>
 378            /// <exception cref="InvalidOperationException">
 379            /// Thrown when <paramref name="bindings"/> references a value parser that is not registered.
 380            /// </exception>
 381            /// <exception cref="ArgumentNullException">Thrown when <paramref name="endpointBuilder"/> or <paramref name
 382            /// <exception cref="ArgumentException">Thrown when the endpoint's captured HTTP method is not supported or 
 383            /// <exception cref="HttpRequestException">Thrown during request processing when the query string is invalid
 384            /// <example>
 385            /// <code>
 386            /// endpoint.WithQueryBindings("{filter?:str}&amp;{page?:int(min=1)}");
 387            /// </code>
 388            /// </example>
 389            public EndpointBuilder WithQueryBindings(string bindings)
 390            {
 391                Ensure.NotNull(endpointBuilder);
 392
 393                return endpointBuilder.WithHandler
 394                (
 395                    CreateHandler(endpointBuilder.Prefix, bindings)
 396                );
 397            }
 398        }
 399    }
 400}