From ffa56ef9544296390d446d8cee9c12fd0c53addf Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Mon, 22 Jul 2024 18:58:36 +0200 Subject: [PATCH 1/4] Worked on ModelInspector + ClassMapping. --- .editorconfig | 4 +- .../FhirPath/Expressions/Typecasts.cs | 4 +- .../Introspection/ClassMapping.cs | 144 ++++++++++-------- .../Introspection/EnumMapping.cs | 2 +- .../Introspection/EnumMemberMapping.cs | 4 +- .../Introspection/ModelInspector.cs | 57 ++++--- .../Introspection/PropertyMapping.cs | 17 ++- .../Utility/CollectionExtensions.cs | 19 +++ src/Hl7.Fhir.Base/Utility/EnumUtility.cs | 6 +- src/Hl7.Fhir.Base/Utility/ReflectionHelper.cs | 136 +++++++++-------- src/Hl7.Fhir.Base/Utility/RequiredMember.cs | 45 ++++++ 11 files changed, 267 insertions(+), 171 deletions(-) create mode 100644 src/Hl7.Fhir.Base/Utility/CollectionExtensions.cs create mode 100644 src/Hl7.Fhir.Base/Utility/RequiredMember.cs diff --git a/.editorconfig b/.editorconfig index d88a95278e..76d669221e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -214,7 +214,7 @@ dotnet_naming_symbols.constants.applicable_accessibilities = * dotnet_naming_symbols.constants.required_modifiers = const dotnet_naming_symbols.static_readonly.applicable_kinds = field -dotnet_naming_symbols.static_readonly.applicable_accessibilities = * +dotnet_naming_symbols.static_readonly.applicable_accessibilities = public dotnet_naming_symbols.static_readonly.required_modifiers = readonly, static # Naming styles @@ -242,4 +242,4 @@ dotnet_naming_style._camelcase.capitalization = camel_case dotnet_naming_style.allupper.required_prefix = dotnet_naming_style.allupper.required_suffix = dotnet_naming_style.allupper.word_separator = -dotnet_naming_style.allupper.capitalization = all_upper \ No newline at end of file +dotnet_naming_style.allupper.capitalization = all_upper diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs index d1c6080dec..5e24077001 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs @@ -200,7 +200,7 @@ public static object CastTo(object source, Type to) public static bool IsNullable(this Type t) { - if (!t.IsAValueType()) return true; // ref-type + if (!t.IsValueType) return true; // ref-type if (Nullable.GetUnderlyingType(t) != null) return true; // Nullable return false; // value-type } @@ -235,4 +235,4 @@ public static string ReadableTypeName(Type t) } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs index ef314bd070..6ed8e008fa 100644 --- a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs @@ -28,8 +28,39 @@ namespace Hl7.Fhir.Introspection /// public class ClassMapping : IStructureDefinitionSummary { - private static readonly ConcurrentDictionary<(Type, FhirRelease), ClassMapping?> _mappedClasses = new(); + /// + /// Construct a default mapping for a type by reflecting on the FHIR metadata attributes. + /// + public ClassMapping(string name, Type nativeType, FhirRelease release) + { + Name = name; + NativeType = nativeType; + Release = release; + _propertyMapper = defaultPropertyMapper; + } + /// + /// Construct a default mapping for a type by reflecting on the FHIR metadata attributes, + /// but the properties are provided lazily by the caller. + /// + public ClassMapping(string name, Type nativeType, FhirRelease release, Func> propertyMapper) + :this(name, nativeType,release) + { + _propertyMapper = propertyMapper; + } + + /// + /// Construct a default mapping for a type by reflecting on the FHIR metadata attributes, using the + /// properties passed in to the constructor. + /// + public ClassMapping(string name, Type nativeType, FhirRelease release, IEnumerable propertyMappings) + :this(name, nativeType,release) + { + _propertyMapper = () => propertyMappings; + } + + private static readonly ConcurrentDictionary<(Type, FhirRelease), ClassMapping?> _mappedClasses = new(); + public static void Clear() => _mappedClasses.Clear(); /// @@ -86,33 +117,22 @@ public static bool TryCreate(Type type, [NotNullWhen(true)]out ClassMapping? res } // Now continue with the normal algorithm, types adorned with the [FhirTypeAttribute] - if (GetAttribute(type.GetTypeInfo(), release) is not { } typeAttribute) return false; + if (ReflectionHelper.GetAttribute(type.GetTypeInfo(), release) is not { } typeAttribute) return false; - var backboneAttribute = GetAttribute(type, release); + var backboneAttribute = ReflectionHelper.GetAttribute(type, release); result = new ClassMapping(collectTypeName(typeAttribute, type), type, release) { - IsResource = typeAttribute.IsResource || type.CanBeTreatedAsType(typeof(Resource)), - IsCodeOfT = ReflectionHelper.IsClosedGenericType(type) && - ReflectionHelper.IsConstructedFromGenericTypeDefinition(type, typeof(Code<>)), - IsFhirPrimitive = typeof(PrimitiveType).IsAssignableFrom(type), IsBackboneType = typeAttribute.IsNestedType || backboneAttribute is not null, DefinitionPath = backboneAttribute?.DefinitionPath, - IsBindable = GetAttribute(type.GetTypeInfo(), release)?.IsBindable ?? false, + IsBindable = ReflectionHelper.GetAttribute(type.GetTypeInfo(), release)?.IsBindable ?? false, Canonical = typeAttribute.Canonical, - ValidationAttributes = GetAttributes(type.GetTypeInfo(), release).ToArray(), + ValidationAttributes = ReflectionHelper.GetAttributes(type.GetTypeInfo(), release).ToArray(), }; return true; } - private ClassMapping(string name, Type nativeType, FhirRelease release) - { - Name = name; - NativeType = nativeType; - Release = release; - } - /// /// The FHIR release which this mapping reflects. /// @@ -133,86 +153,102 @@ private ClassMapping(string name, Type nativeType, FhirRelease release) /// .NET primitive types have their name prepended with "Net.", e.g. "Net.System.Int32". /// /// - public string Name { get; private set; } + public string Name { get; } /// /// The .NET class that implements the FHIR datatype/resource /// - public Type NativeType { get; private set; } + public Type NativeType { get; } /// /// Is true when this class represents a Resource datatype. /// - public bool IsResource { get; private set; } = false; + public bool IsResource => typeof(Resource).IsAssignableFrom(NativeType); /// /// Is true when this class represents a FHIR primitive /// /// This is different from a .NET primitive, as FHIR primitives are complex types with a primitive value. - public bool IsFhirPrimitive { get; private set; } = false; + public bool IsFhirPrimitive => typeof(PrimitiveType).IsAssignableFrom(NativeType); /// /// The element is of an atomic .NET type, not a FHIR generated POCO. /// - public bool IsPrimitive { get; private set; } = false; + public bool IsPrimitive { get; init; } = false; /// /// Is true when this class represents a code with a required binding. /// /// See . - public bool IsCodeOfT { get; private set; } = false; + public bool IsCodeOfT => + !NativeType.ContainsGenericParameters && + NativeType.GetGenericTypeDefinition() == typeof(Code<>); /// /// Indicates whether this class represents the nested complex type for a backbone element. /// [Obsolete("These types are now generally called Backbone types, so use IsBackboneType instead.")] - public bool IsNestedType { get => IsBackboneType; set => IsBackboneType = value; } + public bool IsNestedType { get => IsBackboneType; init => IsBackboneType = value; } /// /// Indicates whether this class represents the nested complex type for a backbone element. /// - public bool IsBackboneType { get; private set; } = false; - + public bool IsBackboneType { get; init; } = false; /// /// If this is a backbone type (), then this contains the path /// in the StructureDefinition where the backbone was defined first. /// - public string? DefinitionPath { get; private set; } + public string? DefinitionPath { get; init; } /// /// Indicates whether this class can be used for binding. /// - public bool IsBindable { get; private set; } + public bool IsBindable { get; init; } = false; /// /// The canonical for the StructureDefinition defining this type /// /// Will be null for backbone types. - public string? Canonical { get; private set; } + public string? Canonical { get; init; } + + /// + /// The collection of zero or more (or subclasses) declared + /// on this class. + /// + public ValidationAttribute[] ValidationAttributes { get; init; } = []; // This list is created lazily. This not only improves initial startup time of // applications but also ensures circular references between types will not cause loops. private PropertyMappingCollection? _mappings; - private PropertyMappingCollection propertyMappings + private readonly Func> _propertyMapper; + + private PropertyMappingCollection AllPropertyMappings { get { - LazyInitializer.EnsureInitialized(ref _mappings, inspectProperties); + LazyInitializer.EnsureInitialized(ref _mappings, + () => new PropertyMappingCollection(_propertyMapper())); + return _mappings!; } } - /// - /// List of PropertyMappings for this class, in the order of listing in the FHIR specification. - /// - public IReadOnlyList PropertyMappings => propertyMappings.ByOrder; + // Enumerate this class' properties using reflection and create PropertyMappings. + // Is used when no external mapping has been passed to the constructor. + private IEnumerable defaultPropertyMapper() + { + foreach (var property in ReflectionHelper.FindPublicProperties(NativeType)) + { + if (!PropertyMapping.TryCreate(property, out var propMapping, this, Release)) continue; + yield return propMapping!; + } + } /// - /// The collection of zero or more (or subclasses) declared - /// on this class. + /// List of PropertyMappings for this class, in the order of listing in the FHIR specification. /// - public ValidationAttribute[] ValidationAttributes { get; private set; } = Array.Empty(); + public IReadOnlyList PropertyMappings => AllPropertyMappings.ByOrder; /// /// Holds a reference to a property that represents the value of a FHIR Primitive. This @@ -236,7 +272,7 @@ private PropertyMappingCollection propertyMappings /// public PropertyMapping? FindMappedElementByName(string name) => name != null - ? propertyMappings.ByName.TryGetValue(name, out var mapping) ? mapping : null + ? AllPropertyMappings.ByName.GetValueOrDefault(name) : throw Error.ArgumentNull(nameof(name)); /// @@ -255,7 +291,7 @@ private PropertyMappingCollection propertyMappings if (FindMappedElementByName(name) is { } pm) return pm; // Now, check the choice elements for a match. - var matches = propertyMappings.ChoiceProperties + var matches = AllPropertyMappings.ChoiceProperties .Where(m => name.StartsWith(m.Name)).ToList(); // Loop through possible matches and return the longest match. @@ -271,15 +307,6 @@ private PropertyMappingCollection propertyMappings } } - internal static T? GetAttribute(MemberInfo t, FhirRelease version) where T : Attribute => GetAttributes(t, version).LastOrDefault(); - - internal static IEnumerable GetAttributes(MemberInfo t, FhirRelease version) where T : Attribute - { - return ReflectionHelper.GetAttributes(t).Where(isRelevant); - - bool isRelevant(Attribute a) => a is not IFhirVersionDependent vd || a.AppliesToRelease(version); - } - #region IStructureDefinitionSummary members /// string IStructureDefinitionSummary.TypeName => @@ -320,21 +347,6 @@ IReadOnlyCollection IStructureDefinitionSummary.GetEl private Func? _listFactory; - // Enumerate this class' properties using reflection, create PropertyMappings - // for them and add them to the PropertyMappings. - private PropertyMappingCollection inspectProperties() - { - return new PropertyMappingCollection(map()); - - IEnumerable map() - { - foreach (var property in ReflectionHelper.FindPublicProperties(NativeType)) - { - if (!PropertyMapping.TryCreate(property, out var propMapping, this, Release)) continue; - yield return propMapping!; - } - } - } private static string collectTypeName(FhirTypeAttribute attr, Type type) { @@ -352,8 +364,8 @@ private static string collectTypeName(FhirTypeAttribute attr, Type type) // This is the list of .NET "primitive" types that can be used in the generated POCOs and that we // can generate ClassMappings for. - internal static Type[] SupportedDotNetPrimitiveTypes = new[] - { + internal static Type[] SupportedDotNetPrimitiveTypes = + [ typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal), typeof(string), @@ -361,7 +373,7 @@ private static string collectTypeName(FhirTypeAttribute attr, Type type) typeof(DateTimeOffset), typeof(byte[]), typeof(Enum) - }; + ]; private static ClassMapping buildCqlClassMapping(Type t, FhirRelease release) => new("System." + t.Name, t, release); diff --git a/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs b/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs index 4d1a00e09f..070e3cc6c2 100644 --- a/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs @@ -54,7 +54,7 @@ public static bool TryCreate(Type type, [NotNullWhen(true)] out EnumMapping? res result = default; if (!type.IsEnum) return false; - if (ClassMapping.GetAttribute(type.GetTypeInfo(), release) is not { } typeAttribute) return false; + if (ReflectionHelper.GetAttribute(type.GetTypeInfo(), release) is not { } typeAttribute) return false; result = new EnumMapping(typeAttribute.BindingName, typeAttribute.Valueset, type, release, (typeAttribute.DefaultCodeSystem is not null) ? string.Intern(typeAttribute.DefaultCodeSystem) : null); return true; diff --git a/src/Hl7.Fhir.Base/Introspection/EnumMemberMapping.cs b/src/Hl7.Fhir.Base/Introspection/EnumMemberMapping.cs index 55389f5966..0b13db5242 100644 --- a/src/Hl7.Fhir.Base/Introspection/EnumMemberMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/EnumMemberMapping.cs @@ -61,11 +61,11 @@ private EnumMemberMapping(FieldInfo fieldInfo, string code, string? system, obje public static bool TryCreate(FieldInfo member, [NotNullWhen(true)] out EnumMemberMapping? result, FhirRelease release = (FhirRelease)int.MaxValue, string? defaultSystem = null) { result = null; - if (ClassMapping.GetAttribute(member, release) is not { } ela) return false; + if (ReflectionHelper.GetAttribute(member, release) is not { } ela) return false; var code = ela.Literal ?? member.Name; var value = (Enum)member.GetValue(null)!; - var desc = ClassMapping.GetAttribute(member, release)?.Description; + var desc = ReflectionHelper.GetAttribute(member, release)?.Description; result = new EnumMemberMapping(member, code, ela.System, value, desc, defaultSystem); return true; diff --git a/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs b/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs index cbb08dae64..b3d4be092a 100644 --- a/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs +++ b/src/Hl7.Fhir.Base/Introspection/ModelInspector.cs @@ -56,13 +56,15 @@ public class ModelInspector : IStructureDefinitionSummaryProvider, IModelInfo /// metadata for the most recent release for those base classes. public static ModelInspector ForAssembly(Assembly a) { - return _inspectedAssemblies.GetOrAdd(a.FullName ?? throw Error.ArgumentNull(nameof(a.FullName)), _ => configureInspector(a)); + // ReSharper disable once HeapView.CanAvoidClosure + return _inspectedAssemblies.GetOrAdd(a.FullName ?? throw Error.ArgumentNull(nameof(a.FullName)), + _ => configureInspector(a)); // NB: The concurrent dictionary will make sure only one of these initializers will run at the same time, // so there is no additional locking done in these nested functions. static ModelInspector configureInspector(Assembly a) { - if (a.GetCustomAttribute() is not FhirModelAssemblyAttribute modelAssemblyAttr) + if (a.GetCustomAttribute() is not { } modelAssemblyAttr) throw new InvalidOperationException($"Assembly {a.FullName} cannot be used to supply FHIR metadata," + $" as it is not marked with a {nameof(FhirModelAssemblyAttribute)} attribute."); @@ -90,7 +92,7 @@ void importRecursively(Assembly a) imported.Add(a); var referencedFhirAssemblies = a.GetReferencedAssemblies() - .Select(an => Assembly.Load(an)) + .Select(Assembly.Load) .Where(isFhirModelAssembly); foreach (var ra in referencedFhirAssemblies) @@ -124,6 +126,13 @@ static bool isFhirModelAssembly(Assembly a) => /// public static ModelInspector Base => ForType(typeof(ModelInspector)); + /// + /// Returns a configured with the given predefined mappings. + /// + public static ModelInspector ForPredefinedMappings(FhirRelease version, + IEnumerable classMappings, + IEnumerable enumMappings) => new(version, classMappings, enumMappings); + /// /// Constructs a ModelInspector that will reflect the FHIR metadata for the given FHIR release /// @@ -132,6 +141,13 @@ public ModelInspector(FhirRelease fhirRelease) FhirRelease = fhirRelease; } + private ModelInspector(FhirRelease fhirRelease, IEnumerable classMappings, + IEnumerable enumMappings) : this(fhirRelease) + { + _classMappings = new ClassMappingCollection(classMappings); + _enumMappings = new EnumMappingCollection(enumMappings); + } + /// /// The release of FHIR (i.e. STU3, R4) that this metadata is constructor for. /// @@ -159,7 +175,7 @@ public IReadOnlyList Import(Assembly assembly) { if (assembly == null) throw Error.ArgumentNull(nameof(assembly)); - IEnumerable exportedTypes = assembly.ExportedTypes; + List exportedTypes = assembly.ExportedTypes.ToList(); // Try to derive the literal FHIR version (e.g. 4.0.3) from the ModelInfo. This will only work // if the added assembly is the satellite for a FHIR release. @@ -174,8 +190,8 @@ public IReadOnlyList Import(Assembly assembly) extractEnums(exportedEnums); // Find and extract all ClassMappings - var exportedClasses = exportedTypes.Where(et => et.IsClass && !et.IsEnum); - return exportedClasses.Select(t => ImportType(t)) + var exportedClasses = exportedTypes.Where(et => et is { IsClass: true, IsEnum: false }); + return exportedClasses.Select(ImportType) .Where(cm => cm is not null) .ToList()!; } @@ -196,7 +212,7 @@ public IReadOnlyList Import(Assembly assembly) var nestedEnums = nestedTypes.Where(t => t.IsEnum); extractEnums(nestedEnums); - var nestedClasses = nestedTypes.Where(t => t.IsClass && !t.IsEnum); + var nestedClasses = nestedTypes.Where(t => t is { IsClass: true, IsEnum: false }); extractBackbonesFromClasses(nestedClasses); return mapping; @@ -232,37 +248,37 @@ private void extractBackbonesFromClasses(IEnumerable classTypes) /// /// The search for the mapping by namem is case-insensitive. public ClassMapping? FindClassMapping(string fhirTypeName) => - _classMappings.ByName.TryGetValue(fhirTypeName, out var entry) ? entry : null; + _classMappings.ByName.GetValueOrDefault(fhirTypeName); /// /// Retrieves an already imported given a Type. /// public ClassMapping? FindClassMapping(Type t) => - _classMappings.ByType.TryGetValue(t, out var entry) ? entry : null; + _classMappings.ByType.GetValueOrDefault(t); /// /// Retrieves an already imported given a canonical. /// public ClassMapping? FindClassMappingByCanonical(string canonical) => - _classMappings.ByCanonical.TryGetValue(canonical, out var entry) ? entry : null; + _classMappings.ByCanonical.GetValueOrDefault(canonical); /// /// Retrieves an already imported , given the name of the valueset. /// public EnumMapping? FindEnumMapping(string valuesetName) => - _enumMappings.ByName.TryGetValue(valuesetName, out var entry) ? entry : null; + _enumMappings.ByName.GetValueOrDefault(valuesetName); /// /// Retrieves an already imported given the enum Type. /// public EnumMapping? FindEnumMapping(Type t) => - _enumMappings.ByType.TryGetValue(t, out var entry) ? entry : null; + _enumMappings.ByType.GetValueOrDefault(t); /// /// Retrieves an already imported given the valueset canonical. /// public EnumMapping? FindEnumMappingByCanonical(string canonical) => - _enumMappings.ByCanonical.TryGetValue(canonical, out var entry) ? entry : null; + _enumMappings.ByCanonical.GetValueOrDefault(canonical); /// /// The class mapping representing the Cql Patient type for the inspected model. @@ -295,7 +311,7 @@ private void extractBackbonesFromClasses(IEnumerable classTypes) public Type? GetTypeForFhirType(string name) => FindClassMapping(name) is { } mapping ? mapping.NativeType : null; - public bool IsBindable(string type) => FindClassMapping(type) is { } mapping && mapping.IsBindable; + public bool IsBindable(string type) => FindClassMapping(type) is { IsBindable: true }; public bool IsConformanceResource(string name) => GetTypeForFhirType(name) is { } type && IsConformanceResource(type); @@ -305,10 +321,7 @@ private void extractBackbonesFromClasses(IEnumerable classTypes) public bool IsCoreModelType(Type type) => FindClassMapping(type) is not null; - public bool IsCoreModelTypeUri(Uri uri) => uri is not null - // [WMR 20181025] Issue #746 - // Note: FhirCoreProfileBaseUri.IsBaseOf(new Uri("Dummy", UriKind.RelativeOrAbsolute)) = true...?! - && uri.IsAbsoluteUri + public bool IsCoreModelTypeUri(Uri uri) => uri.IsAbsoluteUri && Canonical.FHIR_CORE_PROFILE_BASE_URI.IsBaseOf(uri) && IsCoreModelType(Canonical.FHIR_CORE_PROFILE_BASE_URI.MakeRelativeUri(uri).ToString()); @@ -324,9 +337,9 @@ public bool IsCoreSuperType(Type type) => type == typeof(PrimitiveType) || type == typeof(BackboneType); - public bool IsDataType(string name) => FindClassMapping(name) is { } mapping && !mapping.IsFhirPrimitive && !mapping.IsResource; + public bool IsDataType(string name) => FindClassMapping(name) is { IsFhirPrimitive: false, IsResource: false }; - public bool IsDataType(Type type) => FindClassMapping(type) is { } mapping && !mapping.IsFhirPrimitive && !mapping.IsResource; + public bool IsDataType(Type type) => FindClassMapping(type) is { IsFhirPrimitive: false, IsResource: false }; public bool IsInstanceTypeFor(string superclass, string subclass) { @@ -338,9 +351,9 @@ public bool IsInstanceTypeFor(string superclass, string subclass) public bool IsInstanceTypeFor(Type superclass, Type subclass) => superclass == subclass || superclass.IsAssignableFrom(subclass); - public bool IsKnownResource(string name) => FindClassMapping(name) is { } mapping && mapping.IsResource; + public bool IsKnownResource(string name) => FindClassMapping(name) is { IsResource: true }; - public bool IsKnownResource(Type type) => FindClassMapping(type) is { } mapping && mapping.IsResource; + public bool IsKnownResource(Type type) => FindClassMapping(type) is { IsResource: true }; public bool IsPrimitive(string name) => FindClassMapping(name)?.IsFhirPrimitive ?? false; diff --git a/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs b/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs index 03e3257761..27d13f117b 100644 --- a/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs @@ -179,12 +179,12 @@ public static bool TryCreate(PropertyInfo prop, [NotNullWhen(true)] out Property result = default; // If there is no [FhirElement] on the property, skip it - var elementAttr = ClassMapping.GetAttribute(prop, release); + var elementAttr = ReflectionHelper.GetAttribute(prop, release); if (elementAttr == null) return false; // If there is an explicit [NotMapped] on the property, skip it // (in combination with `Since` useful to remove a property from the serialization) - var notmappedAttr = ClassMapping.GetAttribute(prop, release); + var notmappedAttr = ReflectionHelper.GetAttribute(prop, release); if (notmappedAttr != null) return false; // We broadly use .IsArray here - this means arrays in POCOs cannot be used to represent @@ -193,17 +193,18 @@ public static bool TryCreate(PropertyInfo prop, [NotNullWhen(true)] out Property // This is pretty ugly, so we prefer to not support arrays - you should use lists instead. bool isCollection = ReflectionHelper.IsTypedCollection(prop.PropertyType) && !prop.PropertyType.IsArray; - var cardinalityAttr = ClassMapping.GetAttribute(prop, release); + var cardinalityAttr = ReflectionHelper.GetAttribute(prop, release); // Get to the actual (native) type representing this element var implementingType = prop.PropertyType; - if (isCollection) implementingType = ReflectionHelper.GetCollectionItemType(prop.PropertyType); + if (isCollection) implementingType = ReflectionHelper.GetCollectionItemType(prop.PropertyType) + ?? throw new InvalidOperationException("Should always have a collection item type"); if (ReflectionHelper.IsNullableType(implementingType)) implementingType = ReflectionHelper.GetNullableArgument(implementingType); // Determine the .NET type that represents the FHIR type for this element. // This is normally just the ImplementingType itself, but can be overridden // with the [DeclaredType] attribute. - var declaredType = ClassMapping.GetAttribute(prop, release); + var declaredType = ReflectionHelper.GetAttribute(prop, release); var fhirType = declaredType?.Type ?? (typeof(Enum).GetTypeInfo().IsAssignableFrom(implementingType) ? typeof(Enum) : implementingType); @@ -214,7 +215,7 @@ public static bool TryCreate(PropertyInfo prop, [NotNullWhen(true)] out Property // The [AllowedElements] attribute can specify a set of allowed types for this element. // If this is a choice element, then take this list as the declared list of FHIR types, // otherwise assume this is the implementing FHIR type above - var allowedTypes = elementAttr.Choice != ChoiceType.None ? ClassMapping.GetAttribute(prop, release) : null; + var allowedTypes = elementAttr.Choice != ChoiceType.None ? ReflectionHelper.GetAttribute(prop, release) : null; var fhirTypes = allowedTypes?.Types?.Any() == true ? allowedTypes.Types : new[] { fhirType }; @@ -232,9 +233,9 @@ public static bool TryCreate(PropertyInfo prop, [NotNullWhen(true)] out Property IsMandatoryElement = cardinalityAttr?.Min > 0, IsPrimitive = isPrimitive, RepresentsValueElement = isPrimitive && isPrimitiveValueElement(elementAttr, prop), - ValidationAttributes = ClassMapping.GetAttributes(prop, release).ToArray(), + ValidationAttributes = ReflectionHelper.GetAttributes(prop, release).ToArray(), FiveWs = elementAttr.FiveWs, - BindingName = ClassMapping.GetAttribute(prop, release)?.Name + BindingName = ReflectionHelper.GetAttribute(prop, release)?.Name }; return true; diff --git a/src/Hl7.Fhir.Base/Utility/CollectionExtensions.cs b/src/Hl7.Fhir.Base/Utility/CollectionExtensions.cs new file mode 100644 index 0000000000..2463cf1a63 --- /dev/null +++ b/src/Hl7.Fhir.Base/Utility/CollectionExtensions.cs @@ -0,0 +1,19 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace Hl7.Fhir.Utility; + +internal static class CollectionExtensions +{ +#if NETSTANDARD2_0 + public static TValue? GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key) => + dictionary.GetValueOrDefault(key, default!); + + public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key, TValue defaultValue) + { + if (dictionary is null) throw new ArgumentNullException(nameof(dictionary)); + return dictionary.TryGetValue(key, out TValue? value) ? value : defaultValue; + } +#endif +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Utility/EnumUtility.cs b/src/Hl7.Fhir.Base/Utility/EnumUtility.cs index b5ba598679..54b348aa1c 100644 --- a/src/Hl7.Fhir.Base/Utility/EnumUtility.cs +++ b/src/Hl7.Fhir.Base/Utility/EnumUtility.cs @@ -92,7 +92,7 @@ static EnumMappingCache() DefaultCodeSystem = enumAttr?.DefaultCodeSystem; foreach (var enumValue in ReflectionHelper.FindEnumFields(t)) { - var attr = ReflectionHelper.GetAttribute(enumValue); + var attr = enumValue.GetCustomAttribute(); string literal = attr?.Literal ?? enumValue.Name; var value = (TEnum)enumValue.GetValue(null)!; @@ -181,7 +181,7 @@ public static EnumMapping Create(Type enumType) foreach (var enumValue in ReflectionHelper.FindEnumFields(enumType)) { - var attr = ReflectionHelper.GetAttribute(enumValue); + var attr = enumValue.GetCustomAttribute(); string literal = attr?.Literal ?? enumValue.Name; var value = (Enum)enumValue.GetValue(null)!; @@ -202,4 +202,4 @@ static string getEnumName(Type t) } } -#nullable restore +#nullable restore \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Utility/ReflectionHelper.cs b/src/Hl7.Fhir.Base/Utility/ReflectionHelper.cs index 4a3f9fe71f..2fd624aaa1 100644 --- a/src/Hl7.Fhir.Base/Utility/ReflectionHelper.cs +++ b/src/Hl7.Fhir.Base/Utility/ReflectionHelper.cs @@ -6,32 +6,31 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +#nullable enable + +using Hl7.Fhir.Introspection; +using Hl7.Fhir.Specification; using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; namespace Hl7.Fhir.Utility { - public static class ReflectionHelper { - public static bool IsAValueType(this Type t) - { - return t.GetTypeInfo().IsValueType; - } - - public static bool CanBeTreatedAsType(this Type currentType, Type typeToCompareWith) - { - // Always return false if either Type is null - if (currentType == null || typeToCompareWith == null) - return false; - - // Return the result of the assignability test - return typeToCompareWith.IsAssignableFrom(currentType); - } + [Obsolete("This helper method is not used in the FHIR .NET SDK anymore. It may be removed in a future version.")] + public static bool IsAValueType(this Type t) => t.GetTypeInfo().IsValueType; + /// + /// Determines whether the specified type is a subclass of the type in . + /// + /// This function simply inverts the arguments for + /// for better readability. + public static bool CanBeTreatedAsType(this Type currentType, Type typeToCompareWith) => + typeToCompareWith.IsAssignableFrom(currentType); /// /// Gets an attribute on an enum field value @@ -39,17 +38,17 @@ public static bool CanBeTreatedAsType(this Type currentType, Type typeToCompareW /// The type of the attribute you want to retrieve /// The enum value /// The attribute of type T that exists on the enum value - public static T GetAttributeOnEnum(this Enum enumVal) where T : System.Attribute + public static T? GetAttributeOnEnum(this Enum enumVal) where T : Attribute { var type = enumVal.GetType(); - var memInfo = type.GetTypeInfo().GetDeclaredField(enumVal.ToString()); + var memInfo = type.GetTypeInfo().GetDeclaredField(enumVal.ToString())!; var attributes = memInfo.GetCustomAttributes(typeof(T), false); - return (T)attributes.FirstOrDefault(); + return (T?)attributes.FirstOrDefault(); } - public static PropertyInfo FindProperty(Type t, string name) => - t.GetTypeInfo().GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + public static PropertyInfo? FindProperty(Type t, string name) => + t.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); /// /// Returns all public, non-static properties for the given type. @@ -67,58 +66,54 @@ public static IEnumerable FindPublicProperties(Type t) /// Returns true if the type is a primitive value type that has a parameterless public constructor. /// public static bool HasDefaultPublicConstructor(Type t) => - t.GetTypeInfo().IsValueType || GetDefaultPublicConstructor(t) != null; + t.IsValueType || GetDefaultPublicConstructor(t) != null; - internal static ConstructorInfo GetDefaultPublicConstructor(Type t) + internal static ConstructorInfo? GetDefaultPublicConstructor(Type t) { - BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public; - return t.GetTypeInfo().GetConstructors(bindingFlags).SingleOrDefault(c => !c.GetParameters().Any()); + const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public; + return t.GetConstructors(bindingFlags).SingleOrDefault(c => !c.GetParameters().Any()); } /// /// Returns true if the type is a . /// - public static bool IsNullableType(Type type) => type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + public static bool IsNullableType(Type type) => + type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); /// /// If the given type is a , this function will return T. /// public static Type GetNullableArgument(Type type) => IsNullableType(type) - ? type.GetTypeInfo().GenericTypeArguments[0] - : throw Error.Argument("type", "Type {0} is not a Nullable".FormatWith(type.Name)); + ? type.GenericTypeArguments[0] + : throw Error.Argument("type", $"Type {type.Namespace} is not a Nullable"); /// /// Returns true if the given type is a .NET2.0+ typed collection. /// - public static bool IsTypedCollection(Type type) - { - return type.IsArray || ImplementsGenericDefinition(type, typeof(ICollection<>)); - } - + public static bool IsTypedCollection(Type type) => + type.IsArray || ImplementsGenericDefinition(type, typeof(ICollection<>)); - public static IList CreateGenericList(Type itemType) - { - return (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(itemType)); - } + public static IList CreateGenericList(Type itemType) => + (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(itemType))!; - public static bool IsClosedGenericType(Type type) => type.GetTypeInfo().IsGenericType && !type.GetTypeInfo().ContainsGenericParameters; + public static bool IsClosedGenericType(Type type) => + type.IsGenericType && !type.GetTypeInfo().ContainsGenericParameters; - public static bool IsOpenGenericTypeDefinition(Type type) => type.GetTypeInfo().IsGenericTypeDefinition; + public static bool IsOpenGenericTypeDefinition(Type type) => type.IsGenericTypeDefinition; - public static bool IsConstructedFromGenericTypeDefinition(Type type, Type genericBase) - { - return type.GetGenericTypeDefinition() == genericBase; - } + [Obsolete("This helper method is not used in the FHIR .NET SDK anymore. It may be removed in a future version.")] + public static bool IsConstructedFromGenericTypeDefinition(Type type, Type genericBase) => + type.GetGenericTypeDefinition() == genericBase; /// /// Gets the type of the typed collection's items. /// /// The type. /// The type of the typed collection's items. - public static Type GetCollectionItemType(Type type) + public static Type? GetCollectionItemType(Type type) { if (type == null) throw Error.ArgumentNull("type"); @@ -126,13 +121,9 @@ public static Type GetCollectionItemType(Type type) { return type.GetElementType(); } - else if (ImplementsGenericDefinition(type, typeof(ICollection<>), out Type genericListType)) + else if (ImplementsGenericDefinition(type, typeof(ICollection<>), out var genericListType)) { - //EK: If I look at ImplementsGenericDefinition, I don't think this can actually occur. - //if (genericListType.IsGenericTypeDefinition) - //throw Error.Argument("type", "Type {0} is not a collection.", type.Name); - - return genericListType.GetTypeInfo().GenericTypeArguments[0]; + return genericListType.GenericTypeArguments[0]; } else if (typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) { @@ -140,23 +131,24 @@ public static Type GetCollectionItemType(Type type) } else { - throw Error.Argument("type", "Type {0} is not a collection.".FormatWith(type.Name)); + throw Error.Argument("type", $"Type {type.Name} is not a collection."); } } + public static bool ImplementsGenericDefinition(Type type, Type genericInterfaceDefinition) => ImplementsGenericDefinition(type, genericInterfaceDefinition, out _); - public static bool ImplementsGenericDefinition(Type type, Type genericInterfaceDefinition, out Type implementingType) + public static bool ImplementsGenericDefinition(Type type, Type genericInterfaceDefinition, [NotNullWhen(true)] out Type? implementingType) { if (type == null) throw Error.ArgumentNull("type"); if (genericInterfaceDefinition == null) throw Error.ArgumentNull("genericInterfaceDefinition"); - if (!genericInterfaceDefinition.GetTypeInfo().IsInterface || !genericInterfaceDefinition.GetTypeInfo().IsGenericTypeDefinition) + if (!genericInterfaceDefinition.IsInterface || !genericInterfaceDefinition.IsGenericTypeDefinition) throw Error.Argument("genericInterfaceDefinition", "'{0}' is not a generic interface definition.".FormatWith(genericInterfaceDefinition.Name)); - if (type.GetTypeInfo().IsInterface) + if (type.IsInterface) { - if (type.GetTypeInfo().IsGenericType) + if (type.IsGenericType) { Type interfaceDefinition = type.GetGenericTypeDefinition(); @@ -170,7 +162,7 @@ public static bool ImplementsGenericDefinition(Type type, Type genericInterfaceD foreach (Type i in type.GetTypeInfo().ImplementedInterfaces) { - if (i.GetTypeInfo().IsGenericType) + if (i.IsGenericType) { Type interfaceDefinition = i.GetGenericTypeDefinition(); @@ -188,35 +180,49 @@ public static bool ImplementsGenericDefinition(Type type, Type genericInterfaceD public static bool IsEnum(this Type t) => t.GetTypeInfo().IsEnum; - public static T GetAttribute(MemberInfo member) where T : Attribute => member.GetCustomAttribute(); + internal static T? GetAttribute(MemberInfo t, FhirRelease version) where T : Attribute => GetAttributes(t, version).LastOrDefault(); + + internal static IEnumerable GetAttributes(MemberInfo t, FhirRelease version) where T : Attribute + { + return t.GetCustomAttributes().Where(isRelevant); + + bool isRelevant(Attribute a) => a is not IFhirVersionDependent || a.AppliesToRelease(version); + } + + [Obsolete("This helper method is not used in the FHIR .NET SDK anymore. It may be removed in a future version.")] + public static T? GetAttribute(MemberInfo member) where T : Attribute => member.GetCustomAttribute(); + + [Obsolete("This helper method is not used in the FHIR .NET SDK anymore. It may be removed in a future version.")] public static IEnumerable GetAttributes(MemberInfo member) => member.GetCustomAttributes(); - public static IEnumerable GetAttributes(Type type) => type.GetTypeInfo().GetCustomAttributes(); + [Obsolete("This helper method is not used in the FHIR .NET SDK anymore. It may be removed in a future version.")] + public static IEnumerable GetAttributes(Type type) => type.GetCustomAttributes(); + [Obsolete("This helper method is not used in the FHIR .NET SDK anymore. It may be removed in a future version.")] public static IEnumerable GetAttributes(MemberInfo member) where T : Attribute => member.GetCustomAttributes(); - - internal static IEnumerable FindEnumFields(Type t) => t.GetTypeInfo().DeclaredFields.Where(a => a.IsPublic && a.IsStatic); + internal static IEnumerable FindEnumFields(Type t) => + t.GetTypeInfo().DeclaredFields.Where(a => a is { IsPublic: true, IsStatic: true }); public static bool IsRepeatingElement(object value) => IsRepeatingElement(value, out _); - public static bool IsRepeatingElement(object value, out ICollection element) + public static bool IsRepeatingElement(object value, [NotNullWhen(true)] out ICollection? element) { element = value as ICollection; return element is not null && !element.GetType().IsArray; } + [Obsolete("This helper method is not used in the FHIR .NET SDK anymore. It may be removed in a future version.")] public static bool IsArray(object value) => value.GetType().IsArray; public static string PrettyTypeName(Type t) { // http://stackoverflow.com/questions/1533115/get-generictype-name-in-good-format-using-reflection-on-c-sharp#answer-25287378 - return t.GetTypeInfo().IsGenericType ? string.Format( - "{0}<{1}>", t.Name.Substring(0, t.Name.LastIndexOf("`", StringComparison.CurrentCulture)), - string.Join(", ", t.GetTypeInfo().GenericTypeParameters.ToList().Select(PrettyTypeName))) - : t.Name; + return t.GetTypeInfo().IsGenericType ? + $"{t.Name.Substring(0, t.Name.LastIndexOf("`", StringComparison.CurrentCulture))}<{string.Join(", ", t.GetTypeInfo().GenericTypeParameters.ToList().Select(PrettyTypeName))}>" + : t.Name; } public static string GetProductVersion(Assembly a) @@ -227,4 +233,4 @@ public static string GetProductVersion(Assembly a) return cleanedInformationalVersion; } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/Utility/RequiredMember.cs b/src/Hl7.Fhir.Base/Utility/RequiredMember.cs new file mode 100644 index 0000000000..b7da74fb60 --- /dev/null +++ b/src/Hl7.Fhir.Base/Utility/RequiredMember.cs @@ -0,0 +1,45 @@ +namespace System.Runtime.CompilerServices; + +/// Specifies that a type has required members or that a member is required. +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else +internal +#endif + sealed class RequiredMemberAttribute : Attribute +{ + +} + +/// +/// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. +/// +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] +public sealed class CompilerFeatureRequiredAttribute : Attribute +{ + public CompilerFeatureRequiredAttribute(string featureName) + { + FeatureName = featureName; + } + + /// + /// The name of the compiler feature. + /// + public string FeatureName { get; } + + /// + /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . + /// + public bool IsOptional { get; init; } + + /// + /// The used for the ref structs C# feature. + /// + public const string RefStructs = nameof(RefStructs); + + /// + /// The used for the required members C# feature. + /// + public const string RequiredMembers = nameof(RequiredMembers); +} \ No newline at end of file From 4fa44398efbcc56ac6d8a8911485917ea2ea47e3 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 23 Jul 2024 13:39:28 +0200 Subject: [PATCH 2/4] Make sure we update the parent ClassMapping for each property. --- src/Hl7.Fhir.Base/Introspection/ClassMapping.cs | 4 ++-- src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs index 6ed8e008fa..19e31b1af2 100644 --- a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs @@ -60,7 +60,7 @@ public ClassMapping(string name, Type nativeType, FhirRelease release, IEnumerab } private static readonly ConcurrentDictionary<(Type, FhirRelease), ClassMapping?> _mappedClasses = new(); - + public static void Clear() => _mappedClasses.Clear(); /// @@ -228,7 +228,7 @@ private PropertyMappingCollection AllPropertyMappings get { LazyInitializer.EnsureInitialized(ref _mappings, - () => new PropertyMappingCollection(_propertyMapper())); + () => new PropertyMappingCollection(this, _propertyMapper())); return _mappings!; } diff --git a/src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs b/src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs index 6312a44026..ccafc302b8 100644 --- a/src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs +++ b/src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs @@ -17,12 +17,14 @@ namespace Hl7.Fhir.Introspection { internal class PropertyMappingCollection { - public PropertyMappingCollection(IEnumerable mappings) + public PropertyMappingCollection(ClassMapping parent, IEnumerable mappings) { var byName = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var mapping in mappings) { + mapping.DeclaringClass = parent; + var propKey = mapping.Name; if (byName.ContainsKey(propKey)) throw Error.InvalidOperation($"Class has multiple properties that are named '{propKey}'. The property name must be unique."); From 7cb4cde18c4117f48330cbab0f22b7715aaebc53 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Thu, 25 Jul 2024 15:22:38 +0200 Subject: [PATCH 3/4] Worked on PropertyMapping + EnumMapping. --- .../Introspection/ClassMapping.cs | 69 ++++-- .../Introspection/EnumMapping.cs | 66 ++++-- .../Introspection/FhirElementAttribute.cs | 2 +- .../Introspection/PropertyMapping.cs | 202 +++++++++++------- .../PropertyMappingCollection.cs | 5 +- .../Utility/FhirEnumerationAttribute.cs | 29 +-- 6 files changed, 243 insertions(+), 130 deletions(-) diff --git a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs index 19e31b1af2..b89cff545c 100644 --- a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs @@ -26,12 +26,48 @@ namespace Hl7.Fhir.Introspection /// /// A container for the metadata of a FHIR datatype as present on the (generated) .NET POCO class. /// - public class ClassMapping : IStructureDefinitionSummary + public class PocoClassMapping : ClassMapping { /// /// Construct a default mapping for a type by reflecting on the FHIR metadata attributes. /// - public ClassMapping(string name, Type nativeType, FhirRelease release) + public PocoClassMapping(string name, Type nativeType, FhirRelease release) + : base(name, nativeType, release) + { + // Nothing + } + + /// + /// Construct a default mapping for a type by reflecting on the FHIR metadata attributes, + /// but the properties are provided lazily by the caller. + /// + public PocoClassMapping(string name, Type nativeType, FhirRelease release, Func> propertyMapper) + :base(name, nativeType,release, propertyMapper) + { + // Nothing + } + + /// + /// Construct a default mapping for a type by reflecting on the FHIR metadata attributes, using the + /// properties passed in to the constructor. + /// + internal PocoClassMapping(string name, Type nativeType, FhirRelease release, IEnumerable propertyMappings) + :base(name, nativeType,release, propertyMappings) + { + // Nothing + } + } + + + /// + /// A container for the metadata of a FHIR datatype. + /// + public abstract class ClassMapping : IStructureDefinitionSummary + { + /// + /// Construct a default mapping for a type by reflecting on the FHIR metadata attributes. + /// + internal ClassMapping(string name, Type nativeType, FhirRelease release) { Name = name; NativeType = nativeType; @@ -43,7 +79,7 @@ public ClassMapping(string name, Type nativeType, FhirRelease release) /// Construct a default mapping for a type by reflecting on the FHIR metadata attributes, /// but the properties are provided lazily by the caller. /// - public ClassMapping(string name, Type nativeType, FhirRelease release, Func> propertyMapper) + internal ClassMapping(string name, Type nativeType, FhirRelease release, Func> propertyMapper) :this(name, nativeType,release) { _propertyMapper = propertyMapper; @@ -53,7 +89,7 @@ public ClassMapping(string name, Type nativeType, FhirRelease release, Func - public ClassMapping(string name, Type nativeType, FhirRelease release, IEnumerable propertyMappings) + internal ClassMapping(string name, Type nativeType, FhirRelease release, IEnumerable propertyMappings) :this(name, nativeType,release) { _propertyMapper = () => propertyMappings; @@ -121,7 +157,7 @@ public static bool TryCreate(Type type, [NotNullWhen(true)]out ClassMapping? res var backboneAttribute = ReflectionHelper.GetAttribute(type, release); - result = new ClassMapping(collectTypeName(typeAttribute, type), type, release) + result = new PocoClassMapping(collectTypeName(typeAttribute, type), type, release) { IsBackboneType = typeAttribute.IsNestedType || backboneAttribute is not null, DefinitionPath = backboneAttribute?.DefinitionPath, @@ -188,12 +224,14 @@ public static bool TryCreate(Type type, [NotNullWhen(true)]out ClassMapping? res /// Indicates whether this class represents the nested complex type for a backbone element. /// [Obsolete("These types are now generally called Backbone types, so use IsBackboneType instead.")] - public bool IsNestedType { get => IsBackboneType; init => IsBackboneType = value; } + public bool IsNestedType { get => IsBackboneType; set => _isBackboneType = value; } /// /// Indicates whether this class represents the nested complex type for a backbone element. /// - public bool IsBackboneType { get; init; } = false; + public bool IsBackboneType { get => _isBackboneType; init => _isBackboneType = value; } + + private bool _isBackboneType; /// /// If this is a backbone type (), then this contains the path @@ -223,16 +261,9 @@ public static bool TryCreate(Type type, [NotNullWhen(true)]out ClassMapping? res private PropertyMappingCollection? _mappings; private readonly Func> _propertyMapper; - private PropertyMappingCollection AllPropertyMappings - { - get - { + private PropertyMappingCollection AllPropertyMappings => LazyInitializer.EnsureInitialized(ref _mappings, - () => new PropertyMappingCollection(this, _propertyMapper())); - - return _mappings!; - } - } + () => new PropertyMappingCollection(this, _propertyMapper()))!; // Enumerate this class' properties using reflection and create PropertyMappings. // Is used when no external mapping has been passed to the constructor. @@ -240,7 +271,7 @@ private IEnumerable defaultPropertyMapper() { foreach (var property in ReflectionHelper.FindPublicProperties(NativeType)) { - if (!PropertyMapping.TryCreate(property, out var propMapping, this, Release)) continue; + if (!PropertyMapping.TryCreate(property, out var propMapping, this)) continue; yield return propMapping!; } } @@ -376,10 +407,10 @@ private static string collectTypeName(FhirTypeAttribute attr, Type type) ]; private static ClassMapping buildCqlClassMapping(Type t, FhirRelease release) => - new("System." + t.Name, t, release); + new PocoClassMapping("System." + t.Name, t, release); private static ClassMapping buildNetPrimitiveClassMapping(Type t, FhirRelease release) => - new("Net." + t.FullName, t, release) { IsPrimitive = true }; + new PocoClassMapping("Net." + t.FullName, t, release) { IsPrimitive = true }; } } diff --git a/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs b/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs index 070e3cc6c2..a131aaa153 100644 --- a/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/EnumMapping.cs @@ -15,13 +15,33 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Threading; namespace Hl7.Fhir.Introspection { + public class PocoEnumMapping : EnumMapping + { + public PocoEnumMapping(string name, Type nativeType, FhirRelease release, Func> valueFactory) + :base(name, nativeType, release, valueFactory) + { + // Nothing + } + + public PocoEnumMapping(string name, Type nativeType, FhirRelease release, string? defaultCodeSystem) : base(name, nativeType, release, defaultCodeSystem) + { + // Nothing + } + + public PocoEnumMapping(string name, Type nativeType, FhirRelease release, IReadOnlyDictionary mappings) : base(name, nativeType, release, mappings) + { + // Nothing + } + } + /// /// A container for the metadata of a FHIR valueset as present on the .NET Enum. /// - public class EnumMapping + public abstract class EnumMapping { private static readonly ConcurrentDictionary<(Type, FhirRelease), EnumMapping?> _mappedEnums = new(); @@ -56,17 +76,33 @@ public static bool TryCreate(Type type, [NotNullWhen(true)] out EnumMapping? res if (ReflectionHelper.GetAttribute(type.GetTypeInfo(), release) is not { } typeAttribute) return false; - result = new EnumMapping(typeAttribute.BindingName, typeAttribute.Valueset, type, release, (typeAttribute.DefaultCodeSystem is not null) ? string.Intern(typeAttribute.DefaultCodeSystem) : null); + result = new PocoEnumMapping(typeAttribute.BindingName, type, release, + (typeAttribute.DefaultCodeSystem is not null) ? string.Intern(typeAttribute.DefaultCodeSystem) : null) + { + Canonical = typeAttribute.Valueset + }; + return true; } - private EnumMapping(string name, string? canonical, Type nativeType, FhirRelease release, string? defaultCodeSystem) + internal EnumMapping(string name, Type nativeType, FhirRelease release, Func> valueFactory) { Name = name; - Canonical = canonical; NativeType = nativeType; Release = release; - _mappings = new(valueFactory: () => mappingInitializer(defaultCodeSystem)); + _mapper = valueFactory; + } + + internal EnumMapping(string name, Type nativeType, FhirRelease release, string? defaultCodeSystem) + : this(name, nativeType, release, () => defaultMappingInitializer(defaultCodeSystem,nativeType)) + { + // Nothing + } + + internal EnumMapping(string name, Type nativeType, FhirRelease release, IReadOnlyDictionary mappings) + :this(name, nativeType, release, () => mappings) + { + // Nothing } /// @@ -82,12 +118,12 @@ private EnumMapping(string name, string? canonical, Type nativeType, FhirRelease /// /// This is the FHIR name /// - public string Name { get; private set; } + public string Name { get; } /// /// The canonical of the ValueSet where this enum was derived from. /// - public string? Canonical { get; } + public string? Canonical { get; init; } /// /// The code system of most of the member of the ValueSet @@ -96,25 +132,27 @@ private EnumMapping(string name, string? canonical, Type nativeType, FhirRelease /// /// The .NET class that implements the FHIR datatype/resource /// - public Type NativeType { get; private set; } + public Type NativeType { get; } /// /// The list of enum members. /// - public IReadOnlyDictionary Members => _mappings.Value; + public IReadOnlyDictionary Members => + LazyInitializer.EnsureInitialized(ref _mappings, _mapper)!; - private readonly Lazy> _mappings; + private readonly Func> _mapper; + private IReadOnlyDictionary? _mappings; + [Obsolete("This property is not used in the FHIR .NET SDK anymore. It may be removed in a future version.")] public string CqlTypeSpecifier => "{http://hl7.org/fhir}" + Name; - - private IReadOnlyDictionary mappingInitializer(string? defaultCS) + private static IReadOnlyDictionary defaultMappingInitializer(string? defaultCodeSystem, Type nativeType) { var result = new Dictionary(); - foreach (var member in ReflectionHelper.FindEnumFields(NativeType)) + foreach (var member in ReflectionHelper.FindEnumFields(nativeType)) { - var success = EnumMemberMapping.TryCreate(member, out var mapping, (FhirRelease)int.MaxValue, defaultCS); + var success = EnumMemberMapping.TryCreate(member, out var mapping, (FhirRelease)int.MaxValue, defaultCodeSystem); if (success) result.Add(mapping!.Code, mapping); } diff --git a/src/Hl7.Fhir.Base/Introspection/FhirElementAttribute.cs b/src/Hl7.Fhir.Base/Introspection/FhirElementAttribute.cs index 17f9b38c55..0b4e916391 100644 --- a/src/Hl7.Fhir.Base/Introspection/FhirElementAttribute.cs +++ b/src/Hl7.Fhir.Base/Introspection/FhirElementAttribute.cs @@ -87,7 +87,7 @@ public FhirElementAttribute(string name, ChoiceType choice, XmlRepresentation re /// public bool IsModifier { get; set; } - public string FiveWs { get; set; } = string.Empty; + public string? FiveWs { get; set; } = null; // This attribute is a subclass of ValidationAttribute so that IsValid() is called on every // FhirElement while validating. This allows us to extend validation into each FhirElement, diff --git a/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs b/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs index 27d13f117b..c5065326ea 100644 --- a/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs @@ -25,66 +25,118 @@ namespace Hl7.Fhir.Introspection /// A container for the metadata of an element of a FHIR datatype as present on a property of a (generated) .NET POCO class. /// [System.Diagnostics.DebuggerDisplay(@"\{Name={Name} ElementType={ImplementingType.Name}}")] - public class PropertyMapping : IElementDefinitionSummary + public class PocoPropertyMapping : PropertyMapping { - // no public constructors - private PropertyMapping( + /// + /// Construct a PropertyMapping with all required information given as arguments. + /// + public PocoPropertyMapping( + string name, + ClassMapping declaringClass, + PropertyInfo pi, + Type implementingType, + ClassMapping propertyTypeMapping, + Type[] fhirTypes) : base(name, declaringClass, pi, implementingType, propertyTypeMapping, fhirTypes) + { + // Nothing + } + } + + /// + /// A container for the metadata of an element of a FHIR datatype. + /// + [System.Diagnostics.DebuggerDisplay(@"\{Name={Name} ElementType={ImplementingType.Name}}")] + public abstract class PropertyMapping : IElementDefinitionSummary + { + /// + /// Construct a PropertyMapping with all required information given as arguments. + /// + internal PropertyMapping( string name, ClassMapping declaringClass, PropertyInfo pi, Type implementingType, ClassMapping propertyTypeMapping, - Type[] fhirTypes, - FhirRelease version) + Type[] fhirTypes) { Name = name; NativeProperty = pi; - Release = version; ImplementingType = implementingType; FhirType = fhirTypes; PropertyTypeMapping = propertyTypeMapping; DeclaringClass = declaringClass; - FiveWs = string.Empty; - ValidationAttributes = Array.Empty(); } /// /// The name of the element in the FHIR specification. /// - public string Name { get; internal set; } + public string Name { get; } /// /// The ClassMapping for the type this property is a member of. /// - public ClassMapping DeclaringClass { get; internal set; } + public ClassMapping DeclaringClass { get; } + + /// + /// The native type of the element. + /// + /// If the element is a collection or is nullable, this reflects the + /// collection item or the type that is made nullable respectively. + /// + public Type ImplementingType { get; } + + /// + /// The list of possible FHIR types for this element, listed as the representative .NET types. + /// For non-choice types this is a single Type, for choices this is either a list of Types or + /// just . + /// + /// These are the defined (choice) types for this element as specified in the + /// FHIR data definitions. It is derived from the actual property type, + /// or, if present, via a list of types in the [AllowedTypes] attribute. Finally, + /// it the property type does not represent FHIR metadata, it is overridden using + /// the [DeclaredType] attribute. + /// + public Type[] FhirType { get; } + + /// + /// The original the metadata was obtained from. + /// + public PropertyInfo NativeProperty { get; } + + /// + /// The that represents the type of this property. + /// + /// This is effectively the ClassMapping for the unless a + /// specifies otherwise. + public ClassMapping PropertyTypeMapping { get; } /// /// Whether the element can repeat. /// - public bool IsCollection { get; internal set; } + public bool IsCollection { get; init; } /// /// The element is of an atomic .NET type, not a FHIR generated POCO. /// - public bool IsPrimitive { get; private set; } + public bool IsPrimitive { get; init; } /// /// The element is a primitive () and /// represents the primitive `value` attribute/property in the FHIR serialization. /// - public bool RepresentsValueElement { get; private set; } + public bool RepresentsValueElement { get; init; } /// /// Whether the element appears in _summary /// (see https://www.hl7.org/fhir/search.html#summary) /// - public bool InSummary { get; private set; } + public bool InSummary { get; init; } /// /// If this modifies the meaning of other elements /// (see https://www.hl7.org/fhir/conformance-rules.html#isModifier) /// - public bool IsModifier { get; private set; } + public bool IsModifier { get; init; } /// /// Five W's mappings of the element. @@ -92,31 +144,24 @@ private PropertyMapping( /// FiveWs pattern from http://hl7.org/fhir/fivews.html. Choice elements are spelled with the /// [x] suffix, like done[x]. /// - public string FiveWs { get; private set; } + public string? FiveWs { get; init; } /// /// Whether the element has a cardinality higher than 0. /// - public bool IsMandatoryElement { get; private set; } + public bool IsMandatoryElement { get; init; } - /// - /// The native type of the element. - /// - /// If the element is a collection or is nullable, this reflects the - /// collection item or the type that is made nullable respectively. - /// - public Type ImplementingType { get; private set; } /// /// The numeric order of the element (relevant for the XML serialization, which /// needs to be in order). /// - public int Order { get; private set; } + public int Order { get; init; } /// /// How this element is represented in the XML serialization. /// - public XmlRepresentation SerializationHint { get; private set; } + public XmlRepresentation SerializationHint { get; init; } /// /// Specifies whether this element contains a choice (either a choice element or a @@ -125,59 +170,53 @@ private PropertyMapping( /// In the case of a DataChoice, these elements have names ending in [x] in /// the StructureDefinition and allow a (possibly restricted) set of types to be used. /// These are reflected in the property. - public ChoiceType Choice { get; private set; } + public ChoiceType Choice { get; init; } /// - /// The list of possible FHIR types for this element, listed as the representative .NET types. - /// For non-choice types this is a single Type, for choices this is either a list of Types or - /// just . + /// The collection of zero or more (or subclasses) declared + /// on this property. /// - /// These are the defined (choice) types for this element as specified in the - /// FHIR data definitions. It is derived from the actual property type, - /// or, if present, via a list of types in the [AllowedTypes] attribute. Finally, - /// it the property type does not represent FHIR metadata, it is overridden using - /// the [DeclaredType] attribute. - /// - public Type[] FhirType { get; private set; } + public ValidationAttribute[] ValidationAttributes { get; init; } = []; /// - /// The that represents the type of this property. + /// The release of FHIR for which the metadata was extracted from the property. /// - /// This is effectively the ClassMapping for the unless a - /// specifies otherwise. - public ClassMapping PropertyTypeMapping { get; private set; } + public FhirRelease Release => DeclaringClass.Release; /// - /// The collection of zero or more (or subclasses) declared - /// on this property. + /// For a bound element, this is the name of the binding. /// - public ValidationAttribute[] ValidationAttributes { get; private set; } = Array.Empty(); + public string? BindingName { get; init; } /// - /// The original the metadata was obtained from. + /// The function that, when passed an object instance, gets the value of the property represented + /// by this mapping. If not set, a "normal" getter will be generated for the property and used instead. /// - public readonly PropertyInfo NativeProperty; + public Func? Getter { get; init; } /// - /// The release of FHIR for which the metadata was extracted from the property. + /// The function that sets the value of the property represented + /// by this mapping. If not set, a "normal" setter will be generated for the property and used instead. /// - public readonly FhirRelease Release; + public Action? Setter { get; init; } - /// - /// For a bound element, this is the name of the binding. - /// - public string? BindingName { get; private set; } + /// + [Obsolete("The FhirRelease parameter is no longer used as it is derived from the ClassMapping, use the overload without the FhirRelease parameter instead.")] + public static bool TryCreate(PropertyInfo prop, [NotNullWhen(true)] out PropertyMapping? result, ClassMapping declaringClass, FhirRelease _) => + TryCreate(prop, out result, declaringClass); /// /// Inspects the given PropertyInfo, extracting metadata from its attributes and creating a new . /// /// There should generally be no reason to call this method, as you can easily get the required PropertyMapping via /// a ClassMapping - which will cache this information as well. This constructor is public for historical reasons only. - public static bool TryCreate(PropertyInfo prop, [NotNullWhen(true)] out PropertyMapping? result, ClassMapping declaringClass, FhirRelease release) + public static bool TryCreate(PropertyInfo prop, [NotNullWhen(true)] out PropertyMapping? result, ClassMapping declaringClass) { if (prop == null) throw Error.ArgumentNull(nameof(prop)); result = default; + var release = declaringClass.Release; + // If there is no [FhirElement] on the property, skip it var elementAttr = ReflectionHelper.GetAttribute(prop, release); if (elementAttr == null) return false; @@ -218,11 +257,11 @@ public static bool TryCreate(PropertyInfo prop, [NotNullWhen(true)] out Property var allowedTypes = elementAttr.Choice != ChoiceType.None ? ReflectionHelper.GetAttribute(prop, release) : null; var fhirTypes = allowedTypes?.Types?.Any() == true ? - allowedTypes.Types : new[] { fhirType }; + allowedTypes.Types : [fhirType]; var isPrimitive = isAllowedNativeTypeForDataTypeValue(implementingType); - result = new PropertyMapping(elementAttr.Name, declaringClass, prop, implementingType, propertyTypeMapping!, fhirTypes, release) + result = new PocoPropertyMapping(elementAttr.Name, declaringClass, prop, implementingType, propertyTypeMapping, fhirTypes) { InSummary = elementAttr.InSummary, IsModifier = elementAttr.IsModifier, @@ -242,7 +281,7 @@ public static bool TryCreate(PropertyInfo prop, [NotNullWhen(true)] out Property } /// - /// This function tried to figure out a concrete type for the element represented by this property. + /// This function tries to figure out a concrete type for the element represented by this property. /// If it cannot derive a concrete type, it will just return . /// internal Type GetInstantiableType() @@ -260,7 +299,7 @@ internal Type GetInstantiableType() private static bool isPrimitiveValueElement(FhirElementAttribute valueElementAttr, PropertyInfo prop) { - var isValueElement = valueElementAttr != null && valueElementAttr.IsPrimitiveValue; + var isValueElement = valueElementAttr.IsPrimitiveValue; return !isValueElement || isAllowedNativeTypeForDataTypeValue(prop.PropertyType) ? isValueElement @@ -283,17 +322,27 @@ private static bool isAllowedNativeTypeForDataTypeValue(Type type) /// /// Given an instance of the parent class, gets the value for this property. /// - public object? GetValue(object instance) => LazyInitializer.EnsureInitialized(ref _getter, NativeProperty.GetValueGetter)!(instance); + public object? GetValue(object instance) + { + return Getter is not null + ? Getter(instance) + : LazyInitializer.EnsureInitialized(ref _defaultGetter, NativeProperty.GetValueGetter)!(instance); + } - private Func? _getter; + private Func? _defaultGetter; /// /// Given an instance of the parent class, sets the value for this property. /// - public void SetValue(object instance, object? value) => - LazyInitializer.EnsureInitialized(ref _setter, NativeProperty.GetValueSetter)!(instance, value); + public void SetValue(object instance, object? value) + { + if(Setter is not null) + Setter(instance,value); + else + LazyInitializer.EnsureInitialized(ref _defaultSetter, NativeProperty.GetValueSetter)!(instance, value); + } - private Action? _setter; + private Action? _defaultSetter; #region IElementDefinitionSummary members string IElementDefinitionSummary.ElementName => this.Name; @@ -334,14 +383,12 @@ ITypeSerializationInfo[] IElementDefinitionSummary.Type private ITypeSerializationInfo[] buildTypes() { - var elementTypeMapping = PropertyTypeMapping; - - if (elementTypeMapping!.IsBackboneType) + if (PropertyTypeMapping!.IsBackboneType) { - var info = elementTypeMapping; - return new ITypeSerializationInfo[] { info }; + return [PropertyTypeMapping]; } - else if (IsPrimitive) + + if (IsPrimitive) { // Backwards compat hack: the primitives (since .value is never queried, this // means Element.id, Narrative.div and Extension.url) should be returned as FHIR type names, not @@ -354,14 +401,12 @@ private ITypeSerializationInfo[] buildTypes() _ => throw new NotSupportedException($"Encountered unexpected primitive type {Name} in backward compat behaviour for ITypedElement.InstanceType.") }; - return new[] { (ITypeSerializationInfo)new PocoTypeReferenceInfo(bwcompatType) }; - } - else - { - var names = FhirType.Select(ft => getFhirTypeName(ft)); - return names.Select(n => (ITypeSerializationInfo)new PocoTypeReferenceInfo(n)).ToArray(); + return [new PocoTypeReferenceInfo(bwcompatType)]; } + var names = FhirType.Select(getFhirTypeName); + return names.Select(n => (ITypeSerializationInfo)new PocoTypeReferenceInfo(n)).ToArray(); + string getFhirTypeName(Type ft) { // The special case where the mapping name is a backbone element name can safely @@ -374,14 +419,9 @@ string getFhirTypeName(Type ft) } } - private struct PocoTypeReferenceInfo : IStructureDefinitionReference + private readonly struct PocoTypeReferenceInfo(string canonical) : IStructureDefinitionReference { - public PocoTypeReferenceInfo(string canonical) - { - ReferredType = canonical; - } - - public string ReferredType { get; private set; } + public string ReferredType { get; } = canonical; } #endregion diff --git a/src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs b/src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs index ccafc302b8..6fb469a854 100644 --- a/src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs +++ b/src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs @@ -17,13 +17,14 @@ namespace Hl7.Fhir.Introspection { internal class PropertyMappingCollection { - public PropertyMappingCollection(ClassMapping parent, IEnumerable mappings) + internal PropertyMappingCollection(ClassMapping parent, IEnumerable mappings) { var byName = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var mapping in mappings) { - mapping.DeclaringClass = parent; + if(mapping.DeclaringClass != parent) + throw new InvalidOperationException("This property already belongs to another parent ClassMapping."); var propKey = mapping.Name; if (byName.ContainsKey(propKey)) diff --git a/src/Hl7.Fhir.Base/Utility/FhirEnumerationAttribute.cs b/src/Hl7.Fhir.Base/Utility/FhirEnumerationAttribute.cs index 056a5faa22..72923606cb 100644 --- a/src/Hl7.Fhir.Base/Utility/FhirEnumerationAttribute.cs +++ b/src/Hl7.Fhir.Base/Utility/FhirEnumerationAttribute.cs @@ -34,33 +34,36 @@ POSSIBILITY OF SUCH DAMAGE. namespace Hl7.Fhir.Utility { - [AttributeUsage(AttributeTargets.Enum, Inherited = false, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Enum)] public sealed class FhirEnumerationAttribute : Attribute { - private readonly string _bindingName; - - // This is a positional argument public FhirEnumerationAttribute(string bindingName, string? canonical = null, string? defaultCodeSystem = null) { - this._bindingName = bindingName; + this.BindingName = bindingName; Valueset = canonical; DefaultCodeSystem = defaultCodeSystem; } - public FhirEnumerationAttribute(string bindingName) : this(bindingName, null, null) + public FhirEnumerationAttribute(string bindingName) : this(bindingName, null) { // Nothing } - public string BindingName - { - get { return _bindingName; } - } + /// + /// The binding name as used for this valueset in CQL. + /// + /// This seems to be a mistake, since there is a one-to-many relationship between valueset + /// and binding, so this will probably be changed in the future. + public string BindingName { get; } + /// + /// The canonical uri for the valueset that this enum represents. + /// public string? Valueset { get; } + /// + /// The default codesystem to use for members of this enumeration that do not have a codesystem specified. + /// public string? DefaultCodeSystem { get; } } -} - -#nullable restore \ No newline at end of file +} \ No newline at end of file From 98ea08ecd1e1752cc4f157019a6bce94c0511f94 Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Thu, 25 Jul 2024 15:43:05 +0200 Subject: [PATCH 4/4] Fix booboo. --- src/Hl7.Fhir.Base/Introspection/ClassMapping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs index b89cff545c..0d43f0fe60 100644 --- a/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs +++ b/src/Hl7.Fhir.Base/Introspection/ClassMapping.cs @@ -217,7 +217,7 @@ public static bool TryCreate(Type type, [NotNullWhen(true)]out ClassMapping? res /// /// See . public bool IsCodeOfT => - !NativeType.ContainsGenericParameters && + NativeType is { IsGenericType: true, ContainsGenericParameters: false } && NativeType.GetGenericTypeDefinition() == typeof(Code<>); ///