From 8d371936e558a30360ad538560e89149446d30b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 07:42:42 +0000 Subject: [PATCH 1/2] Add AsExpandedProperties extension method with functional tests and snapshots Agent-Logs-Url: https://github.com/EFNext/EntityFrameworkCore.Projectables/sessions/284833b5-1b68-4d3f-aa18-7cba618c9de8 Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- .../Extensions/QueryableExtensions.cs | 19 +++++ ...jectableProperties.DotNet10_0.verified.txt | 2 + ...ojectableProperties.DotNet8_0.verified.txt | 2 + ...ojectableProperties.DotNet9_0.verified.txt | 2 + ...jectableProperties.DotNet10_0.verified.txt | 2 + ...ojectableProperties.DotNet8_0.verified.txt | 2 + ...ojectableProperties.DotNet9_0.verified.txt | 2 + ...pertiesAfterFilter.DotNet10_0.verified.txt | 3 + ...opertiesAfterFilter.DotNet8_0.verified.txt | 3 + ...opertiesAfterFilter.DotNet9_0.verified.txt | 3 + .../AsExpandedPropertiesTests.cs | 71 +++++++++++++++++++ 11 files changed, 111 insertions(+) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet8_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet8_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet8_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.cs diff --git a/src/EntityFrameworkCore.Projectables/Extensions/QueryableExtensions.cs b/src/EntityFrameworkCore.Projectables/Extensions/QueryableExtensions.cs index e3e2fa4d..05442074 100644 --- a/src/EntityFrameworkCore.Projectables/Extensions/QueryableExtensions.cs +++ b/src/EntityFrameworkCore.Projectables/Extensions/QueryableExtensions.cs @@ -7,4 +7,23 @@ public static class QueryableExtensions /// public static IQueryable ExpandProjectables(this IQueryable query) => query.Provider.CreateQuery(query.Expression.ExpandProjectables()); + + /// + /// Ensures that all writable [Projectable] properties on are populated + /// in query results by automatically generating a SELECT projection that merges all EF-mapped columns with + /// the expression-expanded value of every writable projectable property. + /// + /// This method is particularly useful when fetching full entities (e.g. FirstAsync(), + /// ToListAsync()) on a tracking context where the automatic select injection is otherwise + /// suppressed to preserve change-tracking semantics. Call this method after any Where + /// or OrderBy clauses and before terminal operators. + /// + /// + /// Read-only projectable properties (those without a setter) are not included in the generated projection + /// because EF Core's materializer cannot set them on the resulting entity. + /// + /// + public static IQueryable AsExpandedProperties(this IQueryable query) + where TModel : class + => query.ExpandProjectables(); } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet10_0.verified.txt new file mode 100644 index 00000000..8a30188c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Id], [e].[Id] * 3 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet8_0.verified.txt new file mode 100644 index 00000000..8a30188c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet8_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Id], [e].[Id] * 3 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet9_0.verified.txt new file mode 100644 index 00000000..8a30188c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.NoTrackingContext_QueryRoot_InjectsProjectableProperties.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Id], [e].[Id] * 3 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet10_0.verified.txt new file mode 100644 index 00000000..8a30188c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Id], [e].[Id] * 3 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet8_0.verified.txt new file mode 100644 index 00000000..8a30188c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet8_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Id], [e].[Id] * 3 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet9_0.verified.txt new file mode 100644 index 00000000..8a30188c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_QueryRoot_InjectsWritableProjectableProperties.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Id], [e].[Id] * 3 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet10_0.verified.txt new file mode 100644 index 00000000..ef213415 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet10_0.verified.txt @@ -0,0 +1,3 @@ +SELECT [e].[Id], [e].[Id] * 3 +FROM [Entity] AS [e] +WHERE [e].[Id] > 0 \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet8_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet8_0.verified.txt new file mode 100644 index 00000000..ef213415 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet8_0.verified.txt @@ -0,0 +1,3 @@ +SELECT [e].[Id], [e].[Id] * 3 +FROM [Entity] AS [e] +WHERE [e].[Id] > 0 \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet9_0.verified.txt new file mode 100644 index 00000000..ef213415 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter.DotNet9_0.verified.txt @@ -0,0 +1,3 @@ +SELECT [e].[Id], [e].[Id] * 3 +FROM [Entity] AS [e] +WHERE [e].[Id] > 0 \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.cs new file mode 100644 index 00000000..52e05866 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/AsExpandedPropertiesTests.cs @@ -0,0 +1,71 @@ +using EntityFrameworkCore.Projectables.Extensions; +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; + +namespace EntityFrameworkCore.Projectables.FunctionalTests; + +public class AsExpandedPropertiesTests +{ + /// + /// Entity with both a read-only and a writable projectable property. + /// + /// ReadOnlyComputed — expression-bodied (no setter); excluded from auto-inject. + /// WritableComputed — has a setter; included in auto-inject. + /// + /// + public class Entity + { + public int Id { get; set; } + + /// Read-only projectable: NOT included in the auto-generated SELECT (no setter). + [Projectable] + public int ReadOnlyComputed => Id * 2; + + /// Writable projectable: IS included in the auto-generated SELECT (has setter). + [Projectable] + public int WritableComputed + { + get => Id * 3; + set { } + } + } + + [Fact] + public Task TrackingContext_QueryRoot_InjectsWritableProjectableProperties() + { + // Tracking context: without AsExpandedProperties() the auto-inject is suppressed. + // AsExpandedProperties() forces the Select injection regardless of tracking mode. + // ReadOnlyComputed is absent from SELECT (no setter); WritableComputed is present. + using var dbContext = new SampleDbContext(queryTrackingBehavior: QueryTrackingBehavior.TrackAll); + + var query = dbContext.Set().AsExpandedProperties(); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task TrackingContext_WithWhere_InjectsProjectablePropertiesAfterFilter() + { + // Verifies that AsExpandedProperties() wraps the Where clause (not the raw root), + // so the generated SQL contains WHERE and SELECT in the correct order. + using var dbContext = new SampleDbContext(queryTrackingBehavior: QueryTrackingBehavior.TrackAll); + + var query = dbContext.Set() + .Where(e => e.Id > 0) + .AsExpandedProperties(); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task NoTrackingContext_QueryRoot_InjectsProjectableProperties() + { + // NoTracking context: auto-inject already happens automatically via UseProjectables(); + // calling AsExpandedProperties() explicitly should produce the same result. + using var dbContext = new SampleDbContext(queryTrackingBehavior: QueryTrackingBehavior.NoTracking); + + var query = dbContext.Set().AsExpandedProperties(); + + return Verifier.Verify(query.ToQueryString()); + } +} From 80c6bd32efab04587f98c54932af51b40c20960b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabien=20M=C3=A9nager?= Date: Sat, 11 Apr 2026 10:32:37 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Extensions/QueryableExtensions.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/EntityFrameworkCore.Projectables/Extensions/QueryableExtensions.cs b/src/EntityFrameworkCore.Projectables/Extensions/QueryableExtensions.cs index 05442074..0a0b273a 100644 --- a/src/EntityFrameworkCore.Projectables/Extensions/QueryableExtensions.cs +++ b/src/EntityFrameworkCore.Projectables/Extensions/QueryableExtensions.cs @@ -10,15 +10,21 @@ public static IQueryable ExpandProjectables(this IQueryable /// Ensures that all writable [Projectable] properties on are populated - /// in query results by automatically generating a SELECT projection that merges all EF-mapped columns with + /// in query results by automatically generating a SELECT projection that merges all EF-mapped properties and navigations with /// the expression-expanded value of every writable projectable property. /// /// This method is particularly useful when fetching full entities (e.g. FirstAsync(), - /// ToListAsync()) on a tracking context where the automatic select injection is otherwise - /// suppressed to preserve change-tracking semantics. Call this method after any Where + /// ToListAsync()) on queries that are tracked by default, where the automatic select injection is + /// otherwise suppressed to preserve change-tracking semantics. Call this method after any Where /// or OrderBy clauses and before terminal operators. /// /// + /// This method currently delegates to and does + /// not override the root-rewrite guard for queries that already contain an explicit AsTracking() call + /// in the expression tree. As a result, query.AsTracking().AsExpandedProperties() does not currently + /// force projection injection. + /// + /// /// Read-only projectable properties (those without a setter) are not included in the generated projection /// because EF Core's materializer cannot set them on the resulting entity. ///