< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Utilities.XmlHelper
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Utilities/XmlHelper.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
87%
Covered lines: 312
Uncovered lines: 44
Coverable lines: 356
Total lines: 1127
Line coverage: 87.6%
Branch coverage
81%
Covered branches: 237
Total branches: 290
Branch coverage: 81.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/08/2025 - 20:34:03 Line coverage: 96.4% (54/56) Branch coverage: 95.6% (44/46) Total lines: 189 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7210/13/2025 - 16:52:37 Line coverage: 91.9% (103/112) Branch coverage: 85.5% (77/90) Total lines: 331 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e01/21/2026 - 17:07:46 Line coverage: 87.6% (312/356) Branch coverage: 81.7% (237/290) Total lines: 1127 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36 09/08/2025 - 20:34:03 Line coverage: 96.4% (54/56) Branch coverage: 95.6% (44/46) Total lines: 189 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7210/13/2025 - 16:52:37 Line coverage: 91.9% (103/112) Branch coverage: 85.5% (77/90) Total lines: 331 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e01/21/2026 - 17:07:46 Line coverage: 87.6% (312/356) Branch coverage: 81.7% (237/290) Total lines: 1127 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
get_MaxDepth()100%11100%
ToXml(...)100%11100%
ToXmlInternal(...)100%1010100%
TryHandleTerminal(...)81.25%191677.77%
EnterCycle(...)100%44100%
IsSimple(...)85.71%1414100%
DictionaryToXml(...)66.66%66100%
EnumerableToXml(...)100%22100%
ObjectToXml(...)100%1010100%
ResolveObjectElementName(...)83.33%131280%
BuildPropertyMetadataLookup(...)100%1010100%
TryGetPropertyValue(...)100%1150%
AddPropertyWithMetadata(...)92.85%1414100%
AppendEnumerableProperty(...)50%8664.28%
ApplyNamespaceAndPrefix(...)83.33%6690.9%
TryGetXmlMetadata(...)75%12860%
EnumerateStaticMemberValues()50%171475%
LooksLikeXmlMetadata(...)66.66%66100%
AsMetadataHashtable(...)16.66%801222.22%
NameMatchesXmlMetadata(...)100%11100%
GetOpenApiXmlMetadataForType(...)100%22100%
BuildXmlMetadataFromAttributes(...)75%9878.57%
FindOpenApiXmlAttribute(...)50%22100%
GetStringProperty(...)50%22100%
GetBoolProperty(...)50%44100%
BuildEntryFromAttribute(...)100%1010100%
BuildOpenApiXmlMemberMetadata(...)100%11100%
PopulateMemberMetadata(...)100%66100%
BuildOpenApiXmlClassMetadata(...)0%620%
SanitizeName(...)62.5%161690%
.cctor()100%11100%
Equals(...)100%11100%
GetHashCode(...)100%11100%
ToHashtable(...)92.85%141492.3%
HashtableToOpenApiXml(...)50%2288.88%
ApplyOpenApiXmlName(...)100%22100%
ApplyOpenApiXmlNamespace(...)100%44100%
ApplyOpenApiXmlPrefix(...)100%22100%
ApplyOpenApiXmlNodeType(...)100%88100%
ToHashtableInternal(...)100%44100%
GetEffectiveElementName(...)100%44100%
TryConvertNilElement(...)100%88100%
AddAttributesToTable(...)100%44100%
TryAddLeafValue(...)100%44100%
ConvertChildElements(...)100%1010100%
AddOrAppendChildValue(...)100%44100%
FindPropertyKeyForAttribute(...)87.5%88100%
FindPropertyKeyForElement(...)90%1010100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Utilities/XmlHelper.cs

#LineLine coverage
 1using System.Collections;
 2using System.Reflection;
 3using System.Runtime.CompilerServices;
 4using System.Xml.Linq;
 5using Microsoft.OpenApi;
 6using OpenApiXmlModel = Microsoft.OpenApi.OpenApiXml;
 7using Serilog;
 8
 9namespace Kestrun.Utilities;
 10
 11/// <summary>
 12/// Helpers for converting arbitrary objects into <see cref="XElement"/> instances.
 13/// </summary>
 14public static class XmlHelper
 15{
 116    private static readonly XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";
 17    /// <summary>
 18    /// Default maximum recursion depth for object-to-XML conversion.
 19    /// Chosen to balance performance and stack safety for typical object graphs.
 20    /// Adjust if deeper object graphs need to be serialized.
 21    /// </summary>
 22    public const int DefaultMaxDepth = 32;
 23
 24    /// <summary>
 25    /// Maximum recursion depth for object-to-XML conversion. This value can be adjusted if deeper object graphs need to
 26    /// </summary>
 11927    public static int MaxDepth { get; set; } = DefaultMaxDepth;
 28    // Per-call cycle detection now passed explicitly (was ThreadStatic). Avoids potential thread reuse memory retention
 29    // Rationale:
 30    //   * ThreadStatic HashSet could retain large object graphs across requests in thread pool threads causing memory b
 31    //   * A per-call HashSet has a short lifetime and becomes eligible for GC immediately after serialization completes
 32    //   * Passing the set by reference keeps allocation to a single HashSet per root ToXml call (lazy created on first 
 33    //   * No synchronization needed: the set is confined to the call stack; recursive calls share it by reference.
 34
 35    /// <summary>
 36    /// Converts an object to an <see cref="XElement"/> with the specified name, handling nulls, primitives, dictionarie
 37    /// </summary>
 38    /// <param name="name">The name of the XML element.</param>
 39    /// <param name="value">The object to convert to XML.</param>
 40    /// <returns>An <see cref="XElement"/> representing the object.</returns>
 1441    public static XElement ToXml(string name, object? value) => ToXmlInternal(SanitizeName(name), value, 0, visited: nul
 42
 43    /// <summary>
 44    /// Internal recursive method to convert an object to XML, with depth tracking and cycle detection.
 45    /// </summary>
 46    /// <param name="name">The name of the XML element.</param>
 47    /// <param name="value">The object to convert to XML.</param>
 48    /// <param name="depth">The current recursion depth.</param>
 49    /// <returns>An <see cref="XElement"/> representing the object.</returns>
 50    /// <param name="visited">Per-call set of already visited reference objects for cycle detection.</param>
 51    private static XElement ToXmlInternal(string name, object? value, int depth, HashSet<object>? visited)
 52    {
 53        // Fast path & terminal cases extracted to helpers for reduced branching complexity.
 11854        if (TryHandleTerminal(name, value, depth, ref visited, out var terminal))
 55        {
 7356            return terminal;
 57        }
 58
 59        // At this point value is non-null complex/reference or value-type object requiring reflection.
 4560        var type = value!.GetType();
 61
 62        // Check if the type has OpenAPI XML metadata.
 63        // Prefer a static XmlMetadata hashtable, otherwise build it from OpenApiXmlAttribute annotations.
 4564        var xmlMetadata = GetOpenApiXmlMetadataForType(type);
 65
 4566        var needsCycleTracking = !type.IsValueType;
 4567        if (needsCycleTracking && !EnterCycle(value, ref visited, out var cycleElem))
 68        {
 169            return cycleElem!; // Cycle detected
 70        }
 71
 72        try
 73        {
 4474            return ObjectToXml(name, value, depth, visited, xmlMetadata);
 75        }
 76        finally
 77        {
 4478            if (needsCycleTracking && visited is not null)
 79            {
 4480                _ = visited.Remove(value);
 81            }
 4482        }
 4483    }
 84
 85    /// <summary>
 86    /// Handles depth guard, null, enums, primitives, simple temporal types, dictionaries &amp; enumerables.
 87    /// </summary>
 88    /// <param name="name">The name of the XML element.</param>
 89    /// <param name="value">The object to convert to XML.</param>
 90    /// <param name="depth">The current recursion depth.</param>
 91    /// <param name="element">The resulting XML element if handled; otherwise, null.</param>
 92    /// <param name="visited">Per-call set used for cycle detection (reference types only).</param>
 93    /// <returns><c>true</c> if the value was handled; otherwise, <c>false</c>.</returns>
 94    private static bool TryHandleTerminal(string name, object? value, int depth, ref HashSet<object>? visited, out XElem
 95    {
 96        // Depth guard handled below.
 11897        if (depth >= MaxDepth)
 98        {
 299            element = new XElement(name, new XAttribute("warning", "MaxDepthExceeded"));
 2100            return true;
 101        }
 102
 103        // Null
 116104        if (value is null)
 105        {
 6106            element = new XElement(name, new XAttribute(xsi + "nil", true));
 6107            return true;
 108        }
 109
 110110        var type = value.GetType();
 111
 112        // Enum
 110113        if (type.IsEnum)
 114        {
 0115            element = new XElement(name, value.ToString());
 0116            return true;
 117        }
 118
 119        // Primitive / simple
 110120        if (IsSimple(value))
 121        {
 63122            element = new XElement(name, value);
 63123            return true;
 124        }
 125
 126        // DateTimeOffset / TimeSpan (already covered by IsSimple for DateTimeOffset/TimeSpan? DateTimeOffset yes, TimeS
 47127        if (value is DateTimeOffset dto)
 128        {
 0129            element = new XElement(name, dto.ToString("O"));
 0130            return true;
 131        }
 47132        if (value is TimeSpan ts)
 133        {
 0134            element = new XElement(name, ts.ToString());
 0135            return true;
 136        }
 137
 138        // IDictionary
 47139        if (value is IDictionary dict)
 140        {
 1141            element = DictionaryToXml(name, dict, depth, visited);
 1142            return true;
 143        }
 144
 145        // IEnumerable
 46146        if (value is IEnumerable enumerable)
 147        {
 1148            element = EnumerableToXml(name, enumerable, depth, visited);
 1149            return true;
 150        }
 151
 45152        element = null!;
 45153        return false;
 154    }
 155
 156    /// <summary>
 157    /// Enters cycle tracking for the specified object. Returns false if a cycle is detected.
 158    /// </summary>
 159    /// <param name="value">The object to track.</param>
 160    /// <param name="cycleElement">The resulting XML element if a cycle is detected; otherwise, null.</param>
 161    /// <returns><c>true</c> if the object is successfully tracked; otherwise, <c>false</c>.</returns>
 162    /// <param name="visited">Per-call set of visited objects (created lazily).</param>
 163    private static bool EnterCycle(object value, ref HashSet<object>? visited, out XElement? cycleElement)
 164    {
 45165        visited ??= new HashSet<object>(ReferenceEqualityComparer.Instance);
 45166        if (!visited.Add(value))
 167        {
 1168            cycleElement = new XElement("Object", new XAttribute("warning", "CycleDetected"));
 1169            return false;
 170        }
 44171        cycleElement = null;
 44172        return true;
 173    }
 174
 175    /// <summary>
 176    /// Determines whether the specified object is a simple type (primitive, string, DateTime, Guid, or decimal).
 177    /// </summary>
 178    /// <param name="value">The object to check.</param>
 179    /// <returns><c>true</c> if the object is a simple type; otherwise, <c>false</c>.</returns>
 180    private static bool IsSimple(object value)
 181    {
 110182        var type = value.GetType();
 110183        return type.IsPrimitive
 110184            || value is string
 110185            || value is DateTime or DateTimeOffset
 110186            || value is Guid
 110187            || value is decimal
 110188            || value is TimeSpan;
 189    }
 190
 191    /// <summary>Converts a dictionary to an XML element (recursive).</summary>
 192    /// <param name="name">Element name for the dictionary.</param>
 193    /// <param name="dict">Dictionary to serialize.</param>
 194    /// <param name="depth">Current recursion depth (guarded).</param>
 195    /// <param name="visited">Per-call set used for cycle detection.</param>
 196    private static XElement DictionaryToXml(string name, IDictionary dict, int depth, HashSet<object>? visited)
 197    {
 1198        var elem = new XElement(name);
 6199        foreach (DictionaryEntry entry in dict)
 200        {
 2201            var key = SanitizeName(entry.Key?.ToString() ?? "Key");
 2202            elem.Add(ToXmlInternal(key, entry.Value, depth + 1, visited));
 203        }
 1204        return elem;
 205    }
 206    /// <summary>Converts an enumerable to an XML element; each item becomes &lt;Item/&gt;.</summary>
 207    /// <param name="name">Element name for the collection.</param>
 208    /// <param name="enumerable">Sequence to serialize.</param>
 209    /// <param name="depth">Current recursion depth (guarded).</param>
 210    /// <param name="visited">Per-call set used for cycle detection.</param>
 211    private static XElement EnumerableToXml(string name, IEnumerable enumerable, int depth, HashSet<object>? visited)
 212    {
 1213        var elem = new XElement(name);
 6214        foreach (var item in enumerable)
 215        {
 2216            elem.Add(ToXmlInternal("Item", item, depth + 1, visited));
 217        }
 1218        return elem;
 219    }
 220
 221    /// <summary>Reflects an object's public instance properties into XML.</summary>
 222    /// <param name="name">Element name for the object.</param>
 223    /// <param name="value">Object instance to serialize.</param>
 224    /// <param name="depth">Current recursion depth (guarded).</param>
 225    /// <param name="visited">Per-call set used for cycle detection.</param>
 226    /// <param name="xmlMetadata">Optional OpenAPI XML metadata hashtable (typically from a static XmlMetadata property)
 227    private static XElement ObjectToXml(string name, object value, int depth, HashSet<object>? visited, Hashtable? xmlMe
 228    {
 44229        var elementName = ResolveObjectElementName(name, xmlMetadata);
 44230        var objElem = new XElement(elementName);
 44231        var type = value.GetType();
 232
 44233        var propertyMetadata = BuildPropertyMetadataLookup(xmlMetadata);
 234
 286235        foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
 236        {
 99237            if (prop.GetIndexParameters().Length > 0)
 238            {
 239                continue; // skip indexers
 240            }
 99241            if (!TryGetPropertyValue(prop, value, out var propVal))
 242            {
 243                continue; // skip unreadable props
 244            }
 245
 246            // Check if property has OpenAPI XML metadata
 99247            var propName = prop.Name;
 99248            var childName = SanitizeName(propName);
 249
 99250            if (propertyMetadata != null && propertyMetadata.TryGetValue(propName, out var propXml))
 251            {
 4252                AddPropertyWithMetadata(objElem, propName, propVal, propXml, depth, visited);
 4253                continue;
 254            }
 255
 95256            objElem.Add(ToXmlInternal(childName, propVal, depth + 1, visited));
 257        }
 44258        return objElem;
 259    }
 260
 261    /// <summary>
 262    /// Resolves the element name used when serializing an object instance.
 263    /// </summary>
 264    /// <param name="defaultName">Default element name (typically the requested root name).</param>
 265    /// <param name="xmlMetadata">Optional OpenAPI XML metadata hashtable.</param>
 266    /// <returns>The resolved element name.</returns>
 267    private static string ResolveObjectElementName(string defaultName, Hashtable? xmlMetadata)
 268    {
 269        // Use class-level XML name if available.
 44270        if (xmlMetadata?["ClassXml"] is Hashtable classXmlHash && classXmlHash["Name"] is string className)
 271        {
 0272            return className;
 273        }
 274
 44275        if (xmlMetadata?["ClassName"] is string fallbackClassName && !string.IsNullOrWhiteSpace(fallbackClassName))
 276        {
 277            // When responses are written with the default root ("Response"), prefer the model's class name.
 278            // This aligns runtime XML output with OpenAPI schema component names.
 1279            return fallbackClassName;
 280        }
 281
 43282        return defaultName;
 283    }
 284
 285    /// <summary>
 286    /// Builds a property metadata lookup from a metadata hashtable.
 287    /// </summary>
 288    /// <param name="xmlMetadata">Optional OpenAPI XML metadata hashtable.</param>
 289    /// <returns>
 290    /// A dictionary mapping CLR property names to per-property metadata, or <c>null</c> when no property metadata is av
 291    /// </returns>
 292    private static Dictionary<string, Hashtable>? BuildPropertyMetadataLookup(Hashtable? xmlMetadata)
 293    {
 44294        if (xmlMetadata?["Properties"] is not Hashtable propsHash)
 295        {
 43296            return null;
 297        }
 298
 1299        var propertyMetadata = new Dictionary<string, Hashtable>(StringComparer.OrdinalIgnoreCase);
 10300        foreach (DictionaryEntry entry in propsHash)
 301        {
 4302            if (entry.Key is string propName && entry.Value is Hashtable propMeta)
 303            {
 4304                propertyMetadata[propName] = propMeta;
 305            }
 306        }
 307
 1308        return propertyMetadata;
 309    }
 310
 311    /// <summary>
 312    /// Attempts to read a property value via reflection.
 313    /// </summary>
 314    /// <param name="prop">Property info.</param>
 315    /// <param name="instance">Object instance.</param>
 316    /// <param name="value">Property value when readable.</param>
 317    /// <returns><c>true</c> when the property value was read; otherwise <c>false</c>.</returns>
 318    private static bool TryGetPropertyValue(PropertyInfo prop, object instance, out object? value)
 319    {
 320        try
 321        {
 99322            value = prop.GetValue(instance);
 99323            return true;
 324        }
 0325        catch
 326        {
 0327            value = null;
 0328            return false;
 329        }
 99330    }
 331
 332    /// <summary>
 333    /// Adds a property to an object element using OpenAPI XML metadata rules.
 334    /// </summary>
 335    /// <param name="parent">Parent element representing the object.</param>
 336    /// <param name="propName">CLR property name.</param>
 337    /// <param name="propVal">Property value.</param>
 338    /// <param name="propXml">OpenAPI XML metadata hashtable for the property.</param>
 339    /// <param name="depth">Current recursion depth.</param>
 340    /// <param name="visited">Per-call set used for cycle detection.</param>
 341    private static void AddPropertyWithMetadata(XElement parent, string propName, object? propVal, Hashtable propXml, in
 342    {
 4343        var childName = SanitizeName(propName);
 344
 345        // Use custom element name if specified.
 4346        if (propXml["Name"] is string customName)
 347        {
 4348            childName = customName;
 349        }
 350
 351        // Check if this property should be an XML attribute.
 4352        if (propXml["Attribute"] is bool isAttribute && isAttribute && propVal != null)
 353        {
 1354            parent.Add(new XAttribute(childName, propVal));
 1355            return;
 356        }
 357
 358        // Special handling for array/list properties when XML metadata is present.
 359        // - If Wrapped=true, add a wrapper element and put items under it.
 360        // - If Wrapped=false, emit repeated sibling elements for each item.
 3361        if (propVal is IEnumerable enumerable and not string)
 362        {
 1363            var wrapped = propXml["Wrapped"] is bool w && w;
 1364            var nsUri = propXml["Namespace"] as string;
 1365            var prefix = propXml["Prefix"] as string;
 1366            AppendEnumerableProperty(parent, childName, enumerable, wrapped, nsUri, prefix, depth + 1, visited);
 1367            return;
 368        }
 369
 370        // Apply namespace/prefix to the element representing this property (non-attribute, non-collection).
 2371        var nsUriForElement = propXml["Namespace"] as string;
 2372        var prefixForElement = propXml["Prefix"] as string;
 2373        var childElem = ToXmlInternal(childName, propVal, depth + 1, visited);
 2374        ApplyNamespaceAndPrefix(childElem, nsUriForElement, prefixForElement);
 2375        parent.Add(childElem);
 2376    }
 377
 378    /// <summary>
 379    /// Appends an enumerable property to an existing element, honoring OpenAPI XML metadata options.
 380    /// </summary>
 381    /// <param name="parent">Parent element to append to.</param>
 382    /// <param name="itemName">Element name to use for items (and wrapper when <paramref name="wrapped"/> is true).</par
 383    /// <param name="enumerable">Enumerable to serialize.</param>
 384    /// <param name="wrapped">If true, wrap items in a container element.</param>
 385    /// <param name="nsUri">Optional namespace URI for the element(s).</param>
 386    /// <param name="prefix">Optional prefix to declare for <paramref name="nsUri"/>.</param>
 387    /// <param name="depth">Current recursion depth.</param>
 388    /// <param name="visited">Per-call set used for cycle detection.</param>
 389    private static void AppendEnumerableProperty(XElement parent, string itemName, IEnumerable enumerable, bool wrapped,
 390    {
 1391        if (wrapped)
 392        {
 1393            var wrapper = new XElement(itemName);
 1394            ApplyNamespaceAndPrefix(wrapper, nsUri, prefix);
 8395            foreach (var item in enumerable)
 396            {
 3397                var itemElem = ToXmlInternal(itemName, item, depth + 1, visited);
 3398                ApplyNamespaceAndPrefix(itemElem, nsUri, prefix);
 3399                wrapper.Add(itemElem);
 400            }
 1401            parent.Add(wrapper);
 1402            return;
 403        }
 404
 0405        foreach (var item in enumerable)
 406        {
 0407            var itemElem = ToXmlInternal(itemName, item, depth + 1, visited);
 0408            ApplyNamespaceAndPrefix(itemElem, nsUri, prefix);
 0409            parent.Add(itemElem);
 410        }
 0411    }
 412
 413    /// <summary>
 414    /// Applies namespace and prefix settings to an element, ensuring the requested prefix is declared.
 415    /// </summary>
 416    /// <param name="element">The element to update.</param>
 417    /// <param name="nsUri">Namespace URI to apply.</param>
 418    /// <param name="prefix">Prefix to declare for the namespace.</param>
 419    private static void ApplyNamespaceAndPrefix(XElement element, string? nsUri, string? prefix)
 420    {
 6421        if (string.IsNullOrWhiteSpace(nsUri))
 422        {
 5423            return;
 424        }
 425
 1426        var ns = XNamespace.Get(nsUri);
 1427        element.Name = ns + element.Name.LocalName;
 428
 1429        if (string.IsNullOrWhiteSpace(prefix))
 430        {
 0431            return;
 432        }
 433
 434        // Ensure xmlns:prefix="nsUri" is present so the serializer can use the desired prefix.
 435        // Avoid adding duplicates if the declaration already exists.
 1436        var xmlnsName = XNamespace.Xmlns + prefix;
 1437        var existing = element.Attribute(xmlnsName);
 1438        if (existing == null)
 439        {
 1440            element.Add(new XAttribute(xmlnsName, nsUri));
 441        }
 1442    }
 443
 444    /// <summary>
 445    /// Attempts to retrieve OpenAPI XML metadata from a type.
 446    /// </summary>
 447    /// <param name="type">The type to check for metadata.</param>
 448    /// <returns>A Hashtable containing XML metadata, or null if none is available.</returns>
 449    private static Hashtable? TryGetXmlMetadata(Type type)
 450    {
 451        try
 452        {
 453            // Preferred: a static hashtable property/field (safe to read via reflection).
 454            // PowerShell class *methods* require an engine context on the current thread and may throw when invoked
 455            // from C# (even if the type was defined in a runspace).
 48456            var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarc
 457
 97458            foreach (var value in EnumerateStaticMemberValues(type, flags, onlyNamedXmlMetadata: true))
 459            {
 1460                var byName = AsMetadataHashtable(value);
 1461                if (byName is not null)
 462                {
 1463                    return byName;
 464                }
 465            }
 466
 467            // Fallback: scan static members and return the first value that looks like XML metadata.
 94468            foreach (var value in EnumerateStaticMemberValues(type, flags, onlyNamedXmlMetadata: false))
 469            {
 0470                var candidate = AsMetadataHashtable(value);
 0471                if (candidate is not null)
 472                {
 0473                    return candidate;
 474                }
 475            }
 47476        }
 0477        catch (Exception ex)
 478        {
 0479            Log.Logger.Error(ex, "Error retrieving XML metadata for type {TypeName}", type.FullName);
 0480        }
 47481        return null;
 1482    }
 483
 484    /// <summary>
 485    /// Enumerates values of static properties and fields on a type.
 486    /// </summary>
 487    /// <param name="type">Type to reflect.</param>
 488    /// <param name="flags">Binding flags used for reflection.</param>
 489    /// <param name="onlyNamedXmlMetadata">
 490    /// When <c>true</c>, only members named <c>XmlMetadata</c> (ignoring case and an optional leading <c>$</c>) are ret
 491    /// </param>
 492    /// <returns>An enumerable of static member values (may include nulls).</returns>
 493    private static IEnumerable<object?> EnumerateStaticMemberValues(Type type, BindingFlags flags, bool onlyNamedXmlMeta
 494    {
 191495        foreach (var prop in type.GetProperties(flags))
 496        {
 1497            if (onlyNamedXmlMetadata && !NameMatchesXmlMetadata(prop.Name))
 498            {
 499                continue;
 500            }
 501
 1502            if (prop.GetIndexParameters().Length != 0)
 503            {
 504                continue;
 505            }
 506
 1507            yield return prop.GetValue(null);
 508        }
 509
 188510        foreach (var field in type.GetFields(flags))
 511        {
 0512            if (onlyNamedXmlMetadata && !NameMatchesXmlMetadata(field.Name))
 513            {
 514                continue;
 515            }
 516
 0517            yield return field.GetValue(null);
 518        }
 94519    }
 520
 521    /// <summary>
 522    /// Determines whether a hashtable resembles the expected XML metadata shape.
 523    /// </summary>
 524    /// <param name="ht">Hashtable to inspect.</param>
 525    /// <returns><c>true</c> if the hashtable has the expected keys; otherwise <c>false</c>.</returns>
 526    private static bool LooksLikeXmlMetadata(Hashtable ht)
 527    {
 1528        return ht.Count > 0
 1529            && (ht.ContainsKey("ClassXml") || ht.ContainsKey("ClassName"))
 1530            && ht.ContainsKey("Properties");
 531    }
 532
 533    /// <summary>
 534    /// Attempts to normalize an object into an XML metadata hashtable.
 535    /// </summary>
 536    /// <param name="value">Candidate value (hashtable or dictionary).</param>
 537    /// <returns>A metadata hashtable when the candidate matches; otherwise <c>null</c>.</returns>
 538    private static Hashtable? AsMetadataHashtable(object? value)
 539    {
 1540        if (value is Hashtable ht)
 541        {
 1542            return LooksLikeXmlMetadata(ht) ? ht : null;
 543        }
 544
 0545        if (value is IDictionary dict)
 546        {
 0547            var copied = new Hashtable(StringComparer.OrdinalIgnoreCase);
 0548            foreach (DictionaryEntry entry in dict)
 549            {
 0550                if (entry.Key is string k)
 551                {
 0552                    copied[k] = entry.Value;
 553                }
 554            }
 0555            return LooksLikeXmlMetadata(copied) ? copied : null;
 556        }
 557
 0558        return null;
 559    }
 560
 561    /// <summary>
 562    /// Checks whether a reflected member name corresponds to an <c>XmlMetadata</c> property/field.
 563    /// </summary>
 564    /// <param name="name">Member name.</param>
 565    /// <returns><c>true</c> when the name matches; otherwise <c>false</c>.</returns>
 566    private static bool NameMatchesXmlMetadata(string name)
 567    {
 1568        var normalized = name.TrimStart('$');
 1569        return string.Equals(normalized, "XmlMetadata", StringComparison.OrdinalIgnoreCase);
 570    }
 571
 572    /// <summary>
 573    /// Gets OpenAPI XML metadata for a CLR type.
 574    /// </summary>
 575    /// <param name="type">The type to inspect.</param>
 576    /// <returns>
 577    /// A hashtable containing XML metadata (ClassName/ClassXml/Properties), or <c>null</c> when the type has no XML ann
 578    /// </returns>
 579    internal static Hashtable? GetOpenApiXmlMetadataForType(Type type)
 580    {
 48581        ArgumentNullException.ThrowIfNull(type);
 582
 583        // First, try the static hashtable member (used by generated classes).
 48584        var fromStatic = TryGetXmlMetadata(type);
 48585        if (fromStatic is not null)
 586        {
 1587            return fromStatic;
 588        }
 589
 590        // Next, build metadata from attributes on the type (used by user-defined PowerShell classes).
 47591        return BuildXmlMetadataFromAttributes(type);
 592    }
 593
 594    /// <summary>
 595    /// Builds an XmlMetadata hashtable from <see cref="OpenApiXmlAttribute"/> annotations on a type and its members.
 596    /// This supports user-defined PowerShell classes that are not rewritten by the exporter.
 597    /// </summary>
 598    /// <param name="type">The annotated type.</param>
 599    /// <returns>An XmlMetadata hashtable, or <c>null</c> when the type has no XML annotations.</returns>
 600    private static Hashtable? BuildXmlMetadataFromAttributes(Type type)
 601    {
 47602        var classXmlAttr = FindOpenApiXmlAttribute(type);
 47603        var propsHash = BuildOpenApiXmlMemberMetadata(type);
 604
 47605        if (classXmlAttr is null && propsHash.Count == 0)
 606        {
 45607            return null;
 608        }
 609
 2610        var meta = new Hashtable(StringComparer.OrdinalIgnoreCase)
 2611        {
 2612            ["ClassName"] = type.Name,
 2613            ["Properties"] = propsHash,
 2614        };
 615
 2616        if (classXmlAttr is not null)
 617        {
 0618            var classXml = BuildOpenApiXmlClassMetadata(classXmlAttr);
 0619            if (classXml.Count > 0)
 620            {
 0621                meta["ClassXml"] = classXml;
 622            }
 623        }
 624
 2625        return meta;
 626    }
 627
 628    /// <summary>
 629    /// Finds an <c>OpenApiXmlAttribute</c>-like attribute on the provided member or type.
 630    /// </summary>
 631    /// <param name="provider">The attribute provider to inspect.</param>
 632    /// <returns>The attribute instance when present; otherwise <c>null</c>.</returns>
 633    private static object? FindOpenApiXmlAttribute(ICustomAttributeProvider provider)
 634    {
 152635        return provider
 152636            .GetCustomAttributes(inherit: false)
 258637            .FirstOrDefault(a => a?.GetType().Name == "OpenApiXmlAttribute");
 638    }
 639
 640    /// <summary>
 641    /// Reads a string-valued property from an attribute instance by reflection.
 642    /// </summary>
 643    /// <param name="attr">Attribute instance.</param>
 644    /// <param name="propName">Property name to read.</param>
 645    /// <returns>The string value when present; otherwise <c>null</c>.</returns>
 646    private static string? GetStringProperty(object attr, string propName)
 24647        => attr.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.Instance)?.GetValue(attr) as string;
 648
 649    /// <summary>
 650    /// Reads a bool-valued property from an attribute instance by reflection.
 651    /// </summary>
 652    /// <param name="attr">Attribute instance.</param>
 653    /// <param name="propName">Property name to read.</param>
 654    /// <returns><c>true</c> when the property exists and evaluates to true; otherwise <c>false</c>.</returns>
 655    private static bool GetBoolProperty(object attr, string propName)
 656    {
 16657        var value = attr.GetType().GetProperty(propName, BindingFlags.Public | BindingFlags.Instance)?.GetValue(attr);
 16658        return value is bool b && b;
 659    }
 660
 661    /// <summary>
 662    /// Builds a metadata entry hashtable from an <c>OpenApiXmlAttribute</c>-like attribute instance.
 663    /// </summary>
 664    /// <param name="xmlAttr">Attribute instance.</param>
 665    /// <returns>A hashtable containing any provided XML metadata fields.</returns>
 666    private static Hashtable BuildEntryFromAttribute(object xmlAttr)
 667    {
 8668        var entry = new Hashtable(StringComparer.OrdinalIgnoreCase);
 669
 8670        var name = GetStringProperty(xmlAttr, "Name");
 8671        if (!string.IsNullOrWhiteSpace(name))
 672        {
 8673            entry["Name"] = name;
 674        }
 675
 8676        var ns = GetStringProperty(xmlAttr, "Namespace");
 8677        if (!string.IsNullOrWhiteSpace(ns))
 678        {
 2679            entry["Namespace"] = ns;
 680        }
 681
 8682        var prefix = GetStringProperty(xmlAttr, "Prefix");
 8683        if (!string.IsNullOrWhiteSpace(prefix))
 684        {
 2685            entry["Prefix"] = prefix;
 686        }
 687
 8688        if (GetBoolProperty(xmlAttr, "Attribute"))
 689        {
 2690            entry["Attribute"] = true;
 691        }
 692
 8693        if (GetBoolProperty(xmlAttr, "Wrapped"))
 694        {
 2695            entry["Wrapped"] = true;
 696        }
 697
 8698        return entry;
 699    }
 700
 701    /// <summary>
 702    /// Builds a property/field metadata hashtable by scanning public instance members for XML attributes.
 703    /// </summary>
 704    /// <param name="type">Type to scan.</param>
 705    /// <returns>A hashtable mapping member names to per-member metadata entries.</returns>
 706    private static Hashtable BuildOpenApiXmlMemberMetadata(Type type)
 707    {
 47708        var propsHash = new Hashtable(StringComparer.OrdinalIgnoreCase);
 47709        PopulateMemberMetadata(type.GetProperties(BindingFlags.Public | BindingFlags.Instance), propsHash);
 47710        PopulateMemberMetadata(type.GetFields(BindingFlags.Public | BindingFlags.Instance), propsHash);
 47711        return propsHash;
 712    }
 713
 714    /// <summary>
 715    /// Populates a metadata hashtable by scanning members for XML attribute annotations.
 716    /// </summary>
 717    /// <param name="members">Members to scan.</param>
 718    /// <param name="propsHash">Target metadata hashtable to populate.</param>
 719    private static void PopulateMemberMetadata(IEnumerable<MemberInfo> members, Hashtable propsHash)
 720    {
 398721        foreach (var member in members)
 722        {
 105723            var xmlAttr = FindOpenApiXmlAttribute(member);
 105724            if (xmlAttr is null)
 725            {
 726                continue;
 727            }
 728
 8729            var entry = BuildEntryFromAttribute(xmlAttr);
 8730            if (entry.Count > 0)
 731            {
 8732                propsHash[member.Name] = entry;
 733            }
 734        }
 94735    }
 736
 737    /// <summary>
 738    /// Builds class-level XML metadata from an <c>OpenApiXmlAttribute</c>-like attribute instance.
 739    /// </summary>
 740    /// <param name="classXmlAttr">Class-level XML attribute instance.</param>
 741    /// <returns>A hashtable with class-level XML metadata.</returns>
 742    private static Hashtable BuildOpenApiXmlClassMetadata(object classXmlAttr)
 743    {
 0744        var classXml = new Hashtable(StringComparer.OrdinalIgnoreCase);
 745
 0746        var classEntry = BuildEntryFromAttribute(classXmlAttr);
 0747        foreach (DictionaryEntry entry in classEntry)
 748        {
 0749            classXml[entry.Key] = entry.Value;
 750        }
 751
 752        // Class-level metadata should not include member-only flags.
 0753        classXml.Remove("Attribute");
 0754        classXml.Remove("Wrapped");
 755
 0756        return classXml;
 757    }
 758
 759    /// <summary>
 760    /// Sanitizes a raw string to be a valid XML element name by replacing invalid characters with underscores.
 761    /// </summary>
 762    /// <param name="raw">The raw string to sanitize.</param>
 763    /// <returns>A sanitized XML element name.</returns>
 764    private static string SanitizeName(string raw)
 765    {
 119766        if (string.IsNullOrWhiteSpace(raw))
 767        {
 0768            return "Element";
 769        }
 770        // XML element names must start with letter or underscore; replace invalid chars with '_'
 119771        var sb = new System.Text.StringBuilder(raw.Length);
 1310772        for (var i = 0; i < raw.Length; i++)
 773        {
 536774            var ch = raw[i];
 536775            var valid = i == 0
 536776                ? (char.IsLetter(ch) || ch == '_')
 536777                : (char.IsLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == '.');
 536778            _ = sb.Append(valid ? ch : '_');
 779        }
 119780        return sb.ToString();
 781    }
 782
 783    private sealed class ReferenceEqualityComparer : IEqualityComparer<object>
 784    {
 1785        public static readonly ReferenceEqualityComparer Instance = new();
 45786        public new bool Equals(object? x, object? y) => ReferenceEquals(x, y);
 89787        public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
 788    }
 789
 790    /// <summary>
 791    /// Converts an <see cref="XElement"/> into a <see cref="Hashtable"/>, optionally using OpenAPI XML metadata hashtab
 792    /// Nested elements become nested Hashtables; repeated elements become lists.
 793    /// Attributes are stored as keys prefixed with "@" unless guided by OpenAPI metadata, xsi:nil="true" becomes <c>nul
 794    /// </summary>
 795    /// <param name="element">The XML element to convert.</param>
 796    /// <param name="xmlMetadata">Optional OpenAPI XML metadata hashtable (as returned by GetOpenApiXmlMetadata()). Shou
 797    /// <returns>A Hashtable representation of the XML element.</returns>
 798    public static Hashtable ToHashtable(XElement element, Hashtable? xmlMetadata = null)
 799    {
 11800        if (xmlMetadata == null)
 801        {
 9802            return ToHashtableInternal(element, null, []);
 803        }
 804
 805        // Convert hashtable metadata to OpenApiXml object for class-level metadata
 2806        OpenApiXmlModel? classXml = null;
 2807        if (xmlMetadata["ClassXml"] is Hashtable classXmlHash)
 808        {
 0809            classXml = HashtableToOpenApiXml(classXmlHash);
 810        }
 811
 812        // Convert property metadata hashtables to OpenApiXml objects
 2813        var propertyModels = new Dictionary<string, OpenApiXmlModel>();
 2814        if (xmlMetadata["Properties"] is Hashtable propsHash)
 815        {
 20816            foreach (DictionaryEntry entry in propsHash)
 817            {
 8818                if (entry.Key is string propName && entry.Value is Hashtable propXmlHash)
 819                {
 8820                    var propXml = HashtableToOpenApiXml(propXmlHash);
 8821                    if (propXml != null)
 822                    {
 8823                        propertyModels[propName] = propXml;
 824                    }
 825                }
 826            }
 827        }
 828
 2829        return ToHashtableInternal(element, classXml, propertyModels);
 830    }
 831
 832    /// <summary>
 833    /// Converts a hashtable representation of OpenAPI XML metadata to an OpenApiXml object.
 834    /// </summary>
 835    /// <param name="hash">Hashtable containing Name, Namespace, Prefix, Attribute, and/or Wrapped keys.</param>
 836    /// <returns>An OpenApiXml object with the specified properties, or null if the hashtable is empty/invalid.</returns
 837    private static OpenApiXmlModel? HashtableToOpenApiXml(Hashtable hash)
 838    {
 8839        ArgumentNullException.ThrowIfNull(hash);
 840
 8841        if (hash.Count == 0)
 842        {
 0843            return null;
 844        }
 845
 8846        var xml = new OpenApiXmlModel();
 8847        ApplyOpenApiXmlName(hash, xml);
 8848        ApplyOpenApiXmlNamespace(hash, xml);
 8849        ApplyOpenApiXmlPrefix(hash, xml);
 8850        ApplyOpenApiXmlNodeType(hash, xml);
 8851        return xml;
 852    }
 853
 854    /// <summary>
 855    /// Applies the OpenAPI XML <c>name</c> value from a metadata hashtable.
 856    /// </summary>
 857    /// <param name="hash">Metadata hashtable.</param>
 858    /// <param name="xml">Target OpenAPI XML object.</param>
 859    private static void ApplyOpenApiXmlName(Hashtable hash, OpenApiXmlModel xml)
 860    {
 8861        if (hash["Name"] is string name)
 862        {
 8863            xml.Name = name;
 864        }
 8865    }
 866
 867    /// <summary>
 868    /// Applies the OpenAPI XML <c>namespace</c> value from a metadata hashtable.
 869    /// </summary>
 870    /// <param name="hash">Metadata hashtable.</param>
 871    /// <param name="xml">Target OpenAPI XML object.</param>
 872    private static void ApplyOpenApiXmlNamespace(Hashtable hash, OpenApiXmlModel xml)
 873    {
 8874        if (hash["Namespace"] is string ns && !string.IsNullOrWhiteSpace(ns))
 875        {
 2876            xml.Namespace = new Uri(ns);
 877        }
 8878    }
 879
 880    /// <summary>
 881    /// Applies the OpenAPI XML <c>prefix</c> value from a metadata hashtable.
 882    /// </summary>
 883    /// <param name="hash">Metadata hashtable.</param>
 884    /// <param name="xml">Target OpenAPI XML object.</param>
 885    private static void ApplyOpenApiXmlPrefix(Hashtable hash, OpenApiXmlModel xml)
 886    {
 8887        if (hash["Prefix"] is string prefix)
 888        {
 2889            xml.Prefix = prefix;
 890        }
 8891    }
 892
 893    /// <summary>
 894    /// Applies the OpenAPI XML node type (<c>attribute</c> vs element) based on metadata flags.
 895    /// </summary>
 896    /// <param name="hash">Metadata hashtable.</param>
 897    /// <param name="xml">Target OpenAPI XML object.</param>
 898    private static void ApplyOpenApiXmlNodeType(Hashtable hash, OpenApiXmlModel xml)
 899    {
 8900        if (hash["Attribute"] is bool isAttribute && isAttribute)
 901        {
 2902            xml.NodeType = OpenApiXmlNodeType.Attribute;
 2903            return;
 904        }
 905
 6906        if (hash["Wrapped"] is bool isWrapped && isWrapped)
 907        {
 2908            xml.NodeType = OpenApiXmlNodeType.Element;
 909        }
 6910    }
 911
 912    /// <summary>
 913    /// Internal recursive method to convert an <see cref="XElement"/> into a <see cref="Hashtable"/> with OpenAPI XML m
 914    /// </summary>
 915    /// <param name="element">The XML element to convert.</param>
 916    /// <param name="xmlModel">OpenAPI XML model metadata for the current element.</param>
 917    /// <param name="propertyModels">Dictionary of property-specific XML models for child elements.</param>
 918    /// <returns>A Hashtable representation of the XML element.</returns>
 919    private static Hashtable ToHashtableInternal(XElement element, OpenApiXmlModel? xmlModel, Dictionary<string, OpenApi
 920    {
 34921        var elementName = GetEffectiveElementName(element, xmlModel);
 922
 34923        if (TryConvertNilElement(element, elementName, out var nilResult))
 924        {
 2925            return nilResult;
 926        }
 927
 32928        var table = new Hashtable();
 32929        AddAttributesToTable(element, table, propertyModels);
 930
 32931        if (TryAddLeafValue(element, elementName, table))
 932        {
 22933            return table;
 934        }
 935
 10936        table[elementName] = ConvertChildElements(element, propertyModels);
 10937        return table;
 938    }
 939
 940    /// <summary>
 941    /// Gets the effective name for an element, honoring OpenAPI <c>xml.name</c> overrides.
 942    /// </summary>
 943    /// <param name="element">The element whose name is being resolved.</param>
 944    /// <param name="xmlModel">The OpenAPI XML model for the current element.</param>
 945    /// <returns>The resolved element name.</returns>
 946    private static string GetEffectiveElementName(XElement element, OpenApiXmlModel? xmlModel)
 34947        => xmlModel?.Name ?? element.Name.LocalName;
 948
 949    /// <summary>
 950    /// Detects <c>xsi:nil="true"</c> and returns the null-valued hashtable representation.
 951    /// </summary>
 952    /// <param name="element">The element to inspect for <c>xsi:nil</c>.</param>
 953    /// <param name="elementName">The effective element name to use as the hashtable key.</param>
 954    /// <param name="result">The null-valued hashtable when <c>xsi:nil</c> is present; otherwise a default value.</param
 955    /// <returns><c>true</c> when <c>xsi:nil="true"</c> is present; otherwise <c>false</c>.</returns>
 956    private static bool TryConvertNilElement(XElement element, string elementName, out Hashtable result)
 957    {
 82958        foreach (var attr in element.Attributes())
 959        {
 8960            if (attr.Name.NamespaceName == xsi.NamespaceName
 8961                && attr.Name.LocalName == "nil"
 8962                && string.Equals(attr.Value, "true", StringComparison.OrdinalIgnoreCase))
 963            {
 2964                result = new Hashtable { [elementName] = null };
 2965                return true;
 966            }
 967        }
 968
 32969        result = default!;
 32970        return false;
 2971    }
 972
 973    /// <summary>
 974    /// Adds element attributes to the target hashtable, honoring OpenAPI <c>xml.attribute</c> mappings.
 975    /// </summary>
 976    /// <param name="element">The element whose attributes are being read.</param>
 977    /// <param name="table">The hashtable to populate.</param>
 978    /// <param name="propertyModels">Property-level OpenAPI XML models used to map attribute names to property keys.</pa
 979    private static void AddAttributesToTable(XElement element, Hashtable table, Dictionary<string, OpenApiXmlModel> prop
 980    {
 76981        foreach (var attr in element.Attributes())
 982        {
 6983            var attrKey = FindPropertyKeyForAttribute(attr.Name.LocalName, propertyModels);
 6984            if (attrKey is not null)
 985            {
 2986                table[attrKey] = attr.Value;
 2987                continue;
 988            }
 989
 4990            table["@" + attr.Name.LocalName] = attr.Value;
 991        }
 32992    }
 993
 994    /// <summary>
 995    /// Converts a leaf element (no child elements) by storing its string value under the effective element name.
 996    /// </summary>
 997    /// <param name="element">The element to evaluate.</param>
 998    /// <param name="elementName">The effective element name.</param>
 999    /// <param name="table">The hashtable to populate.</param>
 1000    /// <returns><c>true</c> when the element is a leaf; otherwise <c>false</c>.</returns>
 1001    private static bool TryAddLeafValue(XElement element, string elementName, Hashtable table)
 1002    {
 321003        if (element.HasElements)
 1004        {
 101005            return false;
 1006        }
 1007
 221008        if (!string.IsNullOrWhiteSpace(element.Value))
 1009        {
 211010            table[elementName] = element.Value;
 1011        }
 1012
 221013        return true;
 1014    }
 1015
 1016    /// <summary>
 1017    /// Converts child elements into a nested hashtable, merging repeated keys into a list.
 1018    /// </summary>
 1019    /// <param name="element">The parent element whose children are being converted.</param>
 1020    /// <param name="propertyModels">Property-level OpenAPI XML models used to map element names to property keys.</para
 1021    /// <returns>A hashtable of child values.</returns>
 1022    private static Hashtable ConvertChildElements(XElement element, Dictionary<string, OpenApiXmlModel> propertyModels)
 1023    {
 101024        var childMap = new Hashtable();
 1025
 661026        foreach (var child in element.Elements())
 1027        {
 231028            var childLocalName = child.Name.LocalName;
 1029
 1030            // Map the XML element name back to the *property key* (like attributes do) so callers can bind
 1031            // the resulting hashtable to CLR/PowerShell classes.
 231032            var childPropertyKey = FindPropertyKeyForElement(childLocalName, propertyModels);
 231033            var childModel = childPropertyKey != null ? propertyModels[childPropertyKey] : null;
 231034            var childElementName = childModel?.Name ?? childLocalName;
 1035
 231036            var childValue = ToHashtableInternal(child, childModel, []);
 231037            var valueToStore = childValue[childElementName];
 1038
 231039            var keyToStore = childPropertyKey ?? childElementName;
 231040            AddOrAppendChildValue(childMap, keyToStore, valueToStore);
 1041        }
 1042
 101043        return childMap;
 1044    }
 1045
 1046    /// <summary>
 1047    /// Adds a child value to the map, converting repeated keys into a list (allowing null values).
 1048    /// </summary>
 1049    /// <param name="childMap">Target map of child values.</param>
 1050    /// <param name="key">Key to store under.</param>
 1051    /// <param name="value">Value to store.</param>
 1052    private static void AddOrAppendChildValue(Hashtable childMap, string key, object? value)
 1053    {
 231054        if (!childMap.ContainsKey(key))
 1055        {
 161056            childMap[key] = value;
 161057            return;
 1058        }
 1059
 71060        if (childMap[key] is List<object?> list)
 1061        {
 31062            list.Add(value);
 31063            return;
 1064        }
 1065
 41066        childMap[key] = new List<object?>
 41067        {
 41068            childMap[key],
 41069            value
 41070        };
 41071    }
 1072
 1073    /// <summary>
 1074    /// Finds the property key that corresponds to an XML attribute based on OpenAPI XML models.
 1075    /// </summary>
 1076    /// <param name="attributeName">The XML attribute name.</param>
 1077    /// <param name="propertyModels">Dictionary of property-specific XML models.</param>
 1078    /// <returns>The property key if found; otherwise, null.</returns>
 1079    private static string? FindPropertyKeyForAttribute(string attributeName, Dictionary<string, OpenApiXmlModel> propert
 1080    {
 141081        foreach (var kvp in propertyModels)
 1082        {
 21083            var model = kvp.Value;
 1084            // Check if this property is marked as an attribute and matches the name
 21085            if (model.NodeType == OpenApiXmlNodeType.Attribute)
 1086            {
 21087                var expectedName = model.Name ?? kvp.Key;
 21088                if (string.Equals(expectedName, attributeName, StringComparison.OrdinalIgnoreCase))
 1089                {
 21090                    return kvp.Key; // Return the original property name
 1091                }
 1092            }
 1093        }
 41094        return null;
 21095    }
 1096
 1097    /// <summary>
 1098    /// Finds the property key that corresponds to an XML element name based on OpenAPI XML models.
 1099    /// </summary>
 1100    /// <param name="elementName">The XML element local name.</param>
 1101    /// <param name="propertyModels">Dictionary of property-specific XML models.</param>
 1102    /// <returns>The property key if found; otherwise, null.</returns>
 1103    private static string? FindPropertyKeyForElement(string elementName, Dictionary<string, OpenApiXmlModel> propertyMod
 1104    {
 1105        // Fast path: property name matches element name.
 881106        foreach (var kvp in propertyModels)
 1107        {
 221108            if (string.Equals(kvp.Key, elementName, StringComparison.OrdinalIgnoreCase))
 1109            {
 21110                return kvp.Key;
 1111            }
 1112        }
 1113
 1114        // Match using model.Name override.
 621115        foreach (var kvp in propertyModels)
 1116        {
 121117            var model = kvp.Value;
 121118            var expectedName = model.Name ?? kvp.Key;
 121119            if (string.Equals(expectedName, elementName, StringComparison.OrdinalIgnoreCase))
 1120            {
 41121                return kvp.Key;
 1122            }
 1123        }
 1124
 171125        return null;
 61126    }
 1127}

Methods/Properties

.cctor()
get_MaxDepth()
ToXml(System.String,System.Object)
ToXmlInternal(System.String,System.Object,System.Int32,System.Collections.Generic.HashSet`1<System.Object>)
TryHandleTerminal(System.String,System.Object,System.Int32,System.Collections.Generic.HashSet`1<System.Object>&,System.Xml.Linq.XElement&)
EnterCycle(System.Object,System.Collections.Generic.HashSet`1<System.Object>&,System.Xml.Linq.XElement&)
IsSimple(System.Object)
DictionaryToXml(System.String,System.Collections.IDictionary,System.Int32,System.Collections.Generic.HashSet`1<System.Object>)
EnumerableToXml(System.String,System.Collections.IEnumerable,System.Int32,System.Collections.Generic.HashSet`1<System.Object>)
ObjectToXml(System.String,System.Object,System.Int32,System.Collections.Generic.HashSet`1<System.Object>,System.Collections.Hashtable)
ResolveObjectElementName(System.String,System.Collections.Hashtable)
BuildPropertyMetadataLookup(System.Collections.Hashtable)
TryGetPropertyValue(System.Reflection.PropertyInfo,System.Object,System.Object&)
AddPropertyWithMetadata(System.Xml.Linq.XElement,System.String,System.Object,System.Collections.Hashtable,System.Int32,System.Collections.Generic.HashSet`1<System.Object>)
AppendEnumerableProperty(System.Xml.Linq.XElement,System.String,System.Collections.IEnumerable,System.Boolean,System.String,System.String,System.Int32,System.Collections.Generic.HashSet`1<System.Object>)
ApplyNamespaceAndPrefix(System.Xml.Linq.XElement,System.String,System.String)
TryGetXmlMetadata(System.Type)
EnumerateStaticMemberValues()
LooksLikeXmlMetadata(System.Collections.Hashtable)
AsMetadataHashtable(System.Object)
NameMatchesXmlMetadata(System.String)
GetOpenApiXmlMetadataForType(System.Type)
BuildXmlMetadataFromAttributes(System.Type)
FindOpenApiXmlAttribute(System.Reflection.ICustomAttributeProvider)
GetStringProperty(System.Object,System.String)
GetBoolProperty(System.Object,System.String)
BuildEntryFromAttribute(System.Object)
BuildOpenApiXmlMemberMetadata(System.Type)
PopulateMemberMetadata(System.Collections.Generic.IEnumerable`1<System.Reflection.MemberInfo>,System.Collections.Hashtable)
BuildOpenApiXmlClassMetadata(System.Object)
SanitizeName(System.String)
.cctor()
Equals(System.Object,System.Object)
GetHashCode(System.Object)
ToHashtable(System.Xml.Linq.XElement,System.Collections.Hashtable)
HashtableToOpenApiXml(System.Collections.Hashtable)
ApplyOpenApiXmlName(System.Collections.Hashtable,Microsoft.OpenApi.OpenApiXml)
ApplyOpenApiXmlNamespace(System.Collections.Hashtable,Microsoft.OpenApi.OpenApiXml)
ApplyOpenApiXmlPrefix(System.Collections.Hashtable,Microsoft.OpenApi.OpenApiXml)
ApplyOpenApiXmlNodeType(System.Collections.Hashtable,Microsoft.OpenApi.OpenApiXml)
ToHashtableInternal(System.Xml.Linq.XElement,Microsoft.OpenApi.OpenApiXml,System.Collections.Generic.Dictionary`2<System.String,Microsoft.OpenApi.OpenApiXml>)
GetEffectiveElementName(System.Xml.Linq.XElement,Microsoft.OpenApi.OpenApiXml)
TryConvertNilElement(System.Xml.Linq.XElement,System.String,System.Collections.Hashtable&)
AddAttributesToTable(System.Xml.Linq.XElement,System.Collections.Hashtable,System.Collections.Generic.Dictionary`2<System.String,Microsoft.OpenApi.OpenApiXml>)
TryAddLeafValue(System.Xml.Linq.XElement,System.String,System.Collections.Hashtable)
ConvertChildElements(System.Xml.Linq.XElement,System.Collections.Generic.Dictionary`2<System.String,Microsoft.OpenApi.OpenApiXml>)
AddOrAppendChildValue(System.Collections.Hashtable,System.String,System.Object)
FindPropertyKeyForAttribute(System.String,System.Collections.Generic.Dictionary`2<System.String,Microsoft.OpenApi.OpenApiXml>)
FindPropertyKeyForElement(System.String,System.Collections.Generic.Dictionary`2<System.String,Microsoft.OpenApi.OpenApiXml>)