Skip to content

Make lazily-initialized list properties on generated POCOs thread-safe#69

Open
ewoutkramer wants to merge 1 commit into
mainfrom
fix/threadsafe-lazy-list-init
Open

Make lazily-initialized list properties on generated POCOs thread-safe#69
ewoutkramer wants to merge 1 commit into
mainfrom
fix/threadsafe-lazy-list-init

Conversation

@ewoutkramer

Copy link
Copy Markdown
Member

Summary

Generated list-typed element properties (e.g. Patient.Identifier, DomainResource.Extension) use the pattern:

get { ...; return _Identifier ??= []; }

This is not safe under concurrent access: if two threads call the getter while the backing field is still null, both can allocate their own List<>, and only one of those instances ends up published to the field — the other is silently discarded, along with anything already added to it by its caller. This affects every list-typed property across every generated FHIR release (found while analyzing thread-safety of firely-net-sdk POCOs).

This PR switches the getter to LazyInitializer.EnsureInitialized(ref _field, () => []), which:

  • Publishes a single winning instance to every caller (via Interlocked.CompareExchange on the cold path), instead of letting losing callers walk away with their own orphaned list.
  • Costs effectively the same as the current pattern on the (overwhelmingly common) already-initialized hot path — Volatile.Read vs. a plain field read, essentially free on x86/x64.

This is the same idiom already used elsewhere in the base model for the Overflow dictionary and the annotations backing field (Hl7.Fhir.Base/Model/Base.cs), just not yet applied to the codegen template for per-element list properties.

Scalar (non-list) properties are unaffected — they're plain field reads with no laziness, so no race exists there.

Verification

  • dotnet build on Microsoft.Health.Fhir.CodeGen succeeds.
  • Ran the generator against a local R4 package cache and diffed the output against the currently-committed firely-net-sdk sources: the only changes are the added using System.Threading; and the getter line for every list property (e.g. Patient.Identifier, Patient.Name, Patient.ContactComponent.Relationship, etc.) — see below for a representative excerpt.
-        return _Identifier ??= [];
+        return LazyInitializer.EnsureInitialized(ref _Identifier, () => []);

Note: dotnet test's TestFirelyHashes* golden-hash tests already fail on a clean checkout of main in this environment (file count mismatch, e.g. 159 vs. 163 for CSharpFirely2-R4-Satellite.json), independent of this change — that looks like local FHIR package cache drift rather than something this PR introduces, but flagging it since it means the golden hashes for this generator target aren't currently verifiable end-to-end here.

Follow-up (not in this PR)

The regenerated Model/Generated/*.cs files in firely-net-sdk still need to be regenerated and committed there to pick this up — that's a separate PR against FirelyTeam/firely-net-sdk.

🤖 Generated with Claude Code

Generated list-typed element properties used a plain `_field ??= []`
pattern, which races on first access: two threads calling the getter
concurrently while the backing field is still null can each allocate
their own list, and only one ends up published to the field while the
other silently gets discarded. Switching to
LazyInitializer.EnsureInitialized publishes a single winning instance
to every caller via an atomic CompareExchange, at effectively the same
cost on the already-initialized hot path (a Volatile.Read instead of a
plain field read).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant