// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Tags { using System; using System.Collections.Generic; using Core.Attributes; using Core.Helper; using UnityEngine; using Utils; /// /// Serialized cache of attribute metadata to avoid runtime reflection. /// This asset is automatically generated in the Editor. /// When the prewarm toggle is enabled, a runtime hook pre-initializes relational component /// reflection helpers before the first scene loads to avoid first-use stalls. /// [ScriptableSingletonPath("Wallstop Studios/Unity Helpers")] [AllowDuplicateCleanup] [AutoLoadSingleton(RuntimeInitializeLoadType.BeforeSceneLoad)] public sealed class AttributeMetadataCache : ScriptableObjectSingleton { [Header("Initialization")] [Tooltip( "If enabled, pre-warms RelationalComponent reflection caches at runtime before the first scene loads. Useful to avoid first-use stalls on IL2CPP or slow devices." )] [SerializeField] private bool _prewarmRelationalOnLoad = false; /// /// Categorizes a relational attribute reference discovered on an . /// /// /// /// Relational attributes allow a component to expose references to related components so modifications can propagate /// (e.g., parent/child links for hierarchical buffs). These values are serialized into the cache to avoid runtime reflection. /// /// /// Typical usage happens via auto-generated metadata; you generally do not set this manually. /// /// public enum RelationalAttributeKind : byte { [Obsolete("Default uninitialized value - should never be used")] Unknown = 0, /// /// The relational field points to a parent component. /// Parent = 1, /// /// The relational field points to a child component. /// Child = 2, /// /// The relational field points to a sibling component. /// Sibling = 3, } /// /// Describes the collection shape of a relational field captured in metadata. /// public enum FieldKind : byte { [Obsolete("Default uninitialized value - should never be used")] None = 0, /// /// A single reference value. /// Single = 1, /// /// An array of values. /// Array = 2, /// /// A of values. /// List = 3, /// /// A of values. /// HashSet = 4, } /// /// Serializable entry describing attribute field names for a single component type. /// [Serializable] public sealed class TypeFieldMetadata { /// /// Assembly-qualified component type name. /// public string typeName; /// /// Attribute field names discovered on the component. /// public string[] fieldNames; /// /// Creates a new metadata entry for a component type. /// /// Assembly-qualified name of the component type. /// Attribute field names found on that type. public TypeFieldMetadata(string typeName, string[] fieldNames) { this.typeName = typeName; this.fieldNames = fieldNames; } } /// /// Serializable entry describing a relational attribute field on a component. /// [Serializable] public sealed class RelationalFieldMetadata { /// /// The name of the relational field on the component. /// public string fieldName; /// /// The relationship classification (parent/child/sibling). /// public RelationalAttributeKind attributeKind; /// /// The collection shape of the field (single, array, list, hashset). /// public FieldKind fieldKind; /// /// The assembly-qualified element type name for the field (for collections) or the field type (for singles). /// public string elementTypeName; /// /// Indicates whether the element type is an interface (affects resolution and validation). /// public bool isInterface; /// /// Creates a relational metadata entry for a component field. /// /// The field name on the component. /// How the field relates to other components. /// Collection shape of the field. /// Assembly-qualified element or field type. /// Whether the element type is an interface. public RelationalFieldMetadata( string fieldName, RelationalAttributeKind attributeKind, FieldKind fieldKind, string elementTypeName, bool isInterface ) { this.fieldName = fieldName; this.attributeKind = attributeKind; this.fieldKind = fieldKind; this.elementTypeName = elementTypeName; this.isInterface = isInterface; } } /// /// Runtime-resolved relational field metadata with references resolved. /// public readonly struct ResolvedRelationalFieldMetadata { /// /// Creates a resolved relational metadata entry. /// /// The relational field name on the component. /// Relationship classification. /// Collection shape of the field. /// Resolved element type (or field type for singles). /// Whether the element type is an interface. public ResolvedRelationalFieldMetadata( string fieldName, RelationalAttributeKind attributeKind, FieldKind fieldKind, Type elementType, bool isInterface ) { FieldName = fieldName; AttributeKind = attributeKind; FieldKind = fieldKind; ElementType = elementType; IsInterface = isInterface; } /// /// The name of the relational field. /// public string FieldName { get; } /// /// Relationship classification for the field. /// public RelationalAttributeKind AttributeKind { get; } /// /// Collection shape of the field. /// public FieldKind FieldKind { get; } /// /// Resolved CLR type for the element/field. /// public Type ElementType { get; } /// /// Indicates if the element type is an interface. /// public bool IsInterface { get; } } /// /// Serializable entry describing all relational fields for a component type. /// [Serializable] public sealed class RelationalTypeMetadata { /// /// Assembly-qualified component type name. /// public string typeName; /// /// Relational attribute fields discovered on the component. /// public RelationalFieldMetadata[] fields; /// /// Creates relational metadata for a component type. /// /// Assembly-qualified name of the component type. /// Relational fields discovered on that type. public RelationalTypeMetadata(string typeName, RelationalFieldMetadata[] fields) { this.typeName = typeName; this.fields = fields; } } [SerializeField] private string[] _allAttributeNames = Array.Empty(); [NonSerialized] private string[] _computedAllAttributeNames; [NonSerialized] private bool _computedAllAttributeNamesIncludesTests; [SerializeField] private TypeFieldMetadata[] _typeMetadata = Array.Empty(); [SerializeField] internal RelationalTypeMetadata[] _relationalTypeMetadata = Array.Empty(); /// /// Serialized entry describing an auto-loaded singleton dependency. /// [Serializable] public sealed class AutoLoadSingletonEntry { /// /// Assembly-qualified type name of the singleton to load. /// public string typeName; /// /// Whether the singleton should be created, fetched, or ignored. /// public SingletonAutoLoadKind kind; /// /// Unity load phase used to initialize the singleton. /// public RuntimeInitializeLoadType loadType; /// /// Default constructor for serialization. /// public AutoLoadSingletonEntry() { } /// /// Creates a new singleton auto-load entry. /// /// Assembly-qualified type name to load. /// How the singleton should be handled. /// Unity load phase for initialization. public AutoLoadSingletonEntry( string typeName, SingletonAutoLoadKind kind, RuntimeInitializeLoadType loadType ) { this.typeName = typeName; this.kind = kind; this.loadType = loadType; } } [SerializeField] private AutoLoadSingletonEntry[] _autoLoadSingletons = Array.Empty(); internal string[] SerializedAttributeNames => _allAttributeNames ?? Array.Empty(); internal TypeFieldMetadata[] SerializedTypeMetadata => _typeMetadata ?? Array.Empty(); internal RelationalTypeMetadata[] SerializedRelationalTypeMetadata => _relationalTypeMetadata ?? Array.Empty(); internal AutoLoadSingletonEntry[] SerializedAutoLoadSingletons => _autoLoadSingletons ?? Array.Empty(); // Compound key for element type lookup private readonly struct ElementTypeKey : IEquatable { private readonly Type _componentType; private readonly string _fieldName; public ElementTypeKey(Type componentType, string fieldName) { _componentType = componentType; _fieldName = fieldName; } public bool Equals(ElementTypeKey other) { return _componentType == other._componentType && string.Equals(_fieldName, other._fieldName, StringComparison.Ordinal); } public override bool Equals(object obj) { return obj is ElementTypeKey other && Equals(other); } public override int GetHashCode() { return Objects.HashCode(_componentType, _fieldName); } } private readonly object _lookupLock = new(); // Runtime lookups using Type as key (much faster than string comparison) private Dictionary _typeFieldsLookup; private Dictionary _relationalFieldsLookup; private Dictionary _resolvedRelationalFieldsLookup; private Dictionary _elementTypeLookup; // Cache resolved element types private static readonly object _typeResolutionLock = new(); private static readonly Dictionary _resolvedTypeCache = new( StringComparer.Ordinal ); private static readonly object _missingTypeLock = new(); private static readonly HashSet _loggedMissingTypeKeys = new( StringComparer.Ordinal ); /// /// Alphabetical list of all attribute field names discovered across registered types. /// Includes test-only attributes when test assemblies are loaded. /// /// /// Callers typically use this to drive tooling (e.g., dropdowns). The list is cached and refreshed automatically when test assemblies are present. /// /// /// /// AttributeMetadataCache cache = AttributeMetadataCache.Instance; /// foreach (string attributeName in cache.AllAttributeNames) /// { /// Debug.Log($"Attribute: {attributeName}"); /// } /// /// public string[] AllAttributeNames { get { bool hasTestAssemblies = AttributeMetadataFilters.HasTestAssembliesLoaded(); if ( _computedAllAttributeNames == null || (hasTestAssemblies && !_computedAllAttributeNamesIncludesTests) || (!hasTestAssemblies && _computedAllAttributeNamesIncludesTests) ) { string[] baseNames = _allAttributeNames ?? Array.Empty(); _computedAllAttributeNames = hasTestAssemblies ? AttributeMetadataFilters.MergeWithExcludedAttributeNames(baseNames) : baseNames; _computedAllAttributeNamesIncludesTests = hasTestAssemblies; } return _computedAllAttributeNames; } } /// /// Auto-load singleton entries configured for this cache. /// /// Entries are sorted for determinism and safe to iterate without additional allocation. public AutoLoadSingletonEntry[] AutoLoadSingletons => SerializedAutoLoadSingletons; private void OnEnable() { _computedAllAttributeNames = null; _computedAllAttributeNamesIncludesTests = false; BuildLookup(); } #if UNITY_2019_1_OR_NEWER [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void RuntimePrewarmRelationalIfEnabled() { try { AttributeMetadataCache inst = Instance; if (inst != null && inst._prewarmRelationalOnLoad) { RelationalComponentInitializer.Report report = RelationalComponentInitializer.Initialize(types: null, logSummary: false); Debug.Log( $"AttributeMetadataCache: Relational prewarm enabled. TypesWarmed={report.TypesWarmed}, FieldsWarmed={report.FieldsWarmed}, Warnings={report.Warnings}, Errors={report.Errors}." ); } } catch (Exception e) { Debug.LogError( $"AttributeMetadataCache: Exception during relational prewarm on load: {e.Message}\n{e}" ); } } #endif private void BuildLookup() { lock (_lookupLock) { if ( _typeFieldsLookup != null && _relationalFieldsLookup != null && _resolvedRelationalFieldsLookup != null && _elementTypeLookup != null ) { return; } Dictionary typeFieldsLookup = new(_typeMetadata?.Length ?? 0); Dictionary relationalFieldsLookup = new( _relationalTypeMetadata?.Length ?? 0 ); Dictionary resolvedRelationalFieldsLookup = new(_relationalTypeMetadata?.Length ?? 0); Dictionary elementTypeLookup = new( _relationalTypeMetadata?.Length ?? 0 ); if (_typeMetadata != null) { foreach (TypeFieldMetadata metadata in _typeMetadata) { if ( metadata == null || !TryResolveType(metadata.typeName, out Type componentType) ) { LogMissingType(metadata?.typeName, "attribute component"); continue; } string[] fieldNames = metadata.fieldNames ?? Array.Empty(); typeFieldsLookup[componentType] = fieldNames; } } if (_relationalTypeMetadata != null) { foreach (RelationalTypeMetadata metadata in _relationalTypeMetadata) { if ( metadata == null || !TryResolveType(metadata.typeName, out Type relationalType) ) { LogMissingType(metadata?.typeName, "relational component"); continue; } RelationalFieldMetadata[] fields = metadata.fields ?? Array.Empty(); relationalFieldsLookup[relationalType] = fields; ResolvedRelationalFieldMetadata[] resolvedFields = new ResolvedRelationalFieldMetadata[fields.Length]; for (int i = 0; i < fields.Length; i++) { RelationalFieldMetadata field = fields[i]; Type elementType = null; if (!string.IsNullOrWhiteSpace(field.elementTypeName)) { if (TryResolveType(field.elementTypeName, out elementType)) { elementTypeLookup[ new ElementTypeKey(relationalType, field.fieldName) ] = elementType; } else { LogMissingType(field.elementTypeName, "relational element"); } } resolvedFields[i] = new ResolvedRelationalFieldMetadata( field.fieldName, field.attributeKind, field.fieldKind, elementType, field.isInterface ); } resolvedRelationalFieldsLookup[relationalType] = resolvedFields; } } _typeFieldsLookup = typeFieldsLookup; _relationalFieldsLookup = relationalFieldsLookup; _resolvedRelationalFieldsLookup = resolvedRelationalFieldsLookup; _elementTypeLookup = elementTypeLookup; } } #if UNITY_INCLUDE_TESTS /// /// Rebuilds all cached lookup tables. Intended for editor and test usage. /// /// /// This clears internal dictionaries and forces a full rebuild, ensuring test isolation. /// public void ForceRebuildForTests() { lock (_lookupLock) { _typeFieldsLookup = null; _relationalFieldsLookup = null; _resolvedRelationalFieldsLookup = null; _elementTypeLookup = null; } BuildLookup(); } #endif /// /// Attempts to retrieve attribute field names for a given component type. /// /// Component type that owns attribute fields. /// Output array of field names if present. /// true when metadata exists for the type; otherwise, false. /// /// /// if (AttributeMetadataCache.Instance.TryGetFieldNames(typeof(AttributesComponent), out var names)) /// { /// Debug.Log($"Found {names.Length} attribute fields."); /// } /// /// public bool TryGetFieldNames(Type type, out string[] fieldNames) { if (_typeFieldsLookup == null) { BuildLookup(); } // ReSharper disable once PossibleNullReferenceException return _typeFieldsLookup.TryGetValue(type, out fieldNames); } /// /// Attempts to retrieve relational field metadata for a given component type. /// /// Component type declaring relational attributes. /// Output array of serialized relational metadata. /// true when relational metadata exists; otherwise, false. public bool TryGetRelationalFields( Type type, out RelationalFieldMetadata[] relationalFields ) { if (_relationalFieldsLookup == null) { BuildLookup(); } // ReSharper disable once PossibleNullReferenceException return _relationalFieldsLookup.TryGetValue(type, out relationalFields); } /// /// Populates with the set of component types that declare /// relational component fields. /// /// List that receives relational component types. /// Thrown when is null. public void CollectRelationalComponentTypes(List destination) { if (destination == null) { throw new ArgumentNullException(nameof(destination)); } if (_relationalFieldsLookup == null) { BuildLookup(); } // ReSharper disable once PossibleNullReferenceException foreach (KeyValuePair pair in _relationalFieldsLookup) { Type componentType = pair.Key; if (componentType == null) { continue; } if (pair.Value is { Length: > 0 }) { destination.Add(componentType); } } } /// /// Attempts to retrieve relational metadata with resolved runtime references. /// /// Component type declaring relational attributes. /// Output array of resolved relational metadata. /// true when resolved metadata exists; otherwise, false. public bool TryGetResolvedRelationalFields( Type type, out ResolvedRelationalFieldMetadata[] relationalFields ) { if (_resolvedRelationalFieldsLookup == null) { BuildLookup(); } // ReSharper disable once PossibleNullReferenceException return _resolvedRelationalFieldsLookup.TryGetValue(type, out relationalFields); } /// /// Attempts to resolve the element type for a relational field on a component. /// /// Component type declaring the field. /// Name of the relational field. /// Resolved element type when available. /// true when a matching element type exists; otherwise, false. public bool TryGetElementType(Type componentType, string fieldName, out Type elementType) { if (_elementTypeLookup == null) { BuildLookup(); } // ReSharper disable once PossibleNullReferenceException return _elementTypeLookup.TryGetValue( new ElementTypeKey(componentType, fieldName), out elementType ); } private static bool TryResolveType(string typeName, out Type type) { if (string.IsNullOrWhiteSpace(typeName)) { type = null; return false; } lock (_typeResolutionLock) { if (_resolvedTypeCache.TryGetValue(typeName, out type)) { return type != null; } type = ReflectionHelpers.TryResolveType(typeName); _resolvedTypeCache[typeName] = type; return type != null; } } private static void LogMissingType(string typeName, string context) { if (string.IsNullOrWhiteSpace(typeName) || string.IsNullOrWhiteSpace(context)) { return; } string key = string.Concat(context, ":", typeName); lock (_missingTypeLock) { if (!_loggedMissingTypeKeys.Add(key)) { return; } } Debug.LogWarning( string.Format( "AttributeMetadataCache: Unable to resolve {0} type '{1}'. The cached entry will be ignored. Regenerate the cache to refresh the metadata.", context, typeName ) ); } #if UNITY_EDITOR /// /// Sets all serialized metadata fields and rebuilds internal lookups when the normalized data changes. /// /// All attribute names discovered. /// Attribute field metadata per component type. /// Relational field metadata per component type. /// Auto-load singleton entries. /// true when the serialized cache changed; otherwise, false. public bool SetMetadata( string[] allAttributeNames, TypeFieldMetadata[] typeMetadata, RelationalTypeMetadata[] relationalTypeMetadata, AutoLoadSingletonEntry[] autoLoadSingletons ) { string[] normalizedAttributeNames = SortAttributeNames(allAttributeNames); TypeFieldMetadata[] normalizedTypeMetadata = SortTypeMetadata(typeMetadata); RelationalTypeMetadata[] normalizedRelationalMetadata = SortRelationalTypeMetadata( relationalTypeMetadata ); AutoLoadSingletonEntry[] normalizedAutoLoad = SortAutoLoadSingletonEntries( autoLoadSingletons ); if ( StringArraysEqual(_allAttributeNames, normalizedAttributeNames) && TypeMetadataArraysEqual(_typeMetadata, normalizedTypeMetadata) && RelationalMetadataArraysEqual( _relationalTypeMetadata, normalizedRelationalMetadata ) && AutoLoadSingletonEntriesEqual(_autoLoadSingletons, normalizedAutoLoad) ) { return false; } _allAttributeNames = normalizedAttributeNames; _typeMetadata = normalizedTypeMetadata; _relationalTypeMetadata = normalizedRelationalMetadata; _autoLoadSingletons = normalizedAutoLoad; _computedAllAttributeNames = null; _computedAllAttributeNamesIncludesTests = false; _typeFieldsLookup = null; _relationalFieldsLookup = null; _resolvedRelationalFieldsLookup = null; _elementTypeLookup = null; UnityEditor.EditorUtility.SetDirty(this); return true; } private static string[] SortAttributeNames(string[] attributeNames) { if (attributeNames == null || attributeNames.Length == 0) { return Array.Empty(); } string[] result = new string[attributeNames.Length]; Array.Copy(attributeNames, result, attributeNames.Length); Array.Sort(result, StringComparer.Ordinal); return result; } private static TypeFieldMetadata[] SortTypeMetadata(TypeFieldMetadata[] typeMetadata) { if (typeMetadata == null || typeMetadata.Length == 0) { return Array.Empty(); } int nonNullCount = 0; foreach (TypeFieldMetadata typeFieldMetadata in typeMetadata) { if (typeFieldMetadata != null) { nonNullCount++; } } if (nonNullCount == 0) { return Array.Empty(); } TypeFieldMetadata[] result = new TypeFieldMetadata[nonNullCount]; int resultIndex = 0; foreach (TypeFieldMetadata metadata in typeMetadata) { if (metadata == null) { continue; } string typeName = metadata.typeName ?? string.Empty; string[] fieldNames = metadata.fieldNames ?? Array.Empty(); string[] sortedFieldNames = fieldNames.Length == 0 ? Array.Empty() : CopyAndSort(fieldNames); result[resultIndex] = new TypeFieldMetadata(typeName, sortedFieldNames); resultIndex++; } Array.Sort(result, CompareTypeFieldMetadata); return result; } private static RelationalTypeMetadata[] SortRelationalTypeMetadata( RelationalTypeMetadata[] relationalTypeMetadata ) { if (relationalTypeMetadata == null || relationalTypeMetadata.Length == 0) { return Array.Empty(); } int nonNullCount = 0; foreach (RelationalTypeMetadata typeMetadata in relationalTypeMetadata) { if (typeMetadata != null) { nonNullCount++; } } if (nonNullCount == 0) { return Array.Empty(); } RelationalTypeMetadata[] result = new RelationalTypeMetadata[nonNullCount]; int resultIndex = 0; foreach (RelationalTypeMetadata metadata in relationalTypeMetadata) { if (metadata == null) { continue; } string typeName = metadata.typeName ?? string.Empty; RelationalFieldMetadata[] sortedFields = SortRelationalFields(metadata.fields); result[resultIndex] = new RelationalTypeMetadata(typeName, sortedFields); resultIndex++; } Array.Sort(result, CompareRelationalTypeMetadata); return result; } private static RelationalFieldMetadata[] SortRelationalFields( RelationalFieldMetadata[] relationalFields ) { if (relationalFields == null || relationalFields.Length == 0) { return Array.Empty(); } int nonNullCount = 0; foreach (RelationalFieldMetadata relationalField in relationalFields) { if (relationalField != null) { nonNullCount++; } } if (nonNullCount == 0) { return Array.Empty(); } RelationalFieldMetadata[] result = new RelationalFieldMetadata[nonNullCount]; int resultIndex = 0; foreach (RelationalFieldMetadata field in relationalFields) { if (field == null) { continue; } result[resultIndex] = new RelationalFieldMetadata( field.fieldName, field.attributeKind, field.fieldKind, field.elementTypeName, field.isInterface ); resultIndex++; } Array.Sort(result, CompareRelationalFieldMetadata); return result; } private static string[] CopyAndSort(string[] values) { string[] result = new string[values.Length]; Array.Copy(values, result, values.Length); Array.Sort(result, StringComparer.Ordinal); return result; } private static int CompareTypeFieldMetadata(TypeFieldMetadata left, TypeFieldMetadata right) { if (ReferenceEquals(left, right)) { return 0; } if (left == null) { return -1; } if (right == null) { return 1; } int typeNameComparison = string.CompareOrdinal(left.typeName, right.typeName); if (typeNameComparison != 0) { return typeNameComparison; } string[] leftFields = left.fieldNames ?? Array.Empty(); string[] rightFields = right.fieldNames ?? Array.Empty(); int lengthComparison = leftFields.Length.CompareTo(rightFields.Length); if (lengthComparison != 0) { return lengthComparison; } for (int i = 0; i < leftFields.Length; i++) { int fieldComparison = string.CompareOrdinal(leftFields[i], rightFields[i]); if (fieldComparison != 0) { return fieldComparison; } } return 0; } private static bool StringArraysEqual(string[] left, string[] right) { left ??= Array.Empty(); right ??= Array.Empty(); if (left.Length != right.Length) { return false; } for (int i = 0; i < left.Length; i++) { if (!string.Equals(left[i], right[i], StringComparison.Ordinal)) { return false; } } return true; } private static bool TypeMetadataArraysEqual( TypeFieldMetadata[] left, TypeFieldMetadata[] right ) { left ??= Array.Empty(); right ??= Array.Empty(); if (left.Length != right.Length) { return false; } for (int i = 0; i < left.Length; i++) { if (CompareTypeFieldMetadata(left[i], right[i]) != 0) { return false; } } return true; } private static bool RelationalMetadataArraysEqual( RelationalTypeMetadata[] left, RelationalTypeMetadata[] right ) { left ??= Array.Empty(); right ??= Array.Empty(); if (left.Length != right.Length) { return false; } for (int i = 0; i < left.Length; i++) { if (CompareRelationalTypeMetadata(left[i], right[i]) != 0) { return false; } } return true; } private static bool AutoLoadSingletonEntriesEqual( AutoLoadSingletonEntry[] left, AutoLoadSingletonEntry[] right ) { left ??= Array.Empty(); right ??= Array.Empty(); if (left.Length != right.Length) { return false; } for (int i = 0; i < left.Length; i++) { AutoLoadSingletonEntry leftEntry = left[i]; AutoLoadSingletonEntry rightEntry = right[i]; if (ReferenceEquals(leftEntry, rightEntry)) { continue; } if (leftEntry == null || rightEntry == null) { return false; } if ( !string.Equals( leftEntry.typeName, rightEntry.typeName, StringComparison.Ordinal ) || leftEntry.kind != rightEntry.kind || leftEntry.loadType != rightEntry.loadType ) { return false; } } return true; } private static int CompareRelationalTypeMetadata( RelationalTypeMetadata left, RelationalTypeMetadata right ) { if (ReferenceEquals(left, right)) { return 0; } if (left == null) { return -1; } if (right == null) { return 1; } int typeNameComparison = string.CompareOrdinal(left.typeName, right.typeName); if (typeNameComparison != 0) { return typeNameComparison; } RelationalFieldMetadata[] leftFields = left.fields ?? Array.Empty(); RelationalFieldMetadata[] rightFields = right.fields ?? Array.Empty(); int lengthComparison = leftFields.Length.CompareTo(rightFields.Length); if (lengthComparison != 0) { return lengthComparison; } for (int i = 0; i < leftFields.Length; i++) { int fieldComparison = CompareRelationalFieldMetadata(leftFields[i], rightFields[i]); if (fieldComparison != 0) { return fieldComparison; } } return 0; } private static int CompareRelationalFieldMetadata( RelationalFieldMetadata left, RelationalFieldMetadata right ) { if (ReferenceEquals(left, right)) { return 0; } if (left == null) { return -1; } if (right == null) { return 1; } int fieldNameComparison = string.CompareOrdinal(left.fieldName, right.fieldName); if (fieldNameComparison != 0) { return fieldNameComparison; } int attributeComparison = left.attributeKind.CompareTo(right.attributeKind); if (attributeComparison != 0) { return attributeComparison; } int fieldKindComparison = left.fieldKind.CompareTo(right.fieldKind); if (fieldKindComparison != 0) { return fieldKindComparison; } int elementTypeComparison = string.CompareOrdinal( left.elementTypeName, right.elementTypeName ); if (elementTypeComparison != 0) { return elementTypeComparison; } if (left.isInterface == right.isInterface) { return 0; } return left.isInterface ? -1 : 1; } private static AutoLoadSingletonEntry[] SortAutoLoadSingletonEntries( AutoLoadSingletonEntry[] entries ) { if (entries == null || entries.Length == 0) { return Array.Empty(); } List result = new(entries.Length); foreach (AutoLoadSingletonEntry entry in entries) { if (entry == null || string.IsNullOrWhiteSpace(entry.typeName)) { continue; } result.Add(new AutoLoadSingletonEntry(entry.typeName, entry.kind, entry.loadType)); } if (result.Count == 0) { return Array.Empty(); } result.Sort((left, right) => string.CompareOrdinal(left.typeName, right.typeName)); return result.ToArray(); } #endif } }