From 86223a11c9ee1ffb8eeabe4ec042c2651a81659c Mon Sep 17 00:00:00 2001 From: Koen Bekkenutte <2912652+kbekkenutte@users.noreply.github.com> Date: Mon, 18 Oct 2021 22:36:06 +0800 Subject: [PATCH] support for rewriting null conditional access expressions --- .../NullConditionalRewriteSupport.cs | 30 +++ .../ProjectableAttribute.cs | 1 + ...rameworkCore.Projectables.Generator.csproj | 1 + .../ExpressionSyntaxRewriter.cs | 67 ++++- .../ProjectableInterpreter.cs | 17 +- .../ExtensionsMethods/EntityExtensions.cs | 4 +- .../NullConditionals/Entity.cs | 11 + .../NullConditionals/EntityExtensions.cs | 31 +++ ...Tests.ComplexMemberExpression.verified.txt | 2 + ...iteTests.RelationalExpression.verified.txt | 4 + ...eTests.SimpleMemberExpression.verified.txt | 2 + .../IngoreNullConditionalRewriteTests.cs | 48 ++++ ...Tests.ComplexMemberExpression.verified.txt | 2 + ...iteTests.RelationalExpression.verified.txt | 5 + ...eTests.SimpleMemberExpression.verified.txt | 2 + .../RewriteNullConditionalRewriteTests.cs | 48 ++++ ...nExpressionGeneratorTests.Foo.verified.txt | 15 ++ ...gnoreSupport_IsBeingRewritten.verified.txt | 15 ++ ...writeSupport_IsBeingRewritten.verified.txt | 15 ++ ...gnoreSupport_IsBeingRewritten.verified.txt | 14 + ...writeSupport_IsBeingRewritten.verified.txt | 14 + ...gnoreSupport_IsBeingRewritten.verified.txt | 14 + ...writeSupport_IsBeingRewritten.verified.txt | 14 + ...writeSupport_IsBeingRewritten.verified.txt | 15 ++ ...writeSupport_IsBeingRewritten.verified.txt | 14 + ...writeSupport_IsBeingRewritten.verified.txt | 14 + ...gnoreSupport_IsBeingRewritten.verified.txt | 14 + ...writeSupport_IsBeingRewritten.verified.txt | 14 + .../ProjectionExpressionGeneratorTests.cs | 246 +++++++++++++++++- 29 files changed, 682 insertions(+), 11 deletions(-) create mode 100644 src/EntityFrameworkCore.Projectables.Abstractions/NullConditionalRewriteSupport.cs create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/Entity.cs create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/EntityExtensions.cs create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.ComplexMemberExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.RelationalExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.SimpleMemberExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.cs create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.ComplexMemberExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.RelationalExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.SimpleMemberExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.cs create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.Foo.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementAndMemberBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementAndMemberBinding_WithRewriteSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementBinding_WithRewriteSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableMemberBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableMemberBinding_WithRewriteSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableParameters_WithRewriteSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithExplicitNullCheckRewriteSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithIgnoreRewriteSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithRewriteSupport_IsBeingRewritten.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/NullConditionalRewriteSupport.cs b/src/EntityFrameworkCore.Projectables.Abstractions/NullConditionalRewriteSupport.cs new file mode 100644 index 0000000..13d836a --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.Abstractions/NullConditionalRewriteSupport.cs @@ -0,0 +1,30 @@ +namespace EntityFrameworkCore.Projectables +{ + /// + /// Configures how null-conditional operators are handeled + /// + public enum NullConditionalRewriteSupport + { + /// + /// Don't rewrite null conditional operators (Default behavior). + /// Usage of null conditional operators is thereby not allowed + /// + None, + + /// + /// Ignore null-conditional operators in the generated expression tree + /// + /// + /// (A?.B) is rewritten as expression: (A.B) + /// + Ignore, + + /// + /// Translates null-conditional operators into explicit null checks + /// + /// + /// (A?.B) is rewritten as expression: (A != null ? A.B : null) + /// + Rewrite + } +} diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs index 7fd5ffc..dd4a368 100644 --- a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs +++ b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs @@ -9,5 +9,6 @@ namespace EntityFrameworkCore.Projectables [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] public sealed class ProjectableAttribute : Attribute { + public NullConditionalRewriteSupport NullConditionalRewriteSupport { get; set; } } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/EntityFrameworkCore.Projectables.Generator.csproj b/src/EntityFrameworkCore.Projectables.Generator/EntityFrameworkCore.Projectables.Generator.csproj index 4c40f73..ee5dc40 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/EntityFrameworkCore.Projectables.Generator.csproj +++ b/src/EntityFrameworkCore.Projectables.Generator/EntityFrameworkCore.Projectables.Generator.csproj @@ -7,6 +7,7 @@ + diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index 311c62e..951e995 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -14,11 +14,76 @@ namespace EntityFrameworkCore.Projectables.Generator { readonly INamedTypeSymbol _targetTypeSymbol; readonly SemanticModel _semanticModel; + readonly NullConditionalRewriteSupport _nullConditionalRewriteSupport; + readonly Stack _conditionalAccessExpressionsStack = new(); - public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, SemanticModel semanticModel) + public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, SemanticModel semanticModel, NullConditionalRewriteSupport nullConditionalRewriteSupport) { _targetTypeSymbol = targetTypeSymbol; _semanticModel = semanticModel; + _nullConditionalRewriteSupport = nullConditionalRewriteSupport; + } + + public override SyntaxNode? VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node) + { + var targetExpression = (ExpressionSyntax)Visit(node.Expression); + + _conditionalAccessExpressionsStack.Push(targetExpression); + + return _nullConditionalRewriteSupport switch { + NullConditionalRewriteSupport.Ignore => Visit(node.WhenNotNull), + NullConditionalRewriteSupport.Rewrite => + SyntaxFactory.ConditionalExpression( + SyntaxFactory.BinaryExpression( + SyntaxKind.NotEqualsExpression, + targetExpression + .WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) + .WithLeadingTrivia(SyntaxFactory.Whitespace(" ")) + ) + .WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), + SyntaxFactory.ParenthesizedExpression( + (ExpressionSyntax)Visit(node.WhenNotNull) + ) + .WithLeadingTrivia(SyntaxFactory.Whitespace(" ")) + .WithTrailingTrivia(SyntaxFactory.Whitespace(" ")), + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) + .WithLeadingTrivia(SyntaxFactory.Whitespace(" ")) + ), + _ => base.VisitConditionalAccessExpression(node) + }; + } + + public override SyntaxNode? VisitMemberBindingExpression(MemberBindingExpressionSyntax node) + { + if (_conditionalAccessExpressionsStack.Count == 0) + { + throw new InvalidOperationException("Expected at least one conditional expression on the stack"); + } + + var targetExpression = _conditionalAccessExpressionsStack.Pop(); + + return _nullConditionalRewriteSupport switch { + NullConditionalRewriteSupport.Ignore => SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, targetExpression, node.Name), + NullConditionalRewriteSupport.Rewrite => SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, targetExpression, node.Name), + _ => node + }; + } + + public override SyntaxNode? VisitElementBindingExpression(ElementBindingExpressionSyntax node) + { + if (_conditionalAccessExpressionsStack.Count == 0) + { + throw new InvalidOperationException("Expected at least one conditional expression on the stack"); + } + + var targetExpression = _conditionalAccessExpressionsStack.Pop(); + + return _nullConditionalRewriteSupport switch { + NullConditionalRewriteSupport.Ignore => SyntaxFactory.ElementAccessExpression(targetExpression, node.ArgumentList), + NullConditionalRewriteSupport.Rewrite => SyntaxFactory.ElementAccessExpression(targetExpression, node.ArgumentList), + _ => Visit(node) + }; } public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node) diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs index 4b400e1..50f97d7 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -31,7 +32,6 @@ namespace EntityFrameworkCore.Projectables.Generator return null; } - var projectableAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("EntityFrameworkCore.Projectables.ProjectableAttribute"); var projectableAttributeClass = memberSymbol.GetAttributes() @@ -43,7 +43,15 @@ namespace EntityFrameworkCore.Projectables.Generator return null; } - var expressionSyntaxRewriter = new ExpressionSyntaxRewriter(memberSymbol.ContainingType, semanticModel); + var nullConditionalRewriteSupport = projectableAttributeClass.NamedArguments + .Where(x => x.Key == "NullConditionalRewriteSupport") + .Where(x => x.Value.Kind == TypedConstantKind.Enum) + .Select(x => x.Value.Value) + .Where(x => Enum.IsDefined(typeof(NullConditionalRewriteSupport), x)) + .Cast() + .FirstOrDefault(); + + var expressionSyntaxRewriter = new ExpressionSyntaxRewriter(memberSymbol.ContainingType, semanticModel, nullConditionalRewriteSupport); var parameterSyntaxRewriter = new ParameterSyntaxRewriter(semanticModel); var returnTypeSyntaxRewriter = new ReturnTypeSyntaxRewriter(semanticModel); @@ -128,8 +136,6 @@ namespace EntityFrameworkCore.Projectables.Generator return null; } - - descriptor.UsingDirectives = memberDeclarationSyntax.SyntaxTree .GetRoot() @@ -137,7 +143,6 @@ namespace EntityFrameworkCore.Projectables.Generator .OfType() .Select(x => x.ToString()); - return descriptor; } } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/EntityExtensions.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/EntityExtensions.cs index 60f29f3..61d1690 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/EntityExtensions.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/EntityExtensions.cs @@ -11,8 +11,8 @@ namespace EntityFrameworkCore.Projectables.FunctionalTests.ExtensionMethods [Projectable] public static int Foo(this Entity entity) => entity.Id + 1; - [Projectable] - public static int Foo2(this Entity entity) => entity.Foo() + 1; + [Projectable] + public static int Foo2(this Entity entity) => entity.Foo() + 1; [Projectable] public static Entity? LeadingEntity(this Entity entity, DbContext dbContext) diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/Entity.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/Entity.cs new file mode 100644 index 0000000..2de9fb6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/Entity.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace EntityFrameworkCore.Projectables.FunctionalTests.NullConditionals +{ + public record Entity + { + public int Id { get; set; } + public string? Name { get; set; } + public List? RelatedEntities { get; set; } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/EntityExtensions.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/EntityExtensions.cs new file mode 100644 index 0000000..8fa5499 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/EntityExtensions.cs @@ -0,0 +1,31 @@ +#nullable disable + +namespace EntityFrameworkCore.Projectables.FunctionalTests.NullConditionals +{ + public static class EntityExtensions + { + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public static string GetNameIgnoreNulls(this Entity entity) + => entity?.Name; + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public static int? GetNameLengthIgnoreNulls(this Entity entity) + => entity.GetNameIgnoreNulls()?.Length; + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public static Entity GetFirstRelatedIgnoreNulls(this Entity entity) + => entity?.RelatedEntities?[0]; + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static string GetNameRewriteNulls(this Entity entity) + => entity?.Name; + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static int? GetNameLengthRewriteNulls(this Entity entity) + => entity.GetNameIgnoreNulls()?.Length; + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static Entity GetFirstRelatedRewriteNulls(this Entity entity) + => entity?.RelatedEntities?[0]; + } +} diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.ComplexMemberExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.ComplexMemberExpression.verified.txt new file mode 100644 index 0000000..8b2d6b7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.ComplexMemberExpression.verified.txt @@ -0,0 +1,2 @@ +SELECT CAST(LEN([e].[Name]) AS int) +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.RelationalExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.RelationalExpression.verified.txt new file mode 100644 index 0000000..a80b047 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.RelationalExpression.verified.txt @@ -0,0 +1,4 @@ +SELECT [e].[Id], [e0].[Id], [e0].[EntityId], [e0].[Name] +FROM [Entity] AS [e] +LEFT JOIN [Entity] AS [e0] ON [e].[Id] = [e0].[EntityId] +ORDER BY [e].[Id] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.SimpleMemberExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.SimpleMemberExpression.verified.txt new file mode 100644 index 0000000..50627df --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.SimpleMemberExpression.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Name] +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.cs new file mode 100644 index 0000000..44e48d5 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/IngoreNullConditionalRewriteTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using VerifyXunit; +using Xunit; + +namespace EntityFrameworkCore.Projectables.FunctionalTests.NullConditionals +{ + [UsesVerify] + public class IngoreNullConditionalRewriteTests + { + [Fact] + public Task SimpleMemberExpression() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetNameIgnoreNulls()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task ComplexMemberExpression() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetNameLengthIgnoreNulls()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task RelationalExpression() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetFirstRelatedIgnoreNulls()); + + return Verifier.Verify(query.ToQueryString()); + } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.ComplexMemberExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.ComplexMemberExpression.verified.txt new file mode 100644 index 0000000..8b2d6b7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.ComplexMemberExpression.verified.txt @@ -0,0 +1,2 @@ +SELECT CAST(LEN([e].[Name]) AS int) +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.RelationalExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.RelationalExpression.verified.txt new file mode 100644 index 0000000..44b3cf9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.RelationalExpression.verified.txt @@ -0,0 +1,5 @@ +SELECT CAST(1 AS bit), [e].[Id], [e0].[Id], [e0].[EntityId], [e0].[Name], [e1].[Id], [e1].[EntityId], [e1].[Name] +FROM [Entity] AS [e] +LEFT JOIN [Entity] AS [e0] ON [e].[Id] = [e0].[EntityId] +LEFT JOIN [Entity] AS [e1] ON [e].[Id] = [e1].[EntityId] +ORDER BY [e].[Id], [e0].[Id] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.SimpleMemberExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.SimpleMemberExpression.verified.txt new file mode 100644 index 0000000..8b2d6b7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.SimpleMemberExpression.verified.txt @@ -0,0 +1,2 @@ +SELECT CAST(LEN([e].[Name]) AS int) +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.cs new file mode 100644 index 0000000..9acdbfa --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/NullConditionals/RewriteNullConditionalRewriteTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using VerifyXunit; +using Xunit; + +namespace EntityFrameworkCore.Projectables.FunctionalTests.NullConditionals +{ + [UsesVerify] + public class RewriteNullConditionalRewriteTests + { + [Fact] + public Task SimpleMemberExpression() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetNameLengthRewriteNulls()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task ComplexMemberExpression() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetNameLengthRewriteNulls()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task RelationalExpression() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetFirstRelatedRewriteNulls()); + + return Verifier.Verify(query.ToQueryString()); + } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.Foo.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.Foo.verified.txt new file mode 100644 index 0000000..a25a92e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.Foo.verified.txt @@ -0,0 +1,15 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_EntityExtensions_GetFirstRelatedIgnoreNulls + { + public static System.Linq.Expressions.Expression> Expression => + (Entity entity) => entity != null ? (entity.RelatedEntities != null ? (entity.RelatedEntities[0]) : null) : null; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementAndMemberBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementAndMemberBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..5d463d7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementAndMemberBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,15 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_EntityExtensions_GetFirstRelatedIgnoreNulls + { + public static System.Linq.Expressions.Expression> Expression => + (Entity entity) => entity.RelatedEntities[0]; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementAndMemberBinding_WithRewriteSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementAndMemberBinding_WithRewriteSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..a25a92e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementAndMemberBinding_WithRewriteSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,15 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_EntityExtensions_GetFirstRelatedIgnoreNulls + { + public static System.Linq.Expressions.Expression> Expression => + (Entity entity) => entity != null ? (entity.RelatedEntities != null ? (entity.RelatedEntities[0]) : null) : null; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..223687e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_C_GetFirst + { + public static System.Linq.Expressions.Expression> Expression => + (string input) => input[0].ToString(); + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementBinding_WithRewriteSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementBinding_WithRewriteSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..37fb8ca --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableElementBinding_WithRewriteSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_C_GetFirst + { + public static System.Linq.Expressions.Expression> Expression => + (string input) => input != null ? (input[0].ToString()) : null; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableMemberBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableMemberBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..f9b3a83 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableMemberBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_C_GetLength + { + public static System.Linq.Expressions.Expression> Expression => + (string input) => input.Length; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableMemberBinding_WithRewriteSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableMemberBinding_WithRewriteSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..c729b9a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableMemberBinding_WithRewriteSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_C_GetLength + { + public static System.Linq.Expressions.Expression> Expression => + (string input) => input != null ? (input.Length) : null; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableParameters_WithRewriteSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableParameters_WithRewriteSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..5ee3ea1 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableParameters_WithRewriteSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,15 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_EntityExtensions_GetFirstName + { + public static System.Linq.Expressions.Expression> Expression => + (Entity entity) => entity.FullName != null ? (entity.FullName.Substring(entity.FullName != null ? (entity.FullName.IndexOf(' ') ) : null?? 0)) : null; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithExplicitNullCheckRewriteSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithExplicitNullCheckRewriteSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..34c0099 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithExplicitNullCheckRewriteSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_C_GetFirst + { + public static System.Linq.Expressions.Expression> Expression => + (string input) => input != null ? (input[0]) : null; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithIgnoreRewriteSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithIgnoreRewriteSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..811dbf2 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithIgnoreRewriteSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_C_GetFirst + { + public static System.Linq.Expressions.Expression> Expression => + (string input) => input[0]; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..811dbf2 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithIgnoreSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_C_GetFirst + { + public static System.Linq.Expressions.Expression> Expression => + (string input) => input[0]; + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithRewriteSupport_IsBeingRewritten.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithRewriteSupport_IsBeingRewritten.verified.txt new file mode 100644 index 0000000..34c0099 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.NullableSimpleElementBinding_WithRewriteSupport_IsBeingRewritten.verified.txt @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_C_GetFirst + { + public static System.Linq.Expressions.Expression> Expression => + (string input) => input != null ? (input[0]) : null; + } +} \ 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 bc87af0..843c43b 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -487,6 +487,250 @@ namespace Foo { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task NullableMemberBinding_WithIgnoreSupport_IsBeingRewritten() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; + +namespace Foo { + static class C { + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public static int? GetLength(this string input) => input?.Length; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task NullableMemberBinding_WithRewriteSupport_IsBeingRewritten() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; + +namespace Foo { + static class C { + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static int? GetLength(this string input) => input?.Length; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task NullableSimpleElementBinding_WithIgnoreSupport_IsBeingRewritten() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; + +namespace Foo { + static class C { + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public static char? GetFirst(this string input) => input?[0]; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task NullableSimpleElementBinding_WithRewriteSupport_IsBeingRewritten() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; + +namespace Foo { + static class C { + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static char? GetFirst(this string input) => input?[0]; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + + [Fact] + public Task NullableElementBinding_WithIgnoreSupport_IsBeingRewritten() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; + +namespace Foo { + static class C { + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public static string? GetFirst(this string input) => input?[0].ToString(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task NullableElementBinding_WithRewriteSupport_IsBeingRewritten() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using EntityFrameworkCore.Projectables; + +namespace Foo { + static class C { + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static string? GetFirst(this string input) => input?[0].ToString(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task NullableElementAndMemberBinding_WithIgnoreSupport_IsBeingRewritten() + { + 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 List? RelatedEntities { get; set; } + } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public static Entity GetFirstRelatedIgnoreNulls(this Entity entity) + => entity?.RelatedEntities?[0]; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task NullableElementAndMemberBinding_WithRewriteSupport_IsBeingRewritten() + { + 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 List? RelatedEntities { get; set; } + } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static Entity GetFirstRelatedIgnoreNulls(this Entity entity) + => entity?.RelatedEntities?[0]; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task NullableParameters_WithRewriteSupport_IsBeingRewritten() + { + 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 string GetFirstName(this Entity entity) + => entity.FullName?.Substring(entity.FullName?.IndexOf(' ') ?? 0); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + #region Helpers Compilation CreateCompilation(string source, bool expectedToCompile = true) @@ -494,8 +738,6 @@ namespace Foo { var references = Basic.Reference.Assemblies.NetStandard20.All.ToList(); references.Add(MetadataReference.CreateFromFile(typeof(ProjectableAttribute).Assembly.Location)); - var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location); - var compilation = CSharpCompilation.Create("compilation", new[] { CSharpSyntaxTree.ParseText(source) }, references,