Skip to content

CSHARP-6024: Support ReadOnlySet Serialization#1987

Open
papafe wants to merge 4 commits into
mongodb:mainfrom
papafe:csharp6024
Open

CSHARP-6024: Support ReadOnlySet Serialization#1987
papafe wants to merge 4 commits into
mongodb:mainfrom
papafe:csharp6024

Conversation

@papafe

@papafe papafe commented May 5, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@papafe papafe requested a review from adelinowona May 5, 2026 13:39
@papafe papafe requested a review from a team as a code owner May 5, 2026 13:39
Copilot AI review requested due to automatic review settings May 5, 2026 13:39
@papafe papafe added the bug Fixes issues or unintended behavior. label May 5, 2026
return value;
}

// last resort: try a constructor that takes a HashSet<TItem> (e.g. ReadOnlySet<T>(ISet<T>))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've put this as the last possibility in the method so that we do not cause issues with preexisting code.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for deserializing set-shaped IEnumerable<T> implementers (including .NET 9+ ReadOnlySet<T>) by introducing a HashSet<T>-based constructor fallback in EnumerableInterfaceImplementerSerializer, along with new unit tests covering constructor selection and ReadOnlySet<T> roundtripping.

Changes:

  • Updated EnumerableInterfaceImplementerSerializer<TValue, TItem>.FinalizeResult to add a last-resort HashSet<TItem> constructor fallback (enabling ISet<T>-constructor-only targets).
  • Added tests to verify: (1) set-ctor fallback behavior, (2) duplicate-collapsing behavior for set-shaped targets, (3) preference for IEnumerable<T> ctor when available.
  • Added a .NET 9+ roundtrip test for System.Collections.ObjectModel.ReadOnlySet<T>.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
tests/MongoDB.Bson.Tests/Serialization/Serializers/EnumerableInterfaceImplementerSerializerTests.cs Adds regression/behavior tests for set-constructor fallback and .NET 9+ ReadOnlySet<T> roundtrip.
src/MongoDB.Bson/Serialization/Serializers/EnumerableInterfaceImplementerSerializer.cs Implements HashSet<TItem>-based constructor fallback in FinalizeResult to support set-shaped targets like ReadOnlySet<T>.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +176 to +177
// last resort: try a constructor that takes a HashSet<TItem> (e.g. ReadOnlySet<T>(ISet<T>))
// Note: collapses duplicate elements, which is correct for set-shaped targets.

// last resort: try a constructor that takes a HashSet<TItem> (e.g. ReadOnlySet<T>(ISet<T>))
// Note: collapses duplicate elements, which is correct for set-shaped targets.
var setCtor = FindSingleArgumentConstructor(typeof(HashSet<TItem>));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be ISet<TItem>?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, fixed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the change from HashSet<TItem> to ISet<TItem> went in the wrong direction.

Is the intent of this fallback not to "find any single-argument constructor that a HashSet can satisfy"?

In which case the right probe is therefore typeof(HashSet<TItem>), not typeof(ISet<TItem>). The difference matters because HashSet<T> implements IReadOnlySet<T> (added in .NET 5), but ISet<T> does not. A type with a constructor taking IReadOnlySet<T> would work with HashSet<TItem> as the probe but silently fall through to the exception with ISet<TItem>. No such type exists in the BCL today, but the HashSet<TItem> probe is strictly more correct as the intent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@papafe @sanych-sun any thoughts on my comment above?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think we should try to find a constructor for the actual type we're using.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should decide on #1987 (comment) comment first, because it might change the way we approach the problem.


// exercises the HashSet<TItem> ctor fallback in FinalizeResult: target type with
// only an ISet<TItem> constructor — same shape as ReadOnlySet<T>
public class SetOnlyCtorEnumerable<T> : IEnumerable<T>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this mock as we have net10.0 target for tests and can exercise with the ReadOnlySet<> itself?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, removed this one.

@sanych-sun sanych-sun added feature Adds new user-facing functionality. and removed bug Fixes issues or unintended behavior. labels May 5, 2026
@papafe papafe requested a review from sanych-sun May 19, 2026 10:07
@papafe papafe requested a review from sanych-sun June 2, 2026 13:54

@adelinowona adelinowona left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FinalizeResult currently seems to do reflection on every deserialization call to figure out how to construct TValue, but the answer is fixed for any given TValue/TItem pair. A static readonly field on the generic class could compute this once per concrete type thus saving us a reflection cost. @sanych-sun WDYT?

@sanych-sun

Copy link
Copy Markdown
Member

About FinalizeResult: the logic there is based on method's parameter real type, not on class generic type parameter. I'm not sure if we can assume that all instances of this class always get accumulator of this exact type. Worth extra investigation.

@papafe papafe requested a review from adelinowona June 8, 2026 13:07
@adelinowona

Copy link
Copy Markdown
Contributor

@sanych-sun Regarding FinalizeResult: I did some investigation aided by claude. Traced the call chain: FinalizeResult is protected abstract and the only caller is EnumerableSerializerBase.Deserialize, which always passes it the result of CreateAccumulator() directly. Since EnumerableInterfaceImplementerSerializer<TValue, TItem> is sealed and doesn't override Deserialize, the accumulator reaching FinalizeResult is always new List<TItem>() — so the static field assumption seems safe.

@sanych-sun

Copy link
Copy Markdown
Member

@adelinowona In our codebase - yes, but we are talking about changes in public base class, there might be another implementations out of our control.

@adelinowona

Copy link
Copy Markdown
Contributor

@sanych-sun yep, my proposed change is entirely contained within EnumerableInterfaceImplementerSerializer<TValue, TItem> — the sealed class. The static field will be a private member there, and FinalizeResult is already overridden there. Nothing in the public base classes changes. External subclasses of the base would be completely unaffected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Adds new user-facing functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants