From 637941d4a976ba4f2496a1400e8efd208d13af29 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Mon, 12 Jan 2026 22:30:16 +0100 Subject: [PATCH 1/2] Add support for dictionary index initializers in expressions --- .../ExpressionSyntaxRewriter.cs | 58 ++++++++++++++- ...xInitializer_IsBeingRewritten.verified.txt | 25 +++++++ ...itializer_IsNotBeingRewritten.verified.txt | 25 +++++++ .../ProjectionExpressionGeneratorTests.cs | 74 +++++++++++++++++++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryIndexInitializer_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_IsNotBeingRewritten.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index 6340d90..8f88eba 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -385,7 +385,63 @@ public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullCondition return base.VisitNullableType(node); } - + + public override SyntaxNode? VisitInitializerExpression(InitializerExpressionSyntax node) + { + // Only handle object initializers that might contain indexer assignments + if (node.Kind() != SyntaxKind.ObjectInitializerExpression) + { + return base.VisitInitializerExpression(node); + } + + // Check if any expression is an indexer assignment (e.g., ["key"] = value) + var hasIndexerAssignment = node.Expressions.Any(e => + e is AssignmentExpressionSyntax { Left: ImplicitElementAccessSyntax }); + + if (!hasIndexerAssignment) + { + return base.VisitInitializerExpression(node); + } + + var newExpressions = new SeparatedSyntaxList(); + + foreach (var expression in node.Expressions) + { + if (expression is AssignmentExpressionSyntax assignment && + assignment.Left is ImplicitElementAccessSyntax implicitElementAccess) + { + // Transform ["key"] = value into { "key", value } + var arguments = new SeparatedSyntaxList(); + + foreach (var argument in implicitElementAccess.ArgumentList.Arguments) + { + var visitedArgument = (ExpressionSyntax?)Visit(argument.Expression) ?? argument.Expression; + arguments = arguments.Add(visitedArgument); + } + + var visitedValue = (ExpressionSyntax?)Visit(assignment.Right) ?? assignment.Right; + arguments = arguments.Add(visitedValue); + + var complexElementInitializer = SyntaxFactory.InitializerExpression( + SyntaxKind.ComplexElementInitializerExpression, + arguments + ); + + newExpressions = newExpressions.Add(complexElementInitializer); + } + else + { + var visitedExpression = (ExpressionSyntax?)Visit(expression) ?? expression; + newExpressions = newExpressions.Add(visitedExpression); + } + } + + return SyntaxFactory.InitializerExpression( + SyntaxKind.CollectionInitializerExpression, + newExpressions + ).WithTriviaFrom(node); + } + private ExpressionSyntax ReplaceVariableWithCast(ExpressionSyntax expression, DeclarationPatternSyntax declaration, ExpressionSyntax governingExpression) { if (declaration.Designation is SingleVariableDesignationSyntax variableDesignation) diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryIndexInitializer_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryIndexInitializer_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..3b41109 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryIndexInitializer_IsBeingRewritten.verified.txt @@ -0,0 +1,25 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_EntityExtensions_ToDictionary + { + static global::System.Linq.Expressions.Expression>> Expression() + { + return (global::Foo.EntityExtensions.Entity entity) => new Dictionary + { + { + "FullName", + entity.FullName ?? "N/A" + } + }; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_IsNotBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_IsNotBeingRewritten.verified.txt new file mode 100644 index 0000000..3b41109 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_IsNotBeingRewritten.verified.txt @@ -0,0 +1,25 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_EntityExtensions_ToDictionary + { + static global::System.Linq.Expressions.Expression>> Expression() + { + return (global::Foo.EntityExtensions.Entity entity) => new Dictionary + { + { + "FullName", + entity.FullName ?? "N/A" + } + }; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs index deb266a..439eb3b 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -1901,6 +1901,80 @@ public Task GenericTypesWithConstraints() return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + + [Fact] + public Task DictionaryIndexInitializer_IsBeingRewritten() + { + // lang=csharp + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public static class EntityExtensions + { + public record Entity + { + public int Id { get; set; } + public string? FullName { get; set; } + } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static Dictionary ToDictionary(this Entity entity) + => new Dictionary + { + [""FullName""] = entity.FullName ?? ""N/A"" + }; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task DictionaryObjectInitializer_IsNotBeingRewritten() + { + // lang=csharp + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public static class EntityExtensions + { + public record Entity + { + public int Id { get; set; } + public string? FullName { get; set; } + } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static Dictionary ToDictionary(this Entity entity) + => new Dictionary + { + { ""FullName"", entity.FullName ?? ""N/A"" } + }; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } #region Helpers From 08d5acfaff0706e71e18c7b3e4914749aaf342ca Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Tue, 13 Jan 2026 22:55:23 +0100 Subject: [PATCH 2/2] Improve tests --- .../ExpressionSyntaxRewriter.cs | 2 +- ...tionaryIndexInitializer_IsBeingRewritten.verified.txt | 8 ++++++-- ...er_PreservesCollectionInitializerSyntax.verified.txt} | 0 .../ProjectionExpressionGeneratorTests.cs | 9 +++++---- 4 files changed, 12 insertions(+), 7 deletions(-) rename tests/EntityFrameworkCore.Projectables.Generator.Tests/{ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_IsNotBeingRewritten.verified.txt => ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_PreservesCollectionInitializerSyntax.verified.txt} (100%) diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index 8f88eba..98a4b5a 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -389,7 +389,7 @@ public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullCondition public override SyntaxNode? VisitInitializerExpression(InitializerExpressionSyntax node) { // Only handle object initializers that might contain indexer assignments - if (node.Kind() != SyntaxKind.ObjectInitializerExpression) + if (!node.IsKind(SyntaxKind.ObjectInitializerExpression)) { return base.VisitInitializerExpression(node); } diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryIndexInitializer_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryIndexInitializer_IsBeingRewritten.verified.txt index 3b41109..b106ba4 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryIndexInitializer_IsBeingRewritten.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryIndexInitializer_IsBeingRewritten.verified.txt @@ -11,13 +11,17 @@ namespace EntityFrameworkCore.Projectables.Generated [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] static class Foo_EntityExtensions_ToDictionary { - static global::System.Linq.Expressions.Expression>> Expression() + static global::System.Linq.Expressions.Expression>> Expression() { - return (global::Foo.EntityExtensions.Entity entity) => new Dictionary + return (global::Foo.EntityExtensions.Entity entity) => new Dictionary { { "FullName", entity.FullName ?? "N/A" + }, + { + "Id", + entity.Id.ToString() } }; } diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_IsNotBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_PreservesCollectionInitializerSyntax.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_IsNotBeingRewritten.verified.txt rename to tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_PreservesCollectionInitializerSyntax.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs index 439eb3b..eb50a78 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -1922,10 +1922,11 @@ public record Entity } [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] - public static Dictionary ToDictionary(this Entity entity) - => new Dictionary + public static Dictionary ToDictionary(this Entity entity) + => new Dictionary { - [""FullName""] = entity.FullName ?? ""N/A"" + [""FullName""] = entity.FullName ?? ""N/A"", + [""Id""] = entity.Id.ToString(), }; } } @@ -1940,7 +1941,7 @@ public static Dictionary ToDictionary(this Entity entity) } [Fact] - public Task DictionaryObjectInitializer_IsNotBeingRewritten() + public Task DictionaryObjectInitializer_PreservesCollectionInitializerSyntax() { // lang=csharp var compilation = CreateCompilation(@"