< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Languages.ParameterForInjectionInfo
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/ParameterForInjectionInfo.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
62%
Covered lines: 364
Uncovered lines: 216
Coverable lines: 580
Total lines: 1506
Line coverage: 62.7%
Branch coverage
46%
Covered branches: 254
Total branches: 546
Branch coverage: 46.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/12/2025 - 17:27:19 Line coverage: 2.1% (4/182) Branch coverage: 1.2% (2/160) Total lines: 437 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 2.1% (4/187) Branch coverage: 1% (2/182) Total lines: 458 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a01/02/2026 - 00:16:25 Line coverage: 6.1% (11/180) Branch coverage: 2.7% (5/184) Total lines: 434 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/08/2026 - 08:19:25 Line coverage: 70% (126/180) Branch coverage: 46.7% (86/184) Total lines: 438 Tag: Kestrun/Kestrun@6ab94ca7560634c2ac58b36c2b98e2a9b1bf305d01/17/2026 - 04:33:35 Line coverage: 63.6% (249/391) Branch coverage: 43% (174/404) Total lines: 973 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/21/2026 - 17:07:46 Line coverage: 62.7% (364/580) Branch coverage: 46.5% (254/546) Total lines: 1506 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36 12/12/2025 - 17:27:19 Line coverage: 2.1% (4/182) Branch coverage: 1.2% (2/160) Total lines: 437 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 2.1% (4/187) Branch coverage: 1% (2/182) Total lines: 458 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a01/02/2026 - 00:16:25 Line coverage: 6.1% (11/180) Branch coverage: 2.7% (5/184) Total lines: 434 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/08/2026 - 08:19:25 Line coverage: 70% (126/180) Branch coverage: 46.7% (86/184) Total lines: 438 Tag: Kestrun/Kestrun@6ab94ca7560634c2ac58b36c2b98e2a9b1bf305d01/17/2026 - 04:33:35 Line coverage: 63.6% (249/391) Branch coverage: 43% (174/404) Total lines: 973 Tag: Kestrun/Kestrun@aca34ea8d284564e2f9f6616dc937668dce926ba01/21/2026 - 17:07:46 Line coverage: 62.7% (364/580) Branch coverage: 46.5% (254/546) Total lines: 1506 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Validate(...)100%11100%
.ctor(...)37.5%8881.81%
.ctor(...)72.22%1818100%
InjectParameters(...)100%88100%
.cctor()100%1164.7%
ShouldConvertBody(...)50%66100%
TryConvertBodyByContentType(...)0%342180%
InjectSingleParameter(...)100%11100%
LogInjectingParameter(...)50%101090.9%
GetConvertedParameterValue(...)75%44100%
ConvertBodyParameterIfNeeded(...)25%13418.18%
LogAddingParameter(...)50%9880%
StoreResolvedParameter(...)100%22100%
GetParameterValueFromContext(...)70%121073.33%
GetRawValue(...)42.1%371962.96%
NormalizeRaw(...)40%151062.5%
ConvertValue(...)33.33%953058.33%
ConvertBodyBasedOnContentType(...)75%4477.77%
ConvertByCanonicalMediaType(...)58.33%423683.33%
ConvertYamlToHashtable(...)50%2275%
ConvertJsonToHashtable(...)50%2280%
JsonElementToClr(...)36.36%261150%
ToHashtable(...)100%22100%
ToArray(...)0%620%
ConvertXmlBodyToParameterType(...)77.77%461855.88%
ExtractRootMapForBinding(...)66.66%221258.33%
NormalizeWrappedArrays(...)100%1010100%
TryGetXmlMetadataProperties(...)100%44100%
TryGetWrappedArrayMetadata(...)70%121072.72%
TryGetWrapperHashtable(...)50%9655.55%
TryUnwrapWrapper(...)25%44100%
ConvertHashtableToObject(...)62.5%191678.26%
ConvertToTargetType(...)75%8890%
UnwrapNullableTargetType(...)100%22100%
TryConvertHashtableValue(...)25%7442.85%
TryConvertListOrArrayValue(...)75%4475%
ConvertScalarValue(...)50%44100%
TryConvertScalarByType(...)25%10428.57%
TryConvertPrimitiveScalar(...)25%551233.33%
TryParseInt32(...)50%22100%
TryParseInt64(...)0%620%
TryParseDouble(...)0%620%
TryParseDecimal(...)50%22100%
TryParseBoolean(...)0%620%
TryParseEnum(...)0%620%
TryChangeType(...)100%210%
ConvertEnumerableToTargetType(...)33.33%651847.36%
TryGetHashtableValue(...)83.33%7671.42%
ConvertFormToHashtable(...)0%4260%
ConvertFormToValue(...)0%506220%
ConvertBsonToHashtable(...)50%7671.42%
BsonValueToClr(...)20.83%1232444.44%
BsonDocumentToHashtable(...)100%22100%
BsonArrayToClrArray(...)0%620%
ConvertCborToHashtable(...)50%7671.42%
CborToClr(...)62.5%8887.5%
ConvertCborMapToHashtable(...)100%22100%
ConvertCborArrayToClrArray(...)0%620%
ConvertCborScalarToClr(...)50%281458.82%
GetCborMapKeyString(...)37.5%88100%
ConvertCsvToHashtable(...)83.33%121295.83%
DecodeBodyStringToBytes(...)50%12860%
TryDecodeBase64(...)59.37%563271.42%
TryDecodeHex(...)0%600240%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/ParameterForInjectionInfo.cs

#LineLine coverage
 1using System.Collections;
 2using System.Globalization;
 3using System.Management.Automation;
 4using System.Reflection;
 5using System.Text.Json;
 6using System.Xml;
 7using System.Xml.Linq;
 8using CsvHelper;
 9using CsvHelper.Configuration;
 10using Kestrun.Logging;
 11using Kestrun.Models;
 12using Kestrun.Utilities;
 13using Microsoft.Extensions.Primitives;
 14using Microsoft.OpenApi;
 15using MongoDB.Bson;
 16using MongoDB.Bson.Serialization;
 17using PeterO.Cbor;
 18using YamlDotNet.Serialization;
 19using YamlDotNet.Serialization.NamingConventions;
 20
 21namespace Kestrun.Languages;
 22
 23/// <summary>
 24/// Information about a parameter to be injected into a script.
 25/// </summary>
 26public class ParameterForInjectionInfo : ParameterForInjectionInfoBase
 27{
 28    private static ParameterMetadata Validate(ParameterMetadata? paramInfo)
 29    {
 3730        ArgumentNullException.ThrowIfNull(paramInfo);
 3631        return paramInfo;
 32    }
 33
 34    /// <summary>
 35    /// Constructs a ParameterForInjectionInfo from an OpenApiParameter.
 36    /// </summary>
 37    /// <param name="paramInfo">The parameter metadata.</param>
 38    /// <param name="parameter">The OpenApiParameter to construct from.</param>
 39    public ParameterForInjectionInfo(ParameterMetadata paramInfo, OpenApiParameter? parameter) :
 940        base(Validate(paramInfo).Name, Validate(paramInfo).ParameterType)
 41    {
 842        ArgumentNullException.ThrowIfNull(parameter);
 743        Type = parameter.Schema?.Type;
 744        DefaultValue = parameter.Schema?.Default;
 745        In = parameter.In;
 746        if (parameter.Content is not null)
 47        {
 048            foreach (var key in parameter.Content.Keys)
 49            {
 050                ContentTypes.Add(key);
 51            }
 52        }
 53        else
 54        {
 755            Explode = parameter.Explode;
 756            Style = parameter.Style;
 57        }
 758    }
 59    /// <summary>
 60    /// Constructs a ParameterForInjectionInfo from an OpenApiRequestBody.
 61    /// </summary>
 62    /// <param name="paramInfo">The parameter metadata.</param>
 63    /// <param name="requestBody">The OpenApiRequestBody to construct from.</param>
 64    public ParameterForInjectionInfo(ParameterMetadata paramInfo, OpenApiRequestBody requestBody) :
 1065        base(Validate(paramInfo).Name, Validate(paramInfo).ParameterType)
 66    {
 1067        ArgumentNullException.ThrowIfNull(requestBody);
 1068        Type = requestBody.Content?.Values.FirstOrDefault()?.Schema?.Type;
 1069        var schema = requestBody.Content?.Values.FirstOrDefault()?.Schema;
 1070        if (schema is OpenApiSchemaReference)
 71        {
 172            Type = JsonSchemaType.Object;
 73        }
 974        else if (schema is OpenApiSchema sch)
 75        {
 976            Type = sch.Type;
 977            DefaultValue = sch.Default;
 78        }
 1079        In = null;
 1080        if (requestBody.Content is not null)
 81        {
 4082            foreach (var key in requestBody.Content.Keys)
 83            {
 1084                ContentTypes.Add(key);
 85            }
 86        }
 1087    }
 88
 89    /// <summary>
 90    /// Adds parameters from the HTTP context to the PowerShell instance.
 91    /// </summary>
 92    /// <param name="context">The current HTTP context.</param>
 93    /// <param name="ps">The PowerShell instance to which parameters will be added.</param>
 94    internal static void InjectParameters(KestrunContext context, PowerShell ps)
 95    {
 1596        if (context.HttpContext.GetEndpoint()?
 1597               .Metadata
 2698               .FirstOrDefault(m => m is List<ParameterForInjectionInfo>) is not List<ParameterForInjectionInfo> paramet
 99        {
 4100            return;
 101        }
 102
 11103        var logger = context.Host.Logger;
 11104        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 105        {
 11106            logger.Debug("Injecting {Count} parameters into PowerShell script.", parameters.Count);
 107        }
 108
 44109        foreach (var param in parameters)
 110        {
 11111            InjectSingleParameter(context, ps, param);
 112        }
 11113    }
 114
 115    /// <summary>
 116    /// Mapping of content types to body conversion functions.
 117    /// </summary>
 1118    private static readonly IReadOnlyDictionary<string, Func<KestrunContext, string, object?>> BodyConverters =
 1119        new Dictionary<string, Func<KestrunContext, string, object?>>(StringComparer.OrdinalIgnoreCase)
 1120        {
 0121            ["application/json"] = (_, raw) => ConvertJsonToHashtable(raw),
 0122            ["application/yaml"] = (_, raw) => ConvertYamlToHashtable(raw),
 1123            // XML conversion needs to consider OpenAPI XML modeling; handled in the callers that have ParameterType.
 0124            ["application/bson"] = (_, raw) => ConvertBsonToHashtable(raw),
 0125            ["application/cbor"] = (_, raw) => ConvertCborToHashtable(raw),
 0126            ["text/csv"] = (_, raw) => ConvertCsvToHashtable(raw),
 1127
 1128            // This one typically needs the request form, not the raw string.
 0129            ["application/x-www-form-urlencoded"] = (ctx, _) => ConvertFormToHashtable(ctx.Request.Form),
 1130        };
 131
 132    /// <summary>
 133    /// Determines whether the body parameter should be converted based on its type information.
 134    /// </summary>
 135    /// <param name="param">The parameter information.</param>
 136    /// <param name="converted">The converted object.</param>
 137    /// <returns>True if the body should be converted; otherwise, false.</returns>
 138    private static bool ShouldConvertBody(ParameterForInjectionInfo param, object? converted) =>
 11139    converted is string && param.Type is null && param.ParameterType is not null && param.ParameterType != typeof(string
 140
 141    /// <summary>
 142    /// Tries to convert the body parameter based on the content types specified.
 143    /// </summary>
 144    /// <param name="context">The current Kestrun context.</param>
 145    /// <param name="param">The parameter information.</param>
 146    /// <param name="rawString">The raw body string.</param>
 147    private static object? TryConvertBodyByContentType(KestrunContext context, ParameterForInjectionInfo param, string r
 148    {
 149        // Collect canonical content types once
 0150        var canonicalTypes = param.ContentTypes
 0151            .Select(MediaTypeHelper.Canonicalize)
 0152            .Where(ct => !string.IsNullOrWhiteSpace(ct))
 0153            .Distinct(StringComparer.OrdinalIgnoreCase);
 154        // Try each content type in order
 0155        foreach (var ct in canonicalTypes)
 156        {
 0157            if (ct.Equals("application/xml", StringComparison.OrdinalIgnoreCase))
 158            {
 0159                return ConvertXmlBodyToParameterType(rawString, param.ParameterType);
 160            }
 161
 0162            if (BodyConverters.TryGetValue(ct, out var converter))
 163            {
 164                // Special-case: form-url-encoded conversion only makes sense with explode/form style.
 0165                if (ct.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) &&
 0166                    !(param.Explode || param.Style == ParameterStyle.Form))
 167                {
 168                    continue;
 169                }
 170                // Use the converter
 0171                return converter(context, rawString);
 172            }
 173        }
 174
 175        // If it's "form" style explode, you can still treat it as a hashtable even without explicit content-type.
 0176        return param.Style == ParameterStyle.Form && param.Explode && context.Request.Form is not null
 0177            ? ConvertFormToHashtable(context.Request.Form)
 0178            : (object?)null;
 0179    }
 180
 181    /// <summary>
 182    /// Injects a single parameter into the PowerShell instance based on its location and type.
 183    /// </summary>
 184    /// <param name="context">The current Kestrun context.</param>
 185    /// <param name="ps">The PowerShell instance to inject parameters into.</param>
 186    /// <param name="param">The parameter information to inject.</param>
 187    private static void InjectSingleParameter(KestrunContext context, PowerShell ps, ParameterForInjectionInfo param)
 188    {
 11189        var logger = context.Host.Logger;
 11190        var name = param.Name;
 191
 11192        LogInjectingParameter(logger, param);
 193
 11194        var converted = GetConvertedParameterValue(context, param, out var shouldLog);
 11195        converted = ConvertBodyParameterIfNeeded(context, param, converted);
 196
 11197        LogAddingParameter(logger, name, converted, shouldLog);
 198
 11199        _ = ps.AddParameter(name, converted);
 11200        StoreResolvedParameter(context, param, name, converted);
 11201    }
 202
 203    /// <summary>
 204    /// Logs the injection of a parameter when debug logging is enabled.
 205    /// </summary>
 206    /// <param name="logger">The host logger.</param>
 207    /// <param name="param">The parameter being injected.</param>
 208    private static void LogInjectingParameter(Serilog.ILogger logger, ParameterForInjectionInfo param)
 209    {
 11210        if (!logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 211        {
 0212            return;
 213        }
 214
 11215        var schemaType = param.Type?.ToString() ?? "<none>";
 11216        var clrType = param.ParameterType?.FullName ?? "<unknown>";
 11217        logger.Debug(
 11218            "Injecting parameter '{Name}' schemaType='{SchemaType}' clrType='{ClrType}' from '{In}'.",
 11219            param.Name,
 11220            schemaType,
 11221            clrType,
 11222            param.In);
 11223    }
 224
 225    /// <summary>
 226    /// Gets the converted parameter value from the current request context.
 227    /// </summary>
 228    /// <param name="context">The current Kestrun context.</param>
 229    /// <param name="param">The parameter metadata.</param>
 230    /// <param name="shouldLog">Whether the value should be logged.</param>
 231    /// <returns>The converted parameter value.</returns>
 232    private static object? GetConvertedParameterValue(KestrunContext context, ParameterForInjectionInfo param, out bool 
 233    {
 11234        shouldLog = true;
 235
 11236        return context.Request.Form is not null && context.Request.HasFormContentType
 11237            ? ConvertFormToValue(context.Request.Form, param)
 11238            : GetParameterValueFromContext(context, param, out shouldLog);
 239    }
 240
 241    /// <summary>
 242    /// Converts a request-body parameter from a raw string to a structured object when possible.
 243    /// </summary>
 244    /// <param name="context">The current Kestrun context.</param>
 245    /// <param name="param">The parameter metadata.</param>
 246    /// <param name="converted">The current converted value.</param>
 247    /// <returns>The updated value, possibly converted to an object/hashtable.</returns>
 248    private static object? ConvertBodyParameterIfNeeded(KestrunContext context, ParameterForInjectionInfo param, object?
 249    {
 11250        if (!ShouldConvertBody(param, converted))
 251        {
 11252            return converted;
 253        }
 254
 0255        var rawString = (string)converted!;
 0256        var bodyObj = TryConvertBodyByContentType(context, param, rawString);
 257
 0258        if (bodyObj is not null)
 259        {
 0260            return bodyObj;
 261        }
 262
 0263        context.Logger.WarningSanitized(
 0264            "Unable to convert body parameter '{Name}' with content types: {ContentTypes}. Using raw string value.",
 0265            param.Name,
 0266            param.ContentTypes);
 267
 0268        return converted;
 269    }
 270
 271    /// <summary>
 272    /// Logs the addition of a parameter to the PowerShell invocation when requested and debug logging is enabled.
 273    /// </summary>
 274    /// <param name="logger">The host logger.</param>
 275    /// <param name="name">The parameter name.</param>
 276    /// <param name="value">The value to be added.</param>
 277    /// <param name="shouldLog">Whether logging should be performed.</param>
 278    private static void LogAddingParameter(Serilog.ILogger logger, string name, object? value, bool shouldLog)
 279    {
 11280        if (!shouldLog || !logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 281        {
 0282            return;
 283        }
 284
 11285        var valueType = value?.GetType().FullName ?? "<null>";
 11286        logger.DebugSanitized("Adding parameter '{Name}' ({ValueType}): {ConvertedValue}", name, valueType, value);
 11287    }
 288
 289    /// <summary>
 290    /// Stores the resolved parameter on the request context, either as the request body or a named parameter.
 291    /// </summary>
 292    /// <param name="context">The current Kestrun context.</param>
 293    /// <param name="param">The parameter metadata.</param>
 294    /// <param name="name">The parameter name.</param>
 295    /// <param name="value">The resolved value.</param>
 296    private static void StoreResolvedParameter(KestrunContext context, ParameterForInjectionInfo param, string name, obj
 297    {
 11298        var resolved = new ParameterForInjectionResolved(param, value);
 11299        if (param.IsRequestBody)
 300        {
 9301            context.Parameters.Body = resolved;
 9302            return;
 303        }
 304
 2305        context.Parameters.Parameters[name] = resolved;
 2306    }
 307
 308    /// <summary>
 309    /// Retrieves and converts the parameter value from the HTTP context.
 310    /// </summary>
 311    /// <param name="context">The current HTTP context.</param>
 312    /// <param name="param">The parameter information.</param>
 313    /// <param name="shouldLog">Indicates whether logging should be performed.</param>
 314    /// <returns>The converted parameter value.</returns>
 315    private static object? GetParameterValueFromContext(KestrunContext context, ParameterForInjectionInfo param, out boo
 316    {
 11317        shouldLog = true;
 11318        var logger = context.Host.Logger;
 11319        var raw = GetRawValue(param, context);
 320
 11321        if (raw is null)
 322        {
 1323            if (param.DefaultValue is not null)
 324            {
 1325                raw = param.DefaultValue.GetValue<object>();
 326            }
 327            else
 328            {
 0329                shouldLog = false;
 0330                return null;
 331            }
 332        }
 333
 11334        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 335        {
 11336            logger.Debug("Raw value for parameter '{Name}': {RawValue}", param.Name, raw);
 337        }
 338
 11339        var (singleValue, multiValue) = NormalizeRaw(raw);
 340
 11341        if (singleValue is null && multiValue is null)
 342        {
 0343            shouldLog = false;
 0344            return null;
 345        }
 346
 11347        return ConvertValue(context, param, singleValue, multiValue);
 348    }
 349
 350    /// <summary>
 351    /// Retrieves the raw value of a parameter from the HTTP context based on its location.
 352    /// </summary>
 353    /// <param name="param">The parameter information.</param>
 354    /// <param name="context">The current HTTP context.</param>
 355    /// <returns>The raw value of the parameter.</returns>
 356    private static object? GetRawValue(ParameterForInjectionInfo param, KestrunContext context)
 357    {
 11358        return param.In switch
 11359        {
 11360            ParameterLocation.Path =>
 0361            context.Request.RouteValues.TryGetValue(param.Name, out var routeVal)
 0362                ? routeVal
 0363                : null,
 11364
 11365            ParameterLocation.Query =>
 2366                context.Request.Query.TryGetValue(param.Name, out var queryVal)
 2367                    ? (string?)queryVal
 2368                    : null,
 11369
 11370            ParameterLocation.Header =>
 0371                context.Request.Headers.TryGetValue(param.Name, out var headerVal)
 0372                    ? (string?)headerVal
 0373                    : null,
 11374
 11375            ParameterLocation.Cookie =>
 0376                context.Request.Cookies.TryGetValue(param.Name, out var cookieVal)
 0377                    ? cookieVal
 0378                    : null,
 9379            null => (context.Request.Form is not null && context.Request.HasFormContentType) ?
 9380                    context.Request.Form :
 9381                    context.Request.Body,
 11382
 0383            _ => null,
 11384        };
 385    }
 386
 387    /// <summary>
 388    /// Normalizes the raw parameter value into single and multi-value forms.
 389    /// </summary>
 390    /// <param name="raw">The raw parameter value.</param>
 391    /// <returns>A tuple containing the single and multi-value forms of the parameter.</returns>
 392    private static (string? single, string?[]? multi) NormalizeRaw(object raw)
 393    {
 11394        string?[]? multiValue = null;
 395
 396        string? singleValue;
 397        switch (raw)
 398        {
 399            case StringValues sv:
 0400                multiValue = [.. sv];
 0401                singleValue = sv.Count > 0 ? sv[0] : null;
 0402                break;
 403
 404            case string s:
 10405                singleValue = s;
 10406                break;
 407
 408            default:
 1409                singleValue = raw?.ToString();
 410                break;
 411        }
 412
 11413        return (singleValue, multiValue);
 414    }
 415
 416    /// <summary>
 417    /// Converts the parameter value to the appropriate type based on the JSON schema type.
 418    /// </summary>
 419    /// <param name="context">The current HTTP context.</param>
 420    /// <param name="param">The parameter information.</param>
 421    /// <param name="singleValue">The single value of the parameter.</param>
 422    /// <param name="multiValue">The multi-value of the parameter.</param>
 423    /// <returns>The converted parameter value.</returns>
 424    private static object? ConvertValue(KestrunContext context, ParameterForInjectionInfo param,
 425    string? singleValue, string?[]? multiValue)
 426    {
 427        // Convert based on schema type
 11428        return param.Type switch
 11429        {
 2430            JsonSchemaType.Integer => int.TryParse(singleValue, out var i) ? (int?)i : null,
 0431            JsonSchemaType.Number => double.TryParse(singleValue, out var d) ? (double?)d : null,
 0432            JsonSchemaType.Boolean => bool.TryParse(singleValue, out var b) ? (bool?)b : null,
 0433            JsonSchemaType.Array => multiValue ?? (singleValue is not null ? new[] { singleValue } : null), // keep your
 9434            JsonSchemaType.Object => param.IsRequestBody
 9435                                    ? ConvertBodyBasedOnContentType(context, singleValue ?? "", param)
 9436                                    : singleValue,
 0437            JsonSchemaType.String => singleValue,
 0438            _ => singleValue,
 11439        };
 440    }
 441
 442    /// <summary>
 443    /// Converts the request body based on the Content-Type header.
 444    /// </summary>
 445    /// <param name="context">The current Kestrun context.</param>
 446    /// <param name="rawBodyString">The raw body string from the request.</param>
 447    /// <param name="param">The parameter information.</param>
 448    /// <returns>The converted body object.</returns>
 449    /// <exception cref="InvalidOperationException">Thrown when the Content-Type header is missing and cannot convert bo
 450    private static object? ConvertBodyBasedOnContentType(
 451        KestrunContext context,
 452        string rawBodyString,
 453        ParameterForInjectionInfo param)
 454    {
 9455        var isSingleContentType = param.ContentTypes.Count == 1;
 456
 9457        var requestMediaType = MediaTypeHelper.Canonicalize(context.Request.ContentType);
 458
 9459        if (string.IsNullOrEmpty(requestMediaType))
 460        {
 1461            if (!isSingleContentType)
 462            {
 0463                throw new InvalidOperationException(
 0464                    "Content-Type header is missing; cannot convert body to object.");
 465            }
 466
 1467            var inferred = MediaTypeHelper.Canonicalize(param.ContentTypes[0]);
 1468            return ConvertByCanonicalMediaType(inferred, context, rawBodyString, param);
 469        }
 470
 8471        return ConvertByCanonicalMediaType(requestMediaType, context, rawBodyString, param);
 472    }
 473
 474    /// <summary>
 475    /// Converts the body string to an object based on the canonical media type.
 476    /// </summary>
 477    /// <param name="canonicalMediaType">   The canonical media type of the request body.</param>
 478    /// <param name="context"> The current Kestrun context.</param>
 479    /// <param name="rawBodyString"> The raw body string from the request.</param>
 480    /// <param name="param">The parameter information.</param>
 481    /// <returns> The converted body object.</returns>
 482    private static object? ConvertByCanonicalMediaType(
 483        string canonicalMediaType,
 484        KestrunContext context,
 485        string rawBodyString,
 486        ParameterForInjectionInfo param)
 487    {
 9488        return canonicalMediaType switch
 9489        {
 2490            "application/json" => ConvertJsonToHashtable(rawBodyString),
 1491            "application/yaml" => ConvertYamlToHashtable(rawBodyString),
 2492            "application/xml" => ConvertXmlBodyToParameterType(rawBodyString, param.ParameterType),
 1493            "application/bson" => ConvertBsonToHashtable(rawBodyString),
 1494            "application/cbor" => ConvertCborToHashtable(rawBodyString),
 2495            "text/csv" => ConvertCsvToHashtable(rawBodyString),
 9496            "application/x-www-form-urlencoded" =>
 0497                ConvertFormToHashtable(context.Request.Form),
 0498            _ => rawBodyString,
 9499        };
 500    }
 501
 502    /// <summary>
 503    /// CBOR deserializer instance.
 504    /// </summary>
 1505    private static readonly IDeserializer YamlDeserializer =
 1506    new DeserializerBuilder()
 1507        .WithNamingConvention(CamelCaseNamingConvention.Instance)
 1508        .Build();
 509
 510    private static Hashtable? ConvertYamlToHashtable(string yaml)
 511    {
 1512        if (string.IsNullOrWhiteSpace(yaml))
 513        {
 0514            return null;
 515        }
 516
 517        // Top-level YAML mapping → Hashtable
 1518        var ht = YamlDeserializer.Deserialize<Hashtable>(yaml);
 1519        return ht;
 520    }
 521    private static object? ConvertJsonToHashtable(string? json)
 522    {
 2523        if (string.IsNullOrWhiteSpace(json))
 524        {
 0525            return null;
 526        }
 527
 2528        using var doc = JsonDocument.Parse(json);
 2529        return JsonElementToClr(doc.RootElement);
 2530    }
 531
 532    private static object? JsonElementToClr(JsonElement element)
 533    {
 6534        return element.ValueKind switch
 6535        {
 2536            JsonValueKind.Object => ToHashtable(element),
 0537            JsonValueKind.Array => ToArray(element),
 2538            JsonValueKind.String => element.GetString(),
 2539            JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(),
 0540            JsonValueKind.True => true,
 0541            JsonValueKind.False => false,
 0542            JsonValueKind.Null => null,
 0543            JsonValueKind.Undefined => null,
 0544            _ => null
 6545        };
 546    }
 547
 548    private static Hashtable ToHashtable(JsonElement element)
 549    {
 2550        var ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
 12551        foreach (var prop in element.EnumerateObject())
 552        {
 4553            ht[prop.Name] = JsonElementToClr(prop.Value);
 554        }
 2555        return ht;
 556    }
 557
 558    private static object?[] ToArray(JsonElement element)
 559    {
 0560        var list = new List<object?>();
 0561        foreach (var item in element.EnumerateArray())
 562        {
 0563            list.Add(JsonElementToClr(item));
 564        }
 0565        return [.. list];
 566    }
 567
 568    private const int MaxObjectBindingDepth = 32;
 569
 570    private static object? ConvertXmlBodyToParameterType(string xml, Type parameterType)
 571    {
 3572        if (string.IsNullOrWhiteSpace(xml))
 573        {
 0574            return null;
 575        }
 576
 577        XElement root;
 578        try
 579        {
 580            // Clients often include an XML declaration with an encoding (e.g. UTF-8). When parsing from a .NET
 581            // string (already decoded), some parsers can reject mismatched/pointless encoding declarations.
 582            // Strip the declaration if present.
 3583            var cleaned = xml.TrimStart('\uFEFF', '\u200B', '\u0000', ' ', '\t', '\r', '\n');
 3584            if (cleaned.StartsWith("<?xml", StringComparison.OrdinalIgnoreCase))
 585            {
 2586                var endDecl = cleaned.IndexOf("?>", StringComparison.Ordinal);
 2587                if (endDecl >= 0)
 588                {
 2589                    cleaned = cleaned[(endDecl + 2)..].TrimStart();
 590                }
 591            }
 592
 593            XDocument doc;
 594            try
 595            {
 3596                doc = XDocument.Parse(cleaned);
 3597            }
 0598            catch
 599            {
 0600                var settings = new XmlReaderSettings
 0601                {
 0602                    DtdProcessing = DtdProcessing.Prohibit,
 0603                    XmlResolver = null,
 0604                };
 605
 0606                using var reader = XmlReader.Create(new StringReader(cleaned), settings);
 0607                doc = XDocument.Load(reader);
 0608            }
 609
 3610            root = doc.Root ?? throw new InvalidOperationException("XML document has no root element.");
 3611        }
 0612        catch
 613        {
 0614            return null;
 615        }
 616
 617        // If the parameter expects a string, don't attempt to parse.
 3618        if (parameterType == typeof(string))
 619        {
 0620            return xml;
 621        }
 622
 3623        var xmlMetadata = XmlHelper.GetOpenApiXmlMetadataForType(parameterType);
 3624        var wrapped = XmlHelper.ToHashtable(root, xmlMetadata);
 625
 626        // Normalize from XmlHelper's { RootName = { ... } } shape into the element map itself.
 3627        var rootMap = ExtractRootMapForBinding(wrapped, root.Name.LocalName);
 3628        if (rootMap is null)
 629        {
 0630            return null;
 631        }
 632
 3633        NormalizeWrappedArrays(rootMap, xmlMetadata);
 634
 635        // For PowerShell script classes, the runtime may produce a new (dynamic) type in the request runspace.
 636        // Creating an instance here (using a Type captured during route registration) can produce a type-identity
 637        // mismatch at parameter-binding time ("cannot convert Product to Product").
 638        // Returning a hashtable lets PowerShell perform the conversion to the *current* runspace type.
 3639        if (parameterType == typeof(object) || typeof(IDictionary).IsAssignableFrom(parameterType) || parameterType.Asse
 640        {
 1641            return rootMap;
 642        }
 643        // Otherwise, attempt to convert to the target type.
 2644        return ConvertHashtableToObject(rootMap, parameterType, depth: 0);
 0645    }
 646
 647    private static Hashtable? ExtractRootMapForBinding(Hashtable wrapped, string rootLocalName)
 648    {
 649        // XmlHelper.ToHashtable returns { rootName = childMap } plus any mapped attributes at the same level.
 3650        if (!TryGetHashtableValue(wrapped, rootLocalName, out var rootObj))
 651        {
 652            // Fallback: if there's exactly one entry and it's a hashtable, use it.
 0653            if (wrapped.Count == 1)
 654            {
 0655                var only = wrapped.Values.Cast<object?>().FirstOrDefault();
 0656                return only as Hashtable;
 657            }
 0658            return wrapped;
 659        }
 660
 3661        if (rootObj is not Hashtable rootMap)
 662        {
 0663            return null;
 664        }
 665
 666        // Merge any sibling keys (e.g., metadata-guided attributes) into the root map.
 16667        foreach (DictionaryEntry entry in wrapped)
 668        {
 5669            if (entry.Key is not string key)
 670            {
 671                continue;
 672            }
 673
 5674            if (string.Equals(key, rootLocalName, StringComparison.OrdinalIgnoreCase))
 675            {
 676                continue;
 677            }
 678
 2679            rootMap[key] = entry.Value;
 680        }
 681
 3682        return rootMap;
 683    }
 684
 685    /// <summary>
 686    /// Normalizes wrapped arrays in the root map based on XML metadata.
 687    /// </summary>
 688    /// <param name="rootMap">The root hashtable map.</param>
 689    /// <param name="xmlMetadata">The XML metadata hashtable.</param>
 690    private static void NormalizeWrappedArrays(Hashtable rootMap, Hashtable? xmlMetadata)
 691    {
 3692        if (!TryGetXmlMetadataProperties(xmlMetadata, out var propsHash))
 693        {
 1694            return;
 695        }
 696
 20697        foreach (DictionaryEntry entry in propsHash)
 698        {
 8699            if (!TryGetWrappedArrayMetadata(entry, out var propertyName, out var xmlName))
 700            {
 701                continue;
 702            }
 703
 2704            if (!TryGetWrapperHashtable(rootMap, propertyName, xmlName, out var wrapper))
 705            {
 706                continue;
 707            }
 708
 2709            var unwrapped = TryUnwrapWrapper(wrapper);
 2710            if (unwrapped is not null)
 711            {
 2712                rootMap[propertyName] = unwrapped;
 713            }
 714        }
 2715    }
 716
 717    /// <summary>
 718    /// Attempts to retrieve the <c>Properties</c> hashtable from XML metadata.
 719    /// </summary>
 720    /// <param name="xmlMetadata">XML metadata hashtable.</param>
 721    /// <param name="properties">The properties hashtable when present.</param>
 722    /// <returns><c>true</c> when the properties hashtable exists; otherwise <c>false</c>.</returns>
 723    private static bool TryGetXmlMetadataProperties(Hashtable? xmlMetadata, out Hashtable properties)
 724    {
 3725        if (xmlMetadata?["Properties"] is Hashtable propsHash)
 726        {
 2727            properties = propsHash;
 2728            return true;
 729        }
 730
 1731        properties = default!;
 1732        return false;
 733    }
 734
 735    /// <summary>
 736    /// Extracts metadata for a wrapped array property from a single <see cref="DictionaryEntry"/>.
 737    /// </summary>
 738    /// <param name="entry">Entry from <c>xmlMetadata.Properties</c>.</param>
 739    /// <param name="propertyName">The CLR property name.</param>
 740    /// <param name="xmlName">The XML element name (or the CLR name when not overridden).</param>
 741    /// <returns><c>true</c> when the entry describes a wrapped property; otherwise <c>false</c>.</returns>
 742    private static bool TryGetWrappedArrayMetadata(DictionaryEntry entry, out string propertyName, out string xmlName)
 743    {
 8744        if (entry.Key is not string propName || entry.Value is not Hashtable propMeta)
 745        {
 0746            propertyName = default!;
 0747            xmlName = default!;
 0748            return false;
 749        }
 750
 8751        if (propMeta["Wrapped"] is not bool wrapped || !wrapped)
 752        {
 6753            propertyName = default!;
 6754            xmlName = default!;
 6755            return false;
 756        }
 757
 2758        propertyName = propName;
 2759        xmlName = propMeta["Name"] as string ?? propName;
 2760        return true;
 761    }
 762
 763    /// <summary>
 764    /// Attempts to find a wrapper hashtable for a wrapped array property in the root map.
 765    /// </summary>
 766    /// <param name="rootMap">The root map produced by XML parsing.</param>
 767    /// <param name="propertyName">CLR property name to search.</param>
 768    /// <param name="xmlName">XML element name to search (fallback).</param>
 769    /// <param name="wrapper">The wrapper hashtable if found.</param>
 770    /// <returns><c>true</c> when a wrapper hashtable is found; otherwise <c>false</c>.</returns>
 771    private static bool TryGetWrapperHashtable(Hashtable rootMap, string propertyName, string xmlName, out Hashtable wra
 772    {
 2773        if (!TryGetHashtableValue(rootMap, propertyName, out var raw)
 2774            && !TryGetHashtableValue(rootMap, xmlName, out raw))
 775        {
 0776            wrapper = default!;
 0777            return false;
 778        }
 779
 2780        if (raw is Hashtable wrapperHash)
 781        {
 2782            wrapper = wrapperHash;
 2783            return true;
 784        }
 785
 0786        wrapper = default!;
 0787        return false;
 788    }
 789
 790    /// <summary>
 791    /// Unwraps a wrapper hashtable into an item list/value when possible.
 792    /// </summary>
 793    /// <param name="wrapper">Wrapper hashtable.</param>
 794    /// <returns>The unwrapped value, or <c>null</c> if it cannot be unwrapped.</returns>
 795    private static object? TryUnwrapWrapper(Hashtable wrapper)
 796    {
 2797        return TryGetHashtableValue(wrapper, "Item", out var itemValue)
 2798            ? itemValue
 2799            : wrapper.Count == 1
 2800                ? wrapper.Values.Cast<object?>().FirstOrDefault()
 2801                : null;
 802    }
 803
 804    private static object? ConvertHashtableToObject(Hashtable data, Type targetType, int depth)
 805    {
 2806        if (depth >= MaxObjectBindingDepth)
 807        {
 0808            return null;
 809        }
 810
 2811        var instance = Activator.CreateInstance(targetType, nonPublic: true);
 2812        if (instance is null)
 813        {
 0814            return null;
 815        }
 816
 2817        var props = targetType
 2818            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
 8819            .Where(p => p.CanWrite && p.SetMethod is not null)
 10820            .ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
 821
 2822        var fields = targetType
 2823            .GetFields(BindingFlags.Public | BindingFlags.Instance)
 2824            .ToDictionary(f => f.Name, StringComparer.OrdinalIgnoreCase);
 825
 20826        foreach (DictionaryEntry entry in data)
 827        {
 8828            if (entry.Key is not string rawKey)
 829            {
 830                continue;
 831            }
 832
 8833            var key = rawKey.StartsWith('@') ? rawKey[1..] : rawKey;
 834
 8835            if (props.TryGetValue(key, out var prop))
 836            {
 8837                var converted = ConvertToTargetType(entry.Value, prop.PropertyType, depth + 1);
 8838                prop.SetValue(instance, converted);
 8839                continue;
 840            }
 841
 0842            if (fields.TryGetValue(key, out var field))
 843            {
 0844                var converted = ConvertToTargetType(entry.Value, field.FieldType, depth + 1);
 0845                field.SetValue(instance, converted);
 846            }
 847        }
 848
 2849        return instance;
 850    }
 851
 852    /// <summary>
 853    /// Converts a value to the specified target type, handling complex objects and collections.
 854    /// </summary>
 855    /// <param name="value">The value to convert.</param>
 856    /// <param name="targetType">The target type to convert to.</param>
 857    /// <param name="depth">The current recursion depth.</param>
 858    /// <returns>The converted value, or null if conversion is not possible.</returns>
 859    private static object? ConvertToTargetType(object? value, Type targetType, int depth)
 860    {
 14861        if (value is null)
 862        {
 0863            return null;
 864        }
 865
 14866        targetType = UnwrapNullableTargetType(targetType);
 867
 14868        return targetType.IsInstanceOfType(value)
 14869            ? value
 14870            : TryConvertHashtableValue(value, targetType, depth, out var convertedFromHashtable)
 14871                ? convertedFromHashtable
 14872                : TryConvertListOrArrayValue(value, targetType, depth, out var convertedFromEnumerable)
 14873                    ? convertedFromEnumerable
 14874                    : ConvertScalarValue(value, targetType);
 875    }
 876
 877    /// <summary>
 878    /// Unwraps a nullable target type to its underlying non-nullable type.
 879    /// </summary>
 880    /// <param name="targetType">The target type.</param>
 881    /// <returns>The underlying non-nullable type, or the original type when not nullable.</returns>
 882    private static Type UnwrapNullableTargetType(Type targetType)
 14883        => Nullable.GetUnderlyingType(targetType) ?? targetType;
 884
 885    /// <summary>
 886    /// Converts a hashtable value into the target type when applicable.
 887    /// </summary>
 888    /// <param name="value">Value to convert.</param>
 889    /// <param name="targetType">Target type.</param>
 890    /// <param name="depth">Current recursion depth.</param>
 891    /// <param name="converted">Converted result.</param>
 892    /// <returns><c>true</c> when the value was handled; otherwise <c>false</c>.</returns>
 893    private static bool TryConvertHashtableValue(object value, Type targetType, int depth, out object? converted)
 894    {
 6895        if (value is not Hashtable ht)
 896        {
 6897            converted = null;
 6898            return false;
 899        }
 900
 0901        converted = typeof(IDictionary).IsAssignableFrom(targetType)
 0902            ? ht
 0903            : ConvertHashtableToObject(ht, targetType, depth);
 0904        return true;
 905    }
 906
 907    /// <summary>
 908    /// Converts list/array values into the target type when applicable.
 909    /// </summary>
 910    /// <param name="value">Value to convert.</param>
 911    /// <param name="targetType">Target type.</param>
 912    /// <param name="depth">Current recursion depth.</param>
 913    /// <param name="converted">Converted result.</param>
 914    /// <returns><c>true</c> when the value was handled; otherwise <c>false</c>.</returns>
 915    private static bool TryConvertListOrArrayValue(object value, Type targetType, int depth, out object? converted)
 916    {
 6917        if (value is List<object?> list)
 918        {
 2919            converted = ConvertEnumerableToTargetType(list, targetType, depth);
 2920            return true;
 921        }
 922
 4923        if (value is object?[] arr)
 924        {
 0925            converted = ConvertEnumerableToTargetType(arr, targetType, depth);
 0926            return true;
 927        }
 928
 4929        converted = null;
 4930        return false;
 931    }
 932
 933    /// <summary>
 934    /// Converts a scalar (non-hashtable, non-collection) value into the target type.
 935    /// </summary>
 936    /// <param name="value">Scalar value to convert.</param>
 937    /// <param name="targetType">Target type.</param>
 938    /// <returns>The converted value, or <c>null</c> when conversion fails.</returns>
 939    private static object? ConvertScalarValue(object value, Type targetType)
 940    {
 4941        var str = value as string ?? Convert.ToString(value, CultureInfo.InvariantCulture);
 942
 4943        return TryConvertScalarByType(str, targetType, out var converted)
 4944            ? converted
 4945            : TryChangeType(value, targetType);
 946    }
 947
 948    /// <summary>
 949    /// Attempts to convert a scalar string representation to common primitive target types.
 950    /// </summary>
 951    /// <param name="str">String representation of the value.</param>
 952    /// <param name="targetType">Target type.</param>
 953    /// <param name="converted">Converted result.</param>
 954    /// <returns><c>true</c> when converted; otherwise <c>false</c>.</returns>
 955    private static bool TryConvertScalarByType(string? str, Type targetType, out object? converted)
 956    {
 4957        if (TryConvertPrimitiveScalar(str, targetType, out converted))
 958        {
 4959            return true;
 960        }
 961
 0962        if (targetType.IsEnum)
 963        {
 0964            converted = TryParseEnum(targetType, str);
 0965            return converted is not null;
 966        }
 967
 0968        converted = null;
 0969        return false;
 970    }
 971
 972    /// <summary>
 973    /// Attempts to convert a scalar string representation into a primitive CLR type.
 974    /// </summary>
 975    /// <param name="str">String representation of the value.</param>
 976    /// <param name="targetType">Target type.</param>
 977    /// <param name="converted">Converted result.</param>
 978    /// <returns><c>true</c> when converted; otherwise <c>false</c>.</returns>
 979    private static bool TryConvertPrimitiveScalar(string? str, Type targetType, out object? converted)
 980    {
 4981        switch (System.Type.GetTypeCode(targetType))
 982        {
 983            case TypeCode.String:
 0984                converted = str;
 0985                return true;
 986            case TypeCode.Int32:
 2987                converted = TryParseInt32(str);
 2988                return converted is not null;
 989            case TypeCode.Int64:
 0990                converted = TryParseInt64(str);
 0991                return converted is not null;
 992            case TypeCode.Double:
 0993                converted = TryParseDouble(str);
 0994                return converted is not null;
 995            case TypeCode.Decimal:
 2996                converted = TryParseDecimal(str);
 2997                return converted is not null;
 998            case TypeCode.Boolean:
 0999                converted = TryParseBoolean(str);
 01000                return converted is not null;
 1001            default:
 01002                converted = null;
 01003                return false;
 1004        }
 1005    }
 1006
 1007    /// <summary>
 1008    /// Attempts to parse an <see cref="int"/> from a string.
 1009    /// </summary>
 1010    /// <param name="str">String representation.</param>
 1011    /// <returns>The parsed value, or <c>null</c> when parsing fails.</returns>
 1012    private static int? TryParseInt32(string? str)
 21013        => int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : null;
 1014
 1015    /// <summary>
 1016    /// Attempts to parse a <see cref="long"/> from a string.
 1017    /// </summary>
 1018    /// <param name="str">String representation.</param>
 1019    /// <returns>The parsed value, or <c>null</c> when parsing fails.</returns>
 1020    private static long? TryParseInt64(string? str)
 01021        => long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l) ? l : null;
 1022
 1023    /// <summary>
 1024    /// Attempts to parse a <see cref="double"/> from a string.
 1025    /// </summary>
 1026    /// <param name="str">String representation.</param>
 1027    /// <returns>The parsed value, or <c>null</c> when parsing fails.</returns>
 1028    private static double? TryParseDouble(string? str)
 01029        => double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) ? d : null;
 1030
 1031    /// <summary>
 1032    /// Attempts to parse a <see cref="decimal"/> from a string.
 1033    /// </summary>
 1034    /// <param name="str">String representation.</param>
 1035    /// <returns>The parsed value, or <c>null</c> when parsing fails.</returns>
 1036    private static decimal? TryParseDecimal(string? str)
 21037        => decimal.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec) ? dec : null;
 1038
 1039    /// <summary>
 1040    /// Attempts to parse a <see cref="bool"/> from a string.
 1041    /// </summary>
 1042    /// <param name="str">String representation.</param>
 1043    /// <returns>The parsed value, or <c>null</c> when parsing fails.</returns>
 1044    private static bool? TryParseBoolean(string? str)
 01045        => bool.TryParse(str, out var b) ? b : null;
 1046
 1047    /// <summary>
 1048    /// Attempts to parse an enum value from a string.
 1049    /// </summary>
 1050    /// <param name="targetType">Enum type.</param>
 1051    /// <param name="str">String representation.</param>
 1052    /// <returns>The parsed enum value, or <c>null</c> when parsing fails.</returns>
 1053    private static object? TryParseEnum(Type targetType, string? str)
 1054    {
 01055        if (str is null)
 1056        {
 01057            return null;
 1058        }
 1059
 1060        try
 1061        {
 01062            return Enum.Parse(targetType, str, ignoreCase: true);
 1063        }
 01064        catch
 1065        {
 01066            return null;
 1067        }
 01068    }
 1069
 1070    /// <summary>
 1071    /// Attempts a generic scalar conversion via <see cref="Convert.ChangeType(object, Type, IFormatProvider)"/>.
 1072    /// </summary>
 1073    /// <param name="value">Source value.</param>
 1074    /// <param name="targetType">Target type.</param>
 1075    /// <returns>The converted value, or <c>null</c> when conversion fails.</returns>
 1076    private static object? TryChangeType(object value, Type targetType)
 1077    {
 1078        try
 1079        {
 01080            return Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
 1081        }
 01082        catch
 1083        {
 01084            return null;
 1085        }
 01086    }
 1087
 1088    private static object? ConvertEnumerableToTargetType(IEnumerable enumerable, Type targetType, int depth)
 1089    {
 21090        if (targetType.IsArray)
 1091        {
 21092            var elementType = targetType.GetElementType() ?? typeof(object);
 21093            var items = new List<object?>();
 161094            foreach (var item in enumerable)
 1095            {
 61096                items.Add(ConvertToTargetType(item, elementType, depth + 1));
 1097            }
 1098
 21099            var arr = Array.CreateInstance(elementType, items.Count);
 161100            for (var i = 0; i < items.Count; i++)
 1101            {
 61102                arr.SetValue(items[i], i);
 1103            }
 1104
 21105            return arr;
 1106        }
 1107
 1108        // Default to List<T> for generic IEnumerable targets.
 01109        if (targetType.IsGenericType)
 1110        {
 01111            var genDef = targetType.GetGenericTypeDefinition();
 01112            if (genDef == typeof(List<>) || genDef == typeof(IList<>) || genDef == typeof(IEnumerable<>))
 1113            {
 01114                var elementType = targetType.GetGenericArguments()[0];
 01115                var listType = typeof(List<>).MakeGenericType(elementType);
 01116                var list = (IList)Activator.CreateInstance(listType)!;
 01117                foreach (var item in enumerable)
 1118                {
 01119                    _ = list.Add(ConvertToTargetType(item, elementType, depth + 1));
 1120                }
 01121                return list;
 1122            }
 1123        }
 1124
 01125        return null;
 1126    }
 1127
 1128    private static bool TryGetHashtableValue(Hashtable table, string key, out object? value)
 1129    {
 331130        foreach (DictionaryEntry entry in table)
 1131        {
 131132            if (entry.Key is string s && string.Equals(s, key, StringComparison.OrdinalIgnoreCase))
 1133            {
 71134                value = entry.Value;
 71135                return true;
 1136            }
 1137        }
 1138
 01139        value = null;
 01140        return false;
 71141    }
 1142
 1143    /// <summary>
 1144    /// Converts a form dictionary to a hashtable.
 1145    /// </summary>
 1146    /// <param name="form">The form dictionary to convert.</param>
 1147    /// <returns>A hashtable representing the form data.</returns>
 1148    private static Hashtable? ConvertFormToHashtable(Dictionary<string, string>? form)
 1149    {
 01150        if (form is null || form.Count == 0)
 1151        {
 01152            return null;
 1153        }
 1154
 01155        var ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
 1156
 01157        foreach (var kvp in form)
 1158        {
 1159            // x-www-form-urlencoded in your case has a single value per key
 01160            ht[kvp.Key] = kvp.Value;
 1161        }
 1162
 01163        return ht;
 1164    }
 1165
 1166    private static object? ConvertFormToValue(Dictionary<string, string>? form, ParameterForInjectionInfo param)
 1167    {
 01168        if (form is null || form.Count == 0)
 1169        {
 01170            return null;
 1171        }
 1172
 1173        // If the parameter is a simple type, return the first key if there's only one key-value pair
 1174        // and it's a simple type (not an object or array)
 01175        return param.Type is JsonSchemaType.Integer or JsonSchemaType.Number or JsonSchemaType.Boolean or JsonSchemaType
 01176            ? form.Count == 1 ? form.First().Key : null
 01177            : ConvertFormToHashtable(form);
 1178    }
 1179
 1180    private static object? ConvertBsonToHashtable(string bson)
 1181    {
 11182        if (string.IsNullOrWhiteSpace(bson))
 1183        {
 01184            return null;
 1185        }
 1186
 11187        var bytes = DecodeBodyStringToBytes(bson);
 11188        if (bytes is null || bytes.Length == 0)
 1189        {
 01190            return null;
 1191        }
 1192
 11193        var doc = BsonSerializer.Deserialize<BsonDocument>(bytes);
 11194        return BsonValueToClr(doc);
 1195    }
 1196
 1197    private static object? BsonValueToClr(BsonValue value)
 1198    {
 31199        return value is null || value.IsBsonNull
 31200            ? null
 31201            : value.BsonType switch
 31202            {
 11203                BsonType.Document => BsonDocumentToHashtable(value.AsBsonDocument),
 01204                BsonType.Array => BsonArrayToClrArray(value.AsBsonArray),
 01205                BsonType.Boolean => value.AsBoolean,
 11206                BsonType.Int32 => value.AsInt32,
 01207                BsonType.Int64 => value.AsInt64,
 01208                BsonType.Double => value.AsDouble,
 01209                BsonType.Decimal128 => value.AsDecimal,
 11210                BsonType.String => value.AsString,
 01211                BsonType.DateTime => value.ToUniversalTime(),
 01212                BsonType.ObjectId => value.AsObjectId.ToString(),
 01213                BsonType.Binary => value.AsBsonBinaryData.Bytes,
 01214                BsonType.Null => null,
 01215                _ => value.ToString(),
 31216            };
 1217    }
 1218
 1219    private static Hashtable BsonDocumentToHashtable(BsonDocument doc)
 1220    {
 11221        var ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
 61222        foreach (var element in doc.Elements)
 1223        {
 21224            ht[element.Name] = BsonValueToClr(element.Value);
 1225        }
 1226
 11227        return ht;
 1228    }
 1229
 1230    private static object?[] BsonArrayToClrArray(BsonArray arr)
 1231    {
 01232        var list = new object?[arr.Count];
 01233        for (var i = 0; i < arr.Count; i++)
 1234        {
 01235            list[i] = BsonValueToClr(arr[i]);
 1236        }
 1237
 01238        return list;
 1239    }
 1240
 1241    private static object? ConvertCborToHashtable(string cbor)
 1242    {
 11243        if (string.IsNullOrWhiteSpace(cbor))
 1244        {
 01245            return null;
 1246        }
 1247
 11248        var bytes = DecodeBodyStringToBytes(cbor);
 11249        if (bytes is null || bytes.Length == 0)
 1250        {
 01251            return null;
 1252        }
 1253
 11254        var obj = CBORObject.DecodeFromBytes(bytes);
 11255        return CborToClr(obj);
 1256    }
 1257
 1258    /// <summary>
 1259    /// Converts a CBORObject to a CLR object (Hashtable, array, or scalar).
 1260    /// </summary>
 1261    /// <param name="obj">The CBORObject to convert.</param>
 1262    /// <returns>A CLR object representation of the CBORObject.</returns>
 1263    private static object? CborToClr(CBORObject obj)
 1264    {
 31265        return obj is null || obj.IsNull
 31266            ? null
 31267            : obj.Type switch
 31268            {
 11269                CBORType.Map => ConvertCborMapToHashtable(obj),
 01270                CBORType.Array => ConvertCborArrayToClrArray(obj),
 21271                _ => ConvertCborScalarToClr(obj),
 31272            };
 1273    }
 1274
 1275    /// <summary>
 1276    /// Converts a CBOR map into a CLR <see cref="Hashtable"/>.
 1277    /// </summary>
 1278    /// <param name="map">The CBOR object expected to be of type <see cref="CBORType.Map"/>.</param>
 1279    /// <returns>A case-insensitive hashtable representing the map.</returns>
 1280    private static Hashtable ConvertCborMapToHashtable(CBORObject map)
 1281    {
 11282        var ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
 61283        foreach (var key in map.Keys)
 1284        {
 21285            var keyString = GetCborMapKeyString(key);
 21286            ht[keyString] = CborToClr(map[key]);
 1287        }
 1288
 11289        return ht;
 1290    }
 1291
 1292    /// <summary>
 1293    /// Converts a CBOR array into a CLR object array.
 1294    /// </summary>
 1295    /// <param name="array">The CBOR object expected to be of type <see cref="CBORType.Array"/>.</param>
 1296    /// <returns>An array of converted elements.</returns>
 1297    private static object?[] ConvertCborArrayToClrArray(CBORObject array)
 1298    {
 01299        var list = new object?[array.Count];
 01300        for (var i = 0; i < array.Count; i++)
 1301        {
 01302            list[i] = CborToClr(array[i]);
 1303        }
 1304
 01305        return list;
 1306    }
 1307
 1308    /// <summary>
 1309    /// Converts a CBOR scalar value (number, string, boolean, byte string, etc.) into a CLR value.
 1310    /// </summary>
 1311    /// <param name="scalar">The CBOR scalar to convert.</param>
 1312    /// <returns>The converted CLR value.</returns>
 1313    private static object? ConvertCborScalarToClr(CBORObject scalar)
 1314    {
 21315        if (scalar.IsNumber)
 1316        {
 1317            // Prefer integral if representable; else double/decimal as available.
 11318            var number = scalar.AsNumber();
 11319            if (number.CanFitInInt64())
 1320            {
 11321                return number.ToInt64Checked();
 1322            }
 1323
 01324            if (number.CanFitInDouble())
 1325            {
 01326                return scalar.ToObject<double>();
 1327            }
 1328
 1329            // For extremely large/precise numbers, keep a string representation.
 01330            return number.ToString();
 1331        }
 1332
 11333        if (scalar.Type == CBORType.Boolean)
 1334        {
 01335            return scalar.AsBoolean();
 1336        }
 1337
 11338        if (scalar.Type == CBORType.ByteString)
 1339        {
 01340            return scalar.GetByteString();
 1341        }
 1342
 1343        // TextString, SimpleValue, etc.
 11344        return scalar.Type switch
 11345        {
 11346            CBORType.TextString => scalar.AsString(),
 01347            CBORType.SimpleValue => scalar.ToString(),
 01348            _ => scalar.ToString(),
 11349        };
 1350    }
 1351
 1352    /// <summary>
 1353    /// Converts a CBOR map key into a CLR string key.
 1354    /// </summary>
 1355    /// <param name="key">The CBOR key object.</param>
 1356    /// <returns>A best-effort string representation of the key.</returns>
 1357    private static string GetCborMapKeyString(CBORObject? key)
 1358    {
 21359        return key is not null && key.Type == CBORType.TextString
 21360            ? key.AsString()
 21361            : (key?.ToString() ?? string.Empty);
 1362    }
 1363
 1364    /// <summary>
 1365    /// Converts a CSV string to a hashtable or array of hashtables.
 1366    /// </summary>
 1367    /// <param name="csv">The CSV string to convert.</param>
 1368    /// <returns>A hashtable if one record is present, an array of hashtables if multiple records are present, or null i
 1369    private static object? ConvertCsvToHashtable(string csv)
 1370    {
 21371        if (string.IsNullOrWhiteSpace(csv))
 1372        {
 01373            return null;
 1374        }
 1375
 21376        using var reader = new StringReader(csv);
 21377        var config = new CsvConfiguration(CultureInfo.InvariantCulture)
 21378        {
 21379            HasHeaderRecord = true,
 21380            BadDataFound = null,
 21381            MissingFieldFound = null,
 21382            HeaderValidated = null,
 21383            IgnoreBlankLines = true,
 21384            TrimOptions = TrimOptions.Trim,
 21385        };
 1386
 21387        using var csvReader = new CsvReader(reader, config);
 21388        var records = new List<Hashtable>();
 1389
 101390        foreach (var rec in csvReader.GetRecords<dynamic>())
 1391        {
 31392            if (rec is not IDictionary<string, object?> dict)
 1393            {
 1394                continue;
 1395            }
 1396
 31397            var ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
 181398            foreach (var kvp in dict)
 1399            {
 61400                ht[kvp.Key] = kvp.Value;
 1401            }
 1402
 31403            records.Add(ht);
 1404        }
 1405
 21406        return records.Count == 0
 21407            ? null
 21408            : (records.Count == 1 ? records[0] : records.Cast<object?>().ToArray());
 21409    }
 1410
 1411    private static byte[]? DecodeBodyStringToBytes(string body)
 1412    {
 21413        if (string.IsNullOrWhiteSpace(body))
 1414        {
 01415            return null;
 1416        }
 1417
 21418        var trimmed = body.Trim();
 21419        if (trimmed.StartsWith("base64:", StringComparison.OrdinalIgnoreCase))
 1420        {
 21421            trimmed = trimmed["base64:".Length..].Trim();
 1422        }
 1423
 21424        if (TryDecodeBase64(trimmed, out var base64Bytes))
 1425        {
 21426            return base64Bytes;
 1427        }
 1428
 01429        if (TryDecodeHex(trimmed, out var hexBytes))
 1430        {
 01431            return hexBytes;
 1432        }
 1433
 1434        // Fallback: interpret as UTF-8 text (best-effort).
 01435        return System.Text.Encoding.UTF8.GetBytes(trimmed);
 1436    }
 1437
 1438    private static bool TryDecodeBase64(string input, out byte[] bytes)
 1439    {
 21440        bytes = [];
 1441
 1442        // Quick reject for non-base64 strings.
 21443        if (input.Length < 4 || (input.Length % 4) != 0)
 1444        {
 01445            return false;
 1446        }
 1447
 1448        // Avoid throwing on clearly non-base64 content.
 841449        for (var i = 0; i < input.Length; i++)
 1450        {
 401451            var c = input[i];
 401452            var isValid =
 401453                c is (>= 'A' and <= 'Z') or (>= 'a' and <= 'z') or (>= '0' and <= '9') or '+' or '/' or '=' or '\r' or '
 1454
 401455            if (!isValid)
 1456            {
 01457                return false;
 1458            }
 1459        }
 1460
 1461        try
 1462        {
 21463            bytes = Convert.FromBase64String(input);
 21464            return true;
 1465        }
 01466        catch (FormatException)
 1467        {
 01468            return false;
 1469        }
 21470    }
 1471
 1472    private static bool TryDecodeHex(string input, out byte[] bytes)
 1473    {
 01474        bytes = [];
 01475        var s = input.Trim();
 1476
 01477        if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
 1478        {
 01479            s = s[2..];
 1480        }
 1481
 01482        if (s.Length < 2 || (s.Length % 2) != 0)
 1483        {
 01484            return false;
 1485        }
 1486
 01487        for (var i = 0; i < s.Length; i++)
 1488        {
 01489            var c = s[i];
 01490            var isHex = c is (>= '0' and <= '9') or (>= 'a' and <= 'f') or (>= 'A' and <= 'F');
 1491
 01492            if (!isHex)
 1493            {
 01494                return false;
 1495            }
 1496        }
 1497
 01498        bytes = new byte[s.Length / 2];
 01499        for (var i = 0; i < bytes.Length; i++)
 1500        {
 01501            bytes[i] = byte.Parse(s.AsSpan(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
 1502        }
 1503
 01504        return true;
 1505    }
 1506}

Methods/Properties

Validate(System.Management.Automation.ParameterMetadata)
.ctor(System.Management.Automation.ParameterMetadata,Microsoft.OpenApi.OpenApiParameter)
.ctor(System.Management.Automation.ParameterMetadata,Microsoft.OpenApi.OpenApiRequestBody)
InjectParameters(Kestrun.Models.KestrunContext,System.Management.Automation.PowerShell)
.cctor()
ShouldConvertBody(Kestrun.Languages.ParameterForInjectionInfo,System.Object)
TryConvertBodyByContentType(Kestrun.Models.KestrunContext,Kestrun.Languages.ParameterForInjectionInfo,System.String)
InjectSingleParameter(Kestrun.Models.KestrunContext,System.Management.Automation.PowerShell,Kestrun.Languages.ParameterForInjectionInfo)
LogInjectingParameter(Serilog.ILogger,Kestrun.Languages.ParameterForInjectionInfo)
GetConvertedParameterValue(Kestrun.Models.KestrunContext,Kestrun.Languages.ParameterForInjectionInfo,System.Boolean&)
ConvertBodyParameterIfNeeded(Kestrun.Models.KestrunContext,Kestrun.Languages.ParameterForInjectionInfo,System.Object)
LogAddingParameter(Serilog.ILogger,System.String,System.Object,System.Boolean)
StoreResolvedParameter(Kestrun.Models.KestrunContext,Kestrun.Languages.ParameterForInjectionInfo,System.String,System.Object)
GetParameterValueFromContext(Kestrun.Models.KestrunContext,Kestrun.Languages.ParameterForInjectionInfo,System.Boolean&)
GetRawValue(Kestrun.Languages.ParameterForInjectionInfo,Kestrun.Models.KestrunContext)
NormalizeRaw(System.Object)
ConvertValue(Kestrun.Models.KestrunContext,Kestrun.Languages.ParameterForInjectionInfo,System.String,System.String[])
ConvertBodyBasedOnContentType(Kestrun.Models.KestrunContext,System.String,Kestrun.Languages.ParameterForInjectionInfo)
ConvertByCanonicalMediaType(System.String,Kestrun.Models.KestrunContext,System.String,Kestrun.Languages.ParameterForInjectionInfo)
ConvertYamlToHashtable(System.String)
ConvertJsonToHashtable(System.String)
JsonElementToClr(System.Text.Json.JsonElement)
ToHashtable(System.Text.Json.JsonElement)
ToArray(System.Text.Json.JsonElement)
ConvertXmlBodyToParameterType(System.String,System.Type)
ExtractRootMapForBinding(System.Collections.Hashtable,System.String)
NormalizeWrappedArrays(System.Collections.Hashtable,System.Collections.Hashtable)
TryGetXmlMetadataProperties(System.Collections.Hashtable,System.Collections.Hashtable&)
TryGetWrappedArrayMetadata(System.Collections.DictionaryEntry,System.String&,System.String&)
TryGetWrapperHashtable(System.Collections.Hashtable,System.String,System.String,System.Collections.Hashtable&)
TryUnwrapWrapper(System.Collections.Hashtable)
ConvertHashtableToObject(System.Collections.Hashtable,System.Type,System.Int32)
ConvertToTargetType(System.Object,System.Type,System.Int32)
UnwrapNullableTargetType(System.Type)
TryConvertHashtableValue(System.Object,System.Type,System.Int32,System.Object&)
TryConvertListOrArrayValue(System.Object,System.Type,System.Int32,System.Object&)
ConvertScalarValue(System.Object,System.Type)
TryConvertScalarByType(System.String,System.Type,System.Object&)
TryConvertPrimitiveScalar(System.String,System.Type,System.Object&)
TryParseInt32(System.String)
TryParseInt64(System.String)
TryParseDouble(System.String)
TryParseDecimal(System.String)
TryParseBoolean(System.String)
TryParseEnum(System.Type,System.String)
TryChangeType(System.Object,System.Type)
ConvertEnumerableToTargetType(System.Collections.IEnumerable,System.Type,System.Int32)
TryGetHashtableValue(System.Collections.Hashtable,System.String,System.Object&)
ConvertFormToHashtable(System.Collections.Generic.Dictionary`2<System.String,System.String>)
ConvertFormToValue(System.Collections.Generic.Dictionary`2<System.String,System.String>,Kestrun.Languages.ParameterForInjectionInfo)
ConvertBsonToHashtable(System.String)
BsonValueToClr(MongoDB.Bson.BsonValue)
BsonDocumentToHashtable(MongoDB.Bson.BsonDocument)
BsonArrayToClrArray(MongoDB.Bson.BsonArray)
ConvertCborToHashtable(System.String)
CborToClr(PeterO.Cbor.CBORObject)
ConvertCborMapToHashtable(PeterO.Cbor.CBORObject)
ConvertCborArrayToClrArray(PeterO.Cbor.CBORObject)
ConvertCborScalarToClr(PeterO.Cbor.CBORObject)
GetCborMapKeyString(PeterO.Cbor.CBORObject)
ConvertCsvToHashtable(System.String)
DecodeBodyStringToBytes(System.String)
TryDecodeBase64(System.String,System.Byte[]&)
TryDecodeHex(System.String,System.Byte[]&)