From c33f8249ac14b5495c093f468f3a15ea520cc5a3 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 28 Sep 2026 17:58:19 +1000 Subject: [PATCH] Only apply AsSplitQuery in VerifyEntity when a collection is included VerifyEntity always wrapped the set in AsSplitQuery(). Verify.EntityFramework 16 throws for AsSplitQuery on a query that loads no collection, so every VerifyEntity(id) without a collection Include failed. QuerySplitting.SplitIfIncludesCollection walks the query's Include/ThenInclude calls and only splits when one loads a collection (or uses a string path). Applied to the MSTest, NUnit, TUnit and Xunit.V3 variants. --- .../LocalDbTestBase_Verify.cs | 4 +- .../QueryableSettingsTask/QuerySplitting.cs | 68 +++++++++++++++++++ src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj | 1 + src/EfLocalDb.NUnit/LocalDbTestBase_Verify.cs | 4 +- src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj | 1 + src/EfLocalDb.TUnit/LocalDbTestBase_Verify.cs | 4 +- .../EfLocalDb.Xunit.V3.csproj | 1 + .../LocalDbTestBase_Verify.cs | 4 +- 8 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 src/EfLocalDb.MSTest/QueryableSettingsTask/QuerySplitting.cs diff --git a/src/EfLocalDb.MSTest/LocalDbTestBase_Verify.cs b/src/EfLocalDb.MSTest/LocalDbTestBase_Verify.cs index e54e91c7..752f3871 100644 --- a/src/EfLocalDb.MSTest/LocalDbTestBase_Verify.cs +++ b/src/EfLocalDb.MSTest/LocalDbTestBase_Verify.cs @@ -68,12 +68,12 @@ QueryableSettingsTask InnerVerifyEntity(object id, string sour var expression = Expression.Lambda>(predicate, parameter); return new( - set.AsSplitQuery(), + set, null, async (settings, source) => { using var verifier = BuildVerifier(settings, sourceFile); - return await verifier.Verify(source.SingleOrDefaultAsync(expression)); + return await verifier.Verify(QuerySplitting.SplitIfIncludesCollection(source).SingleOrDefaultAsync(expression)); }); } } diff --git a/src/EfLocalDb.MSTest/QueryableSettingsTask/QuerySplitting.cs b/src/EfLocalDb.MSTest/QueryableSettingsTask/QuerySplitting.cs new file mode 100644 index 00000000..b8cc4dcf --- /dev/null +++ b/src/EfLocalDb.MSTest/QueryableSettingsTask/QuerySplitting.cs @@ -0,0 +1,68 @@ +using System.Collections; + +namespace EfLocalDb; + +// AsSplitQuery only changes how collections are loaded. So only apply it when an Include loads a collection, +// since a query that loads no collection is a single query either way (and Verify.EntityFramework throws for it). +static class QuerySplitting +{ + public static IQueryable SplitIfIncludesCollection(IQueryable source) + where TEntity : class + { + if (IncludesCollection(source.Expression)) + { + return source.AsSplitQuery(); + } + + return source; + } + + static bool IncludesCollection(Expression expression) + { + while (expression is MethodCallExpression + { + Method.IsStatic: true, + Arguments.Count: > 0 + } call) + { + if (IsInclude(call.Method) && + call.Arguments.Count > 1) + { + var argument = Unquote(call.Arguments[1]); + // a string Include can not be checked without resolving the path, so it is assumed to be a collection + if (argument is not LambdaExpression lambda || + IsCollection(lambda.Body.Type)) + { + return true; + } + } + + expression = call.Arguments[0]; + } + + return false; + } + + static Expression Unquote(Expression expression) + { + while (expression is UnaryExpression + { + NodeType: ExpressionType.Quote + } unary) + { + expression = unary.Operand; + } + + return expression; + } + + static bool IsInclude(MethodInfo method) => + method.DeclaringType == typeof(EntityFrameworkQueryableExtensions) && + method.Name is + nameof(EntityFrameworkQueryableExtensions.Include) or + nameof(EntityFrameworkQueryableExtensions.ThenInclude); + + static bool IsCollection(Type type) => + type != typeof(string) && + typeof(IEnumerable).IsAssignableFrom(type); +} diff --git a/src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj b/src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj index 41cad747..09f323f1 100644 --- a/src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj +++ b/src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj @@ -17,6 +17,7 @@ + diff --git a/src/EfLocalDb.NUnit/LocalDbTestBase_Verify.cs b/src/EfLocalDb.NUnit/LocalDbTestBase_Verify.cs index ae283fb1..2d8f829f 100644 --- a/src/EfLocalDb.NUnit/LocalDbTestBase_Verify.cs +++ b/src/EfLocalDb.NUnit/LocalDbTestBase_Verify.cs @@ -68,12 +68,12 @@ QueryableSettingsTask InnerVerifyEntity(object id, string sour var expression = Expression.Lambda>(predicate, parameter); return new( - set.AsSplitQuery(), + set, null, async (settings, source) => { using var verifier = BuildVerifier(sourceFile, settings); - return await verifier.Verify(source.SingleOrDefaultAsync(expression)); + return await verifier.Verify(QuerySplitting.SplitIfIncludesCollection(source).SingleOrDefaultAsync(expression)); }); } } \ No newline at end of file diff --git a/src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj b/src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj index 3a96fdab..801fd727 100644 --- a/src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj +++ b/src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj @@ -17,6 +17,7 @@ + diff --git a/src/EfLocalDb.TUnit/LocalDbTestBase_Verify.cs b/src/EfLocalDb.TUnit/LocalDbTestBase_Verify.cs index 0e5e538d..576a6d1c 100644 --- a/src/EfLocalDb.TUnit/LocalDbTestBase_Verify.cs +++ b/src/EfLocalDb.TUnit/LocalDbTestBase_Verify.cs @@ -68,12 +68,12 @@ QueryableSettingsTask InnerVerifyEntity(object id, string sour var expression = Expression.Lambda>(predicate, parameter); return new( - set.AsSplitQuery(), + set, null, async (settings, source) => { using var verifier = BuildVerifier(sourceFile, settings); - return await verifier.Verify(source.SingleOrDefaultAsync(expression)); + return await verifier.Verify(QuerySplitting.SplitIfIncludesCollection(source).SingleOrDefaultAsync(expression)); }); } } diff --git a/src/EfLocalDb.Xunit.V3/EfLocalDb.Xunit.V3.csproj b/src/EfLocalDb.Xunit.V3/EfLocalDb.Xunit.V3.csproj index caa80ae8..cff96422 100644 --- a/src/EfLocalDb.Xunit.V3/EfLocalDb.Xunit.V3.csproj +++ b/src/EfLocalDb.Xunit.V3/EfLocalDb.Xunit.V3.csproj @@ -17,6 +17,7 @@ + diff --git a/src/EfLocalDb.Xunit.V3/LocalDbTestBase_Verify.cs b/src/EfLocalDb.Xunit.V3/LocalDbTestBase_Verify.cs index b21cd0a3..527cdbbf 100644 --- a/src/EfLocalDb.Xunit.V3/LocalDbTestBase_Verify.cs +++ b/src/EfLocalDb.Xunit.V3/LocalDbTestBase_Verify.cs @@ -68,12 +68,12 @@ QueryableSettingsTask InnerVerifyEntity(object id, string sour var expression = Expression.Lambda>(predicate, parameter); return new( - set.AsSplitQuery(), + set, null, async (settings, source) => { using var verifier = BuildVerifier(settings, sourceFile); - return await verifier.Verify(source.SingleOrDefaultAsync(expression)); + return await verifier.Verify(QuerySplitting.SplitIfIncludesCollection(source).SingleOrDefaultAsync(expression)); }); } }