< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Models.KestrunResponse
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Models/KestrunResponse.cs
Tag: Kestrun/Kestrun@eeafbe813231ed23417e7b339e170e307b2c86f9
Line coverage
72%
Covered lines: 688
Uncovered lines: 264
Coverable lines: 952
Total lines: 2626
Line coverage: 72.2%
Branch coverage
62%
Covered branches: 391
Total branches: 624
Branch coverage: 62.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/12/2025 - 13:32:05 Line coverage: 89.3% (404/452) Branch coverage: 73.8% (192/260) Total lines: 1231 Tag: Kestrun/Kestrun@63ea5841fe73fd164406accba17a956e8c08357f09/13/2025 - 17:19:56 Line coverage: 83.4% (404/484) Branch coverage: 66.2% (192/290) Total lines: 1304 Tag: Kestrun/Kestrun@ea635f1ee1937c260a89d1a43a3c203cd8767c7b09/14/2025 - 21:23:16 Line coverage: 88.3% (409/463) Branch coverage: 72.9% (194/266) Total lines: 1279 Tag: Kestrun/Kestrun@c9d2f0b3dd164d7dc0dc2407a9f006293d92422309/16/2025 - 16:28:42 Line coverage: 88.1% (410/465) Branch coverage: 71.6% (195/272) Total lines: 1282 Tag: Kestrun/Kestrun@d5c0d6132e97ca542441289c02a4c9e9d0364d4910/13/2025 - 16:52:37 Line coverage: 86.5% (425/491) Branch coverage: 71% (213/300) Total lines: 1347 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/12/2025 - 17:27:19 Line coverage: 84.7% (433/511) Branch coverage: 68.7% (216/314) Total lines: 1423 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/18/2025 - 21:41:58 Line coverage: 84% (438/521) Branch coverage: 68.6% (217/316) Total lines: 1449 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff01/02/2026 - 00:16:25 Line coverage: 78.1% (450/576) Branch coverage: 65% (221/340) Total lines: 1578 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/12/2026 - 18:03:06 Line coverage: 80.9% (466/576) Branch coverage: 67.6% (230/340) Total lines: 1578 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/15/2026 - 23:50:39 Line coverage: 80.9% (468/578) Branch coverage: 67.4% (228/338) Total lines: 1583 Tag: Kestrun/Kestrun@2d823cb7ceae127151c8880ca073ffbb9c6322aa01/17/2026 - 04:33:35 Line coverage: 82.5% (487/590) Branch coverage: 72.5% (257/354) Total lines: 1620 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba02/05/2026 - 00:28:18 Line coverage: 82.8% (493/595) Branch coverage: 73.3% (267/364) Total lines: 1644 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 72.7% (690/948) Branch coverage: 62.9% (390/620) Total lines: 2618 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b102/18/2026 - 17:13:01 Line coverage: 72.4% (687/948) Branch coverage: 62.9% (390/620) Total lines: 2618 Tag: Kestrun/Kestrun@8f7b81adf87d9e2faaf0b01cb237149d6f1a3cec03/03/2026 - 12:38:30 Line coverage: 72.7% (690/948) Branch coverage: 62.9% (390/620) Total lines: 2618 Tag: Kestrun/Kestrun@7602da75ddb0abe534368b9e9ce2f45ae966769a03/03/2026 - 20:14:57 Line coverage: 72.5% (691/952) Branch coverage: 62.6% (391/624) Total lines: 2626 Tag: Kestrun/Kestrun@d169bd1d1e32ec576bfd2135357b6d01a2ad23ba03/04/2026 - 19:40:34 Line coverage: 72.2% (688/952) Branch coverage: 62.6% (391/624) Total lines: 2626 Tag: Kestrun/Kestrun@eeafbe813231ed23417e7b339e170e307b2c86f9 09/12/2025 - 13:32:05 Line coverage: 89.3% (404/452) Branch coverage: 73.8% (192/260) Total lines: 1231 Tag: Kestrun/Kestrun@63ea5841fe73fd164406accba17a956e8c08357f09/13/2025 - 17:19:56 Line coverage: 83.4% (404/484) Branch coverage: 66.2% (192/290) Total lines: 1304 Tag: Kestrun/Kestrun@ea635f1ee1937c260a89d1a43a3c203cd8767c7b09/14/2025 - 21:23:16 Line coverage: 88.3% (409/463) Branch coverage: 72.9% (194/266) Total lines: 1279 Tag: Kestrun/Kestrun@c9d2f0b3dd164d7dc0dc2407a9f006293d92422309/16/2025 - 16:28:42 Line coverage: 88.1% (410/465) Branch coverage: 71.6% (195/272) Total lines: 1282 Tag: Kestrun/Kestrun@d5c0d6132e97ca542441289c02a4c9e9d0364d4910/13/2025 - 16:52:37 Line coverage: 86.5% (425/491) Branch coverage: 71% (213/300) Total lines: 1347 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/12/2025 - 17:27:19 Line coverage: 84.7% (433/511) Branch coverage: 68.7% (216/314) Total lines: 1423 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/18/2025 - 21:41:58 Line coverage: 84% (438/521) Branch coverage: 68.6% (217/316) Total lines: 1449 Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff01/02/2026 - 00:16:25 Line coverage: 78.1% (450/576) Branch coverage: 65% (221/340) Total lines: 1578 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/12/2026 - 18:03:06 Line coverage: 80.9% (466/576) Branch coverage: 67.6% (230/340) Total lines: 1578 Tag: Kestrun/Kestrun@956332ccc921363590dccd99d5707fb20b50966b01/15/2026 - 23:50:39 Line coverage: 80.9% (468/578) Branch coverage: 67.4% (228/338) Total lines: 1583 Tag: Kestrun/Kestrun@2d823cb7ceae127151c8880ca073ffbb9c6322aa01/17/2026 - 04:33:35 Line coverage: 82.5% (487/590) Branch coverage: 72.5% (257/354) Total lines: 1620 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba02/05/2026 - 00:28:18 Line coverage: 82.8% (493/595) Branch coverage: 73.3% (267/364) Total lines: 1644 Tag: Kestrun/Kestrun@d9261bd752e45afa789d10bc0c82b7d5724d958902/18/2026 - 08:33:07 Line coverage: 72.7% (690/948) Branch coverage: 62.9% (390/620) Total lines: 2618 Tag: Kestrun/Kestrun@bf8a937cfb7e8936c225b9df4608f8ddd85558b102/18/2026 - 17:13:01 Line coverage: 72.4% (687/948) Branch coverage: 62.9% (390/620) Total lines: 2618 Tag: Kestrun/Kestrun@8f7b81adf87d9e2faaf0b01cb237149d6f1a3cec03/03/2026 - 12:38:30 Line coverage: 72.7% (690/948) Branch coverage: 62.9% (390/620) Total lines: 2618 Tag: Kestrun/Kestrun@7602da75ddb0abe534368b9e9ce2f45ae966769a03/03/2026 - 20:14:57 Line coverage: 72.5% (691/952) Branch coverage: 62.6% (391/624) Total lines: 2626 Tag: Kestrun/Kestrun@d169bd1d1e32ec576bfd2135357b6d01a2ad23ba03/04/2026 - 19:40:34 Line coverage: 72.2% (688/952) Branch coverage: 62.6% (391/624) Total lines: 2626 Tag: Kestrun/Kestrun@eeafbe813231ed23417e7b339e170e307b2c86f9

Coverage delta

Coverage delta 11 -11

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
get_CallbackPlan()100%11100%
get_Logger()100%11100%
get_MapRouteOptions()100%11100%
get_KrContext()100%11100%
get_Host()100%210%
.ctor(...)75%44100%
get_Context()100%210%
get_StatusCode()100%11100%
get_Headers()100%11100%
get_ContentType()100%11100%
get_Body()100%11100%
get_RedirectUrl()100%11100%
get_Cookies()100%11100%
get_Encoding()100%11100%
get_ContentDisposition()100%11100%
get_Request()100%11100%
get_AcceptCharset()100%11100%
get_BodyAsyncThreshold()100%11100%
get_CacheControl()100%11100%
get_Value()100%11100%
get_PostPonedWriteObject()100%11100%
get_HasPostPonedWriteObject()100%11100%
GetSafeCurrentDirectoryOrBaseDirectory()100%1128.57%
GetSafeCurrentDirectoryForLogging()100%11100%
GetHeader(...)0%620%
IsTextBasedContentType(...)93.75%161692.85%
AddCallbackParameters(...)0%4260%
WriteFileResponse(...)75%161696.55%
WriteJsonResponse(...)100%11100%
WriteJsonResponseAsync()100%11100%
WriteJsonResponse(...)100%210%
WriteJsonResponseAsync()100%44100%
WriteJsonResponse(...)100%11100%
WriteJsonResponseAsync()100%22100%
WriteCborResponseAsync()75%44100%
WriteCborResponse(...)100%210%
WriteBsonResponseAsync()75%44100%
WriteBsonResponse(...)100%210%
WriteResponse(...)100%11100%
WriteResponseAsync(...)50%2283.33%
WriteResponseAsync()87.5%8881.25%
WriteLegacyNegotiatedResponseAsync()50%6450%
WriteOpenApiNegotiatedResponseAsync()41.66%491236.66%
ShouldEnforceOpenApiResponseContentTypes()100%11100%
QueueResponseForWrite(...)78.57%151485.71%
SelectResponseMediaType(...)61.11%271869.56%
SelectWhenAnyMediaTypeSupported(...)100%44100%
SelectFromConfiguredSupportedMediaTypes(...)0%272160%
ResolveWriterMediaType(...)66.66%151272.72%
TryGetResponseContentTypes(...)92.85%151485.71%
TryGetResponseSchemaTypeForStatus(...)70%101090%
ResolveSchemaType(...)66.66%6677.77%
CollectSchemaTypeCandidatesFromAssembly(...)100%11864.28%
IsMatchingSchemaTypeName(...)83.33%66100%
SelectPreferredSchemaType(...)83.33%66100%
ConvertSchemaValue(...)45.45%662255%
ConvertSchemaArrayValue(...)0%4260%
ConvertEnumerableToTypedArray(...)0%2040%
ConvertSingleValueToTypedArray(...)100%210%
EnsureArrayElementAssignable(...)0%272160%
TryConvertSchemaDictionaryValue(...)50%4475%
TryConvertViaSingleArgumentConstructor(...)0%2040%
UnwrapPowerShellValue(...)30%321040%
IsPowerShellAutomationNull(...)50%22100%
TryConvertDictionaryToType(...)70%111082.35%
TryConvertPowerShellObjectToType(...)0%156120%
FindDictionaryKey(...)83.33%6685.71%
ValidateRequiredProperties(...)70%101086.66%
FormatMissingRequiredProperties(...)10%521025%
IsMapLikeType(...)66.66%7671.42%
TryConvertSimple(...)0%7280%
TryGetValueIgnoreCase(...)83.33%7666.66%
WriteByMediaTypeAsync(...)60%1564058.33%
WriteCsvResponse(...)100%88100%
WriteCsvResponseAsync()100%44100%
WriteYamlResponse(...)100%11100%
WriteYamlResponseAsync()75%44100%
WriteXmlResponse(...)100%11100%
WriteXmlResponseAsync()87.5%88100%
WriteTextResponse(...)100%11100%
WriteTextResponseAsync()83.33%6685.71%
WriteFormUrlEncodedResponse(...)100%11100%
WriteFormUrlEncodedResponseAsync()100%22100%
WriteRedirectResponse(...)83.33%6692.3%
WriteBinaryResponse(...)75%44100%
WriteStreamResponse(...)100%22100%
get_Error()100%11100%
get_Details()100%11100%
get_Exception()100%11100%
get_StackTrace()100%11100%
get_Status()100%11100%
get_Reason()100%11100%
get_Timestamp()100%11100%
get_Path()100%11100%
get_Method()100%11100%
WriteErrorResponseAsync()62.5%8895%
WriteErrorResponse(...)100%11100%
WriteErrorResponseAsync()75%88100%
WriteErrorResponse(...)100%11100%
WriteFormattedErrorResponseAsync()100%2020100%
RenderInlineTemplate(...)64.28%151481.81%
RenderInline(...)85.71%141493.75%
TryResolveValue(...)56.25%271665.21%
RevalidateCache(...)100%210%
WriteHtmlResponseAsync()66.66%6683.33%
WriteHtmlResponseAsync()100%210%
WriteHtmlResponse(...)100%210%
WriteHtmlResponseFromFileAsync()75%4475%
WriteHtmlResponse(...)100%210%
WriteHtmlResponseFromFile(...)100%210%
WriteStatusOnly(...)100%11100%
ApplyTo()62.5%301662.5%
WriteResponseContent()100%1010100%
TryEnqueueCallbacks()42.85%611437.93%
EnsureContentType(...)100%88100%
EnsureStatus(...)100%22100%
ApplyCachingHeaders(...)100%22100%
ApplyContentDispositionHeader(...)78.57%141488.88%
ApplyHeadersAndCookies(...)87.5%8885.71%
WriteBodyAsync()65.38%282686.04%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Models/KestrunResponse.cs

#LineLine coverage
 1
 2using System.Diagnostics.CodeAnalysis;
 3using System.Xml.Linq;
 4using System.Text.Json;
 5using System.Text.Json.Serialization;
 6using Kestrun.Utilities.Json;
 7using Microsoft.AspNetCore.StaticFiles;
 8using System.Text;
 9using Serilog.Events;
 10using System.Buffers;
 11using Microsoft.Extensions.FileProviders;
 12using Microsoft.AspNetCore.WebUtilities;
 13using System.Net;
 14using MongoDB.Bson;
 15using Kestrun.Utilities;
 16using System.Collections;
 17using CsvHelper.Configuration;
 18using System.Globalization;
 19using CsvHelper;
 20using System.Reflection;
 21using Microsoft.Net.Http.Headers;
 22using Kestrun.Utilities.Yaml;
 23using Kestrun.Hosting.Options;
 24using Kestrun.Callback;
 25using System.Management.Automation;
 26using Kestrun.Logging;
 27
 28namespace Kestrun.Models;
 29
 30/// <summary>
 31/// Represents an HTTP response in the Kestrun framework, providing methods to write various content types and manage he
 32/// </summary>
 33/// <remarks>
 34/// Initializes a new instance of the <see cref="KestrunResponse"/> class with the specified request and optional body a
 35/// </remarks>
 36public class KestrunResponse
 37{
 138    private static readonly ContentTypeWithSchema[] LegacyNegotiatedResponseContentTypes =
 139    [
 140        new("*/*")
 141    ];
 42
 43    /// <summary>
 44    /// Flag indicating whether callbacks have already been enqueued.
 45    /// </summary>
 46    internal int CallbacksEnqueuedFlag; // 0 = no, 1 = yes
 47
 48    /// <summary>
 49    ///     Gets the list of callback requests associated with this response.
 50    /// </summary>
 21151    public List<CallBackExecutionPlan> CallbackPlan { get; } = [];
 52
 49553    private Serilog.ILogger Logger => KrContext.Host.Logger;
 54    /// <summary>
 55    /// Gets the route options associated with this response.
 56    /// </summary>
 1557    public MapRouteOptions MapRouteOptions => KrContext.MapRouteOptions;
 58    /// <summary>
 59    /// Gets the associated KestrunContext for this response.
 60    /// </summary>
 121461    public required KestrunContext KrContext { get; init; }
 62
 63    /// <summary>
 64    /// Gets the KestrunHost associated with this response.
 65    /// </summary>
 066    public Hosting.KestrunHost Host => KrContext.Host;
 67    /// <summary>
 68    /// A set of MIME types that are considered text-based for response content.
 69    /// </summary>
 170    public static readonly HashSet<string> TextBasedMimeTypes =
 171#pragma warning disable IDE0028 // Simplify collection initialization
 172    new(StringComparer.OrdinalIgnoreCase)
 173#pragma warning restore IDE0028 // Simplify collection initialization
 174    {
 175        "application/json",
 176        "application/xml",
 177        "application/javascript",
 178        "application/xhtml+xml",
 179        "application/x-www-form-urlencoded",
 180        "application/yaml",
 181        "application/graphql"
 182    };
 83
 84    /// <summary>
 85    /// Initializes a new instance of the <see cref="KestrunResponse"/> class.
 86    /// </summary>
 87    /// <param name="krContext">The associated <see cref="KestrunContext"/> for this response.</param>
 88    /// <param name="bodyAsyncThreshold">The threshold in bytes for using async body write operations. Defaults to 8192.
 89    [SetsRequiredMembers]
 17590    public KestrunResponse(KestrunContext krContext, int bodyAsyncThreshold = 8192)
 91    {
 17592        KrContext = krContext;
 17593        BodyAsyncThreshold = bodyAsyncThreshold;
 17594        Request = KrContext.Request ?? throw new ArgumentNullException(nameof(KrContext));
 17595        AcceptCharset = KrContext.Request.Headers.TryGetValue("Accept-Charset", out var value) ? Encoding.GetEncoding(va
 17596        StatusCode = KrContext.HttpContext.Response.StatusCode;
 17597    }
 98
 99    /// <summary>
 100    /// Gets the <see cref="HttpContext"/> associated with the response.
 101    /// </summary>
 0102    public HttpContext Context => KrContext.HttpContext;
 103    /// <summary>
 104    /// Gets or sets the HTTP status code for the response.
 105    /// </summary>
 358106    public int StatusCode { get; set; }
 107    /// <summary>
 108    /// Gets or sets the collection of HTTP headers for the response.
 109    /// </summary>
 253110    public Dictionary<string, string> Headers { get; set; } = [];
 111    /// <summary>
 112    /// Gets or sets the MIME content type of the response.
 113    /// </summary>
 469114    public string? ContentType { get; set; } = "text/plain";
 115    /// <summary>
 116    /// Gets or sets the body of the response, which can be a string, byte array, stream, or file info.
 117    /// </summary>
 254118    public object? Body { get; set; }
 119    /// <summary>
 120    /// Gets or sets the URL to redirect the response to, if an HTTP redirect is required.
 121    /// </summary>
 67122    public string? RedirectUrl { get; set; } // For HTTP redirects
 123    /// <summary>
 124    /// Gets or sets the list of Set-Cookie header values for the response.
 125    /// </summary>
 35126    public List<string>? Cookies { get; set; } // For Set-Cookie headers
 127
 128    /// <summary>
 129    /// Text encoding for textual MIME types.
 130    /// </summary>
 221131    public Encoding Encoding { get; set; } = Encoding.UTF8;
 132
 133    /// <summary>
 134    /// Content-Disposition header value.
 135    /// </summary>
 218136    public ContentDispositionOptions ContentDisposition { get; set; } = new ContentDispositionOptions();
 137    /// <summary>
 138    /// Gets the associated KestrunRequest for this response.
 139    /// </summary>
 235140    public required KestrunRequest Request { get; init; }
 141
 142    /// <summary>
 143    /// Global text encoding for all responses. Defaults to UTF-8.
 144    /// </summary>
 206145    public required Encoding AcceptCharset { get; init; }
 146
 147    /// <summary>
 148    /// If the response body is larger than this threshold (in bytes), async write will be used.
 149    /// </summary>
 175150    public required int BodyAsyncThreshold { get; init; }
 151
 152    /// <summary>
 153    /// Cache-Control header value for the response.
 154    /// </summary>
 35155    public CacheControlHeaderValue? CacheControl { get; set; }
 156
 157    /// <summary>
 158    /// Represents a simple object for writing responses with a value and status code.
 159    /// </summary>
 160    /// <param name="Value">The value to be written in the response.</param>
 161    /// <param name="Status">The HTTP status code for the response.</param>
 162    /// <param name="Error">An optional error code to include in the response.</param>
 202163    public record WriteObject(object? Value, int Status = StatusCodes.Status200OK, int? Error = null);
 164
 165    /// <summary>
 166    /// Gets or sets a postponed write object that can be used for deferred response writing, allowing the response to b
 167    /// </summary>
 199168    public WriteObject PostPonedWriteObject { get; set; } = new WriteObject(null, StatusCodes.Status200OK);
 169
 170    /// <summary>
 171    /// Indicates whether there is a postponed write object with a non-null value, which can be used to determine if a d
 172    /// </summary>
 2173    public bool HasPostPonedWriteObject => PostPonedWriteObject.Value is not null;
 174    #region Constructors
 175    #endregion
 176
 177    #region Helpers
 178    private static string GetSafeCurrentDirectoryOrBaseDirectory()
 179    {
 180        try
 181        {
 2182            return Directory.GetCurrentDirectory();
 183        }
 0184        catch (Exception ex) when (ex is IOException
 0185                                   or UnauthorizedAccessException
 0186                                   or DirectoryNotFoundException
 0187                                   or FileNotFoundException)
 188        {
 189            // On Unix/macOS, getcwd() can throw if the process CWD was deleted.
 190            // We use AppContext.BaseDirectory as a stable fallback to avoid crashing in diagnostics
 191            // and when resolving relative paths.
 0192            return AppContext.BaseDirectory;
 193        }
 2194    }
 195
 2196    private static string GetSafeCurrentDirectoryForLogging() => GetSafeCurrentDirectoryOrBaseDirectory();
 197
 198    /// <summary>
 199    /// Retrieves the value of the specified header from the response headers.
 200    /// </summary>
 201    /// <param name="key">The name of the header to retrieve.</param>
 202    /// <returns>The value of the header if found; otherwise, null.</returns>
 0203    public string? GetHeader(string key) => Headers.TryGetValue(key, out var value) ? value : null;
 204
 205    /// <summary>
 206    /// Determines whether the specified content type is text-based or supports a charset.
 207    /// </summary>
 208    /// <param name="type">The MIME content type to check.</param>
 209    /// <returns>True if the content type is text-based; otherwise, false.</returns>
 210    public bool IsTextBasedContentType(string type)
 211    {
 37212        if (Logger.IsEnabled(LogEventLevel.Debug))
 213        {
 27214            Logger.Debug("Checking if content type is text-based: {ContentType}", type);
 215        }
 216
 217        // Check if the content type is text-based or has a charset
 37218        if (string.IsNullOrEmpty(type))
 219        {
 1220            return false;
 221        }
 222
 36223        if (type.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
 224        {
 21225            return true;
 226        }
 15227        if (type == "application/x-www-form-urlencoded")
 228        {
 0229            return true;
 230        }
 231
 232        // Include structured types using XML or JSON suffixes
 15233        if (type.EndsWith("xml", StringComparison.OrdinalIgnoreCase) ||
 15234            type.EndsWith("json", StringComparison.OrdinalIgnoreCase) ||
 15235            type.EndsWith("yaml", StringComparison.OrdinalIgnoreCase) ||
 15236            type.EndsWith("csv", StringComparison.OrdinalIgnoreCase))
 237        {
 4238            return true;
 239        }
 240
 241        // Common application types where charset makes sense
 11242        return TextBasedMimeTypes.Contains(type);
 243    }
 244    /// <summary>
 245    /// Adds callback parameters for the specified callback ID, body, and parameters.
 246    /// </summary>
 247    /// <param name="callbackId">The identifier for the callback</param>
 248    /// <param name="bodyParameterName">The name of the body parameter, if any</param>
 249    /// <param name="parameters">The parameters for the callback</param>
 250    public void AddCallbackParameters(string callbackId, string? bodyParameterName, Dictionary<string, object?> paramete
 251    {
 0252        if (MapRouteOptions.CallbackPlan is null || MapRouteOptions.CallbackPlan.Count == 0)
 253        {
 0254            return;
 255        }
 0256        var plan = MapRouteOptions.CallbackPlan.FirstOrDefault(p => p.CallbackId == callbackId);
 0257        if (plan is null)
 258        {
 0259            Logger.Warning("CallbackPlan '{id}' not found.", callbackId);
 0260            return;
 261        }
 262        // Create a new execution plan
 0263        var newExecutionPlan = new CallBackExecutionPlan(
 0264            CallbackId: callbackId,
 0265            Plan: plan,
 0266            BodyParameterName: bodyParameterName,
 0267            Parameters: parameters
 0268        );
 269
 0270        CallbackPlan.Add(newExecutionPlan);
 0271    }
 272    #endregion
 273
 274    #region  Response Writers
 275    /// <summary>
 276    /// Writes a file response with the specified file path, content type, and HTTP status code.
 277    /// </summary>
 278    /// <param name="filePath">The path to the file to be sent in the response.</param>
 279    /// <param name="contentType">The MIME type of the file content.</param>
 280    /// <param name="statusCode">The HTTP status code for the response.</param>
 281    public void WriteFileResponse(
 282        string? filePath,
 283        string? contentType,
 284        int statusCode = StatusCodes.Status200OK
 285    )
 286    {
 2287        if (Logger.IsEnabled(LogEventLevel.Debug))
 288        {
 2289            Logger.Debug("Writing file response,FilePath={FilePath} StatusCode={StatusCode}, ContentType={ContentType}, 
 2290                filePath, statusCode, contentType, GetSafeCurrentDirectoryForLogging());
 291        }
 292
 2293        if (string.IsNullOrEmpty(filePath))
 294        {
 0295            throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
 296        }
 297
 298        // IMPORTANT:
 299        // - Path.GetFullPath(relative) uses the process CWD.
 300        // - If the CWD is missing/deleted (can occur in CI/test scenarios), GetFullPath can fail.
 301        // Resolve relative paths against a safe, existing base directory instead.
 2302        var fullPath = Path.IsPathRooted(filePath)
 2303            ? Path.GetFullPath(filePath)
 2304            : Path.GetFullPath(filePath, GetSafeCurrentDirectoryOrBaseDirectory());
 305
 2306        if (!File.Exists(fullPath))
 307        {
 1308            StatusCode = StatusCodes.Status404NotFound;
 1309            Body = $"File not found: {filePath}";
 1310            ContentType = $"text/plain; charset={Encoding.WebName}";
 1311            return;
 312        }
 313
 314        // 2. Extract the directory to use as the "root"
 1315        var directory = Path.GetDirectoryName(fullPath)
 1316                       ?? throw new InvalidOperationException("Could not determine directory from file path");
 317
 1318        if (Logger.IsEnabled(LogEventLevel.Debug))
 319        {
 1320            Logger.Debug("Serving file: {FilePath}", fullPath);
 321        }
 322
 323        // Create a physical file provider for the directory
 1324        var physicalProvider = new PhysicalFileProvider(directory);
 1325        var fi = physicalProvider.GetFileInfo(Path.GetFileName(fullPath));
 1326        var provider = new FileExtensionContentTypeProvider();
 1327        contentType ??= provider.TryGetContentType(fullPath, out var ct)
 1328                ? ct
 1329                : "application/octet-stream";
 1330        Body = fi;
 331
 332        // headers & metadata
 1333        StatusCode = statusCode;
 1334        ContentType = contentType;
 1335        Logger.Debug("File response prepared: FileName={FileName}, Length={Length}, ContentType={ContentType}",
 1336            fi.Name, fi.Length, ContentType);
 1337    }
 338
 339    /// <summary>
 340    /// Writes a JSON response with the specified input object and HTTP status code.
 341    /// </summary>
 342    /// <param name="inputObject">The object to be converted to JSON.</param>
 343    /// <param name="statusCode">The HTTP status code for the response.</param>
 8344    public void WriteJsonResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) => WriteJsonResponseAsy
 345
 346    /// <summary>
 347    /// Asynchronously writes a JSON response with the specified input object and HTTP status code.
 348    /// </summary>
 349    /// <param name="inputObject">The object to be converted to JSON.</param>
 350    /// <param name="statusCode">The HTTP status code for the response.</param>
 351    /// <param name="contentType">The MIME type of the response content.</param>
 12352    public async Task WriteJsonResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 353
 354    /// <summary>
 355    /// Writes a JSON response using the specified input object and serializer settings.
 356    /// </summary>
 357    /// <param name="inputObject">The object to be converted to JSON.</param>
 358    /// <param name="serializerOptions">The options to use for JSON serialization.</param>
 359    /// <param name="statusCode">The HTTP status code for the response.</param>
 360    /// <param name="contentType">The MIME type of the response content.</param>
 0361    public void WriteJsonResponse(object? inputObject, JsonSerializerOptions serializerOptions, int statusCode = StatusC
 362
 363    /// <summary>
 364    /// Asynchronously writes a JSON response using the specified input object and serializer settings.
 365    /// </summary>
 366    /// <param name="inputObject">The object to be converted to JSON.</param>
 367    /// <param name="serializerOptions">The options to use for JSON serialization.</param>
 368    /// <param name="statusCode">The HTTP status code for the response.</param>
 369    /// <param name="contentType">The MIME type of the response content.</param>
 370    public async Task WriteJsonResponseAsync(object? inputObject, JsonSerializerOptions serializerOptions, int statusCod
 371    {
 24372        if (Logger.IsEnabled(LogEventLevel.Debug))
 373        {
 19374            Logger.Debug("Writing JSON response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode
 375        }
 376
 24377        ArgumentNullException.ThrowIfNull(serializerOptions);
 378
 24379        var sanitizedPayload = PayloadSanitizer.Sanitize(inputObject);
 48380        Body = await Task.Run(() => JsonSerializer.Serialize(sanitizedPayload, serializerOptions));
 24381        ContentType = string.IsNullOrEmpty(contentType) ? $"application/json; charset={Encoding.WebName}" : contentType;
 24382        StatusCode = statusCode;
 24383    }
 384    /// <summary>
 385    /// Writes a JSON response with the specified input object, serialization depth, compression option, status code, an
 386    /// </summary>
 387    /// <param name="inputObject">The object to be converted to JSON.</param>
 388    /// <param name="depth">The maximum depth for JSON serialization.</param>
 389    /// <param name="compress">Whether to compress the JSON output (no indentation).</param>
 390    /// <param name="statusCode">The HTTP status code for the response.</param>
 391    /// <param name="contentType">The MIME type of the response content.</param>
 1392    public void WriteJsonResponse(object? inputObject, int depth, bool compress, int statusCode = StatusCodes.Status200O
 393
 394    /// <summary>
 395    /// Asynchronously writes a JSON response with the specified input object, serialization depth, compression option, 
 396    /// </summary>
 397    /// <param name="inputObject">The object to be converted to JSON.</param>
 398    /// <param name="depth">The maximum depth for JSON serialization.</param>
 399    /// <param name="compress">Whether to compress the JSON output (no indentation).</param>
 400    /// <param name="statusCode">The HTTP status code for the response.</param>
 401    /// <param name="contentType">The MIME type of the response content.</param>
 402    public async Task WriteJsonResponseAsync(object? inputObject, int depth, bool compress, int statusCode = StatusCodes
 403    {
 24404        if (Logger.IsEnabled(LogEventLevel.Debug))
 405        {
 19406            Logger.Debug("Writing JSON response (async), StatusCode={StatusCode}, ContentType={ContentType}, Depth={Dept
 19407                statusCode, contentType, depth, compress);
 408        }
 409
 24410        var serializerOptions = new JsonSerializerOptions
 24411        {
 24412            WriteIndented = !compress,
 24413            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 24414            DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
 24415            ReferenceHandler = ReferenceHandler.IgnoreCycles,
 24416            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 24417            MaxDepth = depth
 24418        };
 419
 24420        await WriteJsonResponseAsync(inputObject, serializerOptions: serializerOptions, statusCode: statusCode, contentT
 24421    }
 422    /// <summary>
 423    /// Writes a CBOR response (binary, efficient, not human-readable).
 424    /// </summary>
 425    public async Task WriteCborResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 426    {
 2427        if (Logger.IsEnabled(LogEventLevel.Debug))
 428        {
 2429            Logger.Debug("Writing CBOR response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, conten
 430        }
 431
 432        // Serialize to CBOR using PeterO.Cbor
 4433        Body = await Task.Run(() => inputObject != null
 4434            ? PeterO.Cbor.CBORObject.FromObject(inputObject).EncodeToBytes()
 4435            : []);
 2436        ContentType = string.IsNullOrEmpty(contentType) ? "application/cbor" : contentType;
 2437        StatusCode = statusCode;
 2438    }
 439
 440    /// <summary>
 441    /// Writes a CBOR response (binary, efficient, not human-readable).
 442    /// </summary>
 443    /// <param name="inputObject">The object to be converted to CBOR.</param>
 444    /// <param name="statusCode">The HTTP status code for the response.</param>
 445    /// <param name="contentType">The MIME type of the response content.</param>
 0446    public void WriteCborResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 447
 448    /// <summary>
 449    /// Asynchronously writes a BSON response with the specified input object, status code, and content type.
 450    /// </summary>
 451    /// <param name="inputObject">The object to be converted to BSON.</param>
 452    /// <param name="statusCode">The HTTP status code for the response.</param>
 453    /// <param name="contentType">The MIME type of the response content.</param>
 454    public async Task WriteBsonResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 455    {
 1456        if (Logger.IsEnabled(LogEventLevel.Debug))
 457        {
 1458            Logger.Debug("Writing BSON response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, conten
 459        }
 460
 461        // Serialize to BSON (as byte[])
 2462        Body = await Task.Run(() => inputObject != null ? inputObject.ToBson() : []);
 1463        ContentType = string.IsNullOrEmpty(contentType) ? "application/bson" : contentType;
 1464        StatusCode = statusCode;
 1465    }
 466
 467    /// <summary>
 468    /// Writes a BSON response with the specified input object, status code, and content type.
 469    /// </summary>
 470    /// <param name="inputObject">The object to be converted to BSON.</param>
 471    /// <param name="statusCode">The HTTP status code for the response.</param>
 472    /// <param name="contentType">The MIME type of the response content.</param>
 0473    public void WriteBsonResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 474
 475    /// <summary>
 476    /// Writes a response with the specified input object and HTTP status code.
 477    /// Chooses the response format based on the Accept header or defaults to text/plain.
 478    /// </summary>
 479    /// <param name="inputObject">The object to be sent in the response body.</param>
 480    /// <param name="statusCode">The HTTP status code for the response.</param>
 1481    public void WriteResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) => WriteResponseAsync(input
 482
 483    /// <summary>
 484    /// Asynchronously writes a response with the specified input object and HTTP status code.
 485    /// </summary>
 486    /// <param name="inputObject">The object to be sent in the response body.</param>
 487    /// <returns>A task that represents the asynchronous write operation.</returns>
 488    public Task WriteResponseAsync(WriteObject inputObject)
 489    {
 1490        ArgumentNullException.ThrowIfNull(inputObject);
 491
 1492        if (inputObject.Value is null)
 493        {
 1494            Body = null;
 1495            StatusCode = inputObject.Status;
 1496            return Task.CompletedTask;
 497        }
 498
 0499        return WriteResponseAsync(inputObject.Value, inputObject.Status);
 500    }
 501
 502    /// <summary>
 503    /// Asynchronously writes a response with the specified input object and HTTP status code.
 504    /// Chooses the response format based on the Accept header or defaults to text/plain.
 505    /// </summary>
 506    /// <param name="inputObject">The object to be sent in the response body.</param>
 507    /// <param name="statusCode">The HTTP status code for the response.</param>
 508    /// <returns>A task that represents the asynchronous write operation.</returns>
 509    public async Task WriteResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK)
 510    {
 13511        if (inputObject is null)
 512        {
 2513            throw new ArgumentNullException(nameof(inputObject), "Input object cannot be null. Use WriteResponseAsync(Wr
 514        }
 515
 11516        if (Logger.IsEnabled(LogEventLevel.Debug))
 517        {
 11518            Logger.Debug("Writing response, StatusCode={StatusCode}", statusCode);
 519        }
 520
 11521        Body = inputObject;
 522
 523        try
 524        {
 525            // Read Accept header (may be missing)
 11526            string? acceptHeader = null;
 11527            _ = Request?.Headers.TryGetValue(HeaderNames.Accept, out acceptHeader);
 528
 11529            if (!ShouldEnforceOpenApiResponseContentTypes())
 530            {
 8531                await WriteLegacyNegotiatedResponseAsync(inputObject, statusCode, acceptHeader);
 8532                return;
 533            }
 534
 3535            await WriteOpenApiNegotiatedResponseAsync(inputObject, statusCode, acceptHeader);
 3536        }
 0537        catch (Exception ex)
 538        {
 0539            Logger.Error(ex, "Error in WriteResponseAsync");
 0540            await WriteErrorResponseAsync("Internal server error.", StatusCodes.Status500InternalServerError);
 541        }
 11542    }
 543
 544    /// <summary>
 545    /// Writes a response using legacy Accept-header negotiation for non-OpenAPI routes.
 546    /// </summary>
 547    /// <param name="inputObject">The response payload.</param>
 548    /// <param name="statusCode">The HTTP status code for the response.</param>
 549    /// <param name="acceptHeader">The incoming Accept header value.</param>
 550    /// <returns>A task representing the asynchronous write operation.</returns>
 551    private async Task WriteLegacyNegotiatedResponseAsync(object inputObject, int statusCode, string? acceptHeader)
 552    {
 8553        var negotiated = SelectResponseMediaType(acceptHeader, LegacyNegotiatedResponseContentTypes, defaultType: "text/
 8554            ?? new ContentTypeWithSchema("text/plain", null);
 555
 8556        if (Logger.IsEnabled(LogEventLevel.Verbose))
 557        {
 0558            Logger.Verbose(
 0559                "Selected legacy response media type for status code: {StatusCode}, MediaType={MediaType}, Accept={Accep
 0560                statusCode,
 0561                negotiated,
 0562                acceptHeader);
 563        }
 564
 8565        await WriteByMediaTypeAsync(negotiated.ContentType, inputObject, statusCode);
 8566    }
 567
 568    /// <summary>
 569    /// Writes a response using OpenAPI-declared response content types for the current status code.
 570    /// </summary>
 571    /// <param name="inputObject">The response payload.</param>
 572    /// <param name="statusCode">The HTTP status code for the response.</param>
 573    /// <param name="acceptHeader">The incoming Accept header value.</param>
 574    /// <returns>A task representing the asynchronous write operation.</returns>
 575    private async Task WriteOpenApiNegotiatedResponseAsync(object inputObject, int statusCode, string? acceptHeader)
 576    {
 3577        if (!TryGetResponseContentTypes(KrContext.MapRouteOptions.DefaultResponseContentType, statusCode, out var values
 578        {
 1579            var msg = $"No default response content type configured for status code {statusCode} and no range/default fa
 1580            Logger.Warning(msg);
 581
 1582            await WriteErrorResponseAsync(msg, StatusCodes.Status406NotAcceptable);
 1583            return;
 584        }
 585
 2586        if (values.Count == 0)
 587        {
 2588            var msg = $"Response status code {statusCode} is declared without content in OpenAPI. Returning a payload fo
 2589            Logger.Warning(msg);
 2590            await WriteErrorResponseAsync(msg, StatusCodes.Status500InternalServerError);
 2591            return;
 592        }
 593
 0594        var supported = values as IReadOnlyList<ContentTypeWithSchema> ?? [.. values];
 595
 0596        var mediaType = SelectResponseMediaType(acceptHeader, supported, defaultType: supported[0].ContentType);
 0597        if (mediaType is null)
 598        {
 599            var supportedMediaTypes = string.Join(", ", supported.Select(x => x.ContentType));
 0600            var msg = $"No supported media type found for status code {statusCode} with Accept header '{acceptHeader}'. 
 0601            Logger.Warning(
 0602                "No supported media type found for status code {StatusCode} with Accept header '{AcceptHeader}'. Support
 0603                statusCode,
 0604                acceptHeader,
 0605                supportedMediaTypes,
 0606                supported);
 607
 0608            await WriteErrorResponseAsync(msg, StatusCodes.Status406NotAcceptable);
 0609            return;
 610        }
 611
 0612        if (Logger.IsEnabled(LogEventLevel.Verbose))
 613        {
 0614            Logger.Verbose(
 0615                "Selected response media type for status code: {StatusCode}, MediaType={MediaType}, Accept={Accept}",
 0616                statusCode,
 0617                mediaType,
 0618                acceptHeader);
 619        }
 620
 0621        await WriteByMediaTypeAsync(mediaType.ContentType, inputObject, statusCode);
 3622    }
 623
 624    /// <summary>
 625    /// Determines whether OpenAPI response content-type enforcement should run for the current route.
 626    /// </summary>
 627    /// <remarks>
 628    /// Enforcement is only enabled for routes that carry OpenAPI descriptive metadata.
 629    /// Non-OpenAPI routes continue using legacy Accept-based negotiation.
 630    /// </remarks>
 631    /// <returns>True when the route has OpenAPI metadata; otherwise false.</returns>
 11632    private bool ShouldEnforceOpenApiResponseContentTypes() => MapRouteOptions.IsOpenApiAnnotatedFunctionRoute;
 633
 634    /// <summary>
 635    /// Queues a response payload for deferred writing, applying configured response schema conversion and validation wh
 636    /// </summary>
 637    /// <param name="inputObject">The payload to queue.</param>
 638    /// <param name="statusCode">The HTTP status code associated with the payload.</param>
 639    public void QueueResponseForWrite(object? inputObject, int statusCode = StatusCodes.Status200OK)
 640    {
 5641        if (inputObject is null)
 642        {
 1643            PostPonedWriteObject = new WriteObject(null, statusCode, StatusCodes.Status500InternalServerError);
 1644            return;
 645        }
 646
 647        try
 648        {
 4649            if (!TryGetResponseSchemaTypeForStatus(statusCode, out var schemaType, out var schemaTypeName))
 650            {
 1651                PostPonedWriteObject = new WriteObject(inputObject, statusCode);
 1652                return;
 653            }
 654
 3655            if (schemaType is null)
 656            {
 1657                Logger.Error("Unable to resolve response schema type '{SchemaTypeName}' for status code {StatusCode}.", 
 1658                PostPonedWriteObject = new WriteObject(null, statusCode, StatusCodes.Status500InternalServerError);
 1659                return;
 660            }
 661
 2662            var inputType = inputObject.GetType();
 2663            var valueToWrite = schemaType.IsInstanceOfType(inputObject) || inputType == schemaType
 2664                ? inputObject
 2665                : ConvertSchemaValue(inputObject, schemaType);
 666
 2667            if (!ValidateRequiredProperties(valueToWrite, out var missingProperties))
 668            {
 1669                Logger.WarningSanitized(
 1670                    "Response object failed required-property validation for schema type {SchemaTypeName}. Missing: {Mis
 1671                    schemaType.FullName,
 1672                    string.IsNullOrEmpty(missingProperties) ? "unknown required properties" : missingProperties);
 673
 1674                PostPonedWriteObject = new WriteObject(null, statusCode, StatusCodes.Status500InternalServerError);
 1675                return;
 676            }
 677
 1678            PostPonedWriteObject = new WriteObject(valueToWrite, statusCode);
 1679        }
 0680        catch (Exception ex)
 681        {
 0682            Logger.Error(ex, "Failed to convert response object for status code {StatusCode}.", statusCode);
 0683            PostPonedWriteObject = new WriteObject(null, statusCode, StatusCodes.Status500InternalServerError);
 0684        }
 4685    }
 686
 687    /// <summary>
 688    /// Selects the most appropriate response media type based on the Accept header.
 689    /// </summary>
 690    /// <param name="acceptHeader">The value of the Accept header from the request.</param>
 691    /// <param name="supported">A list of supported media types to match against the Accept header.</param>
 692    /// <param name="defaultType">The default media type to use if no match is found. Defaults to "text/plain".</param>
 693    /// <returns>The selected media type as a string.</returns>
 694    /// <remarks>
 695    /// This method parses the Accept header, orders the media types by quality factor,
 696    /// and selects the first supported media type. If none are supported returns null
 697    /// </remarks>
 698    private static ContentTypeWithSchema? SelectResponseMediaType(string? acceptHeader, IReadOnlyList<ContentTypeWithSch
 699    {
 8700        if (supported.Count == 0)
 701        {
 0702            return new ContentTypeWithSchema(defaultType, null);
 703        }
 704
 8705        if (string.IsNullOrWhiteSpace(acceptHeader))
 706        {
 0707            return supported[0];
 708        }
 709
 8710        if (!MediaTypeHeaderValue.TryParseList([acceptHeader], out var accepts) || accepts.Count == 0)
 711        {
 0712            return supported[0];
 713        }
 714
 16715        var supportsAnyMediaType = supported.Any(s => string.Equals(MediaTypeHelper.Normalize(s.ContentType), "*/*", Str
 716
 8717        var supportedNormalized = new string[supported.Count];
 8718        var supportedCanonical = new string[supported.Count];
 32719        for (var i = 0; i < supported.Count; i++)
 720        {
 8721            supportedNormalized[i] = MediaTypeHelper.Normalize(supported[i].ContentType);
 8722            supportedCanonical[i] = MediaTypeHelper.Canonicalize(supported[i].ContentType);
 723        }
 724
 33725        foreach (var a in accepts.OrderByDescending(x => x.Quality ?? 1.0))
 726        {
 8727            var accept = a.MediaType.Value;
 728
 8729            if (accept is null)
 730            {
 731                continue;
 732            }
 733
 734            // Normalize first so we can reliably detect wildcards and avoid treating them as canonical aliases.
 8735            var acceptNormalized = MediaTypeHelper.Normalize(accept);
 736
 8737            if (supportsAnyMediaType)
 738            {
 8739                return SelectWhenAnyMediaTypeSupported(acceptNormalized, defaultType);
 740            }
 741
 0742            var matched = SelectFromConfiguredSupportedMediaTypes(acceptNormalized, supported, supportedNormalized, supp
 0743            if (matched is not null)
 744            {
 0745                return matched;
 746            }
 747        }
 748        // No match found; return default
 0749        return null;
 8750    }
 751
 752    /// <summary>
 753    /// Selects a response media type when the configured supported list includes <c>*/*</c>.
 754    /// </summary>
 755    /// <param name="acceptNormalized">The normalized Accept media type.</param>
 756    /// <param name="defaultType">The default media type fallback.</param>
 757    /// <returns>The selected media type entry.</returns>
 758    private static ContentTypeWithSchema SelectWhenAnyMediaTypeSupported(string acceptNormalized, string defaultType)
 759    {
 8760        if (string.Equals(acceptNormalized, "*/*", StringComparison.OrdinalIgnoreCase) ||
 8761            acceptNormalized.EndsWith("/*", StringComparison.OrdinalIgnoreCase))
 762        {
 1763            return new ContentTypeWithSchema(defaultType, null);
 764        }
 765
 7766        var writerMediaType = ResolveWriterMediaType(acceptNormalized, defaultType);
 7767        return new ContentTypeWithSchema(writerMediaType, null);
 768    }
 769
 770    /// <summary>
 771    /// Selects a response media type from explicitly configured supported media types.
 772    /// </summary>
 773    /// <param name="acceptNormalized">The normalized Accept media type.</param>
 774    /// <param name="supported">The configured supported media type entries.</param>
 775    /// <param name="supportedNormalized">Normalized supported media types in index-aligned order.</param>
 776    /// <param name="supportedCanonical">Canonical supported media types in index-aligned order.</param>
 777    /// <returns>The matched media type entry, or null when no match exists.</returns>
 778    private static ContentTypeWithSchema? SelectFromConfiguredSupportedMediaTypes(
 779        string acceptNormalized,
 780        IReadOnlyList<ContentTypeWithSchema> supported,
 781        IReadOnlyList<string> supportedNormalized,
 782        IReadOnlyList<string> supportedCanonical)
 783    {
 0784        if (string.Equals(acceptNormalized, "*/*", StringComparison.OrdinalIgnoreCase))
 785        {
 0786            return supported[0];
 787        }
 788
 0789        if (acceptNormalized.EndsWith("/*", StringComparison.OrdinalIgnoreCase))
 790        {
 0791            var prefix = acceptNormalized[..^1]; // "application/"
 0792            for (var i = 0; i < supported.Count; i++)
 793            {
 0794                if (supportedNormalized[i].StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
 795                {
 0796                    return supported[i];
 797                }
 798            }
 799
 0800            return null;
 801        }
 802
 0803        var acceptCanonical = MediaTypeHelper.Canonicalize(acceptNormalized);
 804
 0805        for (var i = 0; i < supported.Count; i++)
 806        {
 0807            if (string.Equals(supportedNormalized[i], acceptNormalized, StringComparison.OrdinalIgnoreCase))
 808            {
 0809                return supported[i];
 810            }
 811        }
 812
 0813        for (var i = 0; i < supported.Count; i++)
 814        {
 0815            if (string.Equals(supportedCanonical[i], acceptCanonical, StringComparison.OrdinalIgnoreCase))
 816            {
 0817                return supported[i];
 818            }
 819        }
 820
 0821        return null;
 822    }
 823
 824    /// <summary>
 825    /// Resolves an incoming Accept media type to a concrete response writer media type.
 826    /// </summary>
 827    /// <param name="acceptNormalized">The normalized Accept media type.</param>
 828    /// <param name="defaultType">The fallback media type.</param>
 829    /// <returns>A concrete media type supported by response writers.</returns>
 830    private static string ResolveWriterMediaType(string acceptNormalized, string defaultType)
 831    {
 7832        var canonical = MediaTypeHelper.Canonicalize(acceptNormalized);
 833        // For common structured types with well-known suffixes, return the canonical type to ensure we return a support
 7834        if (string.Equals(canonical, "application/json", StringComparison.OrdinalIgnoreCase) ||
 7835            string.Equals(canonical, "application/xml", StringComparison.OrdinalIgnoreCase) ||
 7836            string.Equals(canonical, "application/yaml", StringComparison.OrdinalIgnoreCase))
 837        {
 6838            return canonical;
 839        }
 840        // Allow text/csv and application/x-www-form-urlencoded as they are commonly used text-based formats that suppor
 1841        if (string.Equals(acceptNormalized, "text/csv", StringComparison.OrdinalIgnoreCase) ||
 1842            string.Equals(acceptNormalized, "application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
 843        {
 1844            return acceptNormalized;
 845        }
 846        // For other text/* types, default to text/plain since we don't want to accidentally return HTML or similar type
 0847        if (acceptNormalized.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
 848        {
 0849            return "text/plain";
 850        }
 851        // For other types, we would need explicit support configured to return them; default to the provided default ty
 0852        return defaultType;
 853    }
 854
 855    /// <summary>
 856    /// Resolves response content types for a status code using exact, range (e.g., 4XX), then default.
 857    /// </summary>
 858    /// <param name="contentTypes">The content type map keyed by status code, range, or default.</param>
 859    /// <param name="statusCode">The HTTP status code to resolve.</param>
 860    /// <param name="values">The resolved content types, if found.</param>
 861    /// <returns>True when a matching entry is found, including explicit empty mappings.</returns>
 862    private static bool TryGetResponseContentTypes(
 863        IDictionary<string, ICollection<ContentTypeWithSchema>>? contentTypes,
 864        int statusCode,
 865        out ICollection<ContentTypeWithSchema>? values)
 866    {
 7867        values = null;
 7868        if (contentTypes is null || contentTypes.Count == 0)
 869        {
 1870            return false;
 871        }
 872
 6873        var statusKey = statusCode.ToString(CultureInfo.InvariantCulture);
 6874        if (TryGetValueIgnoreCase(contentTypes, statusKey, out values))
 875        {
 4876            return true;
 877        }
 878
 2879        if (statusCode is >= 100 and <= 599)
 880        {
 881            // Allow OpenAPI-style wildcard keys such as:
 882            // - 4XX (all 4xx)
 883            // These are matched case-insensitively.
 2884            var rangeKey = $"{statusCode / 100}XX";
 2885            if (TryGetValueIgnoreCase(contentTypes, rangeKey, out values))
 886            {
 1887                return true;
 888            }
 889        }
 890
 1891        if (TryGetValueIgnoreCase(contentTypes, "default", out values))
 892        {
 1893            return true;
 894        }
 895
 0896        values = null;
 0897        return false;
 898    }
 899
 900    /// <summary>
 901    /// Attempts to resolve a configured response schema type for the given status code.
 902    /// </summary>
 903    /// <param name="statusCode">The status code for which a schema should be resolved.</param>
 904    /// <param name="schemaType">The resolved schema type, when available.</param>
 905    /// <param name="schemaTypeName">The configured schema type name.</param>
 906    /// <returns>True when a schema mapping exists for the status code and includes schema metadata.</returns>
 907    private bool TryGetResponseSchemaTypeForStatus(int statusCode, out Type? schemaType, out string? schemaTypeName)
 908    {
 4909        schemaType = null;
 4910        schemaTypeName = null;
 911
 4912        if (!TryGetResponseContentTypes(MapRouteOptions.DefaultResponseContentType, statusCode, out var mappings) || map
 913        {
 0914            return false;
 915        }
 916
 4917        var first = mappings.FirstOrDefault();
 4918        if (first is null || string.IsNullOrWhiteSpace(first.Schema))
 919        {
 1920            return false;
 921        }
 922
 3923        schemaTypeName = first.Schema;
 3924        schemaType = ResolveSchemaType(schemaTypeName);
 3925        return true;
 926    }
 927
 928    /// <summary>
 929    /// Resolves a type by full name, short name, or assembly-qualified name from loaded assemblies.
 930    /// </summary>
 931    /// <param name="schemaTypeName">The schema type name to resolve.</param>
 932    /// <returns>The resolved type when found; otherwise null.</returns>
 933    private static Type? ResolveSchemaType(string schemaTypeName)
 934    {
 3935        if (string.IsNullOrWhiteSpace(schemaTypeName))
 936        {
 0937            return null;
 938        }
 939
 3940        var candidates = new List<Type>();
 941
 3942        var directType = Type.GetType(schemaTypeName, throwOnError: false, ignoreCase: true);
 3943        if (directType is not null)
 944        {
 0945            candidates.Add(directType);
 946        }
 947
 1274948        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
 949        {
 634950            CollectSchemaTypeCandidatesFromAssembly(assembly, schemaTypeName, candidates);
 951        }
 952
 3953        return SelectPreferredSchemaType(candidates);
 954    }
 955
 956    /// <summary>
 957    /// Collects schema type candidates from an assembly by direct and name-based matching.
 958    /// </summary>
 959    /// <param name="assembly">The assembly to scan.</param>
 960    /// <param name="schemaTypeName">The schema type name to resolve.</param>
 961    /// <param name="candidates">The destination list of matching candidates.</param>
 962    private static void CollectSchemaTypeCandidatesFromAssembly(Assembly assembly, string schemaTypeName, List<Type> can
 963    {
 634964        var assemblyType = assembly.GetType(schemaTypeName, throwOnError: false, ignoreCase: true);
 634965        if (assemblyType is not null)
 966        {
 2967            candidates.Add(assemblyType);
 968        }
 969
 970        Type[] typeCandidates;
 971        try
 972        {
 634973            typeCandidates = assembly.GetTypes();
 634974        }
 0975        catch (ReflectionTypeLoadException ex)
 976        {
 0977            typeCandidates = [.. ex.Types.Where(t => t is not null)!];
 0978        }
 0979        catch
 980        {
 0981            return;
 982        }
 983
 232618984        foreach (var typeCandidate in typeCandidates)
 985        {
 115675986            if (typeCandidate is not null && IsMatchingSchemaTypeName(typeCandidate, schemaTypeName))
 987            {
 2988                candidates.Add(typeCandidate);
 989            }
 990        }
 634991    }
 992
 993    /// <summary>
 994    /// Determines whether a candidate type matches the provided schema type name.
 995    /// </summary>
 996    /// <param name="typeCandidate">The type candidate to evaluate.</param>
 997    /// <param name="schemaTypeName">The schema type name to match.</param>
 998    /// <returns>True when the candidate matches by full name, short name, or assembly-qualified prefix.</returns>
 999    private static bool IsMatchingSchemaTypeName(Type typeCandidate, string schemaTypeName)
 1156751000        => string.Equals(typeCandidate.FullName, schemaTypeName, StringComparison.OrdinalIgnoreCase)
 1156751001           || string.Equals(typeCandidate.Name, schemaTypeName, StringComparison.OrdinalIgnoreCase)
 1156751002           || (!string.IsNullOrWhiteSpace(typeCandidate.AssemblyQualifiedName)
 1156751003               && typeCandidate.AssemblyQualifiedName.StartsWith(schemaTypeName + ",", StringComparison.OrdinalIgnoreCas
 1004
 1005    /// <summary>
 1006    /// Selects the preferred schema type from collected candidates.
 1007    /// </summary>
 1008    /// <param name="candidates">The collected type candidates.</param>
 1009    /// <returns>The preferred schema type, or null when no candidate exists.</returns>
 1010    private static Type? SelectPreferredSchemaType(IReadOnlyList<Type> candidates)
 1011    {
 31012        var distinct = candidates
 41013            .Where(t => t is not null)
 41014            .GroupBy(t => string.IsNullOrWhiteSpace(t.AssemblyQualifiedName) ? t.FullName : t.AssemblyQualifiedName, Str
 21015            .Select(g => g.First())
 31016            .ToList();
 1017
 31018        if (distinct.Count == 0)
 1019        {
 11020            return null;
 1021        }
 1022
 21023        var generatedCandidate = distinct.FirstOrDefault(t =>
 41024            t.GetProperty("XmlMetadata", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) is n
 1025
 21026        return generatedCandidate ?? distinct[0];
 1027    }
 1028
 1029    /// <summary>
 1030    /// Converts a value to the target schema type.
 1031    /// </summary>
 1032    /// <param name="value">The source value.</param>
 1033    /// <param name="targetType">The destination type.</param>
 1034    /// <returns>The converted value.</returns>
 1035    private static object? ConvertSchemaValue(object? value, Type targetType)
 1036    {
 51037        value = UnwrapPowerShellValue(value);
 51038        if (value is null)
 1039        {
 01040            return null;
 1041        }
 1042        // If the value is already assignable to the target type, return it directly without further conversion.
 51043        var valueType = value.GetType();
 51044        if (targetType.IsInstanceOfType(value) || valueType == targetType)
 1045        {
 31046            return value;
 1047        }
 1048        // Handle nullable types by converting to the underlying type.
 21049        var nullableUnderlying = Nullable.GetUnderlyingType(targetType);
 21050        if (nullableUnderlying is not null)
 1051        {
 01052            return ConvertSchemaValue(value, nullableUnderlying);
 1053        }
 1054        // If the target type is a map-like type (e.g., IDictionary) and the value is an IDictionary, return it directly
 21055        if (IsMapLikeType(targetType) && value is IDictionary)
 1056        {
 01057            return value;
 1058        }
 1059        // If the target type is an array, attempt to convert the value to an array of the target element type. This han
 21060        if (targetType.IsArray)
 1061        {
 01062            return ConvertSchemaArrayValue(value, targetType);
 1063        }
 1064        // Attempt dictionary-to-type conversion for schema values, which allows for flexible object construction from d
 21065        if (TryConvertSchemaDictionaryValue(value, targetType, out var dictionaryConvertedValue))
 1066        {
 21067            return dictionaryConvertedValue;
 1068        }
 1069        // Attempt PowerShell object conversion, which allows for flexible handling of PowerShell-specific objects and t
 01070        if (TryConvertPowerShellObjectToType(value, targetType, out var convertedPowerShellObject))
 1071        {
 01072            return convertedPowerShellObject;
 1073        }
 1074        // Attempt single-argument constructor conversion as a last resort before simple conversions, as it may be more 
 01075        if (TryConvertViaSingleArgumentConstructor(value, targetType, out var constructorConvertedValue))
 1076        {
 01077            return constructorConvertedValue;
 1078        }
 1079        // As a final fallback, attempt simple conversions for primitive types and common convertible types, which allow
 01080        return TryConvertSimple(value, targetType, out var convertedValue) ? convertedValue : value;
 1081    }
 1082
 1083    /// <summary>
 1084    /// Converts a schema value to an array of the requested target type.
 1085    /// </summary>
 1086    /// <param name="value">The source value to convert.</param>
 1087    /// <param name="targetArrayType">The destination array type.</param>
 1088    /// <returns>A typed array instance populated from the source value.</returns>
 1089    private static object ConvertSchemaArrayValue(object value, Type targetArrayType)
 1090    {
 01091        var elementType = targetArrayType.GetElementType() ?? typeof(object);
 01092        if (value is IEnumerable enumerable and not string)
 1093        {
 01094            return ConvertEnumerableToTypedArray(enumerable, elementType);
 1095        }
 1096        // If the value is not an enumerable, attempt to convert it as a single element array.
 01097        return ConvertSingleValueToTypedArray(value, elementType);
 1098    }
 1099
 1100    /// <summary>
 1101    /// Converts an enumerable source to a typed destination array.
 1102    /// </summary>
 1103    /// <param name="enumerable">The source enumerable values.</param>
 1104    /// <param name="elementType">The destination element type.</param>
 1105    /// <returns>A typed array containing converted elements.</returns>
 1106    private static Array ConvertEnumerableToTypedArray(IEnumerable enumerable, Type elementType)
 1107    {
 01108        var materialized = new List<object?>();
 01109        foreach (var item in enumerable)
 1110        {
 01111            materialized.Add(ConvertSchemaValue(item, elementType));
 1112        }
 1113        // Create a typed array of the destination element type and populate it with the converted values, ensuring that
 01114        var typedArray = Array.CreateInstance(elementType, materialized.Count);
 01115        for (var i = 0; i < materialized.Count; i++)
 1116        {
 01117            var itemToAssign = EnsureArrayElementAssignable(materialized[i], elementType, allowSimpleFallback: false);
 01118            typedArray.SetValue(itemToAssign, i);
 1119        }
 1120        // Return the fully converted and typed array to be used as the response value, which ensures that the response 
 01121        return typedArray;
 1122    }
 1123
 1124    /// <summary>
 1125    /// Converts a single source value to a one-element typed destination array.
 1126    /// </summary>
 1127    /// <param name="value">The source value.</param>
 1128    /// <param name="elementType">The destination element type.</param>
 1129    /// <returns>A one-element typed array containing the converted value.</returns>
 1130    private static Array ConvertSingleValueToTypedArray(object value, Type elementType)
 1131    {
 01132        var singleItemArray = Array.CreateInstance(elementType, 1);
 01133        var singleElement = EnsureArrayElementAssignable(ConvertSchemaValue(value, elementType), elementType, allowSimpl
 01134        singleItemArray.SetValue(singleElement, 0);
 01135        return singleItemArray;
 1136    }
 1137
 1138    /// <summary>
 1139    /// Ensures that an array element is assignable to the destination element type.
 1140    /// </summary>
 1141    /// <param name="candidate">The candidate value to assign.</param>
 1142    /// <param name="elementType">The required destination element type.</param>
 1143    /// <param name="allowSimpleFallback">Whether to attempt simple conversion before throwing.</param>
 1144    /// <returns>A value assignable to the destination element type, or null.</returns>
 1145    /// <exception cref="InvalidCastException">Thrown when conversion to the destination element type fails.</exception>
 1146    private static object? EnsureArrayElementAssignable(object? candidate, Type elementType, bool allowSimpleFallback)
 1147    {
 01148        var unwrapped = UnwrapPowerShellValue(candidate);
 01149        if (unwrapped is null || elementType.IsInstanceOfType(unwrapped))
 1150        {
 01151            return unwrapped;
 1152        }
 1153
 1154        // Attempt schema conversion on the element to ensure it is compatible with the destination element type, which 
 01155        var convertedElement = UnwrapPowerShellValue(ConvertSchemaValue(unwrapped, elementType));
 01156        if (convertedElement is null || elementType.IsInstanceOfType(convertedElement))
 1157        {
 01158            return convertedElement;
 1159        }
 1160
 1161        // If allowed, attempt a simple conversion as a final fallback before throwing, which provides some leniency for
 01162        if (allowSimpleFallback &&
 01163            TryConvertSimple(convertedElement, elementType, out var simpleConverted) &&
 01164            (simpleConverted is null || elementType.IsInstanceOfType(simpleConverted)))
 1165        {
 01166            return simpleConverted;
 1167        }
 1168
 1169        // If the element cannot be converted to the required type, throw an exception to indicate a schema validation f
 01170        throw new InvalidCastException($"Object of type '{convertedElement.GetType().FullName}' cannot be converted to '
 1171    }
 1172
 1173    /// <summary>
 1174    /// Attempts dictionary-to-type conversion for schema values.
 1175    /// </summary>
 1176    /// <param name="value">The source value.</param>
 1177    /// <param name="targetType">The destination type.</param>
 1178    /// <param name="convertedValue">The converted value when successful.</param>
 1179    /// <returns>True when dictionary conversion succeeds; otherwise false.</returns>
 1180    private static bool TryConvertSchemaDictionaryValue(object value, Type targetType, out object? convertedValue)
 1181    {
 21182        convertedValue = null;
 21183        if (value is not IDictionary dictionary)
 1184        {
 01185            return false;
 1186        }
 1187
 21188        var (success, convertedDictionaryValue) = TryConvertDictionaryToType(dictionary, targetType);
 21189        if (!success)
 1190        {
 01191            return false;
 1192        }
 1193
 21194        convertedValue = convertedDictionaryValue;
 21195        return true;
 1196    }
 1197
 1198    /// <summary>
 1199    /// Attempts to convert a value to the target type by using single-argument constructors.
 1200    /// </summary>
 1201    /// <param name="value">The source value.</param>
 1202    /// <param name="targetType">The destination type.</param>
 1203    /// <param name="convertedValue">The constructed value when successful.</param>
 1204    /// <returns>True when a single-argument constructor conversion succeeds; otherwise false.</returns>
 1205    private static bool TryConvertViaSingleArgumentConstructor(object value, Type targetType, out object? convertedValue
 1206    {
 01207        convertedValue = null;
 01208        foreach (var constructor in targetType.GetConstructors().Where(c => c.GetParameters().Length == 1))
 1209        {
 01210            var parameterType = constructor.GetParameters()[0].ParameterType;
 01211            if (!TryConvertSimple(value, parameterType, out var convertedArg))
 1212            {
 1213                continue;
 1214            }
 1215
 01216            convertedValue = constructor.Invoke([convertedArg]);
 01217            return true;
 1218        }
 1219
 01220        return false;
 01221    }
 1222
 1223    /// <summary>
 1224    /// Unwraps common PowerShell wrapper/sentinel values into .NET runtime values.
 1225    /// </summary>
 1226    /// <param name="value">The value to unwrap.</param>
 1227    /// <returns>The unwrapped value, or null for AutomationNull.</returns>
 1228    private static object? UnwrapPowerShellValue(object? value)
 1229    {
 51230        if (value is null)
 1231        {
 01232            return null;
 1233        }
 1234
 51235        if (IsPowerShellAutomationNull(value))
 1236        {
 01237            return null;
 1238        }
 1239
 51240        if (value is PSObject psObject)
 1241        {
 01242            var baseObject = psObject.BaseObject;
 01243            return baseObject is null || IsPowerShellAutomationNull(baseObject)
 01244                ? null
 01245                : baseObject;
 1246        }
 1247
 51248        return value;
 1249    }
 1250
 1251    /// <summary>
 1252    /// Determines whether a value represents PowerShell's AutomationNull sentinel.
 1253    /// </summary>
 1254    /// <param name="value">The value to inspect.</param>
 1255    /// <returns>True when the value is PowerShell AutomationNull.</returns>
 1256    private static bool IsPowerShellAutomationNull(object value)
 1257    {
 51258        var type = value.GetType();
 51259        return type.FullName?.Equals("System.Management.Automation.Internal.AutomationNull", StringComparison.Ordinal) =
 1260    }
 1261
 1262    /// <summary>
 1263    /// Attempts to convert dictionary values to a strongly typed object.
 1264    /// </summary>
 1265    /// <param name="dictionary">The source dictionary.</param>
 1266    /// <param name="targetType">The destination type.</param>
 1267    /// <returns>A tuple indicating conversion success and converted value.</returns>
 1268    private static (bool Success, object? Value) TryConvertDictionaryToType(IDictionary dictionary, Type targetType)
 1269    {
 21270        var defaultConstructor = targetType.GetConstructor(Type.EmptyTypes);
 21271        if (defaultConstructor is null)
 1272        {
 01273            return (false, null);
 1274        }
 1275
 21276        var instance = defaultConstructor.Invoke([]);
 21277        var writableProperties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
 31278            .Where(p => p.CanWrite)
 21279            .ToList();
 1280
 101281        foreach (var property in writableProperties)
 1282        {
 31283            var matchKey = FindDictionaryKey(dictionary, property.Name);
 31284            if (matchKey is null)
 1285            {
 1286                continue;
 1287            }
 1288
 31289            var rawValue = dictionary[matchKey];
 31290            if (rawValue is IDictionary && IsMapLikeType(property.PropertyType))
 1291            {
 01292                return (true, dictionary);
 1293            }
 1294
 31295            var convertedPropertyValue = ConvertSchemaValue(rawValue, property.PropertyType);
 31296            property.SetValue(instance, convertedPropertyValue);
 1297        }
 1298
 21299        return (true, instance);
 01300    }
 1301
 1302    /// <summary>
 1303    /// Attempts to convert a PowerShell custom object to a strongly typed object by mapping note properties.
 1304    /// </summary>
 1305    /// <param name="value">The source PowerShell object.</param>
 1306    /// <param name="targetType">The destination type.</param>
 1307    /// <param name="converted">The converted object when successful.</param>
 1308    /// <returns>True when conversion succeeds.</returns>
 1309    private static bool TryConvertPowerShellObjectToType(object value, Type targetType, out object? converted)
 1310    {
 01311        converted = null;
 1312
 01313        var typeName = value.GetType().FullName;
 01314        if (!string.Equals(typeName, "System.Management.Automation.PSCustomObject", StringComparison.Ordinal))
 1315        {
 01316            return false;
 1317        }
 1318
 01319        var asPsObject = value as PSObject ?? new PSObject(value);
 01320        var dictionary = new Hashtable(StringComparer.OrdinalIgnoreCase);
 01321        foreach (var property in asPsObject.Properties)
 1322        {
 01323            if (property is null || string.IsNullOrWhiteSpace(property.Name))
 1324            {
 1325                continue;
 1326            }
 1327
 01328            dictionary[property.Name] = property.Value;
 1329        }
 1330
 01331        var (success, convertedValue) = TryConvertDictionaryToType(dictionary, targetType);
 01332        if (!success)
 1333        {
 01334            return false;
 1335        }
 1336
 01337        converted = convertedValue;
 01338        return true;
 1339    }
 1340
 1341    /// <summary>
 1342    /// Finds a dictionary key by case-insensitive string comparison.
 1343    /// </summary>
 1344    /// <param name="dictionary">The dictionary to search.</param>
 1345    /// <param name="propertyName">The property name to match.</param>
 1346    /// <returns>The matching dictionary key, if found.</returns>
 1347    private static object? FindDictionaryKey(IDictionary dictionary, string propertyName)
 1348    {
 111349        foreach (DictionaryEntry entry in dictionary)
 1350        {
 41351            if (entry.Key is null)
 1352            {
 1353                continue;
 1354            }
 1355
 41356            var key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
 41357            if (string.Equals(key, propertyName, StringComparison.OrdinalIgnoreCase))
 1358            {
 31359                return entry.Key;
 1360            }
 1361        }
 1362
 01363        return null;
 31364    }
 1365
 1366    /// <summary>
 1367    /// Validates required properties using generated helper methods when available.
 1368    /// </summary>
 1369    /// <param name="value">The converted value to validate.</param>
 1370    /// <param name="missingProperties">A comma-separated list of missing properties.</param>
 1371    /// <returns>True when validation succeeds or no validation helper exists.</returns>
 1372    private static bool ValidateRequiredProperties(object? value, out string missingProperties)
 1373    {
 21374        missingProperties = string.Empty;
 21375        if (value is null)
 1376        {
 01377            return true;
 1378        }
 1379
 21380        var runtimeType = value.GetType();
 21381        var validateMethod = runtimeType.GetMethod("ValidateRequiredProperties", BindingFlags.Public | BindingFlags.Inst
 21382        if (validateMethod is null)
 1383        {
 11384            return true;
 1385        }
 1386
 11387        var validationResult = validateMethod.Invoke(value, null);
 11388        if (validationResult is bool isValid && isValid)
 1389        {
 01390            return true;
 1391        }
 1392
 11393        var missingMethod = runtimeType.GetMethod("GetMissingRequiredProperties", BindingFlags.Public | BindingFlags.Ins
 11394        if (missingMethod is not null)
 1395        {
 11396            var missing = missingMethod.Invoke(value, null);
 11397            missingProperties = FormatMissingRequiredProperties(missing);
 1398        }
 1399
 11400        return false;
 1401    }
 1402
 1403    /// <summary>
 1404    /// Formats missing required-property values returned by generated validation helpers.
 1405    /// </summary>
 1406    /// <param name="missing">The missing-properties payload returned by reflection invocation.</param>
 1407    /// <returns>A comma-separated string of missing property names, or an empty string when unavailable.</returns>
 1408    private static string FormatMissingRequiredProperties(object? missing)
 1409    {
 11410        if (missing is IEnumerable<string> missingEnumerable)
 1411        {
 11412            return string.Join(", ", missingEnumerable);
 1413        }
 1414
 01415        if (missing is IEnumerable genericEnumerable)
 1416        {
 01417            var values = new List<string>();
 01418            foreach (var item in genericEnumerable)
 1419            {
 01420                values.Add(item?.ToString() ?? string.Empty);
 1421            }
 1422
 01423            return string.Join(", ", values.Where(v => !string.IsNullOrWhiteSpace(v)));
 1424        }
 1425
 01426        return string.Empty;
 1427    }
 1428
 1429    /// <summary>
 1430    /// Determines whether a type should be treated as map-like for dictionary passthrough.
 1431    /// </summary>
 1432    /// <param name="type">The type to inspect.</param>
 1433    /// <returns>True when the type is map-like.</returns>
 1434    private static bool IsMapLikeType(Type type)
 1435    {
 21436        if (type.GetProperty("AdditionalProperties", BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenH
 1437        {
 01438            return true;
 1439        }
 1440
 21441        var attributes = type.GetCustomAttributes(inherit: true);
 81442        foreach (var attribute in attributes)
 1443        {
 21444            if (attribute.GetType().Name.Equals("OpenApiPatternPropertiesAttribute", StringComparison.OrdinalIgnoreCase)
 1445            {
 01446                return true;
 1447            }
 1448        }
 1449
 21450        return false;
 1451    }
 1452
 1453    /// <summary>
 1454    /// Attempts to perform basic runtime conversions.
 1455    /// </summary>
 1456    /// <param name="value">The source value.</param>
 1457    /// <param name="targetType">The destination type.</param>
 1458    /// <param name="converted">The converted value when successful.</param>
 1459    /// <returns>True when conversion succeeds.</returns>
 1460    private static bool TryConvertSimple(object? value, Type targetType, out object? converted)
 1461    {
 01462        converted = null;
 01463        if (value is null)
 1464        {
 01465            return false;
 1466        }
 1467
 01468        var valueType = value.GetType();
 01469        if (targetType.IsAssignableFrom(valueType))
 1470        {
 01471            converted = value;
 01472            return true;
 1473        }
 1474
 1475        try
 1476        {
 01477            if (targetType.IsEnum)
 1478            {
 01479                converted = value is string s
 01480                    ? Enum.Parse(targetType, s, ignoreCase: true)
 01481                    : Enum.ToObject(targetType, value);
 01482                return true;
 1483            }
 1484
 01485            converted = Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
 01486            return true;
 1487        }
 01488        catch
 1489        {
 1490            try
 1491            {
 01492                converted = LanguagePrimitives.ConvertTo(value, targetType, CultureInfo.InvariantCulture);
 01493                return true;
 1494            }
 01495            catch
 1496            {
 01497                return false;
 1498            }
 1499        }
 01500    }
 1501
 1502    /// <summary>
 1503    /// Attempts to read a dictionary value with case-insensitive key matching.
 1504    /// </summary>
 1505    /// <typeparam name="T">The dictionary value type.</typeparam>
 1506    /// <param name="dict">The dictionary to read from.</param>
 1507    /// <param name="key">The key to search for.</param>
 1508    /// <param name="value">The matched value, if found.</param>
 1509    /// <returns>True when a matching key is found.</returns>
 1510    private static bool TryGetValueIgnoreCase<T>(IDictionary<string, T> dict, string key, out T? value)
 1511    {
 91512        if (dict.TryGetValue(key, out value))
 1513        {
 61514            return true;
 1515        }
 1516
 141517        foreach (var kvp in dict)
 1518        {
 41519            if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
 1520            {
 01521                value = kvp.Value;
 01522                return true;
 1523            }
 1524        }
 1525
 31526        value = default;
 31527        return false;
 01528    }
 1529
 1530    /// <summary>
 1531    /// Writes a response based on the specified media type.
 1532    /// </summary>
 1533    /// <param name="mediaType">The media type to use for the response.</param>
 1534    /// <param name="inputObject">The object to be written in the response body.</param>
 1535    /// <param name="statusCode">The HTTP status code for the response.</param>
 1536    /// <returns>A Task representing the asynchronous operation.</returns>
 1537    private Task WriteByMediaTypeAsync(string mediaType, object? inputObject, int statusCode)
 1538    {
 1539        // If you want, set Response.ContentType here once, centrally.
 81540        ContentType = mediaType;
 1541
 81542        return mediaType switch
 81543        {
 61544            "application/json" => WriteJsonResponseAsync(inputObject, statusCode, mediaType),
 01545            "application/yaml" => WriteYamlResponseAsync(inputObject, statusCode, mediaType),
 01546            "application/xml" => WriteXmlResponseAsync(inputObject, statusCode, mediaType),
 01547            "application/bson" => WriteBsonResponseAsync(inputObject, statusCode, mediaType),
 01548            "application/cbor" => WriteCborResponseAsync(inputObject, statusCode, mediaType),
 11549            "text/csv" => WriteCsvResponseAsync(inputObject, statusCode, mediaType),
 01550            "application/x-www-form-urlencoded" => WriteFormUrlEncodedResponseAsync(inputObject, statusCode),
 11551            _ => WriteTextResponseAsync(inputObject?.ToString() ?? string.Empty, statusCode),
 81552        };
 1553    }
 1554
 1555    /// <summary>
 1556    /// Writes a CSV response with the specified input object, status code, content type, and optional CsvConfiguration.
 1557    /// </summary>
 1558    /// <param name="inputObject">The object to be converted to CSV.</param>
 1559    /// <param name="statusCode">The HTTP status code for the response.</param>
 1560    /// <param name="contentType">The MIME type of the response content.</param>
 1561    /// <param name="config">An optional CsvConfiguration to customize CSV output.</param>
 1562    public void WriteCsvResponse(
 1563            object? inputObject,
 1564            int statusCode = StatusCodes.Status200OK,
 1565            string? contentType = null,
 1566            CsvConfiguration? config = null)
 1567    {
 21568        Action<CsvConfiguration>? tweaker = null;
 1569
 21570        if (config is not null)
 1571        {
 11572            tweaker = target =>
 11573            {
 901574                foreach (var prop in typeof(CsvConfiguration)
 11575                     .GetProperties(BindingFlags.Public | BindingFlags.Instance))
 11576                {
 441577                    if (prop.CanRead && prop.CanWrite)
 11578                    {
 441579                        var value = prop.GetValue(config);
 441580                        prop.SetValue(target, value);
 11581                    }
 11582                }
 21583            };
 1584        }
 21585        WriteCsvResponseAsync(inputObject, statusCode, contentType, tweaker).GetAwaiter().GetResult();
 21586    }
 1587
 1588    /// <summary>
 1589    /// Asynchronously writes a CSV response with the specified input object, status code, content type, and optional co
 1590    /// </summary>
 1591    /// <param name="inputObject">The object to be converted to CSV.</param>
 1592    /// <param name="statusCode">The HTTP status code for the response.</param>
 1593    /// <param name="contentType">The MIME type of the response content.</param>
 1594    /// <param name="tweak">An optional action to tweak the CsvConfiguration.</param>
 1595    public async Task WriteCsvResponseAsync(
 1596        object? inputObject,
 1597        int statusCode = StatusCodes.Status200OK,
 1598        string? contentType = null,
 1599        Action<CsvConfiguration>? tweak = null)
 1600    {
 41601        if (Logger.IsEnabled(LogEventLevel.Debug))
 1602        {
 41603            Logger.Debug("Writing CSV response (async), StatusCode={StatusCode}, ContentType={ContentType}",
 41604                      statusCode, contentType);
 1605        }
 1606
 1607        // Serialize inside a background task so heavy reflection never blocks the caller
 41608        Body = await Task.Run(() =>
 41609        {
 41610            var cfg = new CsvConfiguration(CultureInfo.InvariantCulture)
 41611            {
 41612                HasHeaderRecord = true,
 41613                NewLine = Environment.NewLine
 41614            };
 41615            tweak?.Invoke(cfg);                         // let the caller flirt with the config
 41616
 41617            using var sw = new StringWriter();
 41618            using var csv = new CsvWriter(sw, cfg);
 41619
 41620            // CsvHelper insists on an enumerable; wrap single objects so it stays happy
 41621            if (inputObject is IEnumerable records and not string)
 41622            {
 41623                csv.WriteRecords(records);              // whole collections (IEnumerable<T>)
 41624            }
 01625            else if (inputObject is not null)
 41626            {
 01627                csv.WriteRecords([inputObject]); // lone POCO
 41628            }
 41629            else
 41630            {
 01631                csv.WriteHeader<object>();              // nothing? write only headers for an empty file
 41632            }
 41633
 41634            return sw.ToString();
 81635        }).ConfigureAwait(false);
 1636
 41637        ContentType = string.IsNullOrEmpty(contentType)
 41638            ? $"text/csv; charset={Encoding.WebName}"
 41639            : contentType;
 41640        StatusCode = statusCode;
 41641    }
 1642    /// <summary>
 1643    /// Writes a YAML response with the specified input object, status code, and content type.
 1644    /// </summary>
 1645    /// <param name="inputObject">The object to be converted to YAML.</param>
 1646    /// <param name="statusCode">The HTTP status code for the response.</param>
 1647    /// <param name="contentType">The MIME type of the response content.</param>
 11648    public void WriteYamlResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 1649
 1650    /// <summary>
 1651    /// Asynchronously writes a YAML response with the specified input object, status code, and content type.
 1652    /// </summary>
 1653    /// <param name="inputObject">The object to be converted to YAML.</param>
 1654    /// <param name="statusCode">The HTTP status code for the response.</param>
 1655    /// <param name="contentType">The MIME type of the response content.</param>
 1656    public async Task WriteYamlResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 1657    {
 31658        if (Logger.IsEnabled(LogEventLevel.Debug))
 1659        {
 31660            Logger.Debug("Writing YAML response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode
 1661        }
 1662
 61663        Body = await Task.Run(() => YamlHelper.ToYaml(inputObject));
 31664        ContentType = string.IsNullOrEmpty(contentType) ? $"application/yaml; charset={Encoding.WebName}" : contentType;
 31665        StatusCode = statusCode;
 31666    }
 1667
 1668    /// <summary>
 1669    /// Writes an XML response with the specified input object, status code, and content type.
 1670    /// </summary>
 1671    /// <param name="inputObject">The object to be converted to XML.</param>
 1672    /// <param name="statusCode">The HTTP status code for the response.</param>
 1673    /// <param name="contentType">The MIME type of the response content.</param>
 1674    /// <param name="rootElementName">Optional custom XML root element name. Defaults to <c>Response</c>.</param>
 1675    /// <param name="compress">If true, emits compact XML (no indentation); if false (default) output is human readable.
 1676    public void WriteXmlResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = nu
 61677        => WriteXmlResponseAsync(inputObject, statusCode, contentType, rootElementName, compress).GetAwaiter().GetResult
 1678
 1679    /// <summary>
 1680    /// Asynchronously writes an XML response with the specified input object, status code, and content type.
 1681    /// </summary>
 1682    /// <param name="inputObject">The object to be converted to XML.</param>
 1683    /// <param name="statusCode">The HTTP status code for the response.</param>
 1684    /// <param name="contentType">The MIME type of the response content.</param>
 1685    /// <param name="rootElementName">Optional custom XML root element name. Defaults to <c>Response</c>.</param>
 1686    /// <param name="compress">If true, emits compact XML (no indentation); if false (default) output is human readable.
 1687    public async Task WriteXmlResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? conte
 1688    {
 81689        if (Logger.IsEnabled(LogEventLevel.Debug))
 1690        {
 81691            Logger.Debug("Writing XML response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode,
 1692        }
 1693
 81694        var root = string.IsNullOrWhiteSpace(rootElementName) ? "Response" : rootElementName.Trim();
 161695        var xml = await Task.Run(() => XmlHelper.ToXml(root, inputObject));
 81696        var saveOptions = compress ? SaveOptions.DisableFormatting : SaveOptions.None;
 161697        Body = await Task.Run(() => xml.ToString(saveOptions));
 81698        ContentType = string.IsNullOrEmpty(contentType) ? $"application/xml; charset={Encoding.WebName}" : contentType;
 81699        StatusCode = statusCode;
 81700    }
 1701    /// <summary>
 1702    /// Writes a text response with the specified input object, status code, and content type.
 1703    /// </summary>
 1704    /// <param name="inputObject">The object to be converted to a text response.</param>
 1705    /// <param name="statusCode">The HTTP status code for the response.</param>
 1706    /// <param name="contentType">The MIME type of the response content.</param>
 1707    public void WriteTextResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 81708        WriteTextResponseAsync(inputObject, statusCode, contentType).GetAwaiter().GetResult();
 1709
 1710    /// <summary>
 1711    /// Asynchronously writes a text response with the specified input object, status code, and content type.
 1712    /// </summary>
 1713    /// <param name="inputObject">The object to be converted to a text response.</param>
 1714    /// <param name="statusCode">The HTTP status code for the response.</param>
 1715    /// <param name="contentType">The MIME type of the response content.</param>
 1716    public async Task WriteTextResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 1717    {
 361718        if (Logger.IsEnabled(LogEventLevel.Debug))
 1719        {
 311720            Logger.Debug("Writing text response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode
 1721        }
 1722
 361723        if (inputObject is null)
 1724        {
 01725            throw new ArgumentNullException(nameof(inputObject), "Input object cannot be null for text response.");
 1726        }
 1727
 721728        Body = await Task.Run(() => inputObject?.ToString() ?? string.Empty);
 361729        ContentType = string.IsNullOrEmpty(contentType) ? $"text/plain; charset={Encoding.WebName}" : contentType;
 361730        StatusCode = statusCode;
 361731    }
 1732
 1733    /// <summary>
 1734    /// Writes a form-urlencoded response with the specified input object, status code, and optional content type.
 1735    /// Automatically converts the input object to a Dictionary{string, string} using <see cref="ObjectToDictionaryConve
 1736    /// </summary>
 1737    /// <param name="inputObject">The object to be converted to form-urlencoded data. Can be a dictionary, enumerable, o
 1738    /// <param name="statusCode">The HTTP status code for the response. Defaults to 200 OK.</param>
 1739    public void WriteFormUrlEncodedResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) =>
 81740        WriteFormUrlEncodedResponseAsync(inputObject, statusCode).GetAwaiter().GetResult();
 1741
 1742    /// <summary>
 1743    /// Asynchronously writes a form-urlencoded response with the specified input object, status code, and optional cont
 1744    /// Automatically converts the input object to a Dictionary{string, string} using <see cref="ObjectToDictionaryConve
 1745    /// </summary>
 1746    /// <param name="inputObject">The object to be converted to form-urlencoded data. Can be a dictionary, enumerable, o
 1747    /// <param name="statusCode">The HTTP status code for the response. Defaults to 200 OK.</param>
 1748    public async Task WriteFormUrlEncodedResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK)
 1749    {
 111750        if (inputObject is null)
 1751        {
 21752            throw new ArgumentNullException(nameof(inputObject), "Input object cannot be null for form-urlencoded respon
 1753        }
 1754
 91755        var dictionary = ObjectToDictionaryConverter.ToDictionary(inputObject);
 91756        var formContent = new FormUrlEncodedContent(dictionary);
 91757        var encodedString = await formContent.ReadAsStringAsync();
 1758
 91759        await WriteTextResponseAsync(encodedString, statusCode, "application/x-www-form-urlencoded");
 91760    }
 1761
 1762    /// <summary>
 1763    /// Writes an HTTP redirect response with the specified URL and optional message.
 1764    /// </summary>
 1765    /// <param name="url">The URL to redirect to.</param>
 1766    /// <param name="message">An optional message to include in the response body.</param>
 1767    public void WriteRedirectResponse(string url, string? message = null)
 1768    {
 61769        if (Logger.IsEnabled(LogEventLevel.Debug))
 1770        {
 51771            Logger.Debug("Writing redirect response, StatusCode={StatusCode}, Location={Location}", StatusCode, url);
 1772        }
 1773
 61774        if (string.IsNullOrEmpty(url))
 1775        {
 01776            throw new ArgumentNullException(nameof(url), "URL cannot be null for redirect response.");
 1777        }
 1778        // framework hook
 61779        RedirectUrl = url;
 1780
 1781        // HTTP status + Location header
 61782        StatusCode = StatusCodes.Status302Found;
 61783        Headers["Location"] = url;
 1784
 61785        if (message is not null)
 1786        {
 1787            // include a body
 11788            Body = message;
 11789            ContentType = $"text/plain; charset={Encoding.WebName}";
 1790        }
 1791        else
 1792        {
 1793            // no body: clear any existing body/headers
 51794            Body = null;
 1795            //ContentType = null;
 51796            _ = Headers.Remove("Content-Length");
 1797        }
 51798    }
 1799
 1800    /// <summary>
 1801    /// Writes a binary response with the specified data, status code, and content type.
 1802    /// </summary>
 1803    /// <param name="data">The binary data to send in the response.</param>
 1804    /// <param name="statusCode">The HTTP status code for the response.</param>
 1805    /// <param name="contentType">The MIME type of the response content.</param>
 1806    public void WriteBinaryResponse(byte[] data, int statusCode = StatusCodes.Status200OK, string contentType = "applica
 1807    {
 11808        if (Logger.IsEnabled(LogEventLevel.Debug))
 1809        {
 11810            Logger.Debug("Writing binary response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, cont
 1811        }
 1812
 11813        Body = data ?? throw new ArgumentNullException(nameof(data), "Data cannot be null for binary response.");
 11814        ContentType = contentType;
 11815        StatusCode = statusCode;
 11816    }
 1817    /// <summary>
 1818    /// Writes a stream response with the specified stream, status code, and content type.
 1819    /// </summary>
 1820    /// <param name="stream">The stream to send in the response.</param>
 1821    /// <param name="statusCode">The HTTP status code for the response.</param>
 1822    /// <param name="contentType">The MIME type of the response content.</param>
 1823    public void WriteStreamResponse(Stream stream, int statusCode = StatusCodes.Status200OK, string contentType = "appli
 1824    {
 31825        if (Logger.IsEnabled(LogEventLevel.Debug))
 1826        {
 31827            Logger.Debug("Writing stream response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, cont
 1828        }
 1829
 31830        Body = stream;
 31831        ContentType = contentType;
 31832        StatusCode = statusCode;
 31833    }
 1834    #endregion
 1835
 1836    #region Error Responses
 1837    /// <summary>
 1838    /// Structured payload for error responses.
 1839    /// </summary>
 1840    internal record ErrorPayload
 1841    {
 341842        public string Error { get; init; } = default!;
 351843        public string? Details { get; init; }
 371844        public string? Exception { get; init; }
 361845        public string? StackTrace { get; init; }
 681846        public int Status { get; init; }
 341847        public string Reason { get; init; } = default!;
 341848        public string Timestamp { get; init; } = default!;
 271849        public string? Path { get; init; }
 271850        public string? Method { get; init; }
 1851    }
 1852
 1853    /// <summary>
 1854    /// Write an error response with a custom message.
 1855    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 1856    /// </summary>
 1857    public async Task WriteErrorResponseAsync(
 1858        string message,
 1859        int statusCode = StatusCodes.Status500InternalServerError,
 1860        string? contentType = null,
 1861        string? details = null)
 1862    {
 141863        if (Logger.IsEnabled(LogEventLevel.Debug))
 1864        {
 141865            Logger.Debug("Writing error response, StatusCode={StatusCode}, ContentType={ContentType}, Message={Message}"
 141866                statusCode, contentType, message);
 1867        }
 1868
 141869        if (string.IsNullOrWhiteSpace(message))
 1870        {
 01871            throw new ArgumentNullException(nameof(message));
 1872        }
 1873
 141874        Logger.Warning("Writing error response with status {StatusCode}: {Message}", statusCode, message);
 1875
 141876        var payload = new ErrorPayload
 141877        {
 141878            Error = message,
 141879            Details = details,
 141880            Exception = null,
 141881            StackTrace = null,
 141882            Status = statusCode,
 141883            Reason = ReasonPhrases.GetReasonPhrase(statusCode),
 141884            Timestamp = DateTime.UtcNow.ToString("o"),
 141885            Path = Request?.Path,
 141886            Method = Request?.Method
 141887        };
 1888
 141889        await WriteFormattedErrorResponseAsync(payload, contentType);
 141890    }
 1891
 1892    /// <summary>
 1893    /// Writes an error response with a custom message.
 1894    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 1895    /// </summary>
 1896    /// <param name="message">The error message to include in the response.</param>
 1897    /// <param name="statusCode">The HTTP status code for the response.</param>
 1898    /// <param name="contentType">The MIME type of the response content.</param>
 1899    /// <param name="details">Optional details to include in the response.</param>
 1900    public void WriteErrorResponse(
 1901      string message,
 1902      int statusCode = StatusCodes.Status500InternalServerError,
 1903      string? contentType = null,
 11904      string? details = null) => WriteErrorResponseAsync(message, statusCode, contentType, details).GetAwaiter().GetResu
 1905
 1906    /// <summary>
 1907    /// Asynchronously writes an error response based on an exception.
 1908    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 1909    /// </summary>
 1910    /// <param name="ex">The exception to report.</param>
 1911    /// <param name="statusCode">The HTTP status code for the response.</param>
 1912    /// <param name="contentType">The MIME type of the response content.</param>
 1913    /// <param name="includeStack">Whether to include the stack trace in the response.</param>
 1914    public async Task WriteErrorResponseAsync(
 1915        Exception ex,
 1916        int statusCode = StatusCodes.Status500InternalServerError,
 1917        string? contentType = null,
 1918        bool includeStack = true)
 1919    {
 31920        if (Logger.IsEnabled(LogEventLevel.Debug))
 1921        {
 31922            Logger.Debug("Writing error response from exception, StatusCode={StatusCode}, ContentType={ContentType}, Inc
 31923                statusCode, contentType, includeStack);
 1924        }
 1925
 31926        ArgumentNullException.ThrowIfNull(ex);
 1927
 31928        Logger.Warning(ex, "Writing error response with status {StatusCode}", statusCode);
 1929
 31930        var payload = new ErrorPayload
 31931        {
 31932            Error = ex.Message,
 31933            Details = null,
 31934            Exception = ex.GetType().Name,
 31935            StackTrace = includeStack ? ex.ToString() : null,
 31936            Status = statusCode,
 31937            Reason = ReasonPhrases.GetReasonPhrase(statusCode),
 31938            Timestamp = DateTime.UtcNow.ToString("o"),
 31939            Path = Request?.Path,
 31940            Method = Request?.Method
 31941        };
 1942
 31943        await WriteFormattedErrorResponseAsync(payload, contentType);
 31944    }
 1945    /// <summary>
 1946    /// Writes an error response based on an exception.
 1947    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 1948    /// </summary>
 1949    /// <param name="ex">The exception to report.</param>
 1950    /// <param name="statusCode">The HTTP status code for the response.</param>
 1951    /// <param name="contentType">The MIME type of the response content.</param>
 1952    /// <param name="includeStack">Whether to include the stack trace in the response.</param>
 1953    public void WriteErrorResponse(
 1954            Exception ex,
 1955            int statusCode = StatusCodes.Status500InternalServerError,
 1956            string? contentType = null,
 11957            bool includeStack = true) => WriteErrorResponseAsync(ex, statusCode, contentType, includeStack).GetAwaiter()
 1958
 1959    /// <summary>
 1960    /// Internal dispatcher: serializes the payload according to the chosen content-type.
 1961    /// </summary>
 1962    private async Task WriteFormattedErrorResponseAsync(ErrorPayload payload, string? contentType = null)
 1963    {
 171964        if (Logger.IsEnabled(LogEventLevel.Debug))
 1965        {
 171966            Logger.Debug("Writing formatted error response, ContentType={ContentType}, Status={Status}", contentType, pa
 1967        }
 1968
 171969        if (string.IsNullOrWhiteSpace(contentType))
 1970        {
 151971            _ = Request.Headers.TryGetValue("Accept", out var acceptHeader);
 151972            contentType = (acceptHeader ?? "text/plain")
 151973                                 .ToLowerInvariant();
 1974        }
 171975        if (contentType.Contains("json"))
 1976        {
 61977            await WriteJsonResponseAsync(payload, payload.Status);
 1978        }
 111979        else if (contentType.Contains("yaml") || contentType.Contains("yml"))
 1980        {
 21981            await WriteYamlResponseAsync(payload, payload.Status);
 1982        }
 91983        else if (contentType.Contains("xml"))
 1984        {
 21985            await WriteXmlResponseAsync(payload, payload.Status);
 1986        }
 1987        else
 1988        {
 1989            // Plain-text fallback
 71990            var lines = new List<string>
 71991                {
 71992                    $"Status: {payload.Status} ({payload.Reason})",
 71993                    $"Error: {payload.Error}",
 71994                    $"Time: {payload.Timestamp}"
 71995                };
 1996
 71997            if (!string.IsNullOrWhiteSpace(payload.Details))
 1998            {
 11999                lines.Add("Details:\n" + payload.Details);
 2000            }
 2001
 72002            if (!string.IsNullOrWhiteSpace(payload.Exception))
 2003            {
 32004                lines.Add($"Exception: {payload.Exception}");
 2005            }
 2006
 72007            if (!string.IsNullOrWhiteSpace(payload.StackTrace))
 2008            {
 22009                lines.Add("StackTrace:\n" + payload.StackTrace);
 2010            }
 2011
 72012            var text = string.Join("\n", lines);
 72013            await WriteTextResponseAsync(text, payload.Status, "text/plain");
 2014        }
 172015    }
 2016
 2017    #endregion
 2018    #region HTML Response Helpers
 2019
 2020    /// <summary>
 2021    /// Renders a template string by replacing placeholders in the format {{key}} with corresponding values from the pro
 2022    /// </summary>
 2023    /// <param name="template">The template string containing placeholders.</param>
 2024    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 2025    /// <returns>The rendered string with placeholders replaced by variable values.</returns>
 2026    private string RenderInlineTemplate(
 2027     string template,
 2028     IReadOnlyDictionary<string, object?> vars)
 2029    {
 22030        if (Logger.IsEnabled(LogEventLevel.Debug))
 2031        {
 22032            Logger.Debug("Rendering inline template, TemplateLength={TemplateLength}, VarsCount={VarsCount}",
 22033                      template?.Length ?? 0, vars?.Count ?? 0);
 2034        }
 2035
 22036        if (string.IsNullOrEmpty(template))
 2037        {
 02038            return string.Empty;
 2039        }
 2040
 22041        if (vars is null || vars.Count == 0)
 2042        {
 02043            return template;
 2044        }
 2045
 22046        var render = RenderInline(template, vars);
 2047
 22048        if (Logger.IsEnabled(LogEventLevel.Debug))
 2049        {
 22050            Logger.Debug("Rendered template length: {RenderedLength}", render.Length);
 2051        }
 2052
 22053        return render;
 2054    }
 2055
 2056    /// <summary>
 2057    /// Renders a template string by replacing placeholders in the format {{key}} with corresponding values from the pro
 2058    /// </summary>
 2059    /// <param name="template">The template string containing placeholders.</param>
 2060    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 2061    /// <returns>The rendered string with placeholders replaced by variable values.</returns>
 2062    private static string RenderInline(string template, IReadOnlyDictionary<string, object?> vars)
 2063    {
 22064        var sb = new StringBuilder(template.Length);
 2065
 2066        // Iterate through the template
 22067        var i = 0;
 392068        while (i < template.Length)
 2069        {
 2070            // opening “{{”
 372071            if (template[i] == '{' && i + 1 < template.Length && template[i + 1] == '{')
 2072            {
 32073                var start = i + 2;                                        // after “{{”
 32074                var end = template.IndexOf("}}", start, StringComparison.Ordinal);
 2075
 32076                if (end > start)                                          // found closing “}}”
 2077                {
 32078                    var rawKey = template[start..end].Trim();
 2079
 32080                    if (TryResolveValue(rawKey, vars, out var value) && value is not null)
 2081                    {
 32082                        _ = sb.Append(value); // append resolved value
 2083                    }
 2084                    else
 2085                    {
 02086                        _ = sb.Append("{{").Append(rawKey).Append("}}");      // leave it as-is if unknown
 2087                    }
 2088
 32089                    i = end + 2;    // jump past the “}}”
 32090                    continue;
 2091                }
 2092            }
 2093
 2094            // ordinary character
 342095            _ = sb.Append(template[i]);
 342096            i++; // move to the next character
 2097        }
 22098        return sb.ToString();
 2099    }
 2100
 2101    /// <summary>
 2102    /// Resolves a dotted path like “Request.Path” through nested dictionaries
 2103    /// and/or object properties (case-insensitive).
 2104    /// </summary>
 2105    private static bool TryResolveValue(
 2106        string path,
 2107        IReadOnlyDictionary<string, object?> root,
 2108        out object? value)
 2109    {
 32110        value = null;
 2111
 32112        if (string.IsNullOrWhiteSpace(path))
 2113        {
 02114            return false;
 2115        }
 2116
 32117        object? current = root;
 162118        foreach (var segment in path.Split('.'))
 2119        {
 52120            if (current is null)
 2121            {
 02122                return false;
 2123            }
 2124
 2125            // ① Handle dictionary look-ups (IReadOnlyDictionary or IDictionary)
 52126            if (current is IReadOnlyDictionary<string, object?> roDict)
 2127            {
 32128                if (!roDict.TryGetValue(segment, out current))
 2129                {
 02130                    return false;
 2131                }
 2132
 2133                continue;
 2134            }
 2135
 22136            if (current is IDictionary dict)
 2137            {
 02138                if (!dict.Contains(segment))
 2139                {
 02140                    return false;
 2141                }
 2142
 02143                current = dict[segment];
 02144                continue;
 2145            }
 2146
 2147            // ② Handle property look-ups via reflection
 22148            var prop = current.GetType().GetProperty(
 22149                segment,
 22150                BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
 2151
 22152            if (prop is null)
 2153            {
 02154                return false;
 2155            }
 2156
 22157            current = prop.GetValue(current);
 2158        }
 2159
 32160        value = current;
 32161        return true;
 2162    }
 2163
 2164    /// <summary>
 2165    /// Attempts to revalidate the cache based on ETag and Last-Modified headers.
 2166    /// If the resource is unchanged, sets the response status to 304 Not Modified.
 2167    /// Returns true if a 304 response was written, false otherwise.
 2168    /// </summary>
 2169    /// <param name="payload">The payload to validate.</param>
 2170    /// <param name="etag">The ETag header value.</param>
 2171    /// <param name="weakETag">Indicates if the ETag is a weak ETag.</param>
 2172    /// <param name="lastModified">The Last-Modified header value.</param>
 2173    /// <returns>True if a 304 response was written, false otherwise.</returns>
 2174    public bool RevalidateCache(object? payload,
 2175       string? etag = null,
 2176       bool weakETag = false,
 02177       DateTimeOffset? lastModified = null) => CacheRevalidation.TryWrite304(Context, payload, etag, weakETag, lastModif
 2178
 2179    /// <summary>
 2180    /// Asynchronously writes an HTML response, rendering the provided template string and replacing placeholders with v
 2181    /// </summary>
 2182    /// <param name="template">The HTML template string containing placeholders.</param>
 2183    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 2184    /// <param name="statusCode">The HTTP status code for the response.</param>
 2185    public async Task WriteHtmlResponseAsync(
 2186        string template,
 2187        IReadOnlyDictionary<string, object?>? vars,
 2188        int statusCode = 200)
 2189    {
 22190        if (Logger.IsEnabled(LogEventLevel.Debug))
 2191        {
 22192            Logger.Debug("Writing HTML response (async), StatusCode={StatusCode}, TemplateLength={TemplateLength}", stat
 2193        }
 2194
 22195        if (vars is null || vars.Count == 0)
 2196        {
 02197            await WriteTextResponseAsync(template, statusCode, "text/html");
 2198        }
 2199        else
 2200        {
 22201            await WriteTextResponseAsync(RenderInlineTemplate(template, vars), statusCode, "text/html");
 2202        }
 22203    }
 2204
 2205    /// <summary>
 2206    /// Asynchronously writes an HTML response, rendering the provided template byte array and replacing placeholders wi
 2207    /// </summary>
 2208    /// <param name="template">The HTML template byte array.</param>
 2209    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 2210    /// <param name="statusCode">The HTTP status code for the response.</param>
 2211    /// <returns>A task representing the asynchronous operation.</returns>
 2212    public async Task WriteHtmlResponseAsync(
 2213    byte[] template,
 2214    IReadOnlyDictionary<string, object?>? vars,
 02215    int statusCode = 200) => await WriteHtmlResponseAsync(Encoding.GetString(template), vars, statusCode);
 2216
 2217    /// <summary>
 2218    /// Writes an HTML response, rendering the provided template byte array and replacing placeholders with values from 
 2219    /// </summary>
 2220    /// <param name="template">The HTML template byte array.</param>
 2221    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 2222    /// <param name="statusCode">The HTTP status code for the response.</param>
 2223    public void WriteHtmlResponse(
 2224         byte[] template,
 2225         IReadOnlyDictionary<string, object?>? vars,
 02226         int statusCode = 200) => WriteHtmlResponseAsync(Encoding.GetString(template), vars, statusCode).GetAwaiter().Ge
 2227
 2228    /// <summary>
 2229    /// Asynchronously reads an HTML file, merges in placeholders from the provided dictionary, and writes the result as
 2230    /// </summary>
 2231    /// <param name="filePath">The path to the HTML file to read.</param>
 2232    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 2233    /// <param name="statusCode">The HTTP status code for the response.</param>
 2234    public async Task WriteHtmlResponseFromFileAsync(
 2235        string filePath,
 2236        IReadOnlyDictionary<string, object?> vars,
 2237        int statusCode = 200)
 2238    {
 12239        if (Logger.IsEnabled(LogEventLevel.Debug))
 2240        {
 12241            Logger.Debug("Writing HTML response from file (async), FilePath={FilePath}, StatusCode={StatusCode}", filePa
 2242        }
 2243
 12244        if (!File.Exists(filePath))
 2245        {
 02246            WriteTextResponse($"<!-- File not found: {filePath} -->", 404, "text/html");
 02247            return;
 2248        }
 2249
 12250        var template = await File.ReadAllTextAsync(filePath);
 12251        WriteHtmlResponseAsync(template, vars, statusCode).GetAwaiter().GetResult();
 12252    }
 2253
 2254    /// <summary>
 2255    /// Renders the given HTML string with placeholders and writes it as a response.
 2256    /// </summary>
 2257    /// <param name="template">The HTML template string containing placeholders.</param>
 2258    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 2259    /// <param name="statusCode">The HTTP status code for the response.</param>
 2260    public void WriteHtmlResponse(
 2261        string template,
 2262        IReadOnlyDictionary<string, object?>? vars,
 02263        int statusCode = 200) => WriteHtmlResponseAsync(template, vars, statusCode).GetAwaiter().GetResult();
 2264
 2265    /// <summary>
 2266    /// Reads an .html file, merges in placeholders, and writes it.
 2267    /// </summary>
 2268    public void WriteHtmlResponseFromFile(
 2269        string filePath,
 2270        IReadOnlyDictionary<string, object?> vars,
 02271        int statusCode = 200) => WriteHtmlResponseFromFileAsync(filePath, vars, statusCode).GetAwaiter().GetResult();
 2272
 2273    /// <summary>
 2274    /// Writes only the specified HTTP status code, clearing any body or content type.
 2275    /// </summary>
 2276    /// <param name="statusCode">The HTTP status code to write.</param>
 2277    public void WriteStatusOnly(int statusCode)
 2278    {
 2279        // Clear any body indicators so StatusCodePages can run
 12280        ContentType = null;
 12281        StatusCode = statusCode;
 12282        Body = null;
 12283    }
 2284    #endregion
 2285
 2286    #region Apply to HttpResponse
 2287
 2288    /// <summary>
 2289    /// Applies the current KestrunResponse to the specified HttpResponse, setting status, headers, cookies, and writing
 2290    /// </summary>
 2291    /// <param name="response">The HttpResponse to apply the response to.</param>
 2292    /// <returns>A task representing the asynchronous operation.</returns>
 2293    public async Task ApplyTo(HttpResponse response)
 2294    {
 362295        if (Logger.IsEnabled(LogEventLevel.Debug))
 2296        {
 242297            Logger.Debug("Applying KestrunResponse to HttpResponse, StatusCode={StatusCode}, ContentType={ContentType}, 
 242298             StatusCode, ContentType, Body?.GetType().Name ?? "null");
 2299        }
 2300
 362301        if (response.StatusCode == StatusCodes.Status304NotModified)
 2302        {
 02303            if (Logger.IsEnabled(LogEventLevel.Debug))
 2304            {
 02305                Logger.Debug("Response already has status code 304 Not Modified, skipping ApplyTo");
 2306            }
 02307            return;
 2308        }
 362309        if (response.HasStarted)
 2310        {
 02311            if (Logger.IsEnabled(LogEventLevel.Debug))
 2312            {
 02313                Logger.Debug("HttpResponse has already started, skipping KestrunResponse.ApplyTo().");
 2314            }
 02315            return;
 2316        }
 362317        if (!string.IsNullOrEmpty(RedirectUrl))
 2318        {
 32319            response.Redirect(RedirectUrl);
 32320            return;
 2321        }
 2322
 2323        try
 2324        {
 332325            EnsureStatus(response);
 2326            // Apply headers, cookies, caching
 332327            ApplyHeadersAndCookies(response);
 2328            // Caching
 332329            ApplyCachingHeaders(response);
 2330            // Callbacks
 332331            await TryEnqueueCallbacks();
 2332            // Body
 332333            await WriteResponseContent(response);
 332334        }
 02335        catch (Exception ex)
 2336        {
 02337            Logger.Error(ex, "Error applying KestrunResponse to HttpResponse");
 2338            // Optionally, you can log the exception or handle it as needed
 02339            throw;
 2340        }
 362341    }
 2342
 2343    /// <summary>
 2344    /// Applies the body content to the HTTP response.  If the body is not null, it ensures the content type,
 2345    /// applies the content disposition header, and writes the body asynchronously.  If the body is null,
 2346    /// it clears the content type and content length if the response has not started.  Logs debug information about the
 2347    /// </summary>
 2348    /// <param name="response"> The HTTP response to which the body content will be applied. </param>
 2349    /// <returns> A task representing the asynchronous operation. </returns>
 2350    private async Task WriteResponseContent(HttpResponse response)
 2351    {
 332352        if (Body is not null)
 2353        {
 282354            EnsureContentType(response);
 282355            ApplyContentDispositionHeader(response);
 282356            await WriteBodyAsync(response).ConfigureAwait(false);
 2357        }
 2358        else
 2359        {
 52360            if (!response.HasStarted && string.IsNullOrEmpty(response.ContentType))
 2361            {
 32362                response.ContentType = null;
 2363            }
 2364
 52365            if (!response.HasStarted)
 2366            {
 52367                response.ContentLength = null;
 2368            }
 52369            if (Logger.IsEnabled(LogEventLevel.Debug))
 2370            {
 42371                Logger.Debug("Status-only: HasStarted={HasStarted} CL={CL} CT='{CT}'",
 42372                 response.HasStarted, response.ContentLength, response.ContentType);
 2373            }
 2374        }
 332375    }
 2376
 2377    /// <summary>
 2378    /// Attempts to enqueue any registered callback requests using the ICallbackDispatcher service.
 2379    /// </summary>
 2380    private async ValueTask TryEnqueueCallbacks()
 2381    {
 332382        if (CallbackPlan.Count == 0)
 2383        {
 322384            return;
 2385        }
 2386
 2387        // Prevent multiple enqueues
 12388        if (Interlocked.Exchange(ref CallbacksEnqueuedFlag, 1) == 1)
 2389        {
 02390            return;
 2391        }
 2392
 12393        if (Logger.IsEnabled(LogEventLevel.Information))
 2394        {
 12395            Logger.Information("Enqueuing {Count} callbacks for dispatch.", CallbackPlan.Count);
 2396        }
 2397
 2398        try
 2399        {
 12400            var httpCtx = KrContext.HttpContext;
 2401
 2402            // Resolve DI services while request is alive
 12403            var dispatcher = httpCtx.RequestServices.GetService<ICallbackDispatcher>();
 12404            if (dispatcher is null)
 2405            {
 12406                Logger.Warning("Callbacks present but no ICallbackDispatcher registered. Count={Count}", CallbackPlan.Co
 12407                return;
 2408            }
 2409
 02410            var urlResolver = httpCtx.RequestServices.GetRequiredService<ICallbackUrlResolver>();
 02411            var serializer = httpCtx.RequestServices.GetRequiredService<ICallbackBodySerializer>();
 02412            var options = httpCtx.RequestServices.GetService<CallbackDispatchOptions>() ?? new CallbackDispatchOptions()
 2413
 02414            foreach (var plan in CallbackPlan)
 2415            {
 2416                try
 2417                {
 02418                    var req = CallbackRequestFactory.FromPlan(plan, KrContext, urlResolver, serializer, options);
 2419
 02420                    if (Logger.IsEnabled(LogEventLevel.Debug))
 2421                    {
 02422                        Logger.Debug("Enqueue callback. CallbackId={CallbackId} Url={Url}", req.CallbackId, req.TargetUr
 2423                    }
 2424
 02425                    await dispatcher.EnqueueAsync(req, CancellationToken.None).ConfigureAwait(false);
 02426                }
 02427                catch (Exception ex)
 2428                {
 02429                    Logger.Error(ex, "Failed to enqueue callback. CallbackId={CallbackId}", plan.CallbackId);
 02430                }
 02431            }
 02432        }
 02433        catch (Exception ex)
 2434        {
 02435            Logger.Error(ex, "Unexpected error while scheduling callbacks.");
 02436        }
 332437    }
 2438
 2439    /// <summary>
 2440    /// Ensures the HTTP response has the correct status code and content type.
 2441    /// </summary>
 2442    /// <param name="response">The HTTP response to apply the status and content type to.</param>
 2443    private void EnsureContentType(HttpResponse response)
 2444    {
 282445        if (ContentType != response.ContentType)
 2446        {
 282447            if (!string.IsNullOrEmpty(ContentType) &&
 282448                IsTextBasedContentType(ContentType) &&
 282449                !ContentType.Contains("charset=", StringComparison.OrdinalIgnoreCase))
 2450            {
 62451                ContentType = ContentType.TrimEnd(';') + $"; charset={AcceptCharset.WebName}";
 2452            }
 282453            response.ContentType = ContentType;
 2454        }
 282455    }
 2456
 2457    /// <summary>
 2458    /// Ensures the HTTP response has the correct status code.
 2459    /// </summary>
 2460    /// <param name="response">The HTTP response to apply the status code to.</param>
 2461    private void EnsureStatus(HttpResponse response)
 2462    {
 332463        if (StatusCode != response.StatusCode)
 2464        {
 52465            response.StatusCode = StatusCode;
 2466        }
 332467    }
 2468
 2469    /// <summary>
 2470    /// Adds caching headers to the response based on the provided CacheControlHeaderValue options.
 2471    /// </summary>
 2472    /// <param name="response">The HTTP response to apply caching headers to.</param>
 2473    /// <exception cref="ArgumentNullException">Thrown when options is null.</exception>
 2474    public void ApplyCachingHeaders(HttpResponse response)
 2475    {
 332476        if (CacheControl is not null)
 2477        {
 12478            response.Headers.CacheControl = CacheControl.ToString();
 2479        }
 332480    }
 2481
 2482    /// <summary>
 2483    /// Applies the Content-Disposition header to the HTTP response.
 2484    /// </summary>
 2485    /// <param name="response">The HTTP response to apply the header to.</param>
 2486    private void ApplyContentDispositionHeader(HttpResponse response)
 2487    {
 282488        if (ContentDisposition.Type == ContentDispositionType.NoContentDisposition)
 2489        {
 262490            return;
 2491        }
 2492
 22493        if (Logger.IsEnabled(LogEventLevel.Debug))
 2494        {
 22495            Logger.Debug("Setting Content-Disposition header, Type={Type}, FileName={FileName}",
 22496                      ContentDisposition.Type, ContentDisposition.FileName);
 2497        }
 2498
 22499        var dispositionValue = ContentDisposition.Type switch
 22500        {
 22501            ContentDispositionType.Attachment => "attachment",
 02502            ContentDispositionType.Inline => "inline",
 02503            _ => throw new InvalidOperationException("Invalid Content-Disposition type")
 22504        };
 2505
 22506        if (string.IsNullOrEmpty(ContentDisposition.FileName) && Body is IFileInfo fi)
 2507        {
 2508            // default filename: use the file's name
 12509            ContentDisposition.FileName = fi.Name;
 2510        }
 2511
 22512        if (!string.IsNullOrEmpty(ContentDisposition.FileName))
 2513        {
 22514            var escapedFileName = WebUtility.UrlEncode(ContentDisposition.FileName);
 22515            dispositionValue += $"; filename=\"{escapedFileName}\"";
 2516        }
 2517
 22518        response.Headers.Append("Content-Disposition", dispositionValue);
 22519    }
 2520
 2521    /// <summary>
 2522    /// Applies headers and cookies to the HTTP response.
 2523    /// </summary>
 2524    /// <param name="response">The HTTP response to apply the headers and cookies to.</param>
 2525    private void ApplyHeadersAndCookies(HttpResponse response)
 2526    {
 332527        if (Headers is not null)
 2528        {
 662529            foreach (var kv in Headers)
 2530            {
 02531                response.Headers[kv.Key] = kv.Value;
 2532            }
 2533        }
 332534        if (Cookies is not null)
 2535        {
 62536            foreach (var cookie in Cookies)
 2537            {
 22538                response.Headers.Append("Set-Cookie", cookie);
 2539            }
 2540        }
 332541    }
 2542
 2543    /// <summary>
 2544    /// Writes the response body to the HTTP response.
 2545    /// </summary>
 2546    /// <param name="response">The HTTP response to write to.</param>
 2547    /// <returns>A task representing the asynchronous operation.</returns>
 2548    private async Task WriteBodyAsync(HttpResponse response)
 2549    {
 282550        var bodyValue = Body; // capture to avoid nullability warnings when mutated in default
 2551        switch (bodyValue)
 2552        {
 2553            case IFileInfo fileInfo:
 12554                if (Logger.IsEnabled(LogEventLevel.Debug))
 2555                {
 12556                    Logger.Debug("Sending file {FileName} (Length={Length})", fileInfo.Name, fileInfo.Length);
 2557                }
 12558                response.ContentLength = fileInfo.Length;
 12559                response.Headers.LastModified = fileInfo.LastModified.ToString("R");
 12560                await response.SendFileAsync(
 12561                    file: fileInfo,
 12562                    offset: 0,
 12563                    count: fileInfo.Length,
 12564                    cancellationToken: response.HttpContext.RequestAborted
 12565                );
 12566                break;
 2567
 2568            case byte[] bytes:
 12569                response.ContentLength = bytes.LongLength;
 12570                await response.Body.WriteAsync(bytes, response.HttpContext.RequestAborted);
 12571                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 12572                break;
 2573
 2574            case Stream stream:
 22575                var seekable = stream.CanSeek;
 22576                if (Logger.IsEnabled(LogEventLevel.Debug))
 2577                {
 22578                    Logger.Debug("Sending stream (seekable={Seekable}, len={Len})",
 22579                          seekable, seekable ? stream.Length : -1);
 2580                }
 22581                if (seekable)
 2582                {
 12583                    response.ContentLength = stream.Length;
 12584                    stream.Position = 0;
 2585                }
 2586                else
 2587                {
 12588                    response.ContentLength = null;
 2589                }
 2590
 2591                const int BufferSize = 64 * 1024; // 64 KB
 22592                var buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
 2593                try
 2594                {
 2595                    int bytesRead;
 42596                    while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, BufferSize), response.HttpContext.Requ
 2597                    {
 22598                        await response.Body.WriteAsync(buffer.AsMemory(0, bytesRead), response.HttpContext.RequestAborte
 2599                    }
 22600                }
 2601                finally
 2602                {
 22603                    ArrayPool<byte>.Shared.Return(buffer);
 2604                }
 22605                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 22606                break;
 2607
 2608            case string str:
 242609                var data = AcceptCharset.GetBytes(str);
 242610                response.ContentLength = data.Length;
 242611                await response.Body.WriteAsync(data, response.HttpContext.RequestAborted);
 242612                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 242613                break;
 2614
 2615            default:
 02616                var bodyType = bodyValue?.GetType().Name ?? "null";
 02617                Body = "Unsupported body type: " + bodyType;
 02618                Logger.Warning("Unsupported body type: {BodyType}", bodyType);
 02619                response.StatusCode = StatusCodes.Status500InternalServerError;
 02620                response.ContentType = "text/plain; charset=utf-8";
 02621                response.ContentLength = Body.ToString()?.Length ?? null;
 2622                break;
 2623        }
 282624    }
 2625    #endregion
 2626}

Methods/Properties

.cctor()
get_CallbackPlan()
get_Logger()
get_MapRouteOptions()
get_KrContext()
get_Host()
.ctor(Kestrun.Models.KestrunContext,System.Int32)
get_Context()
get_StatusCode()
get_Headers()
get_ContentType()
get_Body()
get_RedirectUrl()
get_Cookies()
get_Encoding()
get_ContentDisposition()
get_Request()
get_AcceptCharset()
get_BodyAsyncThreshold()
get_CacheControl()
get_Value()
get_PostPonedWriteObject()
get_HasPostPonedWriteObject()
GetSafeCurrentDirectoryOrBaseDirectory()
GetSafeCurrentDirectoryForLogging()
GetHeader(System.String)
IsTextBasedContentType(System.String)
AddCallbackParameters(System.String,System.String,System.Collections.Generic.Dictionary`2<System.String,System.Object>)
WriteFileResponse(System.String,System.String,System.Int32)
WriteJsonResponse(System.Object,System.Int32)
WriteJsonResponseAsync()
WriteJsonResponse(System.Object,System.Text.Json.JsonSerializerOptions,System.Int32,System.String)
WriteJsonResponseAsync()
WriteJsonResponse(System.Object,System.Int32,System.Boolean,System.Int32,System.String)
WriteJsonResponseAsync()
WriteCborResponseAsync()
WriteCborResponse(System.Object,System.Int32,System.String)
WriteBsonResponseAsync()
WriteBsonResponse(System.Object,System.Int32,System.String)
WriteResponse(System.Object,System.Int32)
WriteResponseAsync(Kestrun.Models.KestrunResponse/WriteObject)
WriteResponseAsync()
WriteLegacyNegotiatedResponseAsync()
WriteOpenApiNegotiatedResponseAsync()
ShouldEnforceOpenApiResponseContentTypes()
QueueResponseForWrite(System.Object,System.Int32)
SelectResponseMediaType(System.String,System.Collections.Generic.IReadOnlyList`1<Kestrun.Hosting.Options.ContentTypeWithSchema>,System.String)
SelectWhenAnyMediaTypeSupported(System.String,System.String)
SelectFromConfiguredSupportedMediaTypes(System.String,System.Collections.Generic.IReadOnlyList`1<Kestrun.Hosting.Options.ContentTypeWithSchema>,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Collections.Generic.IReadOnlyList`1<System.String>)
ResolveWriterMediaType(System.String,System.String)
TryGetResponseContentTypes(System.Collections.Generic.IDictionary`2<System.String,System.Collections.Generic.ICollection`1<Kestrun.Hosting.Options.ContentTypeWithSchema>>,System.Int32,System.Collections.Generic.ICollection`1<Kestrun.Hosting.Options.ContentTypeWithSchema>&)
TryGetResponseSchemaTypeForStatus(System.Int32,System.Type&,System.String&)
ResolveSchemaType(System.String)
CollectSchemaTypeCandidatesFromAssembly(System.Reflection.Assembly,System.String,System.Collections.Generic.List`1<System.Type>)
IsMatchingSchemaTypeName(System.Type,System.String)
SelectPreferredSchemaType(System.Collections.Generic.IReadOnlyList`1<System.Type>)
ConvertSchemaValue(System.Object,System.Type)
ConvertSchemaArrayValue(System.Object,System.Type)
ConvertEnumerableToTypedArray(System.Collections.IEnumerable,System.Type)
ConvertSingleValueToTypedArray(System.Object,System.Type)
EnsureArrayElementAssignable(System.Object,System.Type,System.Boolean)
TryConvertSchemaDictionaryValue(System.Object,System.Type,System.Object&)
TryConvertViaSingleArgumentConstructor(System.Object,System.Type,System.Object&)
UnwrapPowerShellValue(System.Object)
IsPowerShellAutomationNull(System.Object)
TryConvertDictionaryToType(System.Collections.IDictionary,System.Type)
TryConvertPowerShellObjectToType(System.Object,System.Type,System.Object&)
FindDictionaryKey(System.Collections.IDictionary,System.String)
ValidateRequiredProperties(System.Object,System.String&)
FormatMissingRequiredProperties(System.Object)
IsMapLikeType(System.Type)
TryConvertSimple(System.Object,System.Type,System.Object&)
TryGetValueIgnoreCase(System.Collections.Generic.IDictionary`2<System.String,T>,System.String,T&)
WriteByMediaTypeAsync(System.String,System.Object,System.Int32)
WriteCsvResponse(System.Object,System.Int32,System.String,CsvHelper.Configuration.CsvConfiguration)
WriteCsvResponseAsync()
WriteYamlResponse(System.Object,System.Int32,System.String)
WriteYamlResponseAsync()
WriteXmlResponse(System.Object,System.Int32,System.String,System.String,System.Boolean)
WriteXmlResponseAsync()
WriteTextResponse(System.Object,System.Int32,System.String)
WriteTextResponseAsync()
WriteFormUrlEncodedResponse(System.Object,System.Int32)
WriteFormUrlEncodedResponseAsync()
WriteRedirectResponse(System.String,System.String)
WriteBinaryResponse(System.Byte[],System.Int32,System.String)
WriteStreamResponse(System.IO.Stream,System.Int32,System.String)
get_Error()
get_Details()
get_Exception()
get_StackTrace()
get_Status()
get_Reason()
get_Timestamp()
get_Path()
get_Method()
WriteErrorResponseAsync()
WriteErrorResponse(System.String,System.Int32,System.String,System.String)
WriteErrorResponseAsync()
WriteErrorResponse(System.Exception,System.Int32,System.String,System.Boolean)
WriteFormattedErrorResponseAsync()
RenderInlineTemplate(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>)
RenderInline(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>)
TryResolveValue(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,System.Object&)
RevalidateCache(System.Object,System.String,System.Boolean,System.Nullable`1<System.DateTimeOffset>)
WriteHtmlResponseAsync()
WriteHtmlResponseAsync()
WriteHtmlResponse(System.Byte[],System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,System.Int32)
WriteHtmlResponseFromFileAsync()
WriteHtmlResponse(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,System.Int32)
WriteHtmlResponseFromFile(System.String,System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Object>,System.Int32)
WriteStatusOnly(System.Int32)
ApplyTo()
WriteResponseContent()
TryEnqueueCallbacks()
EnsureContentType(Microsoft.AspNetCore.Http.HttpResponse)
EnsureStatus(Microsoft.AspNetCore.Http.HttpResponse)
ApplyCachingHeaders(Microsoft.AspNetCore.Http.HttpResponse)
ApplyContentDispositionHeader(Microsoft.AspNetCore.Http.HttpResponse)
ApplyHeadersAndCookies(Microsoft.AspNetCore.Http.HttpResponse)
WriteBodyAsync()