Repository navigation
User documents and ADR statements about Old and EnsureAssignable that the code does not bear out #62
Description
While the documents for #57 were reviewed, one reviewer read README.md,
docs/examples/API_REFERENCE.md, docs/examples/USAGE_EXAMPLES.md and the [Unreleased] section
of CHANGELOG.md whole against the code. It found statements about Old<T>() and
EnsureAssignable<T>() that the code does not bear out. They are older than #57 and were left out
of it on purpose. Most were read from the code and not measured: measure each before acting on
it. Line numbers are those of the #57 branch and move.
A contract can check less than the documents say, or throw where they promise a comparison
- Private fields declared on a base class are not compared when the pair is written in a
method of a derived class (Tis the derived class; reflection does not return a base class's
private fields for it). The XML documentation ofEnsureAssignable<T>()says so; the four user
documents say "a base class … compares the members it declares or inherits" and give no
exception.Old<T>()does copy such a field, which makes the gap harder to guess. No test has
the pair in a derived-class method. - "Two different
Streams are compared member by member." ForT=Streamevery public
property is read on both values;ReadTimeoutandWriteTimeoutthrow on a stream without
timeouts, andLength/Positionon one that cannot seek, so the call would end in
TargetInvocationException, which is neither a pass nor a violation. The two-instance tests use
aComponent, not a stream. Not run. - A class derived from a shared type that is "fixed" (
Regex,Type,MemberInfo,
Assembly, or a class of yours declared inSystem.Collections.Frozen) with state of its own:
Old(() => this)returnsthis, nothing throws, and the pair always passes (a test shows it
for a class derived fromRegex).API_REFERENCE.mdgives the advice "check each state member
withContract.Ensure" for the first group only; the CHANGELOG's migration text gives it for
every shared type. The two disagree.
Too strong or too weak
- "A dictionary value whose
Equalsignores the changed content is reported" (CHANGELOG) holds
for a class; a value of a value type is settled byEqualsfirst, as README and the API
reference say for members and elements. - A copied dictionary or set whose keys have no value
GetHashCodecannot be looked up by key
(enumeration works). Measured for array keys during Old shares an array of frozen collections instead of copying it #57 and stated once in the API reference;
the existing testOld_WhenDictionaryKeyHasIdentityHash_EnumeratesButMissesLookupsshows it for
a key class.[Unreleased]has no entry, although the JSON copy of 2.0.0 rebuilt such a set
(to be confirmed against 2.0.0). - "The comparers of the base class library" are shared: the code shares CoreLib types that
implement the non-generic or thestringcomparer interfaces, which is fewer. - "every … timer" is shared (CHANGELOG): the list holds
System.Threading.Timer
(System.Timers.Timeris shared as aComponent); aPeriodicTimeris copied. The API
reference is exact. - "otherwise its visible fields are compared" for a value type: an
ArraySegment<T>and an
ImmutableArray<T>are compared by what they enumerate. Neither is named in the four files,
while the CHANGELOG says that forMemory<T>a change outside the window is reported. - A collection class as
T(List<T>) is compared by its members, private fields included
(_items,_size,_version), so adding and removing an element reports a violation that
names private fields. The only sentence about collections is "supported". - Under Native AOT,
T= an interface with no property of its own (IList<int>,
IEnumerable<int>) holding two arrays: by the code the call reports "cannot compare … no
properties or fields are visible". The documents name these interfaces as aTunder which
nothing is reported. Not measured.
Smaller
USAGE_EXAMPLES.mdlines 21–23 link to#aggregate-root-with-invariants,#value-objectand
#domain-service; no such headings exist.USAGE_EXAMPLES.md(near line 1206) setsDBCwithEnvironment.SetEnvironmentVariablein a
test-class constructor; the configuration is read once at process start (README says so), so
the sample has no effect once any contract call has run.- "shared" is used for "
Old<T>()returns the original instance" and for "one object referenced
from two places"; "pair" for theOld+EnsureAssignablepair and for a pair of arrays;
"visible" for "through the declared type" and for "to reflection after trimming". - A copy of a finalizable object never runs its finalizer (
GC.SuppressFinalize); the
[Unreleased]entry that lists what does not run leaves the finalizer out. - ADR-0022, the Old + EnsureAssignable never fails in a class derived from a shared type #54 amendment: "every state-holding entry is a class, or a test that only a class
satisfies" does not hold for a struct namedSystem.Threading.Lock(the lock is matched by
full name). - The paragraph "An array as
T" ofAPI_REFERENCE.md(written for Old shares an array of frozen collections instead of copying it #57) is one very long line
with six rules; a list would read better.
Suggested handling. Measure items 1–3 first (one test each); they decide whether a document
sentence or the code is to change. Items 1–3 and 9 touch what #40 and #46 discuss.
Activity
cwouyang commentedon Oct 11, 2026
From the review of the last commit of #63 (the limit for an enumerator of a frozen collection that a copied object holds). Wording points in text that #63 adds, left for this card:
EnsureAssignable<T>()compares the members of the type that holds it, list that member as assignable" (XML remarks ofOld<T>(),API_REFERENCE.md, ADR-0022) holds when that type is theTof the call (measured: a pattern that names the member excuses it). For a holder reached through a member ofTthe patterns are matched against the members ofTonly, so the member ofThas to be listed. Read from the code, not measured.System.Collections.Frozen" holds forFrozenSet<T>.EnumeratorandFrozenDictionary<TKey, TValue>.Enumerator; other enumerators that a frozen collection hands out through an interface are not all of that kind.API_REFERENCE.md: "the enumerator member with no change gives no exception" stands under "Measured" for a member declaredFrozenSet<T>.Enumeratorand for one declaredIEnumerator<T>; only the first was measured.Old<T>()do not say that the same change without an enumerator member is not reported (the API reference does).Tof the call), and the decision recorded there carries no date.