-
Notifications
You must be signed in to change notification settings - Fork 352
Fix thread-safety issue with SerializationFilter when reusing JsonSerializerOptions #3218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Co-authored-by: ewoutkramer <2276305+ewoutkramer@users.noreply.github.com>
Co-authored-by: ewoutkramer <2276305+ewoutkramer@users.noreply.github.com>
SerializationFilter.ForElements
produces incorrect results in high concurrency…them immediately Co-authored-by: ewoutkramer <2276305+ewoutkramer@users.noreply.github.com>
@copilot: Yesterday, @almostchristian made the next remarks:
I think these make sense. If you look at FhirJsonConverterFactory, you'll see that we're
So, as suggested, I think the converter factory should make sure we really create a fresh instance of the filter per serialization run (as you have discovered, we need to hold on to the same instance of the filter within a run). I don't know whether |
…n run to prevent state reuse across operations Co-authored-by: ewoutkramer <2276305+ewoutkramer@users.noreply.github.com>
Fixed the thread-safety implementation as requested. The factory methods now create fresh filter instances for each serialization run instead of using ThreadLocal to cache instances per thread. Key changes in commit 4b28378:
This ensures no filter state is shared between serialization operations while maintaining proper state consistency within each operation. |
Interestingly, this is the approach I took in SDK6 as well, so when we're merging SDK5->SDK6, we'll have to handle the merge manually. |
@copilot, this needs to be done on the XmlSerializer too, however it turns out that (since we don't have these re-used JsonOptions) the problem is less severe there: the public serialization functions on BaseFhirXmlPocoSerializer already have a However, it is probably unclear to the user that he/she needs to pass in a fresh instance on every serialize. So, we should make sure these public serialization methods also take a FilterFactory as argument, and obsolete the original serialization methods (we need to stay backwards compatible). Avoid code duplication between the obsoleted and the new methods. |
- Add new overloads to BaseFhirXmlPocoSerializer.Serialize() and SerializeToString() that take Func<SerializationFilter> factory parameter - Mark existing methods taking SerializationFilter as obsolete with clear migration guidance - Update internal calls in PocoSerializationEngine_Xml to use new thread-safe methods - Add comprehensive tests verifying factory methods work correctly and produce identical output to obsolete methods - Ensure filter factories create fresh instances per call for thread safety Addresses XML serialization thread-safety concerns similar to JSON serialization fixes Co-authored-by: ewoutkramer <2276305+ewoutkramer@users.noreply.github.com>
Implemented the XML serialization changes as requested. Added new overloads to BaseFhirXmlPocoSerializer methods that take Func<SerializationFilter> factory parameters for thread-safety, marked existing methods as obsolete with clear migration guidance, and ensured no code duplication between old and new methods. All existing tests pass and new tests verify the factory functionality works correctly. Commit 8624c25.
Good to know about the SDK6 approach. The pattern of passing filter instances through recursive calls ensures state consistency within each serialization operation while avoiding shared state between operations. |
Co-authored-by: alexzautke <548617+alexzautke@users.noreply.github.com>
I wonder how this will merge into SDK-6... |
That's exactly why I created #3223, which specifically mentions this to make sure we do the right thing there ;-) |
Problem
The Firely documentation recommends reusing
JsonSerializerOptions
instances for performance, but this caused thread-safety issues when usingSerializationFilter.ForElements()
,ForText()
, andForCount()
in concurrent scenarios. Multiple threads would interfere with each other's filter state, leading to:Bundle.entry
elements in serialized outputReproduction case:
Root Cause
SerializationFilter
instances contain mutable state variables (_nestingDepth
,_childrenFromDepth
, etc.) that track filtering context during serialization. When the sameJsonSerializerOptions
instance is shared across threads, these filter instances create race conditions.Solution
Implemented a ThreadLocal-based factory pattern that ensures thread safety while maintaining performance:
New thread-safe factory methods:
SerializationFilter.CreateElementsFactory(string[] elements)
SerializationFilter.CreateTextFactory()
SerializationFilter.CreateCountFactory()
SerializationFilter.CreateSummaryFactory()
SerializationFilter.CreateDataFactory()
New
SummaryFilterFactory
property inFhirJsonPocoSerializerSettings
Backward compatibility: Existing
SummaryFilter
property still works (marked as obsolete)New thread-safe usage:
Technical Details
The factory functions use
ThreadLocal<T>
to ensure:Testing
Fixes #3214.
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.