diff --git a/TUnit.Assertions.Tests/Bugs/Issue5707Tests.cs b/TUnit.Assertions.Tests/Bugs/Issue5707Tests.cs
new file mode 100644
index 0000000000..e4ba59f8b0
--- /dev/null
+++ b/TUnit.Assertions.Tests/Bugs/Issue5707Tests.cs
@@ -0,0 +1,298 @@
+using TUnit.Assertions.Exceptions;
+
+namespace TUnit.Assertions.Tests.Bugs;
+
+///
+/// Regression tests for GitHub issue #5707:
+/// `.Count(itemAssertion)` per-item overload only exposed a generic
+/// `IAssertionSource<TItem>` inside the lambda, so specialised
+/// assertions defined on collection / dictionary / set / list bases
+/// (e.g. HasCount, ContainsKey, IsSubsetOf,
+/// HasItemAt) were unreachable when the items themselves were
+/// collections, dictionaries or sets.
+///
+/// Specialised Count overloads now hand the lambda a typed
+/// source (CollectionAssertion, ListAssertion, DictionaryAssertion,
+/// SetAssertion, etc.) so the failure message also keeps the
+/// specialised assertion's expectation rather than a generic wrapper.
+///
+public class Issue5707Tests
+{
+ [Test]
+ public async Task Count_String_Items_Bind_To_Generic_Instance_Method()
+ {
+ // Strings already resolve through the generic instance method on
+ // CollectionAssertionBase: `IEnumerable` cannot bind to
+ // `string` via type inference (no concrete-to-interface unification),
+ // so no specialised string overload is needed.
+ var items = new List { "apple", "banana", "apricot", "cherry" };
+
+ await Assert.That(items).Count(s => s.IsEqualTo("apple")).IsEqualTo(1);
+ }
+
+ [Test]
+ public async Task Count_Enumerable_Items_Reach_HasCount_On_Inner()
+ {
+ IEnumerable> listOfLists = new List>
+ {
+ new() { 1, 2, 3 },
+ new() { 1 },
+ new() { 1, 2, 3 },
+ };
+
+ await Assert.That(listOfLists).Count(l => l.Count().IsEqualTo(3)).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_List_Items_Reach_HasItemAt_On_Inner()
+ {
+ var listOfLists = new List>
+ {
+ new List { 10, 20 },
+ new List { 99 },
+ new List { 10, 30 },
+ };
+
+ await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_ReadOnlyList_Items_Reach_HasItemAt_On_Inner()
+ {
+ var listOfLists = new List>
+ {
+ new List { 10, 20 },
+ new List { 99 },
+ new List { 10, 30 },
+ };
+
+ await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_ReadOnlyDictionary_Items_Reach_ContainsKey_On_Inner()
+ {
+ IReadOnlyDictionary a = new Dictionary { ["k"] = 1 };
+ IReadOnlyDictionary b = new Dictionary { ["other"] = 2 };
+ IReadOnlyDictionary c = new Dictionary { ["k"] = 5 };
+ var dicts = new List> { a, b, c };
+
+ await Assert.That(dicts).Count(d => d.ContainsKey("k")).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_Dictionary_Items_Reach_ContainsKey_On_Inner()
+ {
+ var dicts = new List>
+ {
+ new Dictionary { ["k"] = 1 },
+ new Dictionary { ["other"] = 2 },
+ new Dictionary { ["k"] = 5 },
+ };
+
+ await Assert.That(dicts).Count(d => d.ContainsKey("k")).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_Set_Items_Reach_IsSubsetOf_On_Inner()
+ {
+ var universe = new HashSet { 1, 2, 3, 4, 5 };
+ var sets = new List>
+ {
+ new HashSet { 1, 2 },
+ new HashSet { 6 },
+ new HashSet { 3, 4 },
+ };
+
+ await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_ReadOnlySet_Items_Reach_IsSubsetOf_On_Inner()
+ {
+ var universe = new HashSet { 1, 2, 3, 4, 5 };
+ var sets = new List>
+ {
+ new HashSet { 1, 2 },
+ new HashSet { 6 },
+ new HashSet { 3, 4 },
+ };
+
+ await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_Array_Items_Reach_IsSingleElement_On_Inner()
+ {
+ var listOfArrays = new List
+ {
+ new[] { 10, 20 },
+ new[] { 99 },
+ new[] { 10 },
+ };
+
+ await Assert.That(listOfArrays).Count(a => a.IsSingleElement()).IsEqualTo(2);
+ }
+
+ // ---- Concrete-type item tests ---------------------------------------
+ // C# generic inference resolves TItem to the exact declared type, never
+ // to an interface, so e.g. List> items must bind to a
+ // List overload, not the IList overload above.
+
+ [Test]
+ public async Task Count_ConcreteList_Items_Reach_HasItemAt_On_Inner()
+ {
+ var listOfLists = new List>
+ {
+ new() { 10, 20 },
+ new() { 99 },
+ new() { 10, 30 },
+ };
+
+ await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_ConcreteHashSet_Items_Reach_IsSubsetOf_On_Inner()
+ {
+ var universe = new HashSet { 1, 2, 3, 4, 5 };
+ var sets = new List>
+ {
+ new() { 1, 2 },
+ new() { 6 },
+ new() { 3, 4 },
+ };
+
+ await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_ConcreteDictionary_Items_Reach_ContainsKey_On_Inner()
+ {
+ var dicts = new List>
+ {
+ new() { ["k"] = 1 },
+ new() { ["other"] = 2 },
+ new() { ["k"] = 5 },
+ };
+
+ await Assert.That(dicts).Count(d => d.ContainsKey("k")).IsEqualTo(2);
+ }
+
+ [Test]
+ public async Task Count_Specialised_Source_Failure_Message_Mentions_Inner_Expectation()
+ {
+ IEnumerable> listOfLists = new List>
+ {
+ new() { 1 },
+ new() { 1 },
+ };
+
+ // Expect 5 items with inner-count==3 → there are 0; ensure failure message
+ // surfaces the specialised inner expectation rather than just "received 0".
+ var ex = await Assert.That(async () =>
+ await Assert.That(listOfLists).Count(l => l.Count().IsEqualTo(3)).IsEqualTo(5))
+ .Throws();
+
+ // The chained expression should include `.Count(...)` per-item filter.
+ await Assert.That(ex.Message).Contains(".Count(l => l.Count().IsEqualTo(3))");
+ }
+
+ [Test]
+ public async Task Count_List_Failure_Message_Mentions_Specialised_Inner_Expectation()
+ {
+ var listOfLists = new List>
+ {
+ new List { 99 },
+ new List { 99 },
+ };
+
+ var ex = await Assert.That(async () =>
+ await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(5))
+ .Throws();
+
+ await Assert.That(ex.Message).Contains(".Count(l => l.HasItemAt(0, 10))");
+ }
+
+ [Test]
+ public async Task Count_Dictionary_Failure_Message_Mentions_Specialised_Inner_Expectation()
+ {
+ var dicts = new List>
+ {
+ new Dictionary { ["other"] = 1 },
+ new Dictionary { ["other"] = 2 },
+ };
+
+ var ex = await Assert.That(async () =>
+ await Assert.That(dicts).Count(d => d.ContainsKey("k")).IsEqualTo(5))
+ .Throws();
+
+ await Assert.That(ex.Message).Contains(".Count(d => d.ContainsKey(\"k\"))");
+ }
+
+ [Test]
+ public async Task Count_Set_Failure_Message_Mentions_Specialised_Inner_Expectation()
+ {
+ var universe = new HashSet { 1, 2, 3 };
+ var sets = new List>
+ {
+ new HashSet { 6 },
+ new HashSet { 7 },
+ };
+
+ var ex = await Assert.That(async () =>
+ await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(5))
+ .Throws();
+
+ await Assert.That(ex.Message).Contains(".Count(s => s.IsSubsetOf(universe))");
+ }
+
+ [Test]
+ public async Task Count_ReadOnlyList_Failure_Message_Mentions_Specialised_Inner_Expectation()
+ {
+ var listOfLists = new List>
+ {
+ new List { 99 },
+ new List { 99 },
+ };
+
+ var ex = await Assert.That(async () =>
+ await Assert.That(listOfLists).Count(l => l.HasItemAt(0, 10)).IsEqualTo(5))
+ .Throws();
+
+ await Assert.That(ex.Message).Contains(".Count(l => l.HasItemAt(0, 10))");
+ }
+
+ [Test]
+ public async Task Count_ReadOnlySet_Failure_Message_Mentions_Specialised_Inner_Expectation()
+ {
+ var universe = new HashSet { 1, 2, 3 };
+ var sets = new List>
+ {
+ new HashSet { 6 },
+ new HashSet { 7 },
+ };
+
+ var ex = await Assert.That(async () =>
+ await Assert.That(sets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(5))
+ .Throws();
+
+ await Assert.That(ex.Message).Contains(".Count(s => s.IsSubsetOf(universe))");
+ }
+
+ [Test]
+ public async Task Count_Array_Failure_Message_Mentions_Specialised_Inner_Expectation()
+ {
+ var listOfArrays = new List
+ {
+ new[] { 1, 2 },
+ new[] { 1, 2 },
+ };
+
+ var ex = await Assert.That(async () =>
+ await Assert.That(listOfArrays).Count(a => a.IsSingleElement()).IsEqualTo(5))
+ .Throws();
+
+ await Assert.That(ex.Message).Contains(".Count(a => a.IsSingleElement())");
+ }
+}
diff --git a/TUnit.Assertions/Conditions/CollectionAssertions.cs b/TUnit.Assertions/Conditions/CollectionAssertions.cs
index 4344fe22e2..6ffa85d8d5 100644
--- a/TUnit.Assertions/Conditions/CollectionAssertions.cs
+++ b/TUnit.Assertions/Conditions/CollectionAssertions.cs
@@ -686,7 +686,7 @@ protected override async Task CheckAsync(EvaluationMetadata CheckAsync(EvaluationMetadata
where TCollection : IEnumerable
{
private readonly AssertionContext _collectionContext;
- private readonly Func, Assertion?>? _assertion;
+ private readonly Func? _itemAssertionFactory;
- public CollectionCountSource(
+ ///
+ /// Constructor used by the generic
+ /// instance method: wraps each item with before
+ /// invoking the user-supplied lambda. Specialised Count(itemAssertion)
+ /// extension overloads use the per-item factory ctor below to preserve
+ /// item-shape-specific assertion sources (issue #5707).
+ ///
+ internal CollectionCountSource(
+ AssertionContext collectionContext,
+ Func, IAssertion?>? assertion)
+ {
+ _collectionContext = collectionContext;
+ _itemAssertionFactory = assertion is null
+ ? null
+ : (item, index) => assertion(new ValueAssertion(item, $"item[{index}]"));
+ }
+
+ ///
+ /// Internal constructor that accepts a per-item assertion factory directly.
+ /// Used by specialised Count(itemAssertion) overloads that supply
+ /// item-shape-specific assertion sources (e.g. collection, dictionary, set)
+ /// instead of the generic .
+ ///
+ internal CollectionCountSource(
AssertionContext collectionContext,
- Func, Assertion?>? assertion)
+ Func? itemAssertionFactory)
{
_collectionContext = collectionContext;
- _assertion = assertion;
+ _itemAssertionFactory = itemAssertionFactory;
}
///
@@ -32,7 +55,7 @@ public CollectionCountEqualsAssertion IsEqualTo(
{
_collectionContext.ExpressionBuilder.Append($".IsEqualTo({expression})");
return new CollectionCountEqualsAssertion(
- _collectionContext, _assertion, expected, CountComparison.Equal);
+ _collectionContext, _itemAssertionFactory, expected, CountComparison.Equal);
}
///
@@ -45,7 +68,7 @@ public CollectionCountEqualsAssertion IsNotEqualTo(
{
_collectionContext.ExpressionBuilder.Append($".IsNotEqualTo({expression})");
return new CollectionCountEqualsAssertion(
- _collectionContext, _assertion, expected, CountComparison.NotEqual);
+ _collectionContext, _itemAssertionFactory, expected, CountComparison.NotEqual);
}
///
@@ -58,7 +81,7 @@ public CollectionCountEqualsAssertion IsGreaterThan(
{
_collectionContext.ExpressionBuilder.Append($".IsGreaterThan({expression})");
return new CollectionCountEqualsAssertion(
- _collectionContext, _assertion, expected, CountComparison.GreaterThan);
+ _collectionContext, _itemAssertionFactory, expected, CountComparison.GreaterThan);
}
///
@@ -71,7 +94,7 @@ public CollectionCountEqualsAssertion IsGreaterThanOrEqualTo
{
_collectionContext.ExpressionBuilder.Append($".IsGreaterThanOrEqualTo({expression})");
return new CollectionCountEqualsAssertion(
- _collectionContext, _assertion, expected, CountComparison.GreaterThanOrEqual);
+ _collectionContext, _itemAssertionFactory, expected, CountComparison.GreaterThanOrEqual);
}
///
@@ -84,7 +107,7 @@ public CollectionCountEqualsAssertion IsLessThan(
{
_collectionContext.ExpressionBuilder.Append($".IsLessThan({expression})");
return new CollectionCountEqualsAssertion(
- _collectionContext, _assertion, expected, CountComparison.LessThan);
+ _collectionContext, _itemAssertionFactory, expected, CountComparison.LessThan);
}
///
@@ -97,7 +120,7 @@ public CollectionCountEqualsAssertion IsLessThanOrEqualTo(
{
_collectionContext.ExpressionBuilder.Append($".IsLessThanOrEqualTo({expression})");
return new CollectionCountEqualsAssertion(
- _collectionContext, _assertion, expected, CountComparison.LessThanOrEqual);
+ _collectionContext, _itemAssertionFactory, expected, CountComparison.LessThanOrEqual);
}
///
@@ -108,7 +131,7 @@ public CollectionCountEqualsAssertion IsZero()
{
_collectionContext.ExpressionBuilder.Append(".IsZero()");
return new CollectionCountEqualsAssertion(
- _collectionContext, _assertion, 0, CountComparison.Equal);
+ _collectionContext, _itemAssertionFactory, 0, CountComparison.Equal);
}
///
@@ -119,7 +142,7 @@ public CollectionCountEqualsAssertion IsPositive()
{
_collectionContext.ExpressionBuilder.Append(".IsPositive()");
return new CollectionCountEqualsAssertion(
- _collectionContext, _assertion, 0, CountComparison.GreaterThan);
+ _collectionContext, _itemAssertionFactory, 0, CountComparison.GreaterThan);
}
}
@@ -140,19 +163,19 @@ internal enum CountComparison
public class CollectionCountEqualsAssertion : CollectionAssertionBase
where TCollection : IEnumerable
{
- private readonly Func, Assertion?>? _itemAssertion;
+ private readonly Func? _itemAssertionFactory;
private readonly int _expected;
private readonly CountComparison _comparison;
private int _actualCount;
internal CollectionCountEqualsAssertion(
AssertionContext context,
- Func, Assertion?>? itemAssertion,
+ Func? itemAssertionFactory,
int expected,
CountComparison comparison)
: base(context)
{
- _itemAssertion = itemAssertion;
+ _itemAssertionFactory = itemAssertionFactory;
_expected = expected;
_comparison = comparison;
}
@@ -173,7 +196,7 @@ protected override async Task CheckAsync(EvaluationMetadata CheckAsync(EvaluationMetadata(item, $"item[{index}]");
- var resultingAssertion = _itemAssertion(itemAssertionSource);
+ var resultingAssertion = _itemAssertionFactory(item, index);
if (resultingAssertion != null)
{
@@ -200,7 +222,7 @@ protected override async Task CheckAsync(EvaluationMetadata IsNotDefined(
return new Assertions.Enums.IsNotDefinedAssertion(source.Context);
}
+ // ========================================================================
+ // Specialised Count(itemAssertion) overloads — issue #5707.
+ //
+ // The instance-method `Count(Func, Assertion?>, ...)`
+ // on CollectionAssertionBase exposes only the generic `IAssertionSource`
+ // inside the lambda. When TItem itself is a collection, dictionary or set,
+ // specialised assertions defined as instance methods on the matching
+ // assertion-source base (e.g. `HasCount`, `ContainsKey`, `IsSubsetOf`,
+ // `HasItemAt`) are unreachable.
+ //
+ // These extension methods preserve the specialised assertion source by
+ // matching on the closed shape of TItem and constructing the appropriate
+ // typed source per item. C# generic type inference requires an exact match
+ // on the receiver's TItem (it does not unify a concrete type with an
+ // interface), so we provide overloads for both the interface shapes
+ // (IList, IDictionary, ISet, ...) and the most common concrete
+ // shapes (List, Dictionary, HashSet, T[]). Items whose shape
+ // does not match a specialised overload still resolve to the generic
+ // instance method.
+ //
+ // No overload is needed for `string` items: the `IEnumerable`
+ // overload below cannot bind to a string item (C# inference does not
+ // unify `string` with `IEnumerable` for a generic type parameter),
+ // so the generic instance method already wraps each string in
+ // `ValueAssertion` — which is exactly what a dedicated overload
+ // would do.
+ // ========================================================================
+
+ ///
+ /// Counts items satisfying an assertion expressed against an -typed source.
+ /// Use this overload when the collection's items are themselves enumerables; the lambda
+ /// receives a with the full collection assertion
+ /// surface (Contains, IsInOrder, HasCount, etc.).
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new CollectionAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+ ///
+ /// Counts items satisfying an assertion expressed against an -typed source.
+ /// Use this overload when the collection's items are themselves lists; the lambda
+ /// receives a with index-based assertions in addition
+ /// to the standard collection surface.
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new ListAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+ ///
+ /// Counts items satisfying an assertion expressed against an -typed source.
+ /// Use this overload when the collection's items are themselves read-only lists; the lambda
+ /// receives a with index-based assertions in addition
+ /// to the standard collection surface.
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new ReadOnlyListAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+ ///
+ /// Counts items satisfying an assertion expressed against an -typed source.
+ /// Use this overload when the collection's items are themselves read-only dictionaries; the lambda
+ /// receives a with dictionary-specific assertions
+ /// (ContainsKey, ContainsValue, etc.) in addition to the standard collection surface.
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ where TKey : notnull
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new DictionaryAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+ ///
+ /// Counts items satisfying an assertion expressed against an -typed source.
+ /// Use this overload when the collection's items are themselves dictionaries; the lambda
+ /// receives a with dictionary-specific assertions
+ /// in addition to the standard collection surface.
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ where TKey : notnull
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new MutableDictionaryAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+ ///
+ /// Counts items satisfying an assertion expressed against an -typed source.
+ /// Use this overload when the collection's items are themselves sets; the lambda
+ /// receives a with set-specific assertions
+ /// (IsSubsetOf, IsSupersetOf, Overlaps, etc.) in addition to the standard collection surface.
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new SetAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+#if NET5_0_OR_GREATER
+ ///
+ /// Counts items satisfying an assertion expressed against an -typed source.
+ /// Use this overload when the collection's items are themselves read-only sets; the lambda
+ /// receives a with set-specific assertions
+ /// (IsSubsetOf, IsSupersetOf, Overlaps, etc.) in addition to the standard collection surface.
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new ReadOnlySetAssertion(item, $"item[{index}]")),
+ expression);
+ }
+#endif
+
+ ///
+ /// Counts items satisfying an assertion expressed against a []-typed source.
+ /// Use this overload when the collection's items are themselves arrays; the lambda
+ /// receives an so array-specific assertions
+ /// (e.g. IsEmpty, IsSingleElement) defined on IAssertionSource<TInner[]>
+ /// are reachable in addition to the standard collection surface.
+ ///
+ public static CollectionCountSource Count(
+ this CollectionAssertionBase source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable
+ {
+ return CountSpecialised(
+ source,
+ (item, index) => itemAssertion(new ArrayAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+ // ----- Concrete-type overloads -----
+ // C# generic inference resolves TItem to the exact declared type, never to
+ // an interface, so e.g. `List>` items would not match the
+ // `ISet` overload above. These cover the common BCL concrete types.
+
+ ///
+ /// Counts items satisfying an assertion expressed against a -typed source.
+ /// Use this overload when the collection's items are themselves instances;
+ /// the lambda receives a with index-based assertions in addition
+ /// to the standard collection surface. Without this overload, C# inference would not unify
+ /// List<TInner> with the IList<TInner> overload.
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new ListAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+ ///
+ /// Counts items satisfying an assertion expressed against a -typed source.
+ /// Use this overload when the collection's items are themselves instances;
+ /// the lambda receives a with set-specific assertions
+ /// (IsSubsetOf, IsSupersetOf, Overlaps, etc.) in addition to the standard collection surface.
+ /// Without this overload, C# inference would not unify HashSet<TInner> with the
+ /// ISet<TInner> overload.
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new HashSetAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+ ///
+ /// Counts items satisfying an assertion expressed against a -typed source.
+ /// Use this overload when the collection's items are themselves instances;
+ /// the lambda receives a with dictionary-specific
+ /// assertions (ContainsKey, ContainsValue, etc.) in addition to the standard collection surface.
+ /// Without this overload, C# inference would not unify Dictionary<TKey, TValue> with the
+ /// IDictionary<TKey, TValue> overload.
+ ///
+ public static CollectionCountSource> Count(
+ this CollectionAssertionBase> source,
+ Func, IAssertion?> itemAssertion,
+ [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
+ where TCollection : IEnumerable>
+ where TKey : notnull
+ {
+ return CountSpecialised>(
+ source,
+ (item, index) => itemAssertion(new MutableDictionaryAssertion(item, $"item[{index}]")),
+ expression);
+ }
+
+ private static CollectionCountSource CountSpecialised(
+ CollectionAssertionBase source,
+ Func itemAssertionFactory,
+ string? expression)
+ where TCollection : IEnumerable
+ {
+ var context = source.InternalContext;
+ context.ExpressionBuilder.Append($".Count({expression})");
+ return new CollectionCountSource(context, itemAssertionFactory);
+ }
}
diff --git a/TUnit.Assertions/Sources/CollectionAssertionBase.cs b/TUnit.Assertions/Sources/CollectionAssertionBase.cs
index 62447cf0ed..c5d7c24454 100644
--- a/TUnit.Assertions/Sources/CollectionAssertionBase.cs
+++ b/TUnit.Assertions/Sources/CollectionAssertionBase.cs
@@ -139,7 +139,8 @@ public CollectionContainsPredicateAssertion Contains(
public CollectionCountSource Count()
{
Context.ExpressionBuilder.Append(".Count()");
- return new CollectionCountSource(Context, null);
+ return new CollectionCountSource(
+ Context, (Func?)null);
}
///
@@ -165,7 +166,7 @@ public CollectionCountWithInlineAssertionAssertion Count(
/// Example: await Assert.That(list).Count(item => item.IsGreaterThan(10)).IsEqualTo(3).And.Contains(1);
///
public CollectionCountSource Count(
- Func, Assertion?> itemAssertion,
+ Func, IAssertion?> itemAssertion,
[CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
{
Context.ExpressionBuilder.Append($".Count({expression})");
diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index 54fa8fd2df..51de4645a0 100644
--- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -817,7 +817,6 @@ namespace .Conditions
public class CollectionCountSource
where TCollection : .
{
- public CollectionCountSource(. collectionContext, <., .?>? assertion) { }
public . IsEqualTo(int expected, [.("expected")] string? expression = null) { }
public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { }
public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { }
@@ -2631,6 +2630,31 @@ namespace .Extensions
{
public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { }
public static . CompletesWithin(this . source, timeout, [.("timeout")] string? expression = null) { }
+ public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null)
+ where TCollection : .<.> { }
+ public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null)
+ where TCollection : .<.> { }
+ public static .> Count(this .> source, <., .?> itemAssertion, [.("itemAssertion")] string? expression = null)
+ where TCollection : .<.> { }
+ public static .> Count