< Summary

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

File(s)

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

#LineLine coverage
 1/********************************************************************************
 2* NanoRouteExceptionExtensions.cs                                               *
 3*                                                                               *
 4* Author: Denes Solti                                                           *
 5********************************************************************************/
 6using System;
 7using System.Diagnostics.CodeAnalysis;
 8using System.Collections.Frozen;
 9using System.Collections.Generic;
 10using System.Collections.Immutable;
 11using System.Linq;
 12using System.Net;
 13using System.Net.Http;
 14
 15namespace NanoRoute
 16{
 17    using Internals;
 18    using Properties;
 19
 20    /// <summary>
 21    /// Converts an unexpected exception into an enriched <see cref="HttpRequestException"/>.
 22    /// </summary>
 23    /// <param name="exception">The exception thrown by a later handler in the routing pipeline.</param>
 24    /// <returns>
 25    /// The <see cref="HttpRequestException"/> that should be thrown by the exception-handling middleware.
 26    /// </returns>
 27    /// <remarks>
 28    /// Normalizers are configured with <see cref="NanoRouteExceptionExtensions.ConfigureExceptionHandling{TBuilder}(TBu
 29    /// They run for exceptions whose runtime type or nearest registered base type appears in
 30    /// <see cref="ExceptionHandlingConfig.ExceptionNormalizers"/>.
 31    /// Existing <see cref="HttpRequestException"/> and <see cref="OperationCanceledException"/> values are not
 32    /// normalized by <see cref="NanoRouteExceptionExtensions.AddExceptionHandler{TBuilder}(TBuilder)"/>.
 33    /// Exceptions thrown by a normalizer propagate from the exception-handling middleware.
 34    /// </remarks>
 35    /// <example>
 36    /// <code>
 37    /// builder.ConfigureExceptionHandling(config =&gt; config with
 38    /// {
 39    ///     ExceptionNormalizers = config.ExceptionNormalizers.SetItems
 40    ///     ([
 41    ///         ExceptionNormalizer.For&lt;InvalidOperationException&gt;
 42    ///         (
 43    ///             static ex =&gt; new HttpRequestException("Bad state", ex, HttpStatusCode.Conflict)
 44    ///         )
 45    ///     ])
 46    /// });
 47    /// </code>
 48    /// </example>
 49    public delegate HttpRequestException ExceptionNormalizer(Exception exception);
 50
 51    /// <summary>
 52    /// Converts an unexpected exception of a registered type into an enriched <see cref="HttpRequestException"/>.
 53    /// </summary>
 54    /// <typeparam name="TException">The exception type handled by the normalizer.</typeparam>
 55    /// <param name="exception">The exception thrown by a later handler in the routing pipeline.</param>
 56    /// <returns>
 57    /// The <see cref="HttpRequestException"/> that should be thrown by the exception-handling middleware.
 58    /// </returns>
 59    /// <remarks>
 60    /// Use this delegate with <c>ExceptionNormalizer.For&lt;TException&gt;(...)</c> to register typed
 61    /// normalizers in <see cref="ExceptionHandlingConfig.ExceptionNormalizers"/> without manually casting from
 62    /// <see cref="Exception"/>. Exception handlers check the exact runtime type first, then walk base exception
 63    /// types, so a base-type normalizer handles derived exceptions unless a more specific normalizer is registered.
 64    /// </remarks>
 65    /// <example>
 66    /// <code>
 67    /// ExceptionNormalizer.For&lt;InvalidOperationException&gt;
 68    /// (
 69    ///     static ex =&gt; new HttpRequestException("Bad state", ex, HttpStatusCode.Conflict)
 70    /// );
 71    /// </code>
 72    /// </example>
 73    public delegate HttpRequestException TypedExceptionNormalizer<TException>(TException exception) where TException : E
 74
 75    /// <summary>
 76    /// Configures how <see cref="NanoRouteExceptionExtensions.AddExceptionHandler{TBuilder}(TBuilder)"/> normalizes
 77    /// unexpected exceptions.
 78    /// </summary>
 79    /// <remarks>
 80    /// The configuration is stored in <see cref="RouteScopeBuilder.Metadata"/> and follows normal builder scoping rules
 81    /// </remarks>
 82    /// <example>
 83    /// <code>
 84    /// builder.ConfigureExceptionHandling(config =&gt; config with
 85    /// {
 86    ///     ExceptionNormalizers = config.ExceptionNormalizers.SetItems
 87    ///     ([
 88    ///         ExceptionNormalizer.For&lt;InvalidOperationException&gt;
 89    ///         (
 90    ///             static ex =&gt; new HttpRequestException("Conflict", ex, HttpStatusCode.Conflict)
 91    ///         )
 92    ///     ])
 93    /// });
 94    /// </code>
 95    /// </example>
 96    public sealed record ExceptionHandlingConfig
 97    {
 98        /// <summary>
 99        /// Gets the exception normalizers keyed by exception type.
 100        /// </summary>
 101        /// <remarks>
 102        /// When a handler throws a non-HTTP, non-cancellation exception, <see cref="NanoRouteExceptionExtensions.AddExc
 103        /// looks up the exception's exact runtime type in this dictionary, then walks base exception types until
 104        /// a normalizer is found. If no normalizer is registered for the exception type or its base types, the
 105        /// exception is converted to a generic internal-server-error <see cref="HttpRequestException"/>.
 106        /// </remarks>
 107        /// <exception cref="ArgumentNullException">Thrown when the assigned value is <see langword="null"/>.</exception
 108        /// <example>
 109        /// <code>
 110        /// builder.ConfigureExceptionHandling(config =&gt; config with
 111        /// {
 112        ///     ExceptionNormalizers = config.ExceptionNormalizers.SetItems
 113        ///     ([
 114        ///         ExceptionNormalizer.For&lt;InvalidOperationException&gt;
 115        ///         (
 116        ///             static ex =&gt; new HttpRequestException("Conflict", ex, HttpStatusCode.Conflict)
 117        ///         )
 118        ///     ])
 119        /// });
 120        /// </code>
 121        /// </example>
 122        public ImmutableDictionary<Type, ExceptionNormalizer> ExceptionNormalizers
 123        {
 124            get;
 125            init
 1126            {
 1127                Ensure.NotNull(value);
 1128                field = value;
 1129            }
 130        } =
 1131        [
 1132            ExceptionNormalizer.For<AggregateException>
 1133            (
 1134                static ex =>
 1135                {
 1136                    HttpRequestException.Throw
 1137                    (
 1138                        HttpStatusCode.InternalServerError,
 1139                        Resources.ERR_INTERNAL_ERROR,
 1140                        ex,
 1141                        developerMessages: ex.InnerExceptions.Select(static ex => ex.ToString())
 1142                    );
 1143                    return null!;
 1144                }
 1145            )
 1146        ];
 147
 148        /// <summary>
 149        /// Gets the default exception-handling configuration.
 150        /// </summary>
 151        /// <remarks>
 152        /// The default configuration expands <see cref="AggregateException"/> into developer messages for its inner
 153        /// exceptions. Other unexpected exceptions are handled by the fallback internal-server-error normalizer in
 154        /// <see cref="NanoRouteExceptionExtensions.AddExceptionHandler{TBuilder}(TBuilder)"/>.
 155        /// </remarks>
 156        /// <example>
 157        /// <code>
 158        /// ExceptionHandlingConfig config = ExceptionHandlingConfig.Default;
 159        /// </code>
 160        /// </example>
 1161        public static ExceptionHandlingConfig Default { get; } = new ExceptionHandlingConfig();
 162    }
 163
 164    /// <summary>
 165    /// Adds helpers for normalizing exceptions and extracting structured error details.
 166    /// </summary>
 167    /// <example>
 168    /// <code>
 169    /// builder.AddExceptionHandler();
 170    /// </code>
 171    /// </example>
 172    public static class NanoRouteExceptionExtensions
 173    {
 174        extension<TBuilder>(TBuilder routeScopeBuilder) where TBuilder : RouteScopeBuilder
 175        {
 176            /// <summary>
 177            /// Updates the exception-handling configuration visible from the current builder scope.
 178            /// </summary>
 179            /// <param name="configure">A callback that receives the current configuration and returns the replacement c
 180            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 181            /// <remarks>
 182            /// The configuration is stored in <see cref="RouteScopeBuilder.Metadata"/>. Child builders created after th
 183            /// method is called inherit the updated configuration; existing child builders keep their own scoped copy.
 184            /// Registered exception handlers snapshot the configuration that is current at registration time.
 185            /// </remarks>
 186            /// <exception cref="ArgumentNullException">
 187            /// Thrown when <paramref name="routeScopeBuilder"/>, <paramref name="configure"/>, or the value returned
 188            /// by <paramref name="configure"/> is <see langword="null"/>.
 189            /// </exception>
 190            /// <example>
 191            /// <code>
 192            /// builder.ConfigureExceptionHandling(config =&gt; config with
 193            /// {
 194            ///     ExceptionNormalizers = config.ExceptionNormalizers.Remove(typeof(AggregateException))
 195            /// });
 196            /// </code>
 197            /// </example>
 198            public TBuilder ConfigureExceptionHandling(ConfigureBuilderDelegate<ExceptionHandlingConfig> configure)
 199            {
 200                Ensure.NotNull(routeScopeBuilder);
 201                Ensure.NotNull(configure);
 202
 203                ExceptionHandlingConfig config = configure(routeScopeBuilder.Metadata.GetOrDefault(ExceptionHandlingConf
 204                Ensure.NotNull(config);
 205
 206                routeScopeBuilder.Metadata.Set(config);
 207
 208                return routeScopeBuilder;
 209            }
 210
 211            /// <summary>
 212            /// Adds an exception-handling middleware for all supported HTTP methods.
 213            /// </summary>
 214            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 215            /// <remarks>
 216            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 217            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 218            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 219            /// not normalized so caller-driven cancellation can propagate unchanged.
 220            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the middleware
 221            /// is bound to the whole current builder scope for all supported HTTP methods.
 222            /// </remarks>
 223            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> is <see langwor
 224            /// <example>
 225            /// <code>
 226            /// builder.AddExceptionHandler();
 227            /// </code>
 228            /// </example>
 229            public TBuilder AddExceptionHandler() => routeScopeBuilder.AddExceptionHandler(RouteScopeBuilder.CurrentPref
 230
 231            /// <summary>
 232            /// Adds an exception-handling middleware for all supported HTTP methods.
 233            /// </summary>
 234            /// <param name="pattern">
 235            /// The route pattern where the exception-handling middleware should be inserted. Use <c>/</c> to apply it
 236            /// to the whole pipeline, or a narrower prefix/exact pattern to scope normalization to selected routes.
 237            /// </param>
 238            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 239            /// <remarks>
 240            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 241            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 242            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 243            /// not normalized so caller-driven cancellation can propagate unchanged.
 244            /// </remarks>
 245            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> or <paramref na
 246            /// <exception cref="ArgumentException">Thrown when <paramref name="pattern"/> has invalid route-template sy
 247            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 248            /// <example>
 249            /// <code>
 250            /// builder.AddExceptionHandler("/api/*");
 251            /// </code>
 252            /// </example>
 253            public TBuilder AddExceptionHandler(string pattern) => routeScopeBuilder.AddExceptionHandler(HttpVerb.Names,
 254
 255            /// <summary>
 256            /// Adds an exception-handling middleware for a single HTTP method.
 257            /// </summary>
 258            /// <param name="verb">The HTTP method that should use the exception-handling middleware.</param>
 259            /// <param name="pattern">
 260            /// The route pattern where the exception-handling middleware should be inserted. Use <c>/</c> to apply it
 261            /// to the whole pipeline, or a narrower prefix/exact pattern to scope normalization to selected routes.
 262            /// </param>
 263            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 264            /// <remarks>
 265            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 266            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 267            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 268            /// not normalized so caller-driven cancellation can propagate unchanged.
 269            /// </remarks>
 270            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 271            /// <exception cref="ArgumentException">Thrown when <paramref name="verb"/> is not supported or <paramref na
 272            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 273            /// <example>
 274            /// <code>
 275            /// builder.AddExceptionHandler("GET", "/api/*");
 276            /// </code>
 277            /// </example>
 278            public TBuilder AddExceptionHandler(string verb, string pattern) => routeScopeBuilder.AddExceptionHandler([v
 279
 280            /// <summary>
 281            /// Adds an exception-handling middleware for the selected HTTP methods.
 282            /// </summary>
 283            /// <param name="verbs">The HTTP methods that should use the exception-handling middleware.</param>
 284            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 285            /// <remarks>
 286            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 287            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 288            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 289            /// not normalized so caller-driven cancellation can propagate unchanged.
 290            /// This overload uses <see cref="RouteScopeBuilder.CurrentPrefix"/> as the route pattern, so the middleware
 291            /// is bound to the whole current builder scope for the selected HTTP methods.
 292            /// </remarks>
 293            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/> or <paramref na
 294            /// <exception cref="ArgumentException">Thrown when an entry in <paramref name="verbs"/> is not a supported 
 295            /// <example>
 296            /// <code>
 297            /// builder.AddExceptionHandler(["GET", "POST"]);
 298            /// </code>
 299            /// </example>
 300            public TBuilder AddExceptionHandler(IEnumerable<string> verbs) => routeScopeBuilder.AddExceptionHandler(verb
 301
 302            /// <summary>
 303            /// Adds an exception-handling middleware for the selected HTTP methods.
 304            /// </summary>
 305            /// <param name="verbs">The HTTP methods that should use the exception-handling middleware.</param>
 306            /// <param name="pattern">
 307            /// The route pattern where the exception-handling middleware should be inserted. Use <c>/</c> to apply it
 308            /// to the whole pipeline, or a narrower prefix/exact pattern to scope normalization to selected routes.
 309            /// </param>
 310            /// <returns>The current <paramref name="routeScopeBuilder"/> instance.</returns>
 311            /// <remarks>
 312            /// The inserted middleware converts unexpected exceptions into <see cref="HttpRequestException"/> values
 313            /// with normalized status codes and diagnostic payloads. Existing <see cref="HttpRequestException"/>
 314            /// values are allowed to flow through unchanged. <see cref="OperationCanceledException"/> is intentionally
 315            /// not normalized so caller-driven cancellation can propagate unchanged.
 316            /// </remarks>
 317            /// <exception cref="ArgumentNullException">Thrown when <paramref name="routeScopeBuilder"/>, <paramref name
 318            /// <exception cref="ArgumentException">Thrown when an entry in <paramref name="verbs"/> is not supported or
 319            /// <exception cref="InvalidOperationException">Thrown when <paramref name="pattern"/> uses unsupported rout
 320            /// <example>
 321            /// <code>
 322            /// builder.AddExceptionHandler(["POST", "PUT"], "/api/users/*");
 323            /// </code>
 324            /// </example>
 325            public TBuilder AddExceptionHandler(IEnumerable<string> verbs, string pattern)
 326            {
 327                Ensure.NotNull(routeScopeBuilder);
 328                Ensure.NotNull(verbs);
 329                Ensure.NotNull(pattern);
 330
 331                FrozenDictionary<Type, ExceptionNormalizer> exceptionNormalizers = routeScopeBuilder
 332                    .Metadata
 333                    .GetOrDefault(ExceptionHandlingConfig.Default)
 334                    .ExceptionNormalizers
 335                    .ToFrozenDictionary();
 336
 337                routeScopeBuilder.AddHandler(verbs, pattern, async (RequestContext context, CallNextHandlerDelegate next
 338                {
 339                    try
 340                    {
 341                        return await next().ConfigureAwait(false);
 342                    }
 343                    catch (Exception ex) when (ex is not (HttpRequestException or OperationCanceledException /*needs to 
 344                    {
 345                        for (Type exceptionType = ex.GetType(); exceptionType != typeof(object); exceptionType = excepti
 346                            if (exceptionNormalizers.TryGetValue(exceptionType, out ExceptionNormalizer? exceptionNormal
 347                                throw exceptionNormalizer(ex);
 348
 349                        HttpRequestException.Throw(HttpStatusCode.InternalServerError, Resources.ERR_INTERNAL_ERROR, ex,
 350                        return null!;
 351                    }
 352                });
 353
 354                return routeScopeBuilder;
 355            }
 356        }
 357
 358        /// <summary>
 359        /// The <see cref="Exception.Data"/> key used to store client-facing error messages.
 360        /// </summary>
 361        /// <remarks>
 362        /// Written by <see cref="Throw(HttpStatusCode, string, Exception, IEnumerable{string}, IEnumerable{string})"/>
 363        /// and read by <see cref="GetErrorDetails(HttpRequestException, bool, string)"/>.
 364        /// </remarks>
 365        /// <example>
 366        /// <code>
 367        /// HttpRequestException exception = ...
 368        /// object? errors = exception.Data[NanoRouteExceptionExtensions.ErrorsName];
 369        /// </code>
 370        /// </example>
 371        public const string ErrorsName = "Errors";
 372
 373        /// <summary>
 374        /// The <see cref="Exception.Data"/> key used to store developer-facing diagnostic details.
 375        /// </summary>
 376        /// <remarks>
 377        /// Written by <see cref="Throw(HttpStatusCode, string, Exception, IEnumerable{string}, IEnumerable{string})"/>
 378        /// and read by <see cref="GetErrorDetails(HttpRequestException, bool, string)"/>.
 379        /// </remarks>
 380        /// <example>
 381        /// <code>
 382        /// HttpRequestException exception = ...
 383        /// object? messages = exception.Data[NanoRouteExceptionExtensions.DeveloperMessagesName];
 384        /// </code>
 385        /// </example>
 386        public const string DeveloperMessagesName = "DeveloperMessages";
 387
 388        /// <summary>
 389        /// The <see cref="Exception.Data"/> key used to store the HTTP status code.
 390        /// </summary>
 391        /// <remarks>
 392        /// Written by <see cref="Throw(HttpStatusCode, string, Exception, IEnumerable{string}, IEnumerable{string})"/>
 393        /// and read by <see cref="GetErrorDetails(HttpRequestException, bool, string)"/>.
 394        /// </remarks>
 395        /// <example>
 396        /// <code>
 397        /// HttpRequestException exception = ...
 398        /// object? status = exception.Data[NanoRouteExceptionExtensions.StatusName];
 399        /// </code>
 400        /// </example>
 401        public const string StatusName = "StatusCode";
 402
 403        extension(HttpRequestException)
 404        {
 405            /// <summary>
 406            /// Throws an <see cref="HttpRequestException"/> enriched with an HTTP status code and public error messages
 407            /// </summary>
 408            /// <param name="status">The HTTP status code that should be associated with the exception.</param>
 409            /// <param name="title">The human-readable error title.</param>
 410            /// <param name="errors">Optional client-facing error messages that should not contain sensitive data.</para
 411            /// <exception cref="HttpRequestException">Always thrown with the supplied status and error metadata.</excep
 412            /// <example>
 413            /// <code>
 414            /// HttpRequestException.Throw(HttpStatusCode.BadRequest, "Bad Request", "Missing id.");
 415            /// </code>
 416            /// </example>
 417            [DoesNotReturn]
 418            public static void Throw(HttpStatusCode status, string title, params IEnumerable<string> errors) => Throw(st
 419
 420            /// <summary>
 421            /// Throws an <see cref="HttpRequestException"/> enriched with routing-specific metadata.
 422            /// </summary>
 423            /// <param name="status">The HTTP status code that should be associated with the exception.</param>
 424            /// <param name="title">The human-readable error title.</param>
 425            /// <param name="original">The original exception, if any.</param>
 426            /// <param name="errors">Optional client-facing error messages that should not contain sensitive data.</para
 427            /// <param name="developerMessages">Optional developer-facing messages that may contain sensitive data.</par
 428            /// <exception cref="HttpRequestException">Always thrown with the supplied status and error metadata.</excep
 429            /// <example>
 430            /// <code>
 431            /// Exception original = ...
 432            /// HttpRequestException.Throw
 433            /// (
 434            ///     HttpStatusCode.Conflict,
 435            ///     "Conflict",
 436            ///     original,
 437            ///     errors: ["The resource has changed."],
 438            ///     developerMessages: [original.ToString()]
 439            /// );
 440            /// </code>
 441            /// </example>
 442            [DoesNotReturn]
 443            public static void Throw(HttpStatusCode status, string title, Exception? original = null, IEnumerable<string
 444            {
 445                HttpRequestException ex = new(title, original);
 446
 447                ex.Data[StatusName] = status;
 448
 449                if (errors?.ToArray() is { Length: > 0 } err)
 450                    ex.Data[ErrorsName] = err;  // On .NET FW the Data members must be serializable (string[] it is)
 451
 452                if (developerMessages?.ToArray() is { Length: > 0 } dev)
 453                    ex.Data[DeveloperMessagesName] = dev;
 454
 455                throw ex;
 456            }
 457        }
 458
 459        extension(HttpRequestException requestException)
 460        {
 461            /// <summary>
 462            /// Converts an <see cref="HttpRequestException"/> into an <see cref="ErrorDetails"/> payload.
 463            /// </summary>
 464            /// <param name="populateErrorInfo">
 465            /// <see langword="true"/> to include developer-facing details when present; otherwise <see langword="false"
 466            /// </param>
 467            /// <param name="traceId">The trace identifier to expose in the resulting payload.</param>
 468            /// <returns>The structured error payload.</returns>
 469            /// <exception cref="ArgumentNullException">Thrown when <paramref name="requestException"/> is <see langword
 470            /// <example>
 471            /// <code>
 472            /// ErrorDetails details = exception.GetErrorDetails(populateErrorInfo: false, traceId);
 473            /// </code>
 474            /// </example>
 475            public ErrorDetails GetErrorDetails(bool populateErrorInfo = false, string? traceId = null)
 476            {
 477                Ensure.NotNull(requestException);
 478
 479                return new ErrorDetails
 480                {
 481                    Status = requestException.Data[StatusName] switch
 482                    {
 483                        HttpStatusCode status => status,
 484                        int intStatus => (HttpStatusCode) intStatus,
 485                        _ => HttpStatusCode.InternalServerError
 486                    },
 487                    Title = requestException.Message,
 488                    TraceId = traceId ?? Guid.NewGuid().ToString("N"),
 489                    Errors = requestException.Data[ErrorsName] as IEnumerable<string>,
 490                    DeveloperMessages = populateErrorInfo ? requestException.Data[DeveloperMessagesName] as IEnumerable<
 491                };
 492            }
 493        }
 494
 495        extension(ExceptionNormalizer)
 496        {
 497            /// <summary>
 498            /// Creates an exception-normalizer registration for an exception type.
 499            /// </summary>
 500            /// <typeparam name="TException">The exception type handled by <paramref name="normalizer"/>.</typeparam>
 501            /// <param name="normalizer">
 502            /// The typed normalizer that converts <typeparamref name="TException"/> into an enriched
 503            /// <see cref="HttpRequestException"/>.
 504            /// </param>
 505            /// <returns>
 506            /// A key/value pair suitable for adding to <see cref="ExceptionHandlingConfig.ExceptionNormalizers"/>.
 507            /// </returns>
 508            /// <remarks>
 509            /// The returned entry is keyed by <c>typeof(TException)</c>. Exception handlers check the exact runtime
 510            /// type first, then walk base exception types, so the most specific registered normalizer wins.
 511            /// </remarks>
 512            /// <exception cref="ArgumentNullException">Thrown when <paramref name="normalizer"/> is <see langword="null
 513            /// <example>
 514            /// <code>
 515            /// builder.ConfigureExceptionHandling(config =&gt; config with
 516            /// {
 517            ///     ExceptionNormalizers = config.ExceptionNormalizers.SetItems
 518            ///     ([
 519            ///         ExceptionNormalizer.For&lt;InvalidOperationException&gt;
 520            ///         (
 521            ///             static ex =&gt; new HttpRequestException("Conflict", ex, HttpStatusCode.Conflict)
 522            ///         )
 523            ///     ])
 524            /// });
 525            /// </code>
 526            /// </example>
 527            public static KeyValuePair<Type, ExceptionNormalizer> For<TException>(TypedExceptionNormalizer<TException> n
 528            {
 529                Ensure.NotNull(normalizer);
 530
 531                return new KeyValuePair<Type, ExceptionNormalizer>(typeof(TException), ex => normalizer((TException) ex)
 532            }
 533        }
 534    }
 535}