Skip to content

.Member() loses specialised assertion source for non-string/non-IEnumerable members #5704

Description

@thomhurst

Related: #5703 (same pattern on Eventually/WaitsFor).

Problem

Member has specialised overloads for string and IEnumerable<T> members, but every other shape downcasts to generic IAssertionSource<TMember>. So on a Dictionary<K,V>, HashSet<T>, concrete List<T>, or any specialised type, the lambda cannot reach methods defined on the specialised source (DictionaryAssertionBase, SetAssertionBase, list-specific APIs, etc.).

Async Task<TMember> and ValueTask<TMember> overloads are also not specialised at all — even for string/IEnumerable returns. So Member(x => x.GetTagsAsync(), tags => tags.HasCount(3)) won't compile.

Repro

// Sync — Dictionary member, generic source only
await Assert.That(obj).Member(x => x.Map, m => m.ContainsKey(\"k\")); // fails: ContainsKey not on IAssertionSource<Dictionary<..>>

// Task<string> member — no string specialisation on Task overload
await Assert.That(obj).Member(x => x.NameAsync(), n => n.Contains(\"foo\")); // fails

// Task<IEnumerable<T>> — no collection specialisation on Task overload
await Assert.That(obj).Member(x => x.GetTagsAsync(), t => t.HasCount(3)); // fails

Affected overloads

TUnit.Assertions/Extensions/AssertionExtensions.cs:

  • :463 Member<TObject, TMember, TTransformed>(..., Expression<Func<TObject, TMember?>>, Func<IAssertionSource<TMember>, Assertion<TTransformed>>)
  • :504 Member<TObject, TMember>(..., Expression<Func<TObject, TMember?>>, Func<IAssertionSource<TMember>, Assertion<TMember>>)
  • :547 Member<TObject, TMember>(..., Expression<Func<TObject, TMember?>>, Func<IAssertionSource<TMember>, object>)
  • :590, :628, :667 Task variants — no specialisation
  • :726, :764, :803 ValueTask variants — no specialisation

Proposed fix

Add specialised overloads preserving the member source type:

  • Dictionary<K,V> / IDictionary<K,V> / IReadOnlyDictionary<K,V>DictionaryAssertionBase-style source
  • HashSet<T> / ISet<T>SetAssertionBase-style source
  • IList<T> / IReadOnlyList<T> / List<T> → list-specialised source
  • Mirror string + IEnumerable<T> specialisations onto Task<> and ValueTask<> overloads

Acceptance

  • Specialised assertions on dictionary/set/list/string members usable inside .Member(...) lambda without explicit casts.
  • Applies to sync, Task<>, and ValueTask<> forms.
  • Tests cover each specialised member shape × each of (sync / Task / ValueTask).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions