Feature/readonly collections and adapters#140
Open
kasyanovandrii wants to merge 9 commits into
Open
Conversation
…attributes - Support IReadOnlyDictionary<TKey,TValue> in [DictionaryEquality] — generator now recognises both IDictionary and IReadOnlyDictionary via AllInterfaces check; generated DictionaryEquals helper uses IEnumerable<KeyValuePair<TKey,TValue>> as the common base and pattern-matches to IReadOnlyDictionary/IDictionary for O(1) TryGetValue lookup without casting - Add ReadOnlyDictionaryEqualityComparer<TKey,TValue> to Equatable.Comparers for use with [EqualityComparer] on nested IReadOnlyDictionary properties - Replace OrderBy in DictionaryHashCode and HashSetHashCode with commutative sum-of-HashCode.Combine approach — order-independent and allocation-free; same fix applied to DictionaryEqualityComparer and HashSetEqualityComparer - Add [DataContractEquatable] adapter attribute — reacts to [DataMember]/ [IgnoreDataMember] instead of [IgnoreEquality]; feeds into the same EquatableWriter pipeline - Add [MessagePackEquatable] adapter attribute — reacts to MessagePack [Key]/[IgnoreMember] attributes - Add entity and generator snapshot tests for all new scenarios Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…d collection composition - Add MultiDimensionalArrayEqualityComparer<T> for int[,]/T[,] with zero-allocation Array.GetEnumerator() iteration - Extend GetBaseEquatableType and HasEquatableAttribute to recognise DataContractEquatable and MessagePackEquatable bases so derived classes emit base.Equals()/base.GetHashCode() - Fix IsIncludedDataContract namespace check to require full System.Runtime.Serialization chain - Auto-compose nested collection comparers (SequenceEqualityComparer, DictionaryEqualityComparer, HashSetEqualityComparer) recursively from a single top-level attribute; explicit [EqualityComparer] overrides at any level - Add ComparerTypes.Expression path in writer for fully-composed comparer instance expressions - Extend analyzer to cover DataContractEquatable/MessagePackEquatable types and accept arrays for [SequenceEquality] - Add pinned MetadataReferences for DataMemberAttribute and KeyAttribute assemblies to fix load-order fragility in tests - Add property-based tests (FsCheck) and snapshot tests for all new scenarios Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Core EquatableGenerator no longer knows about DataContract or MessagePack. RegisterProvider is now public static so adapter generators can call it directly. GetBaseEquatableType detects any *EquatableAttribute in Equatable.Attributes namespace instead of hard-coding adapter names. New packages: - Equatable.SourceGenerator.DataContract — DataContractEquatableGenerator - Equatable.SourceGenerator.MessagePack — MessagePackEquatableGenerator - Equatable.Generator.DataContract — NuGet wrapper + DataContractEquatableAttribute - Equatable.Generator.MessagePack — NuGet wrapper + MessagePackEquatableAttribute DataContractEquatableAttribute and MessagePackEquatableAttribute moved out of Equatable.Generator into their respective adapter packages. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…kObject DataContractEquatableAnalyzer (EQ0020): warns when [DataContractEquatable] is present on a class without [DataContract] — DataMember attributes are silently ignored by DataContractSerializer without it. MessagePackEquatableAnalyzer (EQ0021): warns when [MessagePackEquatable] is present on a class without [MessagePackObject] — Key attributes are ignored by the MessagePack serializer without it. Also add [DataContract]/[MessagePackObject] to all test entities and generator test sources to reflect real-world usage accurately. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…r duplication Both adapter generators repeated the same indexer/accessibility guard. Moved to EquatableGenerator.IsPublicInstanceProperty (public static) so adapters call the shared helper instead of copying the logic. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Structs that don't define op_Equality (e.g. Nullable<T> wrapping a plain struct) would cause a CS0019 compile error in generated code because the generator unconditionally emitted == for all value types. HasEqualityOperator() now checks the underlying type for op_Equality before choosing ComparerTypes.ValueType, falling back to EqualityComparer<T>.Default. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
[DictionaryEquality] defaults to order-independent equality (key-lookup Equals, sum-of-pairs hash). [DictionaryEquality(ordered: true)] opts into order-sensitive equality: SequenceEqual on the key-value pair sequence for Equals, sequential HashCode.Add per pair for GetHashCode. Both modes are guaranteed in sync: - Plain types: single ComparerTypes.OrderedDictionary enum value drives both the Equals and GetHashCode switch arms in the writer. - Nested types: BuildCollectionComparerExpression now always emits an OrderedDictionaryEqualityComparer / OrderedReadOnlyDictionaryEqualityComparer instance for ordered properties, so both Equals and GetHashCode go through the same comparer object and can never diverge. Adds OrderedDictionaryEqualityComparer<TKey,TValue> and OrderedReadOnlyDictionaryEqualityComparer<TKey,TValue> to Equatable.Comparers. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ode, enable property tests - Fix equality contract violation: GetHashCode used order-dependent logic while Equals was order-independent for dictionaries and hashsets - DictionaryEqualityComparer/ReadOnlyDictionaryEqualityComparer: GetHashCode now uses commutative SUM (insertion-order independent, consistent with TryGetValue Equals) - HashSetEqualityComparer: GetHashCode now uses commutative SUM (consistent with SetEquals) - Add [DictionaryEquality(sequential: true)] mode: both Equals and GetHashCode use OrderBy(key) for deterministic key-sorted comparison - OrderedDictionary comparers: fix OrderBy sort comparer to use hash tiebreaker when IComparer<TKey> treats distinct keys as equal (e.g. culture-sensitive string sort) - Migrate property-based tests from FsCheck v2 to FsCheck.Xunit.v3 (xUnit v3 compatible) - Add Arbitraries class with custom generators for HashSet<T> (not auto-generated in v3) - Fix property test semantics for SetOfLists/SetOfDicts (reference equality for inner elements) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
… hashset comparers Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem: Equality Contract Violation in Dictionary and HashSet Comparers
The original DictionaryEqualityComparer, ReadOnlyDictionaryEqualityComparer, and HashSetEqualityComparer violated the
fundamental .NET equality contract:
▎ If Equals(x, y) == true then GetHashCode(x) == GetHashCode(y)
Equals was order-independent (TryGetValue / SetEquals) but GetHashCode was order-dependent (OrderBy + sequential
HashCode.Add). Two logically equal collections in different insertion order would produce different hash codes,
causing incorrect behavior in Dictionary, HashSet, and any equality-based data structure.
Changes
DictionaryEqualityComparer<K,V> and ReadOnlyDictionaryEqualityComparer<K,V>
Equals — unchanged, uses TryGetValue (order-independent).
GetHashCode — was OrderBy(key) + sequential HashCode.Add (order-dependent, inconsistent with Equals). Now uses
commutative SUM of HashCode.Combine(key, value) (order-independent, consistent with Equals).
HashSetEqualityComparer
Equals — unchanged, uses SetEquals (order-independent).
GetHashCode — was OrderBy + sequential HashCode.Add (order-dependent, inconsistent with Equals). Now uses commutative
SUM of element hash codes (order-independent, consistent with Equals).
New: [DictionaryEquality(sequential: true)]
Opt-in key-sorted comparison mode where both Equals and GetHashCode use OrderBy(key) — consistent with each other and
deterministic regardless of Dictionary<K,V> internal iteration order:
[DictionaryEquality(sequential: true)]
public Dictionary<string, int>? Entries { get; set; }
Backed by two new comparers:
Both use OrderBy(key) with a sort comparer that guarantees strict total order: prefers IComparer if available,
falls back to a hash-code tiebreaker to handle cases where Comparer.Default returns 0 for distinct keys (e.g.
culture-sensitive string.CompareTo treating "" and "\x02" as equal).
Nested collections and multidimensional arrays (unchanged)
The generator composes comparers automatically for nested collection types. These were not affected by the bug.
Examples of what the generator produces:
ordered
SequenceEqualityComparer and MultiDimensionalArrayEqualityComparer were always consistent (both order-dependent,
matching their respective Equals semantics) and are unchanged.
Property-Based Tests (FsCheck.Xunit.v3)
compatible)