diff --git a/src/MongoDB.EntityFrameworkCore/Query/Visitors/MongoProjectionBindingExpressionVisitor.cs b/src/MongoDB.EntityFrameworkCore/Query/Visitors/MongoProjectionBindingExpressionVisitor.cs
index d6542f8a3..9598ac80d 100644
--- a/src/MongoDB.EntityFrameworkCore/Query/Visitors/MongoProjectionBindingExpressionVisitor.cs
+++ b/src/MongoDB.EntityFrameworkCore/Query/Visitors/MongoProjectionBindingExpressionVisitor.cs
@@ -258,6 +258,19 @@ protected override Expression VisitExtension(Expression extensionExpression)
///
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
+ // A projected embedded (owned) collection-navigation Count (e.g. select b.Posts.Count / select new
+ // { ..., N = b.Posts.Count }). EF Core lowers this to Queryable.Count(Queryable.AsQueryable(source))
+ // — even though the navigation is a plain in-document List, not a real IQueryable — because EF's
+ // navigation expansion always represents a collection navigation as queryable. Left unhandled, the
+ // generic fallback at the bottom of this method re-visits "source" (which resolves to the embedded
+ // navigation's already-materialized CLR list) and tries to rebuild Queryable.Count's Expression.Call
+ // with it directly, which throws ArgumentException because a List doesn't satisfy the method's
+ // IQueryable parameter.
+ if (TryBindEmbeddedCollectionNavigationCount(methodCallExpression, out var boundEmbeddedCount))
+ {
+ return boundEmbeddedCount;
+ }
+
// A projected cross-collection collection navigation (e.g. select new { ..., Orders = c.Orders.ToList() }).
// EF Core lowers this to Enumerable.ToList(Queryable.Select(Queryable.Where(DbSet(), joinPred), selector)).
// There is no enclosing IncludeExpression to set up the $lookup, so bind it here to a CollectionShaperExpression
@@ -435,6 +448,62 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return updatedMethodCallExpression;
}
+ ///
+ /// Detects and binds a projected embedded (owned) collection-navigation Count/LongCount such as
+ /// select b.Posts.Count or select new { ..., N = b.Posts.Count }, where Posts is an
+ /// owned collection navigation stored in-document. See the call site in for
+ /// why the generic fallback cannot handle this shape.
+ ///
+ private bool TryBindEmbeddedCollectionNavigationCount(
+ MethodCallExpression methodCallExpression,
+ out Expression result)
+ {
+ result = null!;
+
+ if (methodCallExpression.Method.DeclaringType != typeof(Queryable)
+ || methodCallExpression.Arguments.Count != 1)
+ {
+ return false;
+ }
+
+ MethodInfo nullSafeCountMethod;
+ switch (methodCallExpression.Method.Name)
+ {
+ case nameof(Queryable.Count):
+ nullSafeCountMethod = NullSafeCountMethodInfo;
+ break;
+ case nameof(Queryable.LongCount):
+ nullSafeCountMethod = NullSafeLongCountMethodInfo;
+ break;
+ default:
+ return false;
+ }
+
+ // The cross-collection shape (TryBindProjectedCollectionNavigationCount) has its source as a bare
+ // Queryable.Where(...) — no AsQueryable wrapper, since a DbSet-rooted query is already IQueryable.
+ // Only an embedded navigation (a plain in-document List) gets wrapped in AsQueryable by EF's
+ // navigation expansion.
+ if (methodCallExpression.Arguments[0] is not MethodCallExpression
+ {
+ Method: { Name: nameof(Queryable.AsQueryable), DeclaringType: var asQueryableDeclaring }
+ } asQueryableCall
+ || asQueryableDeclaring != typeof(Queryable))
+ {
+ return false;
+ }
+
+ if (Visit(asQueryableCall.Arguments[0]) is not CollectionShaperExpression collectionShaper)
+ {
+ return false;
+ }
+
+ // A missing or explicitly-null stored array materializes the CollectionShaperExpression to a null
+ // reference rather than an empty collection (the same gap plain `.Select(e => e.children)` has), so
+ // Count/LongCount must tolerate a null source rather than throwing ArgumentNullException.
+ result = Expression.Call(nullSafeCountMethod.MakeGenericMethod(collectionShaper.ElementType), collectionShaper);
+ return true;
+ }
+
///
protected override Expression VisitNew(NewExpression newExpression)
{
@@ -672,6 +741,27 @@ private static readonly MethodInfo GetParameterValueMethodInfo
= typeof(MongoProjectionBindingExpressionVisitor)
.GetTypeInfo().GetDeclaredMethod(nameof(GetParameterValue));
+ private static readonly MethodInfo NullSafeCountMethodInfo
+ = typeof(MongoProjectionBindingExpressionVisitor)
+ .GetTypeInfo().GetDeclaredMethod(nameof(NullSafeCount));
+
+ private static readonly MethodInfo NullSafeLongCountMethodInfo
+ = typeof(MongoProjectionBindingExpressionVisitor)
+ .GetTypeInfo().GetDeclaredMethod(nameof(NullSafeLongCount));
+
+ ///
+ /// A missing or explicitly-null stored array materializes an embedded collection navigation to a null
+ /// reference rather than an empty collection, so a projected Count over it must tolerate a null source.
+ ///
+ private static int NullSafeCount(IEnumerable source)
+ => source?.Count() ?? 0;
+
+ ///
+ /// The -returning sibling of , for a projected LongCount.
+ ///
+ private static long NullSafeLongCount(IEnumerable source)
+ => source?.LongCount() ?? 0;
+
#if EF8 || EF9
private static T GetParameterValue(
QueryContext queryContext,
diff --git a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Query/OwnedEntityTests.cs b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Query/OwnedEntityTests.cs
index 36245ded8..13fbd672e 100644
--- a/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Query/OwnedEntityTests.cs
+++ b/tests/MongoDB.EntityFrameworkCore.FunctionalTests/Query/OwnedEntityTests.cs
@@ -1145,6 +1145,51 @@ public void OwnedEntity_can_go_multiple_levels_deep_querying_collection()
}
}
+ [Fact]
+ public void OwnedEntity_collection_bare_count_projection_returns_element_count()
+ {
+ var collection = database.CreateCollection();
+
+ {
+ using var db = SingleEntityDbContext.Create(collection);
+ db.Entities.AddRange(
+ new A { _id = "1", children = [new B { name = "child1" }, new B { name = "child2" }] },
+ new A { _id = "2", children = [] },
+ new A { _id = "3", children = null! });
+ db.SaveChanges();
+ }
+
+ using var db2 = SingleEntityDbContext.Create(collection);
+
+ var counts = db2.Entities.AsNoTracking().OrderBy(e => e._id).Select(e => e.children.Count).ToList();
+ Assert.Equal([2, 0, 0], counts);
+
+ var longCounts = db2.Entities.AsNoTracking().OrderBy(e => e._id).Select(e => e.children.LongCount()).ToList();
+ Assert.Equal([2L, 0L, 0L], longCounts);
+ }
+
+ [Fact]
+ public void OwnedEntity_collection_count_projection_wrapped_in_arithmetic_returns_element_count()
+ {
+ var collection = database.CreateCollection();
+
+ {
+ using var db = SingleEntityDbContext.Create(collection);
+ db.Entities.AddRange(
+ new A { _id = "1", children = [new B { name = "child1" }, new B { name = "child2" }] },
+ new A { _id = "2", children = null! });
+ db.SaveChanges();
+ }
+
+ using var db2 = SingleEntityDbContext.Create(collection);
+
+ var doubled = db2.Entities.AsNoTracking().OrderBy(e => e._id)
+ .Select(e => new { e._id, N = e.children.Count * 2 })
+ .ToList();
+
+ Assert.Equal([4, 0], doubled.Select(r => r.N).ToArray());
+ }
+
[Fact]
public void OwnedEntity_collection_can_be_tested_for_not_null()
{