| | | 1 | | using Microsoft.Extensions.Options; |
| | | 2 | | |
| | | 3 | | namespace ZhEaIsNsAaBn.Utilities.APIExceptionHandlers; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Shared logging and JSON writing for the exception handlers. The HTTP status comes from the app's <see cref="APIRespo |
| | | 7 | | /// (registered with <c>AddAPIResponses</c>; defaults otherwise), so an exception answers with the same status as the sa |
| | | 8 | | /// <see cref="Error"/> returned from an endpoint. |
| | | 9 | | /// </summary> |
| | | 10 | | /// <typeparam name="T">The handler type (its logger category).</typeparam> |
| | | 11 | | public abstract class BaseExceptionHandler< T > |
| | | 12 | | where T : BaseExceptionHandler<T> |
| | | 13 | | { |
| | 26 | 14 | | private static readonly JsonSerializerOptions Compact = Options( false ); |
| | 26 | 15 | | private static readonly JsonSerializerOptions Indented = Options( true ); |
| | | 16 | | |
| | | 17 | | private readonly ILogger<T> _logger; |
| | | 18 | | private readonly IWebHostEnvironment _environment; |
| | | 19 | | private readonly APIResponsesSettings _settings; |
| | | 20 | | |
| | | 21 | | |
| | | 22 | | /// <summary>Stores the shared services.</summary> |
| | | 23 | | /// <param name="logger">The handler's logger.</param> |
| | | 24 | | /// <param name="environment">The host environment; JSON is indented in Development.</param> |
| | | 25 | | /// <param name="settings">The status mapping; defaults when <see langword="null"/>.</param> |
| | | 26 | | /// <exception cref="ArgumentNullException"><paramref name="logger"/> or <paramref name="environment"/> is <see lang |
| | 510 | 27 | | protected BaseExceptionHandler( ILogger<T> logger, IWebHostEnvironment environment, IOptions<APIResponsesSettings>? |
| | | 28 | | { |
| | 510 | 29 | | ArgumentNullException.ThrowIfNull( logger ); |
| | 510 | 30 | | ArgumentNullException.ThrowIfNull( environment ); |
| | | 31 | | |
| | 510 | 32 | | _logger = logger; |
| | 510 | 33 | | _environment = environment; |
| | 510 | 34 | | _settings = settings?.Value ?? new APIResponsesSettings(); |
| | 510 | 35 | | } |
| | | 36 | | |
| | | 37 | | |
| | | 38 | | /// <summary>The handler's logger.</summary> |
| | 82 | 39 | | protected ILogger<T> Logger => _logger; |
| | | 40 | | |
| | | 41 | | /// <summary>The host environment.</summary> |
| | 90 | 42 | | protected IWebHostEnvironment Environment => _environment; |
| | | 43 | | |
| | | 44 | | /// <summary>The status mapping (codes, categories, fallback).</summary> |
| | 82 | 45 | | protected APIResponsesSettings Settings => _settings; |
| | | 46 | | |
| | | 47 | | |
| | | 48 | | /// <summary>Answers with <paramref name="error"/>; see <see cref="GenerateErrors"/>.</summary> |
| | | 49 | | /// <param name="httpContext">The request.</param> |
| | | 50 | | /// <param name="error">The error.</param> |
| | | 51 | | /// <param name="cancellationToken">Token to cancel the write.</param> |
| | | 52 | | /// <returns>A task that completes when the body is written.</returns> |
| | 70 | 53 | | protected ValueTask GenerateError( HttpContext httpContext, Error error, CancellationToken cancellationToken ) => Ge |
| | | 54 | | |
| | | 55 | | /// <summary> |
| | | 56 | | /// Sets the HTTP status from the first error (<see cref="APIResponsesSettings.StatusFor"/>) and writes the errors a |
| | | 57 | | /// <see cref="BaseResponse"/> with the same <c>status</c>, the trace id and the transaction id. |
| | | 58 | | /// </summary> |
| | | 59 | | /// <param name="httpContext">The request.</param> |
| | | 60 | | /// <param name="errors">The errors.</param> |
| | | 61 | | /// <param name="cancellationToken">Token to cancel the write.</param> |
| | | 62 | | /// <returns>A task that completes when the body is written.</returns> |
| | | 63 | | protected async ValueTask GenerateErrors( HttpContext httpContext, List<Error> errors, CancellationToken cancellatio |
| | | 64 | | { |
| | 82 | 65 | | ArgumentNullException.ThrowIfNull( httpContext ); |
| | 82 | 66 | | ArgumentNullException.ThrowIfNull( errors ); |
| | | 67 | | |
| | 82 | 68 | | var status = (int) Settings.StatusFor( errors ); |
| | | 69 | | |
| | 82 | 70 | | BaseResponse response = errors; |
| | 82 | 71 | | response.Status = status; |
| | 82 | 72 | | response.TraceId = GetTraceId( httpContext ); |
| | 82 | 73 | | response.TransId = GetTransId( httpContext ); |
| | | 74 | | |
| | | 75 | | |
| | 82 | 76 | | httpContext.Response.StatusCode = status; |
| | 82 | 77 | | httpContext.Response.ContentType = "application/json"; |
| | | 78 | | |
| | | 79 | | |
| | 82 | 80 | | var json = JsonSerializer.Serialize( response, Environment.IsDevelopment() ? Indented : Compact ); |
| | 82 | 81 | | await httpContext.Response.WriteAsync( json, cancellationToken ).ConfigureAwait( false ); |
| | 82 | 82 | | } |
| | | 83 | | |
| | | 84 | | /// <summary>The trace id of the current activity, else the request's <see cref="HttpContext.TraceIdentifier"/>.</su |
| | | 85 | | /// <param name="httpContext">The request.</param> |
| | | 86 | | /// <returns>The trace id.</returns> |
| | | 87 | | protected static string GetTraceId( HttpContext httpContext ) |
| | | 88 | | { |
| | 82 | 89 | | ArgumentNullException.ThrowIfNull( httpContext ); |
| | | 90 | | |
| | 82 | 91 | | return System.Diagnostics.Activity.Current?.TraceId.ToString() ?? httpContext.TraceIdentifier; |
| | | 92 | | } |
| | | 93 | | |
| | | 94 | | /// <summary>The transaction id: <c>HttpContext.Items["transId"]</c> (as <c>ToAPI</c> uses), else the <c>TransId</c> |
| | | 95 | | /// <param name="httpContext">The request.</param> |
| | | 96 | | /// <returns>The transaction id.</returns> |
| | | 97 | | protected static string GetTransId( HttpContext httpContext ) |
| | | 98 | | { |
| | 82 | 99 | | ArgumentNullException.ThrowIfNull( httpContext ); |
| | | 100 | | |
| | 82 | 101 | | return httpContext.Items[ "transId" ]?.ToString() ?? httpContext.Request.Headers[ "TransId" ].FirstOrDefault() ? |
| | | 102 | | } |
| | | 103 | | |
| | | 104 | | |
| | | 105 | | private static JsonSerializerOptions Options( bool indented ) |
| | 52 | 106 | | => new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenW |
| | | 107 | | } |