< 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@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
86%
Covered lines: 425
Uncovered lines: 66
Coverable lines: 491
Total lines: 1347
Line coverage: 86.5%
Branch coverage
71%
Covered branches: 213
Total branches: 300
Branch coverage: 71%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 01:25:22 Line coverage: 89.3% (404/452) Branch coverage: 73.8% (192/260) Total lines: 1231 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/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@10d476bee71c71ad215bb8ab59f219887b5b4a5e 08/26/2025 - 01:25:22 Line coverage: 89.3% (404/452) Branch coverage: 73.8% (192/260) Total lines: 1231 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/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@10d476bee71c71ad215bb8ab59f219887b5b4a5e

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)75%44100%
.cctor()100%11100%
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%
GetHeader(...)0%620%
DetermineContentType(...)75%44100%
IsTextBasedContentType(...)100%1414100%
WriteFileResponse(...)78.57%141496.29%
WriteJsonResponse(...)100%11100%
WriteJsonResponseAsync()100%11100%
WriteJsonResponse(...)100%210%
WriteJsonResponseAsync()100%44100%
WriteJsonResponse(...)100%11100%
WriteJsonResponseAsync()100%44100%
WriteCborResponseAsync()75%44100%
WriteCborResponse(...)100%210%
WriteBsonResponseAsync()75%44100%
WriteBsonResponse(...)100%210%
WriteResponseAsync()20%2333039.13%
WriteResponse(...)100%210%
WriteCsvResponse(...)100%88100%
WriteCsvResponseAsync()75%44100%
WriteYamlResponse(...)100%11100%
WriteYamlResponseAsync()75%44100%
WriteXmlResponse(...)100%11100%
WriteXmlResponseAsync()87.5%88100%
WriteTextResponse(...)100%11100%
WriteTextResponseAsync()83.33%6685.71%
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%
WriteHtmlResponseFromFileAsync()75%4475%
WriteHtmlResponse(...)100%210%
WriteHtmlResponseFromFile(...)100%210%
WriteStatusOnly(...)100%210%
ApplyTo()81.25%191677.77%
EnsureContentType(...)100%88100%
EnsureStatus(...)100%22100%
ApplyCachingHeaders(...)50%2266.66%
ApplyContentDispositionHeader(...)78.57%141488.88%
ApplyHeadersAndCookies(...)50%13857.14%
WriteBodyAsync()59.09%242285.36%

File(s)

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

#LineLine coverage
 1
 2using System.Xml.Linq;
 3using Newtonsoft.Json;
 4using Newtonsoft.Json.Serialization;
 5using Microsoft.AspNetCore.StaticFiles;
 6using System.Text;
 7using Serilog;
 8using Serilog.Events;
 9using System.Buffers;
 10using Microsoft.Extensions.FileProviders;
 11using Microsoft.AspNetCore.WebUtilities;
 12using System.Net;
 13using MongoDB.Bson;
 14using Kestrun.Utilities;
 15using System.Collections;
 16using CsvHelper.Configuration;
 17using System.Globalization;
 18using CsvHelper;
 19using System.Reflection;
 20using Microsoft.Net.Http.Headers;
 21using Kestrun.Utilities.Yaml;
 22
 23namespace Kestrun.Models;
 24
 25/// <summary>
 26/// Represents an HTTP response in the Kestrun framework, providing methods to write various content types and manage he
 27/// </summary>
 28/// <remarks>
 29/// Initializes a new instance of the <see cref="KestrunResponse"/> class with the specified request and optional body a
 30/// </remarks>
 31/// <param name="request">The associated <see cref="KestrunRequest"/> for this response.</param>
 32/// <param name="bodyAsyncThreshold">The threshold in bytes for using async body write operations. Defaults to 8192.</pa
 9733public class KestrunResponse(KestrunRequest request, int bodyAsyncThreshold = 8192)
 34{
 35    /// <summary>
 36    /// A set of MIME types that are considered text-based for response content.
 37    /// </summary>
 138    public static readonly HashSet<string> TextBasedMimeTypes =
 139    new(StringComparer.OrdinalIgnoreCase)
 140    {
 141        "application/json",
 142        "application/xml",
 143        "application/javascript",
 144        "application/xhtml+xml",
 145        "application/x-www-form-urlencoded",
 146        "application/yaml",
 147        "application/graphql"
 148    };
 49
 50    /// <summary>
 51    /// Gets the <see cref="HttpContext"/> associated with the response.
 52    /// </summary>
 053    public HttpContext Context => Request.HttpContext;
 54    /// <summary>
 55    /// Gets or sets the HTTP status code for the response.
 56    /// </summary>
 22757    public int StatusCode { get; set; } = request.HttpContext.Response.StatusCode;
 58    /// <summary>
 59    /// Gets or sets the collection of HTTP headers for the response.
 60    /// </summary>
 15761    public Dictionary<string, string> Headers { get; set; } = [];
 62    /// <summary>
 63    /// Gets or sets the MIME content type of the response.
 64    /// </summary>
 31665    public string? ContentType { get; set; } = "text/plain";
 66    /// <summary>
 67    /// Gets or sets the body of the response, which can be a string, byte array, stream, or file info.
 68    /// </summary>
 17769    public object? Body { get; set; }
 70    /// <summary>
 71    /// Gets or sets the URL to redirect the response to, if an HTTP redirect is required.
 72    /// </summary>
 5473    public string? RedirectUrl { get; set; } // For HTTP redirects
 74    /// <summary>
 75    /// Gets or sets the list of Set-Cookie header values for the response.
 76    /// </summary>
 2577    public List<string>? Cookies { get; set; } // For Set-Cookie headers
 78
 79
 80    /// <summary>
 81    /// Text encoding for textual MIME types.
 82    /// </summary>
 13683    public Encoding Encoding { get; set; } = Encoding.UTF8;
 84
 85    /// <summary>
 86    /// Content-Disposition header value.
 87    /// </summary>
 13488    public ContentDispositionOptions ContentDisposition { get; set; } = new ContentDispositionOptions();
 89    /// <summary>
 90    /// Gets the associated KestrunRequest for this response.
 91    /// </summary>
 13592    public KestrunRequest Request { get; private set; } = request ?? throw new ArgumentNullException(nameof(request));
 93
 94    /// <summary>
 95    /// Global text encoding for all responses. Defaults to UTF-8.
 96    /// </summary>
 11897    public Encoding AcceptCharset { get; private set; } = request.Headers.TryGetValue("Accept-Charset", out var value) ?
 98
 99    /// <summary>
 100    /// If the response body is larger than this threshold (in bytes), async write will be used.
 101    /// </summary>
 97102    public int BodyAsyncThreshold { get; set; } = bodyAsyncThreshold;
 103
 104    /// <summary>
 105    /// Cache-Control header value for the response.
 106    /// </summary>
 25107    public CacheControlHeaderValue? CacheControl { get; set; }
 108
 109    #region Constructors
 110    #endregion
 111
 112    #region Helpers
 113    /// <summary>
 114    /// Retrieves the value of the specified header from the response headers.
 115    /// </summary>
 116    /// <param name="key">The name of the header to retrieve.</param>
 117    /// <returns>The value of the header if found; otherwise, null.</returns>
 0118    public string? GetHeader(string key) => Headers.TryGetValue(key, out var value) ? value : null;
 119
 120    private string DetermineContentType(string? contentType, string defaultType = "text/plain")
 121    {
 1122        if (string.IsNullOrWhiteSpace(contentType))
 123        {
 1124            _ = Request.Headers.TryGetValue("Accept", out var acceptHeader);
 1125            contentType = (acceptHeader ?? defaultType)
 1126                                 .ToLowerInvariant();
 127        }
 128
 1129        return contentType;
 130    }
 131
 132    /// <summary>
 133    /// Determines whether the specified content type is text-based or supports a charset.
 134    /// </summary>
 135    /// <param name="type">The MIME content type to check.</param>
 136    /// <returns>True if the content type is text-based; otherwise, false.</returns>
 137    public static bool IsTextBasedContentType(string type)
 138    {
 31139        if (Log.IsEnabled(LogEventLevel.Debug))
 140        {
 30141            Log.Debug("Checking if content type is text-based: {ContentType}", type);
 142        }
 143
 144        // Check if the content type is text-based or has a charset
 31145        if (string.IsNullOrEmpty(type))
 146        {
 1147            return false;
 148        }
 149
 30150        if (type.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
 151        {
 17152            return true;
 153        }
 154
 155        // Include structured types using XML or JSON suffixes
 13156        if (type.EndsWith("xml", StringComparison.OrdinalIgnoreCase) ||
 13157            type.EndsWith("json", StringComparison.OrdinalIgnoreCase) ||
 13158            type.EndsWith("yaml", StringComparison.OrdinalIgnoreCase) ||
 13159            type.EndsWith("csv", StringComparison.OrdinalIgnoreCase))
 160        {
 4161            return true;
 162        }
 163
 164        // Common application types where charset makes sense
 9165        return TextBasedMimeTypes.Contains(type);
 166    }
 167    #endregion
 168
 169    #region  Response Writers
 170    /// <summary>
 171    /// Writes a file response with the specified file path, content type, and HTTP status code.
 172    /// </summary>
 173    /// <param name="filePath">The path to the file to be sent in the response.</param>
 174    /// <param name="contentType">The MIME type of the file content.</param>
 175    /// <param name="statusCode">The HTTP status code for the response.</param>
 176    public void WriteFileResponse(
 177        string? filePath,
 178        string? contentType,
 179        int statusCode = StatusCodes.Status200OK
 180    )
 181    {
 2182        if (Log.IsEnabled(LogEventLevel.Debug))
 183        {
 2184            Log.Debug("Writing file response,FilePath={FilePath} StatusCode={StatusCode}, ContentType={ContentType}, Cur
 2185                filePath, statusCode, contentType, Directory.GetCurrentDirectory());
 186        }
 187
 2188        if (string.IsNullOrEmpty(filePath))
 189        {
 0190            throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
 191        }
 192
 2193        if (!File.Exists(filePath))
 194        {
 1195            StatusCode = StatusCodes.Status404NotFound;
 1196            Body = $"File not found: {filePath}";
 1197            ContentType = $"text/plain; charset={Encoding.WebName}";
 1198            return;
 199        }
 200        // 1. Make sure you have an absolute file path
 1201        var fullPath = Path.GetFullPath(filePath);
 202
 203        // 2. Extract the directory to use as the "root"
 1204        var directory = Path.GetDirectoryName(fullPath)
 1205                       ?? throw new InvalidOperationException("Could not determine directory from file path");
 206
 1207        if (Log.IsEnabled(LogEventLevel.Debug))
 208        {
 1209            Log.Debug("Serving file: {FilePath}", fullPath);
 210        }
 211
 212        // Create a physical file provider for the directory
 1213        var physicalProvider = new PhysicalFileProvider(directory);
 1214        var fi = physicalProvider.GetFileInfo(Path.GetFileName(filePath));
 1215        var provider = new FileExtensionContentTypeProvider();
 1216        contentType ??= provider.TryGetContentType(fullPath, out var ct)
 1217                ? ct
 1218                : "application/octet-stream";
 1219        Body = fi;
 220
 221        // headers & metadata
 1222        StatusCode = statusCode;
 1223        ContentType = contentType;
 1224        Log.Debug("File response prepared: FileName={FileName}, Length={Length}, ContentType={ContentType}",
 1225            fi.Name, fi.Length, ContentType);
 1226    }
 227
 228    /// <summary>
 229    /// Writes a JSON response with the specified input object and HTTP status code.
 230    /// </summary>
 231    /// <param name="inputObject">The object to be converted to JSON.</param>
 232    /// <param name="statusCode">The HTTP status code for the response.</param>
 5233    public void WriteJsonResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) => WriteJsonResponseAsy
 234
 235    /// <summary>
 236    /// Asynchronously writes a JSON response with the specified input object and HTTP status code.
 237    /// </summary>
 238    /// <param name="inputObject">The object to be converted to JSON.</param>
 239    /// <param name="statusCode">The HTTP status code for the response.</param>
 240    /// <param name="contentType">The MIME type of the response content.</param>
 4241    public async Task WriteJsonResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 242
 243    /// <summary>
 244    /// Writes a JSON response using the specified input object and serializer settings.
 245    /// </summary>
 246    /// <param name="inputObject">The object to be converted to JSON.</param>
 247    /// <param name="serializerSettings">The settings to use for JSON serialization.</param>
 248    /// <param name="statusCode">The HTTP status code for the response.</param>
 249    /// <param name="contentType">The MIME type of the response content.</param>
 0250    public void WriteJsonResponse(object? inputObject, JsonSerializerSettings serializerSettings, int statusCode = Statu
 251
 252    /// <summary>
 253    /// Asynchronously writes a JSON response using the specified input object and serializer settings.
 254    /// </summary>
 255    /// <param name="inputObject">The object to be converted to JSON.</param>
 256    /// <param name="serializerSettings">The settings to use for JSON serialization.</param>
 257    /// <param name="statusCode">The HTTP status code for the response.</param>
 258    /// <param name="contentType">The MIME type of the response content.</param>
 259    public async Task WriteJsonResponseAsync(object? inputObject, JsonSerializerSettings serializerSettings, int statusC
 260    {
 13261        if (Log.IsEnabled(LogEventLevel.Debug))
 262        {
 13263            Log.Debug("Writing JSON response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, c
 264        }
 265
 26266        Body = await Task.Run(() => JsonConvert.SerializeObject(inputObject, serializerSettings));
 13267        ContentType = string.IsNullOrEmpty(contentType) ? $"application/json; charset={Encoding.WebName}" : contentType;
 13268        StatusCode = statusCode;
 13269    }
 270    /// <summary>
 271    /// Writes a JSON response with the specified input object, serialization depth, compression option, status code, an
 272    /// </summary>
 273    /// <param name="inputObject">The object to be converted to JSON.</param>
 274    /// <param name="depth">The maximum depth for JSON serialization.</param>
 275    /// <param name="compress">Whether to compress the JSON output (no indentation).</param>
 276    /// <param name="statusCode">The HTTP status code for the response.</param>
 277    /// <param name="contentType">The MIME type of the response content.</param>
 1278    public void WriteJsonResponse(object? inputObject, int depth, bool compress, int statusCode = StatusCodes.Status200O
 279
 280    /// <summary>
 281    /// Asynchronously writes a JSON response with the specified input object, serialization depth, compression option, 
 282    /// </summary>
 283    /// <param name="inputObject">The object to be converted to JSON.</param>
 284    /// <param name="depth">The maximum depth for JSON serialization.</param>
 285    /// <param name="compress">Whether to compress the JSON output (no indentation).</param>
 286    /// <param name="statusCode">The HTTP status code for the response.</param>
 287    /// <param name="contentType">The MIME type of the response content.</param>
 288    public async Task WriteJsonResponseAsync(object? inputObject, int depth, bool compress, int statusCode = StatusCodes
 289    {
 13290        if (Log.IsEnabled(LogEventLevel.Debug))
 291        {
 13292            Log.Debug("Writing JSON response (async), StatusCode={StatusCode}, ContentType={ContentType}, Depth={Depth},
 13293                statusCode, contentType, depth, compress);
 294        }
 295
 13296        var serializerSettings = new JsonSerializerSettings
 13297        {
 13298            Formatting = compress ? Formatting.None : Formatting.Indented,
 13299            ContractResolver = new CamelCasePropertyNamesContractResolver(),
 13300            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
 13301            NullValueHandling = NullValueHandling.Ignore,
 13302            DefaultValueHandling = DefaultValueHandling.Ignore,
 13303            MaxDepth = depth
 13304        };
 13305        await WriteJsonResponseAsync(inputObject, serializerSettings: serializerSettings, statusCode: statusCode, conten
 13306    }
 307    /// <summary>
 308    /// Writes a CBOR response (binary, efficient, not human-readable).
 309    /// </summary>
 310    public async Task WriteCborResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 311    {
 2312        if (Log.IsEnabled(LogEventLevel.Debug))
 313        {
 2314            Log.Debug("Writing CBOR response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, contentTy
 315        }
 316
 317        // Serialize to CBOR using PeterO.Cbor
 4318        Body = await Task.Run(() => inputObject != null
 4319            ? PeterO.Cbor.CBORObject.FromObject(inputObject).EncodeToBytes()
 4320            : []);
 2321        ContentType = string.IsNullOrEmpty(contentType) ? "application/cbor" : contentType;
 2322        StatusCode = statusCode;
 2323    }
 324
 325    /// <summary>
 326    /// Writes a CBOR response (binary, efficient, not human-readable).
 327    /// </summary>
 328    /// <param name="inputObject">The object to be converted to CBOR.</param>
 329    /// <param name="statusCode">The HTTP status code for the response.</param>
 330    /// <param name="contentType">The MIME type of the response content.</param>
 0331    public void WriteCborResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 332
 333    /// <summary>
 334    /// Asynchronously writes a BSON response with the specified input object, status code, and content type.
 335    /// </summary>
 336    /// <param name="inputObject">The object to be converted to BSON.</param>
 337    /// <param name="statusCode">The HTTP status code for the response.</param>
 338    /// <param name="contentType">The MIME type of the response content.</param>
 339    public async Task WriteBsonResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 340    {
 1341        if (Log.IsEnabled(LogEventLevel.Debug))
 342        {
 1343            Log.Debug("Writing BSON response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, contentTy
 344        }
 345
 346        // Serialize to BSON (as byte[])
 2347        Body = await Task.Run(() => inputObject != null ? inputObject.ToBson() : []);
 1348        ContentType = string.IsNullOrEmpty(contentType) ? "application/bson" : contentType;
 1349        StatusCode = statusCode;
 1350    }
 351
 352    /// <summary>
 353    /// Writes a BSON response with the specified input object, status code, and content type.
 354    /// </summary>
 355    /// <param name="inputObject">The object to be converted to BSON.</param>
 356    /// <param name="statusCode">The HTTP status code for the response.</param>
 357    /// <param name="contentType">The MIME type of the response content.</param>
 0358    public void WriteBsonResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 359
 360    /// <summary>
 361    /// Asynchronously writes a response with the specified input object and HTTP status code.
 362    /// Chooses the response format based on the Accept header or defaults to text/plain.
 363    /// </summary>
 364    /// <param name="inputObject">The object to be sent in the response body.</param>
 365    /// <param name="statusCode">The HTTP status code for the response.</param>
 366    public async Task WriteResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK)
 367    {
 1368        if (Log.IsEnabled(LogEventLevel.Debug))
 369        {
 1370            Log.Debug("Writing response, StatusCode={StatusCode}", statusCode);
 371        }
 372
 1373        Body = inputObject;
 1374        ContentType = DetermineContentType(contentType: string.Empty); // Ensure ContentType is set based on Accept head
 1375        if (ContentType.Contains(','))
 376        {
 0377            var ContentTypes = ContentType.Split(','); // Take the first type only
 0378            ContentType = "application/json"; // fallback
 0379            foreach (var ct in ContentTypes)
 380            {
 0381                if (ct.Contains("json") || ct.Contains("xml") || ct.Contains("yaml") || ct.Contains("yml"))
 382                {
 0383                    if (Log.IsEnabled(LogEventLevel.Verbose))
 384                    {
 0385                        Log.Verbose("Multiple content types in Accept header, selecting {ContentType}", ct);
 386                    }
 0387                    ContentType = ct;
 0388                    break;
 389                }
 390            }
 391        }
 1392        if (Log.IsEnabled(LogEventLevel.Verbose))
 393        {
 0394            Log.Verbose("Determined ContentType={ContentType}", ContentType);
 395        }
 1396        if (ContentType.Contains("json"))
 397        {
 1398            await WriteJsonResponseAsync(inputObject: inputObject, statusCode: statusCode, contentType: ContentType);
 399        }
 0400        else if (ContentType.Contains("yaml") || ContentType.Contains("yml"))
 401        {
 0402            await WriteYamlResponseAsync(inputObject: inputObject, statusCode: statusCode, contentType: ContentType);
 403        }
 0404        else if (ContentType.Contains("xml"))
 405        {
 0406            await WriteXmlResponseAsync(inputObject: inputObject, statusCode: statusCode, contentType: ContentType);
 407        }
 408        else
 409        {
 0410            await WriteTextResponseAsync(inputObject: inputObject?.ToString() ?? string.Empty, statusCode: statusCode);
 411        }
 1412    }
 413
 414    /// <summary>
 415    /// Writes a response with the specified input object and HTTP status code.
 416    /// Chooses the response format based on the Accept header or defaults to text/plain.
 417    /// </summary>
 418    /// <param name="inputObject">The object to be sent in the response body.</param>
 419    /// <param name="statusCode">The HTTP status code for the response.</param>
 0420    public void WriteResponse(object? inputObject, int statusCode = StatusCodes.Status200OK) => WriteResponseAsync(input
 421
 422    /// <summary>
 423    /// Writes a CSV response with the specified input object, status code, content type, and optional CsvConfiguration.
 424    /// </summary>
 425    /// <param name="inputObject">The object to be converted to CSV.</param>
 426    /// <param name="statusCode">The HTTP status code for the response.</param>
 427    /// <param name="contentType">The MIME type of the response content.</param>
 428    /// <param name="config">An optional CsvConfiguration to customize CSV output.</param>
 429    public void WriteCsvResponse(
 430            object? inputObject,
 431            int statusCode = StatusCodes.Status200OK,
 432            string? contentType = null,
 433            CsvConfiguration? config = null)
 434    {
 2435        Action<CsvConfiguration>? tweaker = null;
 436
 2437        if (config is not null)
 438        {
 1439            tweaker = target =>
 1440            {
 90441                foreach (var prop in typeof(CsvConfiguration)
 1442                     .GetProperties(BindingFlags.Public | BindingFlags.Instance))
 1443                {
 44444                    if (prop.CanRead && prop.CanWrite)
 1445                    {
 44446                        var value = prop.GetValue(config);
 44447                        prop.SetValue(target, value);
 1448                    }
 1449                }
 2450            };
 451        }
 2452        WriteCsvResponseAsync(inputObject, statusCode, contentType, tweaker).GetAwaiter().GetResult();
 2453    }
 454
 455    /// <summary>
 456    /// Asynchronously writes a CSV response with the specified input object, status code, content type, and optional co
 457    /// </summary>
 458    /// <param name="inputObject">The object to be converted to CSV.</param>
 459    /// <param name="statusCode">The HTTP status code for the response.</param>
 460    /// <param name="contentType">The MIME type of the response content.</param>
 461    /// <param name="tweak">An optional action to tweak the CsvConfiguration.</param>
 462    public async Task WriteCsvResponseAsync(
 463        object? inputObject,
 464        int statusCode = StatusCodes.Status200OK,
 465        string? contentType = null,
 466        Action<CsvConfiguration>? tweak = null)
 467    {
 3468        if (Log.IsEnabled(LogEventLevel.Debug))
 469        {
 3470            Log.Debug("Writing CSV response (async), StatusCode={StatusCode}, ContentType={ContentType}",
 3471                      statusCode, contentType);
 472        }
 473
 474        // Serialize inside a background task so heavy reflection never blocks the caller
 3475        Body = await Task.Run(() =>
 3476        {
 3477            var cfg = new CsvConfiguration(CultureInfo.InvariantCulture)
 3478            {
 3479                HasHeaderRecord = true,
 3480                NewLine = Environment.NewLine
 3481            };
 3482            tweak?.Invoke(cfg);                         // let the caller flirt with the config
 3483
 3484            using var sw = new StringWriter();
 3485            using var csv = new CsvWriter(sw, cfg);
 3486
 3487            // CsvHelper insists on an enumerable; wrap single objects so it stays happy
 3488            if (inputObject is IEnumerable records and not string)
 3489            {
 3490                csv.WriteRecords(records);              // whole collections (IEnumerable<T>)
 3491            }
 0492            else if (inputObject is not null)
 3493            {
 0494                csv.WriteRecords([inputObject]); // lone POCO
 3495            }
 3496            else
 3497            {
 0498                csv.WriteHeader<object>();              // nothing? write only headers for an empty file
 3499            }
 3500
 3501            return sw.ToString();
 6502        }).ConfigureAwait(false);
 503
 3504        ContentType = string.IsNullOrEmpty(contentType)
 3505            ? $"text/csv; charset={Encoding.WebName}"
 3506            : contentType;
 3507        StatusCode = statusCode;
 3508    }
 509    /// <summary>
 510    /// Writes a YAML response with the specified input object, status code, and content type.
 511    /// </summary>
 512    /// <param name="inputObject">The object to be converted to YAML.</param>
 513    /// <param name="statusCode">The HTTP status code for the response.</param>
 514    /// <param name="contentType">The MIME type of the response content.</param>
 1515    public void WriteYamlResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 516
 517    /// <summary>
 518    /// Asynchronously writes a YAML response with the specified input object, status code, and content type.
 519    /// </summary>
 520    /// <param name="inputObject">The object to be converted to YAML.</param>
 521    /// <param name="statusCode">The HTTP status code for the response.</param>
 522    /// <param name="contentType">The MIME type of the response content.</param>
 523    public async Task WriteYamlResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 524    {
 3525        if (Log.IsEnabled(LogEventLevel.Debug))
 526        {
 3527            Log.Debug("Writing YAML response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, c
 528        }
 529
 6530        Body = await Task.Run(() => YamlHelper.ToYaml(inputObject));
 3531        ContentType = string.IsNullOrEmpty(contentType) ? $"application/yaml; charset={Encoding.WebName}" : contentType;
 3532        StatusCode = statusCode;
 3533    }
 534
 535    /// <summary>
 536    /// Writes an XML response with the specified input object, status code, and content type.
 537    /// </summary>
 538    /// <param name="inputObject">The object to be converted to XML.</param>
 539    /// <param name="statusCode">The HTTP status code for the response.</param>
 540    /// <param name="contentType">The MIME type of the response content.</param>
 541    /// <param name="rootElementName">Optional custom XML root element name. Defaults to <c>Response</c>.</param>
 542    /// <param name="compress">If true, emits compact XML (no indentation); if false (default) output is human readable.
 543    public void WriteXmlResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = nu
 6544        => WriteXmlResponseAsync(inputObject, statusCode, contentType, rootElementName, compress).GetAwaiter().GetResult
 545
 546    /// <summary>
 547    /// Asynchronously writes an XML response with the specified input object, status code, and content type.
 548    /// </summary>
 549    /// <param name="inputObject">The object to be converted to XML.</param>
 550    /// <param name="statusCode">The HTTP status code for the response.</param>
 551    /// <param name="contentType">The MIME type of the response content.</param>
 552    /// <param name="rootElementName">Optional custom XML root element name. Defaults to <c>Response</c>.</param>
 553    /// <param name="compress">If true, emits compact XML (no indentation); if false (default) output is human readable.
 554    public async Task WriteXmlResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? conte
 555    {
 8556        if (Log.IsEnabled(LogEventLevel.Debug))
 557        {
 8558            Log.Debug("Writing XML response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, co
 559        }
 560
 8561        var root = string.IsNullOrWhiteSpace(rootElementName) ? "Response" : rootElementName!.Trim();
 16562        var xml = await Task.Run(() => XmlHelper.ToXml(root, inputObject));
 8563        var saveOptions = compress ? SaveOptions.DisableFormatting : SaveOptions.None;
 16564        Body = await Task.Run(() => xml.ToString(saveOptions));
 8565        ContentType = string.IsNullOrEmpty(contentType) ? $"application/xml; charset={Encoding.WebName}" : contentType;
 8566        StatusCode = statusCode;
 8567    }
 568    /// <summary>
 569    /// Writes a text response with the specified input object, status code, and content type.
 570    /// </summary>
 571    /// <param name="inputObject">The object to be converted to a text response.</param>
 572    /// <param name="statusCode">The HTTP status code for the response.</param>
 573    /// <param name="contentType">The MIME type of the response content.</param>
 574    public void WriteTextResponse(object? inputObject, int statusCode = StatusCodes.Status200OK, string? contentType = n
 8575        WriteTextResponseAsync(inputObject, statusCode, contentType).GetAwaiter().GetResult();
 576
 577    /// <summary>
 578    /// Asynchronously writes a text response with the specified input object, status code, and content type.
 579    /// </summary>
 580    /// <param name="inputObject">The object to be converted to a text response.</param>
 581    /// <param name="statusCode">The HTTP status code for the response.</param>
 582    /// <param name="contentType">The MIME type of the response content.</param>
 583    public async Task WriteTextResponseAsync(object? inputObject, int statusCode = StatusCodes.Status200OK, string? cont
 584    {
 22585        if (Log.IsEnabled(LogEventLevel.Debug))
 586        {
 21587            Log.Debug("Writing text response (async), StatusCode={StatusCode}, ContentType={ContentType}", statusCode, c
 588        }
 589
 22590        if (inputObject is null)
 591        {
 0592            throw new ArgumentNullException(nameof(inputObject), "Input object cannot be null for text response.");
 593        }
 594
 44595        Body = await Task.Run(() => inputObject?.ToString() ?? string.Empty);
 22596        ContentType = string.IsNullOrEmpty(contentType) ? $"text/plain; charset={Encoding.WebName}" : contentType;
 22597        StatusCode = statusCode;
 22598    }
 599
 600    /// <summary>
 601    /// Writes an HTTP redirect response with the specified URL and optional message.
 602    /// </summary>
 603    /// <param name="url">The URL to redirect to.</param>
 604    /// <param name="message">An optional message to include in the response body.</param>
 605    public void WriteRedirectResponse(string url, string? message = null)
 606    {
 5607        if (Log.IsEnabled(LogEventLevel.Debug))
 608        {
 4609            Log.Debug("Writing redirect response, StatusCode={StatusCode}, Location={Location}", StatusCode, url);
 610        }
 611
 5612        if (string.IsNullOrEmpty(url))
 613        {
 0614            throw new ArgumentNullException(nameof(url), "URL cannot be null for redirect response.");
 615        }
 616        // framework hook
 5617        RedirectUrl = url;
 618
 619        // HTTP status + Location header
 5620        StatusCode = StatusCodes.Status302Found;
 5621        Headers["Location"] = url;
 622
 5623        if (message is not null)
 624        {
 625            // include a body
 1626            Body = message;
 1627            ContentType = $"text/plain; charset={Encoding.WebName}";
 628        }
 629        else
 630        {
 631            // no body: clear any existing body/headers
 4632            Body = null;
 633            //ContentType = null;
 4634            _ = Headers.Remove("Content-Length");
 635        }
 4636    }
 637
 638
 639
 640    /// <summary>
 641    /// Writes a binary response with the specified data, status code, and content type.
 642    /// </summary>
 643    /// <param name="data">The binary data to send in the response.</param>
 644    /// <param name="statusCode">The HTTP status code for the response.</param>
 645    /// <param name="contentType">The MIME type of the response content.</param>
 646    public void WriteBinaryResponse(byte[] data, int statusCode = StatusCodes.Status200OK, string contentType = "applica
 647    {
 1648        if (Log.IsEnabled(LogEventLevel.Debug))
 649        {
 1650            Log.Debug("Writing binary response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, content
 651        }
 652
 1653        Body = data ?? throw new ArgumentNullException(nameof(data), "Data cannot be null for binary response.");
 1654        ContentType = contentType;
 1655        StatusCode = statusCode;
 1656    }
 657    /// <summary>
 658    /// Writes a stream response with the specified stream, status code, and content type.
 659    /// </summary>
 660    /// <param name="stream">The stream to send in the response.</param>
 661    /// <param name="statusCode">The HTTP status code for the response.</param>
 662    /// <param name="contentType">The MIME type of the response content.</param>
 663    public void WriteStreamResponse(Stream stream, int statusCode = StatusCodes.Status200OK, string contentType = "appli
 664    {
 3665        if (Log.IsEnabled(LogEventLevel.Debug))
 666        {
 3667            Log.Debug("Writing stream response, StatusCode={StatusCode}, ContentType={ContentType}", statusCode, content
 668        }
 669
 3670        Body = stream;
 3671        ContentType = contentType;
 3672        StatusCode = statusCode;
 3673    }
 674    #endregion
 675
 676    #region Error Responses
 677    /// <summary>
 678    /// Structured payload for error responses.
 679    /// </summary>
 680    internal record ErrorPayload
 681    {
 26682        public string Error { get; init; } = default!;
 27683        public string? Details { get; init; }
 29684        public string? Exception { get; init; }
 28685        public string? StackTrace { get; init; }
 52686        public int Status { get; init; }
 26687        public string Reason { get; init; } = default!;
 26688        public string Timestamp { get; init; } = default!;
 20689        public string? Path { get; init; }
 20690        public string? Method { get; init; }
 691    }
 692
 693    /// <summary>
 694    /// Write an error response with a custom message.
 695    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 696    /// </summary>
 697    public async Task WriteErrorResponseAsync(
 698        string message,
 699        int statusCode = StatusCodes.Status500InternalServerError,
 700        string? contentType = null,
 701        string? details = null)
 702    {
 10703        if (Log.IsEnabled(LogEventLevel.Debug))
 704        {
 10705            Log.Debug("Writing error response, StatusCode={StatusCode}, ContentType={ContentType}, Message={Message}",
 10706                statusCode, contentType, message);
 707        }
 708
 10709        if (string.IsNullOrWhiteSpace(message))
 710        {
 0711            throw new ArgumentNullException(nameof(message));
 712        }
 713
 10714        Log.Warning("Writing error response with status {StatusCode}: {Message}", statusCode, message);
 715
 10716        var payload = new ErrorPayload
 10717        {
 10718            Error = message,
 10719            Details = details,
 10720            Exception = null,
 10721            StackTrace = null,
 10722            Status = statusCode,
 10723            Reason = ReasonPhrases.GetReasonPhrase(statusCode),
 10724            Timestamp = DateTime.UtcNow.ToString("o"),
 10725            Path = Request?.Path,
 10726            Method = Request?.Method
 10727        };
 728
 10729        await WriteFormattedErrorResponseAsync(payload, contentType);
 10730    }
 731
 732    /// <summary>
 733    /// Writes an error response with a custom message.
 734    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 735    /// </summary>
 736    /// <param name="message">The error message to include in the response.</param>
 737    /// <param name="statusCode">The HTTP status code for the response.</param>
 738    /// <param name="contentType">The MIME type of the response content.</param>
 739    /// <param name="details">Optional details to include in the response.</param>
 740    public void WriteErrorResponse(
 741      string message,
 742      int statusCode = StatusCodes.Status500InternalServerError,
 743      string? contentType = null,
 1744      string? details = null) => WriteErrorResponseAsync(message, statusCode, contentType, details).GetAwaiter().GetResu
 745
 746
 747    /// <summary>
 748    /// Asynchronously writes an error response based on an exception.
 749    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 750    /// </summary>
 751    /// <param name="ex">The exception to report.</param>
 752    /// <param name="statusCode">The HTTP status code for the response.</param>
 753    /// <param name="contentType">The MIME type of the response content.</param>
 754    /// <param name="includeStack">Whether to include the stack trace in the response.</param>
 755    public async Task WriteErrorResponseAsync(
 756        Exception ex,
 757        int statusCode = StatusCodes.Status500InternalServerError,
 758        string? contentType = null,
 759        bool includeStack = true)
 760    {
 3761        if (Log.IsEnabled(LogEventLevel.Debug))
 762        {
 3763            Log.Debug("Writing error response from exception, StatusCode={StatusCode}, ContentType={ContentType}, Includ
 3764                statusCode, contentType, includeStack);
 765        }
 766
 3767        ArgumentNullException.ThrowIfNull(ex);
 768
 3769        Log.Warning(ex, "Writing error response with status {StatusCode}", statusCode);
 770
 3771        var payload = new ErrorPayload
 3772        {
 3773            Error = ex.Message,
 3774            Details = null,
 3775            Exception = ex.GetType().Name,
 3776            StackTrace = includeStack ? ex.ToString() : null,
 3777            Status = statusCode,
 3778            Reason = ReasonPhrases.GetReasonPhrase(statusCode),
 3779            Timestamp = DateTime.UtcNow.ToString("o"),
 3780            Path = Request?.Path,
 3781            Method = Request?.Method
 3782        };
 783
 3784        await WriteFormattedErrorResponseAsync(payload, contentType);
 3785    }
 786    /// <summary>
 787    /// Writes an error response based on an exception.
 788    /// Chooses JSON/YAML/XML/plain-text based on override → Accept → default JSON.
 789    /// </summary>
 790    /// <param name="ex">The exception to report.</param>
 791    /// <param name="statusCode">The HTTP status code for the response.</param>
 792    /// <param name="contentType">The MIME type of the response content.</param>
 793    /// <param name="includeStack">Whether to include the stack trace in the response.</param>
 794    public void WriteErrorResponse(
 795            Exception ex,
 796            int statusCode = StatusCodes.Status500InternalServerError,
 797            string? contentType = null,
 1798            bool includeStack = true) => WriteErrorResponseAsync(ex, statusCode, contentType, includeStack).GetAwaiter()
 799
 800    /// <summary>
 801    /// Internal dispatcher: serializes the payload according to the chosen content-type.
 802    /// </summary>
 803    private async Task WriteFormattedErrorResponseAsync(ErrorPayload payload, string? contentType = null)
 804    {
 13805        if (Log.IsEnabled(LogEventLevel.Debug))
 806        {
 13807            Log.Debug("Writing formatted error response, ContentType={ContentType}, Status={Status}", contentType, paylo
 808        }
 809
 13810        if (string.IsNullOrWhiteSpace(contentType))
 811        {
 11812            _ = Request.Headers.TryGetValue("Accept", out var acceptHeader);
 11813            contentType = (acceptHeader ?? "text/plain")
 11814                                 .ToLowerInvariant();
 815        }
 13816        if (contentType.Contains("json"))
 817        {
 3818            await WriteJsonResponseAsync(payload, payload.Status);
 819        }
 10820        else if (contentType.Contains("yaml") || contentType.Contains("yml"))
 821        {
 2822            await WriteYamlResponseAsync(payload, payload.Status);
 823        }
 8824        else if (contentType.Contains("xml"))
 825        {
 2826            await WriteXmlResponseAsync(payload, payload.Status);
 827        }
 828        else
 829        {
 830            // Plain-text fallback
 6831            var lines = new List<string>
 6832                {
 6833                    $"Status: {payload.Status} ({payload.Reason})",
 6834                    $"Error: {payload.Error}",
 6835                    $"Time: {payload.Timestamp}"
 6836                };
 837
 6838            if (!string.IsNullOrWhiteSpace(payload.Details))
 839            {
 1840                lines.Add("Details:\n" + payload.Details);
 841            }
 842
 6843            if (!string.IsNullOrWhiteSpace(payload.Exception))
 844            {
 3845                lines.Add($"Exception: {payload.Exception}");
 846            }
 847
 6848            if (!string.IsNullOrWhiteSpace(payload.StackTrace))
 849            {
 2850                lines.Add("StackTrace:\n" + payload.StackTrace);
 851            }
 852
 6853            var text = string.Join("\n", lines);
 6854            await WriteTextResponseAsync(text, payload.Status, "text/plain");
 855        }
 13856    }
 857
 858    #endregion
 859    #region HTML Response Helpers
 860
 861    /// <summary>
 862    /// Renders a template string by replacing placeholders in the format {{key}} with corresponding values from the pro
 863    /// </summary>
 864    /// <param name="template">The template string containing placeholders.</param>
 865    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 866    /// <returns>The rendered string with placeholders replaced by variable values.</returns>
 867    private static string RenderInlineTemplate(
 868     string template,
 869     IReadOnlyDictionary<string, object?> vars)
 870    {
 2871        if (Log.IsEnabled(LogEventLevel.Debug))
 872        {
 2873            Log.Debug("Rendering inline template, TemplateLength={TemplateLength}, VarsCount={VarsCount}",
 2874                      template?.Length ?? 0, vars?.Count ?? 0);
 875        }
 876
 2877        if (string.IsNullOrEmpty(template))
 878        {
 0879            return string.Empty;
 880        }
 881
 2882        if (vars is null || vars.Count == 0)
 883        {
 0884            return template;
 885        }
 886
 2887        var render = RenderInline(template, vars);
 888
 2889        if (Log.IsEnabled(LogEventLevel.Debug))
 890        {
 2891            Log.Debug("Rendered template length: {RenderedLength}", render.Length);
 892        }
 893
 2894        return render;
 895    }
 896
 897    /// <summary>
 898    /// Renders a template string by replacing placeholders in the format {{key}} with corresponding values from the pro
 899    /// </summary>
 900    /// <param name="template">The template string containing placeholders.</param>
 901    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 902    /// <returns>The rendered string with placeholders replaced by variable values.</returns>
 903    private static string RenderInline(string template, IReadOnlyDictionary<string, object?> vars)
 904    {
 2905        var sb = new StringBuilder(template.Length);
 906
 907        // Iterate through the template
 2908        var i = 0;
 39909        while (i < template.Length)
 910        {
 911            // opening “{{”
 37912            if (template[i] == '{' && i + 1 < template.Length && template[i + 1] == '{')
 913            {
 3914                var start = i + 2;                                        // after “{{”
 3915                var end = template.IndexOf("}}", start, StringComparison.Ordinal);
 916
 3917                if (end > start)                                          // found closing “}}”
 918                {
 3919                    var rawKey = template[start..end].Trim();
 920
 3921                    if (TryResolveValue(rawKey, vars, out var value) && value is not null)
 922                    {
 3923                        _ = sb.Append(value); // append resolved value
 924                    }
 925                    else
 926                    {
 0927                        _ = sb.Append("{{").Append(rawKey).Append("}}");      // leave it as-is if unknown
 928                    }
 929
 3930                    i = end + 2;    // jump past the “}}”
 3931                    continue;
 932                }
 933            }
 934
 935            // ordinary character
 34936            _ = sb.Append(template[i]);
 34937            i++; // move to the next character
 938        }
 2939        return sb.ToString();
 940    }
 941
 942
 943
 944    /// <summary>
 945    /// Resolves a dotted path like “Request.Path” through nested dictionaries
 946    /// and/or object properties (case-insensitive).
 947    /// </summary>
 948    private static bool TryResolveValue(
 949        string path,
 950        IReadOnlyDictionary<string, object?> root,
 951        out object? value)
 952    {
 3953        value = null;
 954
 3955        if (string.IsNullOrWhiteSpace(path))
 956        {
 0957            return false;
 958        }
 959
 3960        object? current = root;
 16961        foreach (var segment in path.Split('.'))
 962        {
 5963            if (current is null)
 964            {
 0965                return false;
 966            }
 967
 968            // ① Handle dictionary look-ups (IReadOnlyDictionary or IDictionary)
 5969            if (current is IReadOnlyDictionary<string, object?> roDict)
 970            {
 3971                if (!roDict.TryGetValue(segment, out current))
 972                {
 0973                    return false;
 974                }
 975
 976                continue;
 977            }
 978
 2979            if (current is IDictionary dict)
 980            {
 0981                if (!dict.Contains(segment))
 982                {
 0983                    return false;
 984                }
 985
 0986                current = dict[segment];
 0987                continue;
 988            }
 989
 990            // ② Handle property look-ups via reflection
 2991            var prop = current.GetType().GetProperty(
 2992                segment,
 2993                BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
 994
 2995            if (prop is null)
 996            {
 0997                return false;
 998            }
 999
 21000            current = prop.GetValue(current);
 1001        }
 1002
 31003        value = current;
 31004        return true;
 1005    }
 1006
 1007    /// <summary>
 1008    /// Attempts to revalidate the cache based on ETag and Last-Modified headers.
 1009    /// If the resource is unchanged, sets the response status to 304 Not Modified.
 1010    /// Returns true if a 304 response was written, false otherwise.
 1011    /// </summary>
 1012    /// <param name="payload">The payload to validate.</param>
 1013    /// <param name="etag">The ETag header value.</param>
 1014    /// <param name="weakETag">Indicates if the ETag is a weak ETag.</param>
 1015    /// <param name="lastModified">The Last-Modified header value.</param>
 1016    /// <returns>True if a 304 response was written, false otherwise.</returns>
 1017    public bool RevalidateCache(object? payload,
 1018       string? etag = null,
 1019       bool weakETag = false,
 01020       DateTimeOffset? lastModified = null) => CacheRevalidation.TryWrite304(Context, payload, etag, weakETag, lastModif
 1021
 1022    /// <summary>
 1023    /// Asynchronously writes an HTML response, rendering the provided template string and replacing placeholders with v
 1024    /// </summary>
 1025    /// <param name="template">The HTML template string containing placeholders.</param>
 1026    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1027    /// <param name="statusCode">The HTTP status code for the response.</param>
 1028    public async Task WriteHtmlResponseAsync(
 1029        string template,
 1030        IReadOnlyDictionary<string, object?>? vars,
 1031        int statusCode = 200)
 1032    {
 21033        if (Log.IsEnabled(LogEventLevel.Debug))
 1034        {
 21035            Log.Debug("Writing HTML response (async), StatusCode={StatusCode}, TemplateLength={TemplateLength}", statusC
 1036        }
 1037
 21038        if (vars is null || vars.Count == 0)
 1039        {
 01040            await WriteTextResponseAsync(template, statusCode, "text/html");
 1041        }
 1042        else
 1043        {
 21044            await WriteTextResponseAsync(RenderInlineTemplate(template, vars), statusCode, "text/html");
 1045        }
 21046    }
 1047
 1048    /// <summary>
 1049    /// Asynchronously reads an HTML file, merges in placeholders from the provided dictionary, and writes the result as
 1050    /// </summary>
 1051    /// <param name="filePath">The path to the HTML file to read.</param>
 1052    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1053    /// <param name="statusCode">The HTTP status code for the response.</param>
 1054    public async Task WriteHtmlResponseFromFileAsync(
 1055        string filePath,
 1056        IReadOnlyDictionary<string, object?> vars,
 1057        int statusCode = 200)
 1058    {
 11059        if (Log.IsEnabled(LogEventLevel.Debug))
 1060        {
 11061            Log.Debug("Writing HTML response from file (async), FilePath={FilePath}, StatusCode={StatusCode}", filePath,
 1062        }
 1063
 11064        if (!File.Exists(filePath))
 1065        {
 01066            WriteTextResponse($"<!-- File not found: {filePath} -->", 404, "text/html");
 01067            return;
 1068        }
 1069
 11070        var template = await File.ReadAllTextAsync(filePath);
 11071        WriteHtmlResponseAsync(template, vars, statusCode).GetAwaiter().GetResult();
 11072    }
 1073
 1074
 1075    /// <summary>
 1076    /// Renders the given HTML string with placeholders and writes it as a response.
 1077    /// </summary>
 1078    /// <param name="template">The HTML template string containing placeholders.</param>
 1079    /// <param name="vars">A dictionary of variables to replace in the template.</param>
 1080    /// <param name="statusCode">The HTTP status code for the response.</param>
 1081    public void WriteHtmlResponse(
 1082        string template,
 1083        IReadOnlyDictionary<string, object?>? vars,
 01084        int statusCode = 200) => WriteHtmlResponseAsync(template, vars, statusCode).GetAwaiter().GetResult();
 1085
 1086    /// <summary>
 1087    /// Reads an .html file, merges in placeholders, and writes it.
 1088    /// </summary>
 1089    public void WriteHtmlResponseFromFile(
 1090        string filePath,
 1091        IReadOnlyDictionary<string, object?> vars,
 01092        int statusCode = 200) => WriteHtmlResponseFromFileAsync(filePath, vars, statusCode).GetAwaiter().GetResult();
 1093
 1094    /// <summary>
 1095    /// Writes only the specified HTTP status code, clearing any body or content type.
 1096    /// </summary>
 1097    /// <param name="statusCode">The HTTP status code to write.</param>
 1098    public void WriteStatusOnly(int statusCode)
 1099    {
 1100        // Clear any body indicators so StatusCodePages can run
 01101        ContentType = null;
 01102        StatusCode = statusCode;
 01103        Body = null;
 01104    }
 1105    #endregion
 1106
 1107    #region Apply to HttpResponse
 1108    /// <summary>
 1109    /// Applies the current KestrunResponse to the specified HttpResponse, setting status, headers, cookies, and writing
 1110    /// </summary>
 1111    /// <param name="response">The HttpResponse to apply the response to.</param>
 1112    /// <returns>A task representing the asynchronous operation.</returns>
 1113    public async Task ApplyTo(HttpResponse response)
 1114    {
 261115        if (Log.IsEnabled(LogEventLevel.Debug))
 1116        {
 251117            Log.Debug("Applying KestrunResponse to HttpResponse, StatusCode={StatusCode}, ContentType={ContentType}, Bod
 251118                StatusCode, ContentType, Body?.GetType().Name ?? "null");
 1119        }
 1120
 261121        if (response.StatusCode == StatusCodes.Status304NotModified)
 1122        {
 01123            if (Log.IsEnabled(LogEventLevel.Debug))
 1124            {
 01125                Log.Debug("Response already has status code 304 Not Modified, skipping ApplyTo");
 1126            }
 01127            return;
 1128        }
 261129        if (!string.IsNullOrEmpty(RedirectUrl))
 1130        {
 11131            response.Redirect(RedirectUrl);
 11132            return;
 1133        }
 1134
 1135        try
 1136        {
 251137            EnsureStatus(response);
 251138            ApplyHeadersAndCookies(response);
 251139            ApplyCachingHeaders(response);
 251140            if (Body is not null)
 1141            {
 221142                EnsureContentType(response);
 221143                ApplyContentDispositionHeader(response);
 221144                await WriteBodyAsync(response).ConfigureAwait(false);
 1145            }
 1146            else
 1147            {
 31148                response.ContentType = null;
 31149                response.ContentLength = null;
 31150                if (Log.IsEnabled(LogEventLevel.Debug))
 1151                {
 31152                    Log.Debug("Status-only: HasStarted={HasStarted} CL={CL} CT='{CT}'",
 31153                        response.HasStarted, response.ContentLength, response.ContentType);
 1154                }
 1155            }
 251156        }
 01157        catch (Exception ex)
 1158        {
 01159            Console.WriteLine($"Error applying response: {ex.Message}");
 1160            // Optionally, you can log the exception or handle it as needed
 01161            throw;
 1162        }
 261163    }
 1164
 1165    /// <summary>
 1166    /// Ensures the HTTP response has the correct status code and content type.
 1167    /// </summary>
 1168    /// <param name="response">The HTTP response to apply the status and content type to.</param>
 1169    private void EnsureContentType(HttpResponse response)
 1170    {
 221171        if (ContentType != response.ContentType)
 1172        {
 221173            if (!string.IsNullOrEmpty(ContentType) &&
 221174                IsTextBasedContentType(ContentType) &&
 221175                !ContentType.Contains("charset=", StringComparison.OrdinalIgnoreCase))
 1176            {
 21177                ContentType = ContentType.TrimEnd(';') + $"; charset={AcceptCharset.WebName}";
 1178            }
 221179            response.ContentType = ContentType;
 1180        }
 221181    }
 1182
 1183    /// <summary>
 1184    /// Ensures the HTTP response has the correct status code.
 1185    /// </summary>
 1186    /// <param name="response">The HTTP response to apply the status code to.</param>
 1187    private void EnsureStatus(HttpResponse response)
 1188    {
 251189        if (StatusCode != response.StatusCode)
 1190        {
 11191            response.StatusCode = StatusCode;
 1192        }
 251193    }
 1194
 1195    /// <summary>
 1196    /// Adds caching headers to the response based on the provided CacheControlHeaderValue options.
 1197    /// </summary>
 1198    /// <param name="response">The HTTP response to apply caching headers to.</param>
 1199    /// <exception cref="ArgumentNullException">Thrown when options is null.</exception>
 1200    public void ApplyCachingHeaders(HttpResponse response)
 1201    {
 251202        if (CacheControl is not null)
 1203        {
 01204            response.Headers.CacheControl = CacheControl.ToString();
 1205        }
 251206    }
 1207
 1208    /// <summary>
 1209    /// Applies the Content-Disposition header to the HTTP response.
 1210    /// </summary>
 1211    /// <param name="response">The HTTP response to apply the header to.</param>
 1212    private void ApplyContentDispositionHeader(HttpResponse response)
 1213    {
 221214        if (ContentDisposition.Type == ContentDispositionType.NoContentDisposition)
 1215        {
 201216            return;
 1217        }
 1218
 21219        if (Log.IsEnabled(LogEventLevel.Debug))
 1220        {
 21221            Log.Debug("Setting Content-Disposition header, Type={Type}, FileName={FileName}",
 21222                      ContentDisposition.Type, ContentDisposition.FileName);
 1223        }
 1224
 21225        var dispositionValue = ContentDisposition.Type switch
 21226        {
 21227            ContentDispositionType.Attachment => "attachment",
 01228            ContentDispositionType.Inline => "inline",
 01229            _ => throw new InvalidOperationException("Invalid Content-Disposition type")
 21230        };
 1231
 21232        if (string.IsNullOrEmpty(ContentDisposition.FileName) && Body is IFileInfo fi)
 1233        {
 1234            // default filename: use the file's name
 11235            ContentDisposition.FileName = fi.Name;
 1236        }
 1237
 21238        if (!string.IsNullOrEmpty(ContentDisposition.FileName))
 1239        {
 21240            var escapedFileName = WebUtility.UrlEncode(ContentDisposition.FileName);
 21241            dispositionValue += $"; filename=\"{escapedFileName}\"";
 1242        }
 1243
 21244        response.Headers.Append("Content-Disposition", dispositionValue);
 21245    }
 1246
 1247    /// <summary>
 1248    /// Applies headers and cookies to the HTTP response.
 1249    /// </summary>
 1250    /// <param name="response">The HTTP response to apply the headers and cookies to.</param>
 1251    private void ApplyHeadersAndCookies(HttpResponse response)
 1252    {
 251253        if (Headers is not null)
 1254        {
 501255            foreach (var kv in Headers)
 1256            {
 01257                response.Headers[kv.Key] = kv.Value;
 1258            }
 1259        }
 251260        if (Cookies is not null)
 1261        {
 01262            foreach (var cookie in Cookies)
 1263            {
 01264                response.Headers.Append("Set-Cookie", cookie);
 1265            }
 1266        }
 251267    }
 1268
 1269    /// <summary>
 1270    /// Writes the response body to the HTTP response.
 1271    /// </summary>
 1272    /// <param name="response">The HTTP response to write to.</param>
 1273    /// <returns>A task representing the asynchronous operation.</returns>
 1274    private async Task WriteBodyAsync(HttpResponse response)
 1275    {
 221276        var bodyValue = Body; // capture to avoid nullability warnings when mutated in default
 1277        switch (bodyValue)
 1278        {
 1279            case IFileInfo fileInfo:
 11280                Log.Debug("Sending file {FileName} (Length={Length})", fileInfo.Name, fileInfo.Length);
 11281                response.ContentLength = fileInfo.Length;
 11282                response.Headers.LastModified = fileInfo.LastModified.ToString("R");
 11283                await response.SendFileAsync(
 11284                    file: fileInfo,
 11285                    offset: 0,
 11286                    count: fileInfo.Length,
 11287                    cancellationToken: response.HttpContext.RequestAborted
 11288                );
 11289                break;
 1290
 1291            case byte[] bytes:
 11292                response.ContentLength = bytes.LongLength;
 11293                await response.Body.WriteAsync(bytes, response.HttpContext.RequestAborted);
 11294                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 11295                break;
 1296
 1297            case Stream stream:
 21298                var seekable = stream.CanSeek;
 21299                Log.Debug("Sending stream (seekable={Seekable}, len={Len})",
 21300                          seekable, seekable ? stream.Length : -1);
 1301
 21302                if (seekable)
 1303                {
 11304                    response.ContentLength = stream.Length;
 11305                    stream.Position = 0;
 1306                }
 1307                else
 1308                {
 11309                    response.ContentLength = null;
 1310                }
 1311
 1312                const int BufferSize = 64 * 1024; // 64 KB
 21313                var buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
 1314                try
 1315                {
 1316                    int bytesRead;
 41317                    while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, BufferSize), response.HttpContext.Requ
 1318                    {
 21319                        await response.Body.WriteAsync(buffer.AsMemory(0, bytesRead), response.HttpContext.RequestAborte
 1320                    }
 21321                }
 1322                finally
 1323                {
 21324                    ArrayPool<byte>.Shared.Return(buffer);
 1325                }
 21326                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 21327                break;
 1328
 1329            case string str:
 181330                var data = AcceptCharset.GetBytes(str);
 181331                response.ContentLength = data.Length;
 181332                await response.Body.WriteAsync(data, response.HttpContext.RequestAborted);
 181333                await response.Body.FlushAsync(response.HttpContext.RequestAborted);
 181334                break;
 1335
 1336            default:
 01337                var bodyType = bodyValue?.GetType().Name ?? "null";
 01338                Body = "Unsupported body type: " + bodyType;
 01339                Log.Warning("Unsupported body type: {BodyType}", bodyType);
 01340                response.StatusCode = StatusCodes.Status500InternalServerError;
 01341                response.ContentType = "text/plain; charset=utf-8";
 01342                response.ContentLength = Body.ToString()?.Length ?? null;
 1343                break;
 1344        }
 221345    }
 1346    #endregion
 1347}

Methods/Properties

.ctor(Kestrun.Models.KestrunRequest,System.Int32)
.cctor()
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()
GetHeader(System.String)
DetermineContentType(System.String,System.String)
IsTextBasedContentType(System.String)
WriteFileResponse(System.String,System.String,System.Int32)
WriteJsonResponse(System.Object,System.Int32)
WriteJsonResponseAsync()
WriteJsonResponse(System.Object,Newtonsoft.Json.JsonSerializerSettings,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)
WriteResponseAsync()
WriteResponse(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()
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()
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()
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()