Skip to content

Commit de454f5

Browse files
Merge remote-tracking branch 'origin/develop' into fix/serialization-improvements-2
2 parents 9aadfc2 + 24eb0f4 commit de454f5

12 files changed

Lines changed: 1103 additions & 321 deletions

Directory.Packages.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
<!-- Testing Packages (all frameworks) -->
99
<ItemGroup>
1010
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
11-
<PackageVersion Include="MSTest.TestFramework" Version="4.3.2" />
12-
<PackageVersion Include="MSTest.TestAdapter" Version="4.3.2" />
11+
<PackageVersion Include="MSTest.TestFramework" Version="4.3.3" />
12+
<PackageVersion Include="MSTest.TestAdapter" Version="4.3.3" />
1313
<PackageVersion Include="xunit" Version="2.9.2" />
1414
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.0" />
1515
<PackageVersion Include="FluentAssertions" Version="7.2.2" />
1616
<PackageVersion Include="NSubstitute" Version="6.0.0" />
17-
<PackageVersion Include="Verify.MSTest" Version="31.27.0" />
17+
<PackageVersion Include="Verify.MSTest" Version="31.28.0" />
1818
</ItemGroup>
1919

2020
<!-- Main Dependencies (all frameworks) -->
2121
<ItemGroup>
22-
<PackageVersion Include="Fhir.Metrics" Version="1.3.1" />
22+
<PackageVersion Include="Fhir.Metrics" Version="1.4.0" />
2323
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.301" />
2424
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
2525
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />

src/Hl7.Fhir.Base/Introspection/ClassMapping.cs

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -187,22 +187,35 @@ internal static bool TryCreate(ModelInspector parent, Type type, [NotNullWhen(tr
187187
// This list is created lazily. This not only improves initial startup time of
188188
// applications but also ensures circular references between types will not cause loops.
189189
private PropertyMappingCollection? _mappings;
190-
191-
private PropertyMappingCollection PropertyMappingsInternal
190+
private object? _mappingsLock;
191+
192+
// Note on the `_field ?? LazyInitializer.EnsureInitialized(...)` shape of this member, the
193+
// other lazily initialized members below and the equivalents in PropertyMapping and
194+
// PropertyMappingCollection: calling EnsureInitialized unconditionally would allocate its
195+
// factory delegate on *every* access, including the warm path where the field has long been
196+
// initialized, and these members are read per element on the (de)serialization path. The
197+
// null-coalescing read short-circuits that: once the field is initialized, an access is an
198+
// ordinary read that allocates nothing, and the factory delegate is only created on the
199+
// once-only cold path. The ordinary read is enough because the runtime guarantees that
200+
// object reference stores are release stores and that data-dependent reads are ordered, so a
201+
// thread observing the field non-null also observes the fully initialized object; see
202+
// https://github.com/dotnet/runtime/blob/main/docs/design/specs/Memory-model.md.
203+
//
204+
// This member uses the EnsureInitialized overload with a syncLock, so that the (possibly
205+
// user-supplied) property mapper building this mutable collection runs exactly once. The other
206+
// lazy members have idempotent factories whose results are never mutated afterwards, so they
207+
// use the lock-free overload, where racing factories may run concurrently but only a single
208+
// result is ever published and handed out.
209+
private PropertyMappingCollection PropertyMappingsInternal =>
210+
_mappings ?? LazyInitializer.EnsureInitialized(ref _mappings, ref _mappingsLock, createPropertyMappings)!;
211+
212+
private PropertyMappingCollection createPropertyMappings()
192213
{
193-
get
194-
{
195-
return LazyInitializer.EnsureInitialized(ref _mappings, createCollection)!;
214+
var properties = propertyMapper(this).ToList();
215+
if(properties.FirstOrDefault(m => m.DeclaringClass != this) is {} errorMapping)
216+
throw new InvalidOperationException($"PropertyMapping '{errorMapping.Name}' is already used for another ClassMapping '{errorMapping.DeclaringClass.Name}'.");
196217

197-
PropertyMappingCollection createCollection()
198-
{
199-
var properties = propertyMapper(this).ToList();
200-
if(properties.FirstOrDefault(m => m.DeclaringClass != this) is {} errorMapping)
201-
throw new InvalidOperationException($"PropertyMapping '{errorMapping.Name}' is already used for another ClassMapping '{errorMapping.DeclaringClass.Name}'.");
202-
203-
return new PropertyMappingCollection(properties);
204-
}
205-
}
218+
return new PropertyMappingCollection(properties);
206219
}
207220

208221
/// <summary>
@@ -215,7 +228,7 @@ PropertyMappingCollection createCollection()
215228
/// property will also be present in the PropertyMappings collection. If this class has
216229
/// no such property, it is null.
217230
/// </summary>
218-
public PropertyMapping? PrimitiveValueProperty => PropertyMappings.SingleOrDefault(pm => pm.RepresentsValueElement);
231+
public PropertyMapping? PrimitiveValueProperty => PropertyMappingsInternal.PrimitiveValueProperty;
219232

220233
/// <summary>
221234
/// This indicates that this class is representing the Patient data (and implements <see cref="IPatient"/>).
@@ -225,7 +238,7 @@ PropertyMappingCollection createCollection()
225238
/// <summary>
226239
/// Whether the reflected type has a member that represent a primitive value.
227240
/// </summary>
228-
public bool HasPrimitiveValueMember => PropertyMappings.Any(pm => pm.RepresentsValueElement);
241+
public bool HasPrimitiveValueMember => PropertyMappingsInternal.HasPrimitiveValueMember;
229242

230243
/// <summary>
231244
/// Returns the mapping for an element of this class by its name.
@@ -358,7 +371,7 @@ IReadOnlyCollection<IElementDefinitionSummary> IStructureDefinitionSummary.GetEl
358371
/// <remarks>If not set, the default constructor for the <see cref="NativeType"/> will be used.</remarks>
359372
public Base CreateInstance()
360373
{
361-
var factory = LazyInitializer.EnsureInitialized(ref _factory, NativeType.BuildFactoryMethod)!;
374+
var factory = _factory ?? LazyInitializer.EnsureInitialized(ref _factory, NativeType.BuildFactoryMethod)!;
362375
var newInstance = factory();
363376
if (newInstance is IDynamicType idt) idt.DynamicTypeName = Name;
364377
return (Base)newInstance;
@@ -372,7 +385,7 @@ public Base CreateInstance()
372385
/// <remarks>If not set, the default List constructor for the <see cref="NativeType"/> will be used.</remarks>
373386
public IList CreateList()
374387
{
375-
var factory = LazyInitializer.EnsureInitialized(ref _listFactory, NativeType.BuildListFactoryMethod)!;
388+
var factory = _listFactory ?? LazyInitializer.EnsureInitialized(ref _listFactory, NativeType.BuildListFactoryMethod)!;
376389
return factory();
377390
}
378391

src/Hl7.Fhir.Base/Introspection/PropertyMapping.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,12 @@ private static ClassMapping findMappingOrThrow(ClassMapping declaringClass, stri
192192
/// <see cref="FhirType"/> it can distinguish between custom types that share the
193193
/// same (dynamic) .NET type. For mappings created by reflection, the list is resolved
194194
/// lazily from <see cref="FhirType"/>.</remarks>
195-
public IReadOnlyList<ClassMapping> FhirTypeMappings
196-
{
197-
get
198-
{
199-
LazyInitializer.EnsureInitialized(ref _fhirTypeMappings, buildFhirTypeMappings);
200-
return _fhirTypeMappings!;
201-
}
202-
}
195+
public IReadOnlyList<ClassMapping> FhirTypeMappings =>
196+
// GetInstantiableType() reads this for every element with an abstract property type while
197+
// parsing, so LazyInitializer.EnsureInitialized() - and the factory delegate it allocates
198+
// per call - is only reached when the field is still null; see the note on
199+
// ClassMapping.PropertyMappingsInternal.
200+
_fhirTypeMappings ?? LazyInitializer.EnsureInitialized(ref _fhirTypeMappings, buildFhirTypeMappings)!;
203201

204202
private ClassMapping[]? _fhirTypeMappings;
205203

src/Hl7.Fhir.Base/Introspection/PropertyMappingCollection.cs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ private void clearCaches()
4747
{
4848
_byOrder = null;
4949
_choice = null;
50+
_valueElements = null;
5051
}
5152

5253
/// <summary>
@@ -74,8 +75,12 @@ public void Clear()
7475
public bool Remove(PropertyMapping item)
7576
{
7677
if (!_byName.TryRemove(item.Name, out _)) return false;
77-
_byOrder?.Remove(item);
78-
_choice?.Remove(item);
78+
79+
// Note: removing `item` from the cached lists is not enough - since the name dictionary is
80+
// case-insensitive, the mapping just evicted may be a *different* instance that happens to
81+
// share `item`'s name, which would leave that evicted mapping behind in the caches. Drop
82+
// the caches instead, so they are rebuilt from the name dictionary on the next read.
83+
clearCaches();
7984

8085
return true;
8186
}
@@ -90,20 +95,51 @@ public bool Remove(PropertyMapping item)
9095
public IReadOnlyDictionary<string, PropertyMapping> ByName => _byName;
9196
private readonly ConcurrentDictionary<string, PropertyMapping> _byName = new(StringComparer.OrdinalIgnoreCase);
9297

98+
// The lazily computed lists below only call LazyInitializer.EnsureInitialized() when the field
99+
// is still null, so that reading them on the warm path allocates nothing; see the note on
100+
// ClassMapping.PropertyMappingsInternal for the reasoning. All three are dropped by
101+
// clearCaches() when the collection changes.
102+
93103
/// <summary>
94104
/// List of the properties, in the order of appearance.
95105
/// </summary>
96-
public IReadOnlyList<PropertyMapping> ByOrder => LazyInitializer.EnsureInitialized(ref _byOrder,
97-
() => ByName.Values.OrderBy(pm => pm.Order).ToList())!;
106+
public IReadOnlyList<PropertyMapping> ByOrder =>
107+
_byOrder ?? LazyInitializer.EnsureInitialized(ref _byOrder,
108+
() => ByName.Values.OrderBy(pm => pm.Order).ToList())!;
109+
98110
private List<PropertyMapping>? _byOrder;
99111

100112
/// <summary>
101113
/// The list of properties that represent choice elements.
102114
/// </summary>
103-
public IReadOnlyList<PropertyMapping> ChoiceProperties => LazyInitializer.EnsureInitialized(ref _choice,
104-
() => ByName.Values.Where(pm => pm.Choice == ChoiceType.DatatypeChoice).ToList())!;
115+
public IReadOnlyList<PropertyMapping> ChoiceProperties =>
116+
_choice ?? LazyInitializer.EnsureInitialized(ref _choice,
117+
() => ByName.Values.Where(pm => pm.Choice == ChoiceType.DatatypeChoice).ToList())!;
118+
105119
private List<PropertyMapping>? _choice;
106120

121+
/// <summary>
122+
/// The property that represents the value of a FHIR primitive, or <c>null</c> when this
123+
/// collection has no such property.
124+
/// </summary>
125+
/// <remarks>Determining which properties are value elements requires a scan of the collection,
126+
/// and this is read for every element encountered during (de)serialization, so the scan is done
127+
/// once and its (near-always empty or single-entry) result cached. Picking the single value
128+
/// element out of that cached result is what still happens per read - including the throw when
129+
/// a malformed mapping declares more than one value element.</remarks>
130+
public PropertyMapping? PrimitiveValueProperty => valueElements.SingleOrDefault();
131+
132+
/// <summary>
133+
/// Whether this collection contains a property that represents the value of a FHIR primitive.
134+
/// </summary>
135+
public bool HasPrimitiveValueMember => valueElements.Count > 0;
136+
137+
private List<PropertyMapping> valueElements =>
138+
_valueElements ?? LazyInitializer.EnsureInitialized(ref _valueElements,
139+
() => ByName.Values.Where(pm => pm.RepresentsValueElement).ToList())!;
140+
141+
private List<PropertyMapping>? _valueElements;
142+
107143
IEnumerator<PropertyMapping> IEnumerable<PropertyMapping>.GetEnumerator() => _byName.Values.GetEnumerator();
108144

109145
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_byName.Values).GetEnumerator();

0 commit comments

Comments
 (0)