diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index 6340d90..98a4b5a 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.IsKind(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..b106ba4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryIndexInitializer_IsBeingRewritten.verified.txt @@ -0,0 +1,29 @@ +// +#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" + }, + { + "Id", + entity.Id.ToString() + } + }; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_PreservesCollectionInitializerSyntax.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_PreservesCollectionInitializerSyntax.verified.txt new file mode 100644 index 0000000..3b41109 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.DictionaryObjectInitializer_PreservesCollectionInitializerSyntax.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..eb50a78 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -1901,6 +1901,81 @@ 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"", + [""Id""] = entity.Id.ToString(), + }; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task DictionaryObjectInitializer_PreservesCollectionInitializerSyntax() + { + // 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