From ef840f69385b4da55e0b43c31488be8bcfa47bff Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 11:26:52 +0000
Subject: [PATCH 01/26] Initial plan
From 8468dc516a7faee3e87eaf4974abbc00108224a1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 11:36:26 +0000
Subject: [PATCH 02/26] Add support for block-bodied methods with common
statements
- Created BlockStatementConverter to transform block bodies to expressions
- Added support for simple return statements
- Added support for if-else statements (converted to ternary)
- Added support for local variable declarations (inlined)
- Added diagnostics for unsupported statements (EFP0003)
- Added comprehensive test cases
- Updated existing test that expected block methods to fail
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
.../AnalyzerReleases.Unshipped.md | 1 +
.../BlockStatementConverter.cs | 222 ++++++++++++++++
.../Diagnostics.cs | 8 +
.../ProjectableInterpreter.cs | 30 ++-
...lockBodiedMethod_SimpleReturn.verified.txt | 17 ++
...upportedStatement_WithoutElse.verified.txt | 3 +
....BlockBodiedMethod_WithIfElse.verified.txt | 17 ++
...Method_WithIfElseAndCondition.verified.txt | 17 ++
...odiedMethod_WithLocalVariable.verified.txt | 17 ++
...Method_WithMultipleParameters.verified.txt | 17 ++
...BodiedMethod_WithNestedIfElse.verified.txt | 17 ++
...diedMethod_WithPropertyAccess.verified.txt | 17 ++
.../ProjectionExpressionGeneratorTests.cs | 250 +++++++++++++++++-
13 files changed, 628 insertions(+), 5 deletions(-)
create mode 100644 src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt
diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
index ef168b8..4911eaa 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
@@ -3,3 +3,4 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
EFP0002 | Design | Error |
+EFP0003 | Design | Warning |
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
new file mode 100644
index 0000000..7c000af
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -0,0 +1,222 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace EntityFrameworkCore.Projectables.Generator
+{
+ ///
+ /// Converts block-bodied methods to expression syntax that can be used in expression trees.
+ /// Only supports a subset of C# statements.
+ ///
+ public class BlockStatementConverter
+ {
+ private readonly SemanticModel _semanticModel;
+ private readonly SourceProductionContext _context;
+ private readonly ExpressionSyntaxRewriter _expressionRewriter;
+ private readonly Dictionary _localVariables = new();
+
+ public BlockStatementConverter(SemanticModel semanticModel, SourceProductionContext context, ExpressionSyntaxRewriter expressionRewriter)
+ {
+ _semanticModel = semanticModel;
+ _context = context;
+ _expressionRewriter = expressionRewriter;
+ }
+
+ ///
+ /// Attempts to convert a block statement into a single expression.
+ /// Returns null if the block contains unsupported statements.
+ ///
+ public ExpressionSyntax? TryConvertBlock(BlockSyntax block, string memberName)
+ {
+ if (block == null || block.Statements.Count == 0)
+ {
+ return null;
+ }
+
+ // Try to convert the block statements into an expression
+ var result = TryConvertStatements(block.Statements.ToList(), memberName);
+ return result;
+ }
+
+ private ExpressionSyntax? TryConvertStatements(List statements, string memberName)
+ {
+ if (statements.Count == 0)
+ {
+ return null;
+ }
+
+ if (statements.Count == 1)
+ {
+ return TryConvertStatement(statements[0], memberName);
+ }
+
+ // Multiple statements - try to convert them into a chain of expressions
+ // This is done by converting local variable declarations and then the final return
+ var nonReturnStatements = statements.Take(statements.Count - 1).ToList();
+ var lastStatement = statements.Last();
+
+ // Process local variable declarations
+ foreach (var stmt in nonReturnStatements)
+ {
+ if (stmt is LocalDeclarationStatementSyntax localDecl)
+ {
+ if (!TryProcessLocalDeclaration(localDecl, memberName))
+ {
+ return null;
+ }
+ }
+ else
+ {
+ ReportUnsupportedStatement(stmt, memberName, "Only local variable declarations are supported before the return statement");
+ return null;
+ }
+ }
+
+ // Convert the final statement (should be a return)
+ return TryConvertStatement(lastStatement, memberName);
+ }
+
+ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDecl, string memberName)
+ {
+ foreach (var variable in localDecl.Declaration.Variables)
+ {
+ if (variable.Initializer == null)
+ {
+ ReportUnsupportedStatement(localDecl, memberName, "Local variables must have an initializer");
+ return false;
+ }
+
+ var variableName = variable.Identifier.Text;
+ // Rewrite the initializer expression NOW while it's still in the tree
+ var rewrittenInitializer = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value);
+ _localVariables[variableName] = rewrittenInitializer;
+ }
+
+ return true;
+ }
+
+ private ExpressionSyntax? TryConvertStatement(StatementSyntax statement, string memberName)
+ {
+ switch (statement)
+ {
+ case ReturnStatementSyntax returnStmt:
+ return TryConvertReturnStatement(returnStmt, memberName);
+
+ case IfStatementSyntax ifStmt:
+ return TryConvertIfStatement(ifStmt, memberName);
+
+ case BlockSyntax blockStmt:
+ return TryConvertStatements(blockStmt.Statements.ToList(), memberName);
+
+ case ExpressionStatementSyntax exprStmt:
+ // Expression statements are generally not useful in expression trees
+ ReportUnsupportedStatement(statement, memberName, "Expression statements are not supported");
+ return null;
+
+ case LocalDeclarationStatementSyntax:
+ // Local declarations should be handled before the return statement
+ ReportUnsupportedStatement(statement, memberName, "Local declarations must appear before the return statement");
+ return null;
+
+ default:
+ ReportUnsupportedStatement(statement, memberName, $"Statement type '{statement.GetType().Name}' is not supported");
+ return null;
+ }
+ }
+
+ private ExpressionSyntax? TryConvertReturnStatement(ReturnStatementSyntax returnStmt, string memberName)
+ {
+ if (returnStmt.Expression == null)
+ {
+ ReportUnsupportedStatement(returnStmt, memberName, "Return statement must have an expression");
+ return null;
+ }
+
+ // First rewrite the return expression
+ var expression = (ExpressionSyntax)_expressionRewriter.Visit(returnStmt.Expression);
+
+ // Then replace any local variable references with their already-rewritten initializers
+ expression = ReplaceLocalVariables(expression);
+
+ return expression;
+ }
+
+ private ExpressionSyntax? TryConvertIfStatement(IfStatementSyntax ifStmt, string memberName)
+ {
+ // Convert if-else to conditional (ternary) expression
+ // First, rewrite the condition using the expression rewriter
+ var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifStmt.Condition);
+
+ var whenTrue = TryConvertStatement(ifStmt.Statement, memberName);
+ if (whenTrue == null)
+ {
+ return null;
+ }
+
+ ExpressionSyntax? whenFalse;
+ if (ifStmt.Else != null)
+ {
+ whenFalse = TryConvertStatement(ifStmt.Else.Statement, memberName);
+ if (whenFalse == null)
+ {
+ return null;
+ }
+ }
+ else
+ {
+ // If there's no else clause, we can't convert to a ternary
+ ReportUnsupportedStatement(ifStmt, memberName, "If statements must have an else clause to be converted to expressions");
+ return null;
+ }
+
+ // Create a conditional expression with the rewritten nodes
+ return SyntaxFactory.ConditionalExpression(
+ condition,
+ whenTrue,
+ whenFalse
+ );
+ }
+
+ private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression)
+ {
+ // Use a rewriter to replace local variable references with their initializer expressions
+ var rewriter = new LocalVariableReplacer(_localVariables);
+ return (ExpressionSyntax)rewriter.Visit(expression);
+ }
+
+ private void ReportUnsupportedStatement(StatementSyntax statement, string memberName, string reason)
+ {
+ var diagnostic = Diagnostic.Create(
+ Diagnostics.UnsupportedStatementInBlockBody,
+ statement.GetLocation(),
+ memberName,
+ reason
+ );
+ _context.ReportDiagnostic(diagnostic);
+ }
+
+ private class LocalVariableReplacer : CSharpSyntaxRewriter
+ {
+ private readonly Dictionary _localVariables;
+
+ public LocalVariableReplacer(Dictionary localVariables)
+ {
+ _localVariables = localVariables;
+ }
+
+ public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
+ {
+ var identifier = node.Identifier.Text;
+ if (_localVariables.TryGetValue(identifier, out var replacement))
+ {
+ // Replace the identifier with the expression it was initialized with
+ return replacement.WithTriviaFrom(node);
+ }
+
+ return base.VisitIdentifierName(node);
+ }
+ }
+ }
+}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
index 18f87c1..d98a1b8 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
@@ -25,5 +25,13 @@ public static class Diagnostics
DiagnosticSeverity.Error,
isEnabledByDefault: true);
+ public static readonly DiagnosticDescriptor UnsupportedStatementInBlockBody = new DiagnosticDescriptor(
+ id: "EFP0003",
+ title: "Unsupported statement in block-bodied method",
+ messageFormat: "Method '{0}' contains an unsupported statement: {1}",
+ category: "Design",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
index 339b46b..36bd51c 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
@@ -97,7 +97,7 @@ x is IPropertySymbol xProperty &&
return false;
}
else if (x is MethodDeclarationSyntax xMethod &&
- xMethod.ExpressionBody is not null)
+ (xMethod.ExpressionBody is not null || xMethod.Body is not null))
{
return true;
}
@@ -212,7 +212,28 @@ x is IPropertySymbol xProperty &&
if (memberBody is MethodDeclarationSyntax methodDeclarationSyntax)
{
- if (methodDeclarationSyntax.ExpressionBody is null)
+ ExpressionSyntax? bodyExpression = null;
+
+ if (methodDeclarationSyntax.ExpressionBody is not null)
+ {
+ // Expression-bodied method (e.g., int Foo() => 1;)
+ bodyExpression = methodDeclarationSyntax.ExpressionBody.Expression;
+ }
+ else if (methodDeclarationSyntax.Body is not null)
+ {
+ // Block-bodied method (e.g., int Foo() { return 1; })
+ var blockConverter = new BlockStatementConverter(semanticModel, context, expressionSyntaxRewriter);
+ bodyExpression = blockConverter.TryConvertBlock(methodDeclarationSyntax.Body, memberSymbol.Name);
+
+ if (bodyExpression is null)
+ {
+ // Diagnostics already reported by BlockStatementConverter
+ return null;
+ }
+
+ // The expression has already been rewritten by BlockStatementConverter, so we don't rewrite it again
+ }
+ else
{
var diagnostic = Diagnostic.Create(Diagnostics.RequiresExpressionBodyDefinition, methodDeclarationSyntax.GetLocation(), memberSymbol.Name);
context.ReportDiagnostic(diagnostic);
@@ -222,7 +243,10 @@ x is IPropertySymbol xProperty &&
var returnType = declarationSyntaxRewriter.Visit(methodDeclarationSyntax.ReturnType);
descriptor.ReturnTypeName = returnType.ToString();
- descriptor.ExpressionBody = (ExpressionSyntax)expressionSyntaxRewriter.Visit(methodDeclarationSyntax.ExpressionBody.Expression);
+ // Only rewrite expression-bodied methods, block-bodied methods are already rewritten
+ descriptor.ExpressionBody = methodDeclarationSyntax.ExpressionBody is not null
+ ? (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression)
+ : bodyExpression;
foreach (var additionalParameter in ((ParameterListSyntax)declarationSyntaxRewriter.Visit(methodDeclarationSyntax.ParameterList)).Parameters)
{
descriptor.ParametersList = descriptor.ParametersList.AddParameters(additionalParameter);
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt
new file mode 100644
index 0000000..eeb0754
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => 42;
+ }
+ }
+}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt
new file mode 100644
index 0000000..ed766a2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt
@@ -0,0 +1,3 @@
+[
+ (11,13): warning EFP0003: Method 'Foo' contains an unsupported statement: Only local variable declarations are supported before the return statement
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt
new file mode 100644
index 0000000..c22d885
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar > 10 ? 1 : 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt
new file mode 100644
index 0000000..ef8f31a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.IsActive && @this.Bar > 0 ? @this.Bar * 2 : 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt
new file mode 100644
index 0000000..d863659
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar * 2 + 5;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt
new file mode 100644
index 0000000..c454e34
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Add
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this, int a, int b) => a + b;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt
new file mode 100644
index 0000000..216b8f2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar > 10 ? "High" : @this.Bar > 5 ? "Medium" : "Low";
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt
new file mode 100644
index 0000000..19e29c9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar + 10;
+ }
+ }
+}
\ 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 eb50a78..c408db9 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -493,7 +493,7 @@ public int Foo
}
[Fact]
- public void BlockBodiedMethod_RaisesDiagnostics()
+ public void BlockBodiedMethod_NoLongerRaisesDiagnostics()
{
var compilation = CreateCompilation(@"
using System;
@@ -511,7 +511,9 @@ public int Foo()
var result = RunGenerator(compilation);
- Assert.Single(result.Diagnostics);
+ // Block-bodied methods are now supported, so no diagnostics should be raised
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
}
[Fact]
@@ -1977,6 +1979,250 @@ public static Dictionary ToDictionary(this Entity entity)
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public Task BlockBodiedMethod_SimpleReturn()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ [Projectable]
+ public int Foo()
+ {
+ return 42;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithPropertyAccess()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ return Bar + 10;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithIfElse()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ if (Bar > 10)
+ {
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithNestedIfElse()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public string Foo()
+ {
+ if (Bar > 10)
+ {
+ return ""High"";
+ }
+ else if (Bar > 5)
+ {
+ return ""Medium"";
+ }
+ else
+ {
+ return ""Low"";
+ }
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithLocalVariable()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ var temp = Bar * 2;
+ return temp + 5;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithMultipleParameters()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ [Projectable]
+ public int Add(int a, int b)
+ {
+ return a + b;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithIfElseAndCondition()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+ public bool IsActive { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ if (IsActive && Bar > 0)
+ {
+ return Bar * 2;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_UnsupportedStatement_WithoutElse()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ if (Bar > 10)
+ {
+ return 1;
+ }
+ return 0;
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ // Should have a warning diagnostic
+ Assert.NotEmpty(result.Diagnostics);
+ Assert.Contains(result.Diagnostics, d => d.Id == "EFP0003");
+
+ return Verifier.Verify(result.Diagnostics.Select(d => d.ToString()));
+ }
+
#region Helpers
Compilation CreateCompilation(string source, bool expectedToCompile = true)
From f68c572e7eff047ee6dd4e4a04c491d054088a9b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 11:40:27 +0000
Subject: [PATCH 03/26] Add functional tests and documentation for block-bodied
methods
- Created 7 functional tests demonstrating EF Core SQL translation
- Added comprehensive documentation explaining feature, limitations, and benefits
- All 174 tests passing across all projects
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
docs/BlockBodiedMethods.md | 159 ++++++++++++++++
...ers_WorksCorrectly.DotNet10_0.verified.txt | 2 +
...ters_WorksCorrectly.DotNet9_0.verified.txt | 2 +
...WithParameters_WorksCorrectly.verified.txt | 2 +
...ranslatedCorrectly.DotNet10_0.verified.txt | 5 +
...TranslatedCorrectly.DotNet9_0.verified.txt | 5 +
...itional_IsTranslatedCorrectly.verified.txt | 5 +
...ranslatedToTernary.DotNet10_0.verified.txt | 5 +
...TranslatedToTernary.DotNet9_0.verified.txt | 5 +
...atement_IsTranslatedToTernary.verified.txt | 5 +
...Variable_IsInlined.DotNet10_0.verified.txt | 2 +
...lVariable_IsInlined.DotNet9_0.verified.txt | 2 +
...Tests.LocalVariable_IsInlined.verified.txt | 2 +
...tedToNestedTernary.DotNet10_0.verified.txt | 6 +
...atedToNestedTernary.DotNet9_0.verified.txt | 6 +
...e_IsTranslatedToNestedTernary.verified.txt | 6 +
..._IsTranslatedToSql.DotNet10_0.verified.txt | 2 +
...s_IsTranslatedToSql.DotNet9_0.verified.txt | 2 +
...pertyAccess_IsTranslatedToSql.verified.txt | 2 +
..._IsTranslatedToSql.DotNet10_0.verified.txt | 2 +
...n_IsTranslatedToSql.DotNet9_0.verified.txt | 2 +
...impleReturn_IsTranslatedToSql.verified.txt | 2 +
.../BlockBodiedMethodTests.cs | 169 ++++++++++++++++++
23 files changed, 400 insertions(+)
create mode 100644 docs/BlockBodiedMethods.md
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md
new file mode 100644
index 0000000..a62cace
--- /dev/null
+++ b/docs/BlockBodiedMethods.md
@@ -0,0 +1,159 @@
+# Block-Bodied Methods Support
+
+As of this version, EntityFrameworkCore.Projectables now supports "classic" block-bodied methods decorated with `[Projectable]`, in addition to expression-bodied methods.
+
+## What's Supported
+
+Block-bodied methods can now be transformed into expression trees when they contain:
+
+### 1. Simple Return Statements
+```csharp
+[Projectable]
+public int GetConstant()
+{
+ return 42;
+}
+```
+
+### 2. If-Else Statements (converted to ternary expressions)
+```csharp
+[Projectable]
+public string GetCategory()
+{
+ if (Value > 100)
+ {
+ return "High";
+ }
+ else
+ {
+ return "Low";
+ }
+}
+```
+
+### 3. Nested If-Else Statements
+```csharp
+[Projectable]
+public string GetLevel()
+{
+ if (Value > 100)
+ {
+ return "High";
+ }
+ else if (Value > 50)
+ {
+ return "Medium";
+ }
+ else
+ {
+ return "Low";
+ }
+}
+```
+
+### 4. Local Variable Declarations (inlined into the expression)
+```csharp
+[Projectable]
+public int CalculateDouble()
+{
+ var doubled = Value * 2;
+ return doubled + 5;
+}
+```
+
+## Limitations and Warnings
+
+The source generator will produce **warning EFP0003** when it encounters unsupported statements in block-bodied methods:
+
+### Unsupported Statements:
+- If statements without else clauses
+- While, for, foreach loops
+- Switch statements (use switch expressions instead)
+- Try-catch-finally blocks
+- Throw statements
+- New object instantiation in statement position
+- Multiple statements (except local variable declarations before return)
+
+### Example of Unsupported Pattern:
+```csharp
+[Projectable]
+public int GetValue()
+{
+ if (IsActive) // ❌ No else clause - will produce EFP0003 warning
+ {
+ return Value;
+ }
+ return 0;
+}
+```
+
+Should be written as:
+```csharp
+[Projectable]
+public int GetValue()
+{
+ if (IsActive) // ✅ Has else clause
+ {
+ return Value;
+ }
+ else
+ {
+ return 0;
+ }
+}
+```
+
+Or as expression-bodied:
+```csharp
+[Projectable]
+public int GetValue() => IsActive ? Value : 0; // ✅ Expression-bodied
+```
+
+## How It Works
+
+The source generator:
+1. Parses block-bodied methods
+2. Converts if-else statements to conditional (ternary) expressions
+3. Inlines local variables into the return expression
+4. Rewrites the resulting expression using the existing expression transformation pipeline
+5. Generates the same output as expression-bodied methods
+
+## Benefits
+
+- **More readable code**: Complex logic with nested conditions is often easier to read with if-else blocks than with nested ternary operators
+- **Gradual migration**: Existing code with block bodies can now be marked as `[Projectable]` without rewriting
+- **Intermediate variables**: Local variables can make complex calculations more understandable
+
+## Example Output
+
+Given this code:
+```csharp
+public record Entity
+{
+ public int Value { get; set; }
+ public bool IsActive { get; set; }
+
+ [Projectable]
+ public int GetAdjustedValue()
+ {
+ if (IsActive && Value > 0)
+ {
+ return Value * 2;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+}
+```
+
+The generated SQL will be:
+```sql
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 0
+ THEN [e].[Value] * 2
+ ELSE 0
+END
+FROM [Entity] AS [e]
+```
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..dab6bd4
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT 15
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..dab6bd4
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT 15
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..dab6bd4
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt
@@ -0,0 +1,2 @@
+SELECT 15
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..a19f725
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 0 THEN [e].[Value] * 2
+ ELSE 0
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..a19f725
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 0 THEN [e].[Value] * 2
+ ELSE 0
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt
new file mode 100644
index 0000000..a19f725
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 0 THEN [e].[Value] * 2
+ ELSE 0
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt
new file mode 100644
index 0000000..26ac26b
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt
new file mode 100644
index 0000000..26ac26b
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt
new file mode 100644
index 0000000..26ac26b
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt
new file mode 100644
index 0000000..9689484
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + 5
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt
new file mode 100644
index 0000000..9689484
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + 5
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt
new file mode 100644
index 0000000..9689484
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + 5
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt
new file mode 100644
index 0000000..9d42002
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt
@@ -0,0 +1,6 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ WHEN [e].[Value] > 50 THEN N'Medium'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt
new file mode 100644
index 0000000..9d42002
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt
@@ -0,0 +1,6 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ WHEN [e].[Value] > 50 THEN N'Medium'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt
new file mode 100644
index 0000000..9d42002
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt
@@ -0,0 +1,6 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ WHEN [e].[Value] > 50 THEN N'Medium'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt
new file mode 100644
index 0000000..06a56fa
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] + 10
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt
new file mode 100644
index 0000000..06a56fa
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] + 10
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt
new file mode 100644
index 0000000..06a56fa
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] + 10
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt
new file mode 100644
index 0000000..6efc8d2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT 42
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt
new file mode 100644
index 0000000..6efc8d2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT 42
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt
new file mode 100644
index 0000000..6efc8d2
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt
@@ -0,0 +1,2 @@
+SELECT 42
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
new file mode 100644
index 0000000..435c2a6
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
@@ -0,0 +1,169 @@
+using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
+using Microsoft.EntityFrameworkCore;
+using System.Linq;
+using System.Threading.Tasks;
+using VerifyXunit;
+using Xunit;
+
+namespace EntityFrameworkCore.Projectables.FunctionalTests
+{
+ [UsesVerify]
+ public class BlockBodiedMethodTests
+ {
+ public record Entity
+ {
+ public int Id { get; set; }
+ public int Value { get; set; }
+ public bool IsActive { get; set; }
+ public string? Name { get; set; }
+ }
+
+ [Fact]
+ public Task SimpleReturn_IsTranslatedToSql()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetConstant());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task ReturnWithPropertyAccess_IsTranslatedToSql()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetValuePlusTen());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task IfElseStatement_IsTranslatedToTernary()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetCategory());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task NestedIfElse_IsTranslatedToNestedTernary()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetLevel());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task LocalVariable_IsInlined()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.CalculateDouble());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task ComplexConditional_IsTranslatedCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetAdjustedValue());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockMethodWithParameters_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.Add(5, 10));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+ }
+
+ public static class EntityExtensions
+ {
+ [Projectable]
+ public static int GetConstant(this BlockBodiedMethodTests.Entity entity)
+ {
+ return 42;
+ }
+
+ [Projectable]
+ public static int GetValuePlusTen(this BlockBodiedMethodTests.Entity entity)
+ {
+ return entity.Value + 10;
+ }
+
+ [Projectable]
+ public static string GetCategory(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.Value > 100)
+ {
+ return "High";
+ }
+ else
+ {
+ return "Low";
+ }
+ }
+
+ [Projectable]
+ public static string GetLevel(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.Value > 100)
+ {
+ return "High";
+ }
+ else if (entity.Value > 50)
+ {
+ return "Medium";
+ }
+ else
+ {
+ return "Low";
+ }
+ }
+
+ [Projectable]
+ public static int CalculateDouble(this BlockBodiedMethodTests.Entity entity)
+ {
+ var doubled = entity.Value * 2;
+ return doubled + 5;
+ }
+
+ [Projectable]
+ public static int GetAdjustedValue(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.IsActive && entity.Value > 0)
+ {
+ return entity.Value * 2;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+
+ [Projectable]
+ public static int Add(this BlockBodiedMethodTests.Entity entity, int a, int b)
+ {
+ return a + b;
+ }
+ }
+}
From 7067bc28c48a95e4068ff23f38bda9e7c1ad5e2e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 12:05:34 +0000
Subject: [PATCH 04/26] Add support for if-without-else and switch statements
- Support if statements without else clause (uses default or fallback return)
- Support switch statements (converted to nested conditionals)
- Handle if { return x; } return y; pattern
- Added 5 generator tests and 4 functional tests
- Updated documentation with new features and SQL examples
- All 182 tests passing (84 generator + 76 functional + 22 unit)
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
docs/BlockBodiedMethods.md | 124 +++++++++++--
.../BlockStatementConverter.cs | 164 +++++++++++++++++-
...utElse_UsesDefault.DotNet10_0.verified.txt | 4 +
...outElse_UsesDefault.DotNet9_0.verified.txt | 4 +
...sts.IfWithoutElse_UsesDefault.verified.txt | 5 +
...WithFallbackReturn.DotNet10_0.verified.txt | 5 +
..._WithFallbackReturn.DotNet9_0.verified.txt | 5 +
...ithoutElse_WithFallbackReturn.verified.txt | 5 +
...chStatement_Simple.DotNet10_0.verified.txt | 7 +
...tchStatement_Simple.DotNet9_0.verified.txt | 7 +
...dTests.SwitchStatement_Simple.verified.txt | 7 +
..._WithMultipleCases.DotNet10_0.verified.txt | 7 +
...t_WithMultipleCases.DotNet9_0.verified.txt | 7 +
...chStatement_WithMultipleCases.verified.txt | 7 +
.../BlockBodiedMethodTests.cs | 101 +++++++++++
..._IfWithoutElse_ReturnsDefault.verified.txt | 17 ++
...hod_IfWithoutElse_UsesDefault.verified.txt | 17 ++
...Method_SwitchStatement_Simple.verified.txt | 17 ++
...chStatement_WithMultipleCases.verified.txt | 17 ++
...witchStatement_WithoutDefault.verified.txt | 17 ++
...upportedStatement_WithoutElse.verified.txt | 3 -
.../ProjectionExpressionGeneratorTests.cs | 148 +++++++++++++++-
22 files changed, 671 insertions(+), 24 deletions(-)
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt
delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt
diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md
index a62cace..9dc6e7a 100644
--- a/docs/BlockBodiedMethods.md
+++ b/docs/BlockBodiedMethods.md
@@ -61,38 +61,78 @@ public int CalculateDouble()
}
```
+### 5. Switch Statements (converted to nested ternary expressions)
+```csharp
+[Projectable]
+public string GetValueLabel()
+{
+ switch (Value)
+ {
+ case 1:
+ return "One";
+ case 2:
+ return "Two";
+ case 3:
+ return "Three";
+ default:
+ return "Many";
+ }
+}
+```
+
+### 6. If Statements Without Else (uses default value)
+```csharp
+[Projectable]
+public int? GetPremiumIfActive()
+{
+ if (IsActive)
+ {
+ return Value * 2;
+ }
+ // Implicitly returns null (default for int?)
+}
+
+// Or with explicit fallback:
+[Projectable]
+public string GetStatus()
+{
+ if (IsActive)
+ {
+ return "Active";
+ }
+ return "Inactive"; // Explicit fallback
+}
+```
+
## Limitations and Warnings
The source generator will produce **warning EFP0003** when it encounters unsupported statements in block-bodied methods:
### Unsupported Statements:
-- If statements without else clauses
- While, for, foreach loops
-- Switch statements (use switch expressions instead)
- Try-catch-finally blocks
- Throw statements
- New object instantiation in statement position
-- Multiple statements (except local variable declarations before return)
### Example of Unsupported Pattern:
```csharp
[Projectable]
public int GetValue()
{
- if (IsActive) // ❌ No else clause - will produce EFP0003 warning
+ for (int i = 0; i < 10; i++) // ❌ Loops not supported
{
- return Value;
+ // ...
}
return 0;
}
```
-Should be written as:
+Supported patterns:
```csharp
[Projectable]
public int GetValue()
{
- if (IsActive) // ✅ Has else clause
+ if (IsActive) // ✅ If without else is now supported!
{
return Value;
}
@@ -103,6 +143,35 @@ public int GetValue()
}
```
+Additional supported patterns:
+```csharp
+// If without else using fallback return:
+[Projectable]
+public int GetValue()
+{
+ if (IsActive)
+ {
+ return Value;
+ }
+ return 0; // ✅ Fallback return
+}
+
+// Switch statement:
+[Projectable]
+public string GetLabel()
+{
+ switch (Value) // ✅ Switch statements now supported!
+ {
+ case 1:
+ return "One";
+ case 2:
+ return "Two";
+ default:
+ return "Other";
+ }
+}
+```
+
Or as expression-bodied:
```csharp
[Projectable]
@@ -114,17 +183,48 @@ public int GetValue() => IsActive ? Value : 0; // ✅ Expression-bodied
The source generator:
1. Parses block-bodied methods
2. Converts if-else statements to conditional (ternary) expressions
-3. Inlines local variables into the return expression
-4. Rewrites the resulting expression using the existing expression transformation pipeline
-5. Generates the same output as expression-bodied methods
+3. Converts switch statements to nested conditional expressions
+4. Inlines local variables into the return expression
+5. Rewrites the resulting expression using the existing expression transformation pipeline
+6. Generates the same output as expression-bodied methods
## Benefits
-- **More readable code**: Complex logic with nested conditions is often easier to read with if-else blocks than with nested ternary operators
+- **More readable code**: Complex logic with nested conditions and switch statements is often easier to read than nested ternary operators
- **Gradual migration**: Existing code with block bodies can now be marked as `[Projectable]` without rewriting
- **Intermediate variables**: Local variables can make complex calculations more understandable
+- **Switch support**: Traditional switch statements now work alongside switch expressions
+
+## SQL Output Examples
+
+### Switch Statement with Multiple Cases
+Given this code:
+```csharp
+switch (Value)
+{
+ case 1:
+ case 2:
+ return "Low";
+ case 3:
+ case 4:
+ case 5:
+ return "Medium";
+ default:
+ return "High";
+}
+```
+
+Generates optimized SQL:
+```sql
+SELECT CASE
+ WHEN [e].[Value] IN (1, 2) THEN N'Low'
+ WHEN [e].[Value] IN (3, 4, 5) THEN N'Medium'
+ ELSE N'High'
+END
+FROM [Entity] AS [e]
+```
-## Example Output
+### If-Else Example Output
Given this code:
```csharp
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index 7c000af..db9170c 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -57,6 +57,31 @@ public BlockStatementConverter(SemanticModel semanticModel, SourceProductionCont
var nonReturnStatements = statements.Take(statements.Count - 1).ToList();
var lastStatement = statements.Last();
+ // Check if we have a pattern like: if { return x; } return y;
+ // This can be converted to: condition ? x : y
+ if (nonReturnStatements.Count == 1 &&
+ nonReturnStatements[0] is IfStatementSyntax ifWithoutElse &&
+ ifWithoutElse.Else == null &&
+ lastStatement is ReturnStatementSyntax finalReturn)
+ {
+ // Convert: if (condition) { return x; } return y;
+ // To: condition ? x : y
+ var ifBody = TryConvertStatement(ifWithoutElse.Statement, memberName);
+ if (ifBody == null)
+ {
+ return null;
+ }
+
+ var elseBody = TryConvertReturnStatement(finalReturn, memberName);
+ if (elseBody == null)
+ {
+ return null;
+ }
+
+ var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifWithoutElse.Condition);
+ return SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody);
+ }
+
// Process local variable declarations
foreach (var stmt in nonReturnStatements)
{
@@ -107,6 +132,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
case IfStatementSyntax ifStmt:
return TryConvertIfStatement(ifStmt, memberName);
+ case SwitchStatementSyntax switchStmt:
+ return TryConvertSwitchStatement(switchStmt, memberName);
+
case BlockSyntax blockStmt:
return TryConvertStatements(blockStmt.Statements.ToList(), memberName);
@@ -166,9 +194,12 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
}
else
{
- // If there's no else clause, we can't convert to a ternary
- ReportUnsupportedStatement(ifStmt, memberName, "If statements must have an else clause to be converted to expressions");
- return null;
+ // If there's no else clause, use a default literal
+ // This will be inferred to the correct type by the compiler
+ whenFalse = SyntaxFactory.LiteralExpression(
+ SyntaxKind.DefaultLiteralExpression,
+ SyntaxFactory.Token(SyntaxKind.DefaultKeyword)
+ );
}
// Create a conditional expression with the rewritten nodes
@@ -179,6 +210,133 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
);
}
+ private ExpressionSyntax? TryConvertSwitchStatement(SwitchStatementSyntax switchStmt, string memberName)
+ {
+ // Convert switch statement to nested conditional expressions
+ // Process sections in reverse order to build from the default case up
+
+ var switchExpression = (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression);
+ ExpressionSyntax? currentExpression = null;
+
+ // Find default case first
+ SwitchSectionSyntax? defaultSection = null;
+ var nonDefaultSections = new List();
+
+ foreach (var section in switchStmt.Sections)
+ {
+ bool hasDefault = section.Labels.Any(label => label is DefaultSwitchLabelSyntax);
+ if (hasDefault)
+ {
+ defaultSection = section;
+ }
+ else
+ {
+ nonDefaultSections.Add(section);
+ }
+ }
+
+ // Start with default case or null
+ if (defaultSection != null)
+ {
+ currentExpression = ConvertSwitchSection(defaultSection, memberName);
+ if (currentExpression == null)
+ {
+ return null;
+ }
+ }
+ else
+ {
+ // No default case - use default literal
+ currentExpression = SyntaxFactory.LiteralExpression(
+ SyntaxKind.DefaultLiteralExpression,
+ SyntaxFactory.Token(SyntaxKind.DefaultKeyword)
+ );
+ }
+
+ // Process non-default sections in reverse order
+ for (int i = nonDefaultSections.Count - 1; i >= 0; i--)
+ {
+ var section = nonDefaultSections[i];
+ var sectionExpression = ConvertSwitchSection(section, memberName);
+ if (sectionExpression == null)
+ {
+ return null;
+ }
+
+ // Build condition for all labels in this section (OR'd together)
+ ExpressionSyntax? condition = null;
+ foreach (var label in section.Labels)
+ {
+ if (label is CaseSwitchLabelSyntax caseLabel)
+ {
+ var labelCondition = SyntaxFactory.BinaryExpression(
+ SyntaxKind.EqualsExpression,
+ switchExpression,
+ (ExpressionSyntax)_expressionRewriter.Visit(caseLabel.Value)
+ );
+
+ condition = condition == null
+ ? labelCondition
+ : SyntaxFactory.BinaryExpression(
+ SyntaxKind.LogicalOrExpression,
+ condition,
+ labelCondition
+ );
+ }
+ else if (label is not DefaultSwitchLabelSyntax)
+ {
+ // Unsupported label type (e.g., pattern-based switch in older syntax)
+ ReportUnsupportedStatement(switchStmt, memberName,
+ $"Switch label type '{label.GetType().Name}' is not supported. Use case labels or switch expressions instead.");
+ return null;
+ }
+ }
+
+ if (condition != null)
+ {
+ currentExpression = SyntaxFactory.ConditionalExpression(
+ condition,
+ sectionExpression,
+ currentExpression
+ );
+ }
+ }
+
+ return currentExpression;
+ }
+
+ private ExpressionSyntax? ConvertSwitchSection(SwitchSectionSyntax section, string memberName)
+ {
+ // Convert the statements in the switch section
+ // Most switch sections end with break, return, or throw
+ var statements = section.Statements.ToList();
+
+ // Remove trailing break statements as they're not needed in expressions
+ if (statements.Count > 0 && statements.Last() is BreakStatementSyntax)
+ {
+ statements = statements.Take(statements.Count - 1).ToList();
+ }
+
+ if (statements.Count == 0)
+ {
+ // Use the section's first label location for error reporting
+ var firstLabel = section.Labels.FirstOrDefault();
+ if (firstLabel != null)
+ {
+ var diagnostic = Diagnostic.Create(
+ Diagnostics.UnsupportedStatementInBlockBody,
+ firstLabel.GetLocation(),
+ memberName,
+ "Switch section must have at least one statement"
+ );
+ _context.ReportDiagnostic(diagnostic);
+ }
+ return null;
+ }
+
+ return TryConvertStatements(statements, memberName);
+ }
+
private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression)
{
// Use a rewriter to replace local variable references with their initializer expressions
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt
new file mode 100644
index 0000000..0c5fe1e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt
@@ -0,0 +1,4 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN [e].[Value] * 2
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt
new file mode 100644
index 0000000..0c5fe1e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt
@@ -0,0 +1,4 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN [e].[Value] * 2
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt
new file mode 100644
index 0000000..7e3c8c6
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN [e].[Value] * 2
+ ELSE NULL
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt
new file mode 100644
index 0000000..f3f5c43
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active'
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt
new file mode 100644
index 0000000..f3f5c43
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active'
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt
new file mode 100644
index 0000000..f3f5c43
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active'
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt
new file mode 100644
index 0000000..9ed7fa8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] = 1 THEN N'One'
+ WHEN [e].[Value] = 2 THEN N'Two'
+ WHEN [e].[Value] = 3 THEN N'Three'
+ ELSE N'Many'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt
new file mode 100644
index 0000000..9ed7fa8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] = 1 THEN N'One'
+ WHEN [e].[Value] = 2 THEN N'Two'
+ WHEN [e].[Value] = 3 THEN N'Three'
+ ELSE N'Many'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt
new file mode 100644
index 0000000..9ed7fa8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] = 1 THEN N'One'
+ WHEN [e].[Value] = 2 THEN N'Two'
+ WHEN [e].[Value] = 3 THEN N'Three'
+ ELSE N'Many'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt
new file mode 100644
index 0000000..9c8b78e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] IN (1, 2) THEN N'Low'
+ WHEN [e].[Value] IN (3, 4, 5) THEN N'Medium'
+ WHEN [e].[Value] IN (6, 7, 8) THEN N'High'
+ ELSE N'Critical'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt
new file mode 100644
index 0000000..9c8b78e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] IN (1, 2) THEN N'Low'
+ WHEN [e].[Value] IN (3, 4, 5) THEN N'Medium'
+ WHEN [e].[Value] IN (6, 7, 8) THEN N'High'
+ ELSE N'Critical'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt
new file mode 100644
index 0000000..9c8b78e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] IN (1, 2) THEN N'Low'
+ WHEN [e].[Value] IN (3, 4, 5) THEN N'Medium'
+ WHEN [e].[Value] IN (6, 7, 8) THEN N'High'
+ ELSE N'Critical'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
index 435c2a6..71ae4d9 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
@@ -94,6 +94,50 @@ public Task BlockMethodWithParameters_WorksCorrectly()
return Verifier.Verify(query.ToQueryString());
}
+
+ [Fact]
+ public Task IfWithoutElse_UsesDefault()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetPremiumIfActive());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task IfWithoutElse_WithFallbackReturn()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetStatus());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task SwitchStatement_Simple()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetValueLabel());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task SwitchStatement_WithMultipleCases()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetPriority());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
}
public static class EntityExtensions
@@ -165,5 +209,62 @@ public static int Add(this BlockBodiedMethodTests.Entity entity, int a, int b)
{
return a + b;
}
+
+ [Projectable]
+ public static int? GetPremiumIfActive(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.IsActive)
+ {
+ return entity.Value * 2;
+ }
+ return null;
+ }
+
+ [Projectable]
+ public static string GetStatus(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.IsActive)
+ {
+ return "Active";
+ }
+ return "Inactive";
+ }
+
+ [Projectable]
+ public static string GetValueLabel(this BlockBodiedMethodTests.Entity entity)
+ {
+ switch (entity.Value)
+ {
+ case 1:
+ return "One";
+ case 2:
+ return "Two";
+ case 3:
+ return "Three";
+ default:
+ return "Many";
+ }
+ }
+
+ [Projectable]
+ public static string GetPriority(this BlockBodiedMethodTests.Entity entity)
+ {
+ switch (entity.Value)
+ {
+ case 1:
+ case 2:
+ return "Low";
+ case 3:
+ case 4:
+ case 5:
+ return "Medium";
+ case 6:
+ case 7:
+ case 8:
+ return "High";
+ default:
+ return "Critical";
+ }
+ }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt
new file mode 100644
index 0000000..b5f9f5b
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar > 10 ? 1 : default;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt
new file mode 100644
index 0000000..c22d885
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar > 10 ? 1 : 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt
new file mode 100644
index 0000000..d1a7eb5
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar == 1 ? "One" : @this.Bar == 2 ? "Two" : "Other";
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt
new file mode 100644
index 0000000..c90d6b7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar == 1 || @this.Bar == 2 ? "Low" : @this.Bar == 3 || @this.Bar == 4 || @this.Bar == 5 ? "Medium" : "High";
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt
new file mode 100644
index 0000000..0a4d15d
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar == 1 ? "One" : @this.Bar == 2 ? "Two" : default;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt
deleted file mode 100644
index ed766a2..0000000
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-[
- (11,13): warning EFP0003: Method 'Foo' contains an unsupported statement: Only local variable declarations are supported before the return statement
-]
\ 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 c408db9..8baad79 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -2191,8 +2191,9 @@ public int Foo()
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+
[Fact]
- public Task BlockBodiedMethod_UnsupportedStatement_WithoutElse()
+ public Task BlockBodiedMethod_IfWithoutElse_UsesDefault()
{
var compilation = CreateCompilation(@"
using System;
@@ -2212,15 +2213,150 @@ public int Foo()
}
}
}
-", expectedToCompile: true);
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_IfWithoutElse_ReturnsDefault()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int? Foo()
+ {
+ if (Bar > 10)
+ {
+ return 1;
+ }
+ }
+ }
+}
+", expectedToCompile: false);
var result = RunGenerator(compilation);
- // Should have a warning diagnostic
- Assert.NotEmpty(result.Diagnostics);
- Assert.Contains(result.Diagnostics, d => d.Id == "EFP0003");
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_SwitchStatement_Simple()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public string Foo()
+ {
+ switch (Bar)
+ {
+ case 1:
+ return ""One"";
+ case 2:
+ return ""Two"";
+ default:
+ return ""Other"";
+ }
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
- return Verifier.Verify(result.Diagnostics.Select(d => d.ToString()));
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_SwitchStatement_WithMultipleCases()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public string Foo()
+ {
+ switch (Bar)
+ {
+ case 1:
+ case 2:
+ return ""Low"";
+ case 3:
+ case 4:
+ case 5:
+ return ""Medium"";
+ default:
+ return ""High"";
+ }
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_SwitchStatement_WithoutDefault()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public string? Foo()
+ {
+ switch (Bar)
+ {
+ case 1:
+ return ""One"";
+ case 2:
+ return ""Two"";
+ }
+ }
+ }
+}
+", expectedToCompile: false);
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
#region Helpers
From 75c638e3d0d2681fa9a03459f90c471ed15f1d78 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 12:08:51 +0000
Subject: [PATCH 05/26] Address code review feedback
- Rename test to better reflect implicit return pattern
- Add clarifying comment about control flow in BlockStatementConverter
- All tests still passing
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
.../BlockStatementConverter.cs | 3 ++-
...lockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt} | 0
.../ProjectionExpressionGeneratorTests.cs | 2 +-
3 files changed, 3 insertions(+), 2 deletions(-)
rename tests/EntityFrameworkCore.Projectables.Generator.Tests/{ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt => ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt} (100%)
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index db9170c..59ede35 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -82,7 +82,8 @@ nonReturnStatements[0] is IfStatementSyntax ifWithoutElse &&
return SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody);
}
- // Process local variable declarations
+ // If we reach here, the pattern was not detected
+ // Process local variable declarations before the final return
foreach (var stmt in nonReturnStatements)
{
if (stmt is LocalDeclarationStatementSyntax localDecl)
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt
rename to tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
index 8baad79..9e2d34a 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -2224,7 +2224,7 @@ public int Foo()
}
[Fact]
- public Task BlockBodiedMethod_IfWithoutElse_ReturnsDefault()
+ public Task BlockBodiedMethod_IfWithoutElse_ImplicitReturn()
{
var compilation = CreateCompilation(@"
using System;
From 06627c292ceea0ad05389a1cc5ae3495c9bf8ed8 Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Sun, 15 Feb 2026 10:04:14 +0100
Subject: [PATCH 06/26] Remove unused code and add support for multiple early
returns
---
.../BlockStatementConverter.cs | 66 +++++++++++++------
.../ProjectableInterpreter.cs | 2 +-
.../BlockBodiedMethodTests.cs | 32 +++++++++
...Method_WithMultipleParameters.verified.txt | 2 +-
4 files changed, 81 insertions(+), 21 deletions(-)
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index 59ede35..d5817e2 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -1,8 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-using System.Collections.Generic;
-using System.Linq;
namespace EntityFrameworkCore.Projectables.Generator
{
@@ -12,14 +10,12 @@ namespace EntityFrameworkCore.Projectables.Generator
///
public class BlockStatementConverter
{
- private readonly SemanticModel _semanticModel;
private readonly SourceProductionContext _context;
private readonly ExpressionSyntaxRewriter _expressionRewriter;
private readonly Dictionary _localVariables = new();
- public BlockStatementConverter(SemanticModel semanticModel, SourceProductionContext context, ExpressionSyntaxRewriter expressionRewriter)
+ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntaxRewriter expressionRewriter)
{
- _semanticModel = semanticModel;
_context = context;
_expressionRewriter = expressionRewriter;
}
@@ -30,7 +26,7 @@ public BlockStatementConverter(SemanticModel semanticModel, SourceProductionCont
///
public ExpressionSyntax? TryConvertBlock(BlockSyntax block, string memberName)
{
- if (block == null || block.Statements.Count == 0)
+ if (block.Statements.Count == 0)
{
return null;
}
@@ -57,12 +53,44 @@ public BlockStatementConverter(SemanticModel semanticModel, SourceProductionCont
var nonReturnStatements = statements.Take(statements.Count - 1).ToList();
var lastStatement = statements.Last();
- // Check if we have a pattern like: if { return x; } return y;
- // This can be converted to: condition ? x : y
- if (nonReturnStatements.Count == 1 &&
- nonReturnStatements[0] is IfStatementSyntax ifWithoutElse &&
- ifWithoutElse.Else == null &&
- lastStatement is ReturnStatementSyntax finalReturn)
+ // Check if we have a pattern like multiple if statements without else followed by a final return:
+ // if (a) return 1; if (b) return 2; return 3;
+ // This can be converted to nested ternaries: a ? 1 : (b ? 2 : 3)
+ if (lastStatement is ReturnStatementSyntax finalReturn &&
+ nonReturnStatements.All(s => s is IfStatementSyntax { Else: null }))
+ {
+ // All non-return statements are if statements without else
+ var ifStatements = nonReturnStatements.Cast().ToList();
+
+ // Start with the final return as the base expression
+ var elseBody = TryConvertReturnStatement(finalReturn, memberName);
+ if (elseBody == null)
+ {
+ return null;
+ }
+
+ // Build nested conditionals from right to left (last to first)
+ for (var i = ifStatements.Count - 1; i >= 0; i--)
+ {
+ var ifStmt = ifStatements[i];
+ var ifBody = TryConvertStatement(ifStmt.Statement, memberName);
+ if (ifBody == null)
+ {
+ return null;
+ }
+
+ var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifStmt.Condition);
+ elseBody = SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody);
+ }
+
+ return elseBody;
+ }
+
+ // Check if we have a single if without else followed by a return (legacy path)
+ // This is now redundant with the above logic but kept for clarity and potential optimization
+ if (nonReturnStatements.Count == 1 &&
+ nonReturnStatements[0] is IfStatementSyntax { Else: null } ifWithoutElse &&
+ lastStatement is ReturnStatementSyntax singleFinalReturn)
{
// Convert: if (condition) { return x; } return y;
// To: condition ? x : y
@@ -72,7 +100,7 @@ nonReturnStatements[0] is IfStatementSyntax ifWithoutElse &&
return null;
}
- var elseBody = TryConvertReturnStatement(finalReturn, memberName);
+ var elseBody = TryConvertReturnStatement(singleFinalReturn, memberName);
if (elseBody == null)
{
return null;
@@ -115,9 +143,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
}
var variableName = variable.Identifier.Text;
+
// Rewrite the initializer expression NOW while it's still in the tree
- var rewrittenInitializer = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value);
- _localVariables[variableName] = rewrittenInitializer;
+ _localVariables[variableName] = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value);
}
return true;
@@ -139,7 +167,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
case BlockSyntax blockStmt:
return TryConvertStatements(blockStmt.Statements.ToList(), memberName);
- case ExpressionStatementSyntax exprStmt:
+ case ExpressionStatementSyntax:
// Expression statements are generally not useful in expression trees
ReportUnsupportedStatement(statement, memberName, "Expression statements are not supported");
return null;
@@ -217,7 +245,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
// Process sections in reverse order to build from the default case up
var switchExpression = (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression);
- ExpressionSyntax? currentExpression = null;
+ ExpressionSyntax? currentExpression;
// Find default case first
SwitchSectionSyntax? defaultSection = null;
@@ -225,7 +253,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
foreach (var section in switchStmt.Sections)
{
- bool hasDefault = section.Labels.Any(label => label is DefaultSwitchLabelSyntax);
+ var hasDefault = section.Labels.Any(label => label is DefaultSwitchLabelSyntax);
if (hasDefault)
{
defaultSection = section;
@@ -255,7 +283,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
}
// Process non-default sections in reverse order
- for (int i = nonDefaultSections.Count - 1; i >= 0; i--)
+ for (var i = nonDefaultSections.Count - 1; i >= 0; i--)
{
var section = nonDefaultSections[i];
var sectionExpression = ConvertSwitchSection(section, memberName);
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
index 7bca8cc..5006081 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
@@ -312,7 +312,7 @@ x is IPropertySymbol xProperty &&
else if (methodDeclarationSyntax.Body is not null)
{
// Block-bodied method (e.g., int Foo() { return 1; })
- var blockConverter = new BlockStatementConverter(semanticModel, context, expressionSyntaxRewriter);
+ var blockConverter = new BlockStatementConverter(context, expressionSyntaxRewriter);
bodyExpression = blockConverter.TryConvertBlock(methodDeclarationSyntax.Body, memberSymbol.Name);
if (bodyExpression is null)
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
index 71ae4d9..a961442 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
@@ -138,6 +138,17 @@ public Task SwitchStatement_WithMultipleCases()
return Verifier.Verify(query.ToQueryString());
}
+
+ [Fact]
+ public Task MultipleEarlyReturns_ConvertedToNestedTernaries()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetValueCategory());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
}
public static class EntityExtensions
@@ -266,5 +277,26 @@ public static string GetPriority(this BlockBodiedMethodTests.Entity entity)
return "Critical";
}
}
+
+ [Projectable]
+ public static string GetValueCategory(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.Value > 100)
+ {
+ return "Very High";
+ }
+
+ if (entity.Value > 50)
+ {
+ return "High";
+ }
+
+ if (entity.Value > 10)
+ {
+ return "Medium";
+ }
+
+ return "Low";
+ }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt
index c454e34..7c1426a 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt
@@ -7,7 +7,7 @@ using Foo;
namespace EntityFrameworkCore.Projectables.Generated
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- static class Foo_C_Add
+ static class Foo_C_Add_P0_int_P1_int
{
static global::System.Linq.Expressions.Expression> Expression()
{
From 9add2c922dd3af070f451632bc6b1531cc19d8cf Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Sun, 15 Feb 2026 10:08:46 +0100
Subject: [PATCH 07/26] Missing verify files
---
...urns_ConvertedToNestedTernaries.DotNet10_0.verified.txt | 7 +++++++
...turns_ConvertedToNestedTernaries.DotNet9_0.verified.txt | 7 +++++++
...pleEarlyReturns_ConvertedToNestedTernaries.verified.txt | 7 +++++++
3 files changed, 21 insertions(+)
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt
new file mode 100644
index 0000000..1ae6355
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'Very High'
+ WHEN [e].[Value] > 50 THEN N'High'
+ WHEN [e].[Value] > 10 THEN N'Medium'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt
new file mode 100644
index 0000000..1ae6355
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'Very High'
+ WHEN [e].[Value] > 50 THEN N'High'
+ WHEN [e].[Value] > 10 THEN N'Medium'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt
new file mode 100644
index 0000000..1ae6355
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'Very High'
+ WHEN [e].[Value] > 50 THEN N'High'
+ WHEN [e].[Value] > 10 THEN N'Medium'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
From 3c91bf9ff734f380a4396ec4ff5346a823eba93f Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Sun, 15 Feb 2026 10:09:03 +0100
Subject: [PATCH 08/26] Update docs
---
docs/BlockBodiedMethods.md | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md
index 9dc6e7a..c74bed8 100644
--- a/docs/BlockBodiedMethods.md
+++ b/docs/BlockBodiedMethods.md
@@ -104,6 +104,32 @@ public string GetStatus()
}
```
+### 7. Multiple Early Returns (converted to nested ternary expressions)
+```csharp
+[Projectable]
+public string GetValueCategory()
+{
+ if (Value > 100)
+ {
+ return "Very High";
+ }
+
+ if (Value > 50)
+ {
+ return "High";
+ }
+
+ if (Value > 10)
+ {
+ return "Medium";
+ }
+
+ return "Low";
+}
+
+// Converted to: Value > 100 ? "Very High" : (Value > 50 ? "High" : (Value > 10 ? "Medium" : "Low"))
+```
+
## Limitations and Warnings
The source generator will produce **warning EFP0003** when it encounters unsupported statements in block-bodied methods:
From e2b1fad5504c358eaed019d542231be3c9c248ef Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 15 Feb 2026 09:32:52 +0000
Subject: [PATCH 09/26] Address code review feedback - fix semantics and
scoping issues
- Add diagnostic for empty block bodies (EFP0003)
- Fix transitive local variable inlining (var a = 1; var b = a + 2; now fully expands)
- Add warning when local variables are used multiple times (semantics preservation)
- Prevent locals in nested blocks from leaking into outer scopes
- Fix documentation to show compilable C# code (no implicit returns)
- Add tests for transitive locals and nested block restrictions
- All 197 tests passing (96 net8.0 + 101 net10.0)
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
docs/BlockBodiedMethods.md | 20 ++++-
.../BlockStatementConverter.cs | 75 ++++++++++++++++++-
...estedBlock_ProducesDiagnostic.verified.txt | 3 +
..._WithTransitiveLocalVariables.verified.txt | 17 +++++
.../ProjectionExpressionGeneratorTests.cs | 62 +++++++++++++++
5 files changed, 174 insertions(+), 3 deletions(-)
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt
diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md
index c74bed8..494b5fe 100644
--- a/docs/BlockBodiedMethods.md
+++ b/docs/BlockBodiedMethods.md
@@ -59,8 +59,23 @@ public int CalculateDouble()
var doubled = Value * 2;
return doubled + 5;
}
+
+// Transitive inlining is also supported:
+[Projectable]
+public int CalculateComplex()
+{
+ var a = Value * 2;
+ var b = a + 5;
+ return b + 10; // Fully expanded to: Value * 2 + 5 + 10
+}
```
+**⚠️ Important Notes:**
+- Local variables are inlined at each usage point, which duplicates the initializer expression
+- If a local variable is used multiple times, the generator will emit a warning (EFP0003) as this could change semantics if the initializer has side effects
+- Local variables can only be declared at the method body level, not inside nested blocks (if/switch/etc.)
+- Variables are fully expanded transitively (variables that reference other variables are fully inlined)
+
### 5. Switch Statements (converted to nested ternary expressions)
```csharp
[Projectable]
@@ -82,6 +97,7 @@ public string GetValueLabel()
### 6. If Statements Without Else (uses default value)
```csharp
+// Pattern 1: Explicit null return
[Projectable]
public int? GetPremiumIfActive()
{
@@ -89,10 +105,10 @@ public int? GetPremiumIfActive()
{
return Value * 2;
}
- // Implicitly returns null (default for int?)
+ return null; // Explicit return for all code paths
}
-// Or with explicit fallback:
+// Pattern 2: Explicit fallback return
[Projectable]
public string GetStatus()
{
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index d5817e2..eead01d 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -1,3 +1,5 @@
+using System.Collections.Generic;
+using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -28,6 +30,13 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
{
if (block.Statements.Count == 0)
{
+ var diagnostic = Diagnostic.Create(
+ Diagnostics.UnsupportedStatementInBlockBody,
+ block.GetLocation(),
+ memberName,
+ "Block body must contain at least one statement"
+ );
+ _context.ReportDiagnostic(diagnostic);
return null;
}
@@ -145,7 +154,13 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
var variableName = variable.Identifier.Text;
// Rewrite the initializer expression NOW while it's still in the tree
- _localVariables[variableName] = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value);
+ var rewrittenInitializer = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value);
+
+ // Also expand any previously defined local variables in this initializer
+ // This ensures transitive inlining (e.g., var a = 1; var b = a + 2; return b; -> 1 + 2)
+ rewrittenInitializer = ReplaceLocalVariables(rewrittenInitializer);
+
+ _localVariables[variableName] = rewrittenInitializer;
}
return true;
@@ -165,6 +180,17 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
return TryConvertSwitchStatement(switchStmt, memberName);
case BlockSyntax blockStmt:
+ // Prevent locals declared in nested blocks from leaking into outer scopes
+ var nestedLocal = blockStmt.DescendantNodes()
+ .OfType()
+ .FirstOrDefault();
+
+ if (nestedLocal is not null)
+ {
+ ReportUnsupportedStatement(nestedLocal, memberName, "Local declarations in nested blocks are not supported");
+ return null;
+ }
+
return TryConvertStatements(blockStmt.Statements.ToList(), memberName);
case ExpressionStatementSyntax:
@@ -368,6 +394,28 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression)
{
+ // Count how many times each local variable is referenced
+ var referenceCounter = new LocalVariableReferenceCounter(_localVariables.Keys);
+ referenceCounter.Visit(expression);
+
+ // Warn if any local variable is referenced more than once (semantics could change due to duplication)
+ foreach (var kvp in referenceCounter.ReferenceCounts)
+ {
+ if (kvp.Value > 1)
+ {
+ // This is a warning because inlining still produces correct results for pure expressions,
+ // but could change behavior if the initializer has side effects or is expensive
+ var diagnostic = Diagnostic.Create(
+ Diagnostics.UnsupportedStatementInBlockBody,
+ expression.GetLocation(),
+ "local variable",
+ $"Local variable '{kvp.Key}' is referenced {kvp.Value} times and will be inlined at each use. " +
+ "This may change semantics if the initializer has side effects or is evaluated multiple times."
+ );
+ _context.ReportDiagnostic(diagnostic);
+ }
+ }
+
// Use a rewriter to replace local variable references with their initializer expressions
var rewriter = new LocalVariableReplacer(_localVariables);
return (ExpressionSyntax)rewriter.Visit(expression);
@@ -384,6 +432,31 @@ private void ReportUnsupportedStatement(StatementSyntax statement, string member
_context.ReportDiagnostic(diagnostic);
}
+ private class LocalVariableReferenceCounter : CSharpSyntaxWalker
+ {
+ private readonly HashSet _localVariableNames;
+ public Dictionary ReferenceCounts { get; } = new Dictionary();
+
+ public LocalVariableReferenceCounter(IEnumerable localVariableNames)
+ {
+ _localVariableNames = new HashSet(localVariableNames);
+ foreach (var name in localVariableNames)
+ {
+ ReferenceCounts[name] = 0;
+ }
+ }
+
+ public override void VisitIdentifierName(IdentifierNameSyntax node)
+ {
+ var identifier = node.Identifier.Text;
+ if (_localVariableNames.Contains(identifier))
+ {
+ ReferenceCounts[identifier]++;
+ }
+ base.VisitIdentifierName(node);
+ }
+ }
+
private class LocalVariableReplacer : CSharpSyntaxRewriter
{
private readonly Dictionary _localVariables;
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt
new file mode 100644
index 0000000..587f792
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt
@@ -0,0 +1,3 @@
+[
+ (13,17): warning EFP0003: Method 'Foo' contains an unsupported statement: Local declarations in nested blocks are not supported
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt
new file mode 100644
index 0000000..24ae821
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar * 2 + 5 + 10;
+ }
+ }
+}
\ 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 3d220b1..c11232b 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -2131,6 +2131,68 @@ public int Foo()
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public Task BlockBodiedMethod_WithTransitiveLocalVariables()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ var a = Bar * 2;
+ var b = a + 5;
+ return b + 10;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ if (Bar > 10)
+ {
+ var temp = Bar * 2;
+ return temp;
+ }
+ return 0;
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ // Should have a diagnostic about locals in nested blocks
+ Assert.NotEmpty(result.Diagnostics);
+ Assert.Contains(result.Diagnostics, d => d.Id == "EFP0003");
+
+ return Verifier.Verify(result.Diagnostics.Select(d => d.ToString()));
+ }
+
[Fact]
public Task BlockBodiedMethod_WithMultipleParameters()
{
From f7f296b4efb35d1bcda48b3baac7daa2f7f639ff Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Sun, 15 Feb 2026 19:14:01 +0100
Subject: [PATCH 10/26] Improve switch expression support and improve variable
handling
---
.../BlockStatementConverter.cs | 46 ---
.../ExpressionSyntaxRewriter.cs | 43 +-
...urn_WorksCorrectly.DotNet10_0.verified.txt | 2 +
...turn_WorksCorrectly.DotNet9_0.verified.txt | 2 +
...hmeticInReturn_WorksCorrectly.verified.txt | 2 +
...urn_WorksCorrectly.DotNet10_0.verified.txt | 5 +
...turn_WorksCorrectly.DotNet9_0.verified.txt | 5 +
....BooleanReturn_WorksCorrectly.verified.txt | 5 +
...ess_WorksCorrectly.DotNet10_0.verified.txt | 2 +
...cess_WorksCorrectly.DotNet9_0.verified.txt | 2 +
...ditionalAccess_WorksCorrectly.verified.txt | 2 +
...ion_WorksCorrectly.DotNet10_0.verified.txt | 5 +
...tion_WorksCorrectly.DotNet9_0.verified.txt | 5 +
...alWithNegation_WorksCorrectly.verified.txt | 5 +
...se_WithEarlyReturn.DotNet10_0.verified.txt | 6 +
...use_WithEarlyReturn.DotNet9_0.verified.txt | 6 +
...s.GuardClause_WithEarlyReturn.verified.txt | 6 +
...linedMultipleTimes.DotNet10_0.verified.txt | 2 +
...nlinedMultipleTimes.DotNet9_0.verified.txt | 2 +
...eReuse_IsInlinedMultipleTimes.verified.txt | 2 +
...thMultiplePatterns.DotNet10_0.verified.txt | 9 +
...ithMultiplePatterns.DotNet9_0.verified.txt | 9 +
...ndSwitch_WithMultiplePatterns.verified.txt | 9 +
...reInlinedCorrectly.DotNet10_0.verified.txt | 2 +
...AreInlinedCorrectly.DotNet9_0.verified.txt | 2 +
...Variables_AreInlinedCorrectly.verified.txt | 2 +
...thLogicalOperators.DotNet10_0.verified.txt | 7 +
...ithLogicalOperators.DotNet9_0.verified.txt | 7 +
...itionals_WithLogicalOperators.verified.txt | 7 +
...nIf_WorksCorrectly.DotNet10_0.verified.txt | 9 +
...InIf_WorksCorrectly.DotNet9_0.verified.txt | 9 +
...stedSwitchInIf_WorksCorrectly.verified.txt | 9 +
...ary_WorksCorrectly.DotNet10_0.verified.txt | 6 +
...nary_WorksCorrectly.DotNet9_0.verified.txt | 6 +
....NestedTernary_WorksCorrectly.verified.txt | 6 +
...ing_WorksCorrectly.DotNet10_0.verified.txt | 2 +
...cing_WorksCorrectly.DotNet9_0.verified.txt | 2 +
...NullCoalescing_WorksCorrectly.verified.txt | 2 +
...ion_WorksCorrectly.DotNet10_0.verified.txt | 2 +
...tion_WorksCorrectly.DotNet9_0.verified.txt | 2 +
...gInterpolation_WorksCorrectly.verified.txt | 2 +
...hExpression_Simple.DotNet10_0.verified.txt | 7 +
...chExpression_Simple.DotNet9_0.verified.txt | 7 +
...Tests.SwitchExpression_Simple.verified.txt | 7 +
...ession_WithDiscard.DotNet10_0.verified.txt | 7 +
...ression_WithDiscard.DotNet9_0.verified.txt | 7 +
....SwitchExpression_WithDiscard.verified.txt | 7 +
...use_WorksCorrectly.DotNet10_0.verified.txt | 7 +
...ause_WorksCorrectly.DotNet9_0.verified.txt | 7 +
...WithWhenClause_WorksCorrectly.verified.txt | 7 +
...ion_WorksCorrectly.DotNet10_0.verified.txt | 5 +
...sion_WorksCorrectly.DotNet9_0.verified.txt | 5 +
...naryExpression_WorksCorrectly.verified.txt | 5 +
.../BlockBodiedMethodTests.cs | 370 ++++++++++++++++++
54 files changed, 666 insertions(+), 48 deletions(-)
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index eead01d..e768940 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -394,28 +394,6 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression)
{
- // Count how many times each local variable is referenced
- var referenceCounter = new LocalVariableReferenceCounter(_localVariables.Keys);
- referenceCounter.Visit(expression);
-
- // Warn if any local variable is referenced more than once (semantics could change due to duplication)
- foreach (var kvp in referenceCounter.ReferenceCounts)
- {
- if (kvp.Value > 1)
- {
- // This is a warning because inlining still produces correct results for pure expressions,
- // but could change behavior if the initializer has side effects or is expensive
- var diagnostic = Diagnostic.Create(
- Diagnostics.UnsupportedStatementInBlockBody,
- expression.GetLocation(),
- "local variable",
- $"Local variable '{kvp.Key}' is referenced {kvp.Value} times and will be inlined at each use. " +
- "This may change semantics if the initializer has side effects or is evaluated multiple times."
- );
- _context.ReportDiagnostic(diagnostic);
- }
- }
-
// Use a rewriter to replace local variable references with their initializer expressions
var rewriter = new LocalVariableReplacer(_localVariables);
return (ExpressionSyntax)rewriter.Visit(expression);
@@ -432,30 +410,6 @@ private void ReportUnsupportedStatement(StatementSyntax statement, string member
_context.ReportDiagnostic(diagnostic);
}
- private class LocalVariableReferenceCounter : CSharpSyntaxWalker
- {
- private readonly HashSet _localVariableNames;
- public Dictionary ReferenceCounts { get; } = new Dictionary();
-
- public LocalVariableReferenceCounter(IEnumerable localVariableNames)
- {
- _localVariableNames = new HashSet(localVariableNames);
- foreach (var name in localVariableNames)
- {
- ReferenceCounts[name] = 0;
- }
- }
-
- public override void VisitIdentifierName(IdentifierNameSyntax node)
- {
- var identifier = node.Identifier.Text;
- if (_localVariableNames.Contains(identifier))
- {
- ReferenceCounts[identifier]++;
- }
- base.VisitIdentifierName(node);
- }
- }
private class LocalVariableReplacer : CSharpSyntaxRewriter
{
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
index f51c8f6..2b152dd 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
@@ -1,4 +1,4 @@
-using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
@@ -386,8 +386,47 @@ private ExpressionSyntax CreateMethodCallOnEnumValue(IMethodSymbol methodSymbol,
continue;
}
+ // Handle relational patterns (<=, <, >=, >)
+ if (arm.Pattern is RelationalPatternSyntax relational)
+ {
+ // Map the pattern operator token to a binary expression kind
+ var binaryKind = relational.OperatorToken.Kind() switch
+ {
+ SyntaxKind.LessThanToken => SyntaxKind.LessThanExpression,
+ SyntaxKind.LessThanEqualsToken => SyntaxKind.LessThanOrEqualExpression,
+ SyntaxKind.GreaterThanToken => SyntaxKind.GreaterThanExpression,
+ SyntaxKind.GreaterThanEqualsToken => SyntaxKind.GreaterThanOrEqualExpression,
+ _ => throw new InvalidOperationException(
+ $"Unsupported relational operator in switch expression: {relational.OperatorToken.Kind()}")
+ };
+
+ var condition = SyntaxFactory.BinaryExpression(
+ binaryKind,
+ (ExpressionSyntax)Visit(node.GoverningExpression),
+ (ExpressionSyntax)Visit(relational.Expression)
+ );
+
+ // Add the when clause as a AND expression
+ if (arm.WhenClause != null)
+ {
+ condition = SyntaxFactory.BinaryExpression(
+ SyntaxKind.LogicalAndExpression,
+ condition,
+ (ExpressionSyntax)Visit(arm.WhenClause.Condition)
+ );
+ }
+
+ currentExpression = SyntaxFactory.ConditionalExpression(
+ condition,
+ armExpression,
+ currentExpression
+ );
+
+ continue;
+ }
+
throw new InvalidOperationException(
- $"Switch expressions rewriting supports only constant values and declaration patterns (Type var). " +
+ $"Switch expressions rewriting supports constant values, relational patterns (<=, <, >=, >), and declaration patterns (Type var). " +
$"Unsupported pattern: {arm.Pattern.GetType().Name}"
);
}
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..3eaf767
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT (CAST([e].[Value] AS float) / 100.0E0) * 50.0E0
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..3eaf767
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT (CAST([e].[Value] AS float) / 100.0E0) * 50.0E0
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..3eaf767
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt
@@ -0,0 +1,2 @@
+SELECT (CAST([e].[Value] AS float) / 100.0E0) * 50.0E0
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..e5b6efb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..e5b6efb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..e5b6efb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..ba1f2c1
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.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/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..ba1f2c1
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.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/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..ba1f2c1
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.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/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..4d0592a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(0 AS bit) THEN N'Not Active'
+ ELSE N'Active'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..4d0592a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(0 AS bit) THEN N'Not Active'
+ ELSE N'Active'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..4d0592a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(0 AS bit) THEN N'Not Active'
+ ELSE N'Active'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt
new file mode 100644
index 0000000..a29be77
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt
@@ -0,0 +1,6 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(0 AS bit) THEN 0
+ WHEN [e].[Value] < 0 THEN 0
+ ELSE [e].[Value] * 2
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt
new file mode 100644
index 0000000..a29be77
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt
@@ -0,0 +1,6 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(0 AS bit) THEN 0
+ WHEN [e].[Value] < 0 THEN 0
+ ELSE [e].[Value] * 2
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt
new file mode 100644
index 0000000..a29be77
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt
@@ -0,0 +1,6 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(0 AS bit) THEN 0
+ WHEN [e].[Value] < 0 THEN 0
+ ELSE [e].[Value] * 2
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt
new file mode 100644
index 0000000..eec38d9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + [e].[Value] * 2
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt
new file mode 100644
index 0000000..eec38d9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + [e].[Value] * 2
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt
new file mode 100644
index 0000000..eec38d9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + [e].[Value] * 2
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt
new file mode 100644
index 0000000..257f6f0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt
@@ -0,0 +1,9 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE
+ WHEN [e].[Value] > 100 THEN N'Active High'
+ WHEN [e].[Value] > 50 THEN N'Active Medium'
+ ELSE N'Active Low'
+ END
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt
new file mode 100644
index 0000000..257f6f0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt
@@ -0,0 +1,9 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE
+ WHEN [e].[Value] > 100 THEN N'Active High'
+ WHEN [e].[Value] > 50 THEN N'Active Medium'
+ ELSE N'Active Low'
+ END
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt
new file mode 100644
index 0000000..257f6f0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt
@@ -0,0 +1,9 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE
+ WHEN [e].[Value] > 100 THEN N'Active High'
+ WHEN [e].[Value] > 50 THEN N'Active Medium'
+ ELSE N'Active Low'
+ END
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..4a903b0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + [e].[Value] * 3 + 10
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..4a903b0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + [e].[Value] * 3 + 10
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt
new file mode 100644
index 0000000..4a903b0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + [e].[Value] * 3 + 10
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt
new file mode 100644
index 0000000..6973619
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100 THEN N'Active High'
+ WHEN [e].[IsActive] = CAST(1 AS bit) OR [e].[Value] > 50 THEN N'Active or Medium'
+ WHEN [e].[IsActive] = CAST(0 AS bit) AND [e].[Value] <= 10 THEN N'Inactive Low'
+ ELSE N'Other'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt
new file mode 100644
index 0000000..6973619
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100 THEN N'Active High'
+ WHEN [e].[IsActive] = CAST(1 AS bit) OR [e].[Value] > 50 THEN N'Active or Medium'
+ WHEN [e].[IsActive] = CAST(0 AS bit) AND [e].[Value] <= 10 THEN N'Inactive Low'
+ ELSE N'Other'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt
new file mode 100644
index 0000000..6973619
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100 THEN N'Active High'
+ WHEN [e].[IsActive] = CAST(1 AS bit) OR [e].[Value] > 50 THEN N'Active or Medium'
+ WHEN [e].[IsActive] = CAST(0 AS bit) AND [e].[Value] <= 10 THEN N'Inactive Low'
+ ELSE N'Other'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..5f5a209
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,9 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE
+ WHEN [e].[Value] = 1 THEN N'Active One'
+ WHEN [e].[Value] = 2 THEN N'Active Two'
+ ELSE N'Active Other'
+ END
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..5f5a209
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,9 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE
+ WHEN [e].[Value] = 1 THEN N'Active One'
+ WHEN [e].[Value] = 2 THEN N'Active Two'
+ ELSE N'Active Other'
+ END
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..5f5a209
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt
@@ -0,0 +1,9 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE
+ WHEN [e].[Value] = 1 THEN N'Active One'
+ WHEN [e].[Value] = 2 THEN N'Active Two'
+ ELSE N'Active Other'
+ END
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..9d42002
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,6 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ WHEN [e].[Value] > 50 THEN N'Medium'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..9d42002
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,6 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ WHEN [e].[Value] > 50 THEN N'Medium'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..9d42002
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt
@@ -0,0 +1,6 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ WHEN [e].[Value] > 50 THEN N'Medium'
+ ELSE N'Low'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..52f2a3e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([e].[Name], N'Unknown')
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..52f2a3e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([e].[Name], N'Unknown')
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..52f2a3e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt
@@ -0,0 +1,2 @@
+SELECT COALESCE([e].[Name], N'Unknown')
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..e6bf43e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value]
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..e6bf43e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value]
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..e6bf43e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value]
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt
new file mode 100644
index 0000000..9ed7fa8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] = 1 THEN N'One'
+ WHEN [e].[Value] = 2 THEN N'Two'
+ WHEN [e].[Value] = 3 THEN N'Three'
+ ELSE N'Many'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt
new file mode 100644
index 0000000..9ed7fa8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] = 1 THEN N'One'
+ WHEN [e].[Value] = 2 THEN N'Two'
+ WHEN [e].[Value] = 3 THEN N'Three'
+ ELSE N'Many'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt
new file mode 100644
index 0000000..9ed7fa8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] = 1 THEN N'One'
+ WHEN [e].[Value] = 2 THEN N'Two'
+ WHEN [e].[Value] = 3 THEN N'Three'
+ ELSE N'Many'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt
new file mode 100644
index 0000000..727148f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] <= 2 THEN N'Low'
+ WHEN [e].[Value] <= 5 THEN N'Medium'
+ WHEN [e].[Value] <= 8 THEN N'High'
+ ELSE N'Critical'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt
new file mode 100644
index 0000000..727148f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] <= 2 THEN N'Low'
+ WHEN [e].[Value] <= 5 THEN N'Medium'
+ WHEN [e].[Value] <= 8 THEN N'High'
+ ELSE N'Critical'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt
new file mode 100644
index 0000000..727148f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] <= 2 THEN N'Low'
+ WHEN [e].[Value] <= 5 THEN N'Medium'
+ WHEN [e].[Value] <= 8 THEN N'High'
+ ELSE N'Critical'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..f2343d3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] = 1 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active One'
+ WHEN [e].[Value] = 1 THEN N'Inactive One'
+ WHEN [e].[Value] > 10 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active High'
+ ELSE N'Other'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..f2343d3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] = 1 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active One'
+ WHEN [e].[Value] = 1 THEN N'Inactive One'
+ WHEN [e].[Value] > 10 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active High'
+ ELSE N'Other'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..f2343d3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt
@@ -0,0 +1,7 @@
+SELECT CASE
+ WHEN [e].[Value] = 1 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active One'
+ WHEN [e].[Value] = 1 THEN N'Inactive One'
+ WHEN [e].[Value] > 10 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active High'
+ ELSE N'Other'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt
new file mode 100644
index 0000000..f3f5c43
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active'
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt
new file mode 100644
index 0000000..f3f5c43
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active'
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt
new file mode 100644
index 0000000..f3f5c43
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active'
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
index a961442..37a6898 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
@@ -149,6 +149,193 @@ public Task MultipleEarlyReturns_ConvertedToNestedTernaries()
return Verifier.Verify(query.ToQueryString());
}
+
+ [Fact]
+ public Task NullCoalescing_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetNameOrDefault());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task ConditionalAccess_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetNameLength());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task SwitchExpression_Simple()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetValueLabelModern());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task SwitchExpression_WithDiscard()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetPriorityModern());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task MultipleLocalVariables_AreInlinedCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.CalculateComplex());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task NestedConditionals_WithLogicalOperators()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetComplexCategory());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task GuardClause_WithEarlyReturn()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetGuardedValue());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task NestedSwitchInIf_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetCombinedLogic());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task TernaryExpression_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetValueUsingTernary());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task NestedTernary_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetNestedTernary());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task MixedIfAndSwitch_WithMultiplePatterns()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetComplexMix());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task SwitchWithWhenClause_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetValueWithCondition());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task LocalVariableReuse_IsInlinedMultipleTimes()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.CalculateWithReuse());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BooleanReturn_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.IsHighValue());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task ConditionalWithNegation_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetInactiveStatus());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task StringInterpolation_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.GetFormattedValue());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task ArithmeticInReturn_WorksCorrectly()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.CalculatePercentage());
+
+ return Verifier.Verify(query.ToQueryString());
+ }
}
public static class EntityExtensions
@@ -298,5 +485,188 @@ public static string GetValueCategory(this BlockBodiedMethodTests.Entity entity)
return "Low";
}
+
+ [Projectable]
+ public static string GetNameOrDefault(this BlockBodiedMethodTests.Entity entity)
+ {
+ return entity.Name ?? "Unknown";
+ }
+
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)]
+ public static int? GetNameLength(this BlockBodiedMethodTests.Entity entity)
+ {
+ return entity.Name?.Length;
+ }
+
+ [Projectable]
+ public static string GetValueLabelModern(this BlockBodiedMethodTests.Entity entity)
+ {
+ return entity.Value switch
+ {
+ 1 => "One",
+ 2 => "Two",
+ 3 => "Three",
+ _ => "Many"
+ };
+ }
+
+ [Projectable]
+ public static string GetPriorityModern(this BlockBodiedMethodTests.Entity entity)
+ {
+ return entity.Value switch
+ {
+ <= 2 => "Low",
+ <= 5 => "Medium",
+ <= 8 => "High",
+ _ => "Critical"
+ };
+ }
+
+ [Projectable]
+ public static int CalculateComplex(this BlockBodiedMethodTests.Entity entity)
+ {
+ var doubled = entity.Value * 2;
+ var tripled = entity.Value * 3;
+ var sum = doubled + tripled;
+ return sum + 10;
+ }
+
+ [Projectable]
+ public static string GetComplexCategory(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.IsActive && entity.Value > 100)
+ {
+ return "Active High";
+ }
+
+ if (entity.IsActive || entity.Value > 50)
+ {
+ return "Active or Medium";
+ }
+
+ if (!entity.IsActive && entity.Value <= 10)
+ {
+ return "Inactive Low";
+ }
+
+ return "Other";
+ }
+
+ [Projectable]
+ public static int GetGuardedValue(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (!entity.IsActive)
+ {
+ return 0;
+ }
+
+ if (entity.Value < 0)
+ {
+ return 0;
+ }
+
+ return entity.Value * 2;
+ }
+
+ [Projectable]
+ public static string GetCombinedLogic(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.IsActive)
+ {
+ switch (entity.Value)
+ {
+ case 1:
+ return "Active One";
+ case 2:
+ return "Active Two";
+ default:
+ return "Active Other";
+ }
+ }
+
+ return "Inactive";
+ }
+
+ [Projectable]
+ public static string GetValueUsingTernary(this BlockBodiedMethodTests.Entity entity)
+ {
+ return entity.IsActive ? "Active" : "Inactive";
+ }
+
+ [Projectable]
+ public static string GetNestedTernary(this BlockBodiedMethodTests.Entity entity)
+ {
+ return entity.Value > 100 ? "High" : entity.Value > 50 ? "Medium" : "Low";
+ }
+
+ [Projectable]
+ public static string GetComplexMix(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.IsActive)
+ {
+ return entity.Value switch
+ {
+ > 100 => "Active High",
+ > 50 => "Active Medium",
+ _ => "Active Low"
+ };
+ }
+
+ return "Inactive";
+ }
+
+ [Projectable]
+ public static string GetValueWithCondition(this BlockBodiedMethodTests.Entity entity)
+ {
+ return entity.Value switch
+ {
+ 1 when entity.IsActive => "Active One",
+ 1 => "Inactive One",
+ > 10 when entity.IsActive => "Active High",
+ _ => "Other"
+ };
+ }
+
+ [Projectable]
+ public static int CalculateWithReuse(this BlockBodiedMethodTests.Entity entity)
+ {
+ var doubled = entity.Value * 2;
+ return doubled + doubled;
+ }
+
+ [Projectable]
+ public static bool IsHighValue(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (entity.Value > 100)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ [Projectable]
+ public static string GetInactiveStatus(this BlockBodiedMethodTests.Entity entity)
+ {
+ if (!entity.IsActive)
+ {
+ return "Not Active";
+ }
+ else
+ {
+ return "Active";
+ }
+ }
+
+ [Projectable]
+ public static string GetFormattedValue(this BlockBodiedMethodTests.Entity entity)
+ {
+ return $"Value: {entity.Value}";
+ }
+
+ [Projectable]
+ public static double CalculatePercentage(this BlockBodiedMethodTests.Entity entity)
+ {
+ return (double)entity.Value / 100.0 * 50.0;
+ }
}
}
From 3b56faaca7a0f81e22ccde7b3656f47eaa2cfcf1 Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Sun, 15 Feb 2026 22:20:17 +0100
Subject: [PATCH 11/26] Remove redundant code and add test for projectables in
block bodied methods
---
.../BlockStatementConverter.cs | 64 ++--
...urn_WorksCorrectly.DotNet10_0.verified.txt | 0
...turn_WorksCorrectly.DotNet9_0.verified.txt | 0
...hmeticInReturn_WorksCorrectly.verified.txt | 0
...ers_WorksCorrectly.DotNet10_0.verified.txt | 0
...ters_WorksCorrectly.DotNet9_0.verified.txt | 0
...WithParameters_WorksCorrectly.verified.txt | 0
...urn_WorksCorrectly.DotNet10_0.verified.txt | 0
...turn_WorksCorrectly.DotNet9_0.verified.txt | 0
....BooleanReturn_WorksCorrectly.verified.txt | 0
...ranslatedCorrectly.DotNet10_0.verified.txt | 0
...TranslatedCorrectly.DotNet9_0.verified.txt | 0
...itional_IsTranslatedCorrectly.verified.txt | 0
...ess_WorksCorrectly.DotNet10_0.verified.txt | 0
...cess_WorksCorrectly.DotNet9_0.verified.txt | 0
...ditionalAccess_WorksCorrectly.verified.txt | 0
...ion_WorksCorrectly.DotNet10_0.verified.txt | 0
...tion_WorksCorrectly.DotNet9_0.verified.txt | 0
...alWithNegation_WorksCorrectly.verified.txt | 0
...se_WithEarlyReturn.DotNet10_0.verified.txt | 0
...use_WithEarlyReturn.DotNet9_0.verified.txt | 0
...s.GuardClause_WithEarlyReturn.verified.txt | 0
...ranslatedToTernary.DotNet10_0.verified.txt | 0
...TranslatedToTernary.DotNet9_0.verified.txt | 0
...atement_IsTranslatedToTernary.verified.txt | 0
...utElse_UsesDefault.DotNet10_0.verified.txt | 0
...outElse_UsesDefault.DotNet9_0.verified.txt | 0
...sts.IfWithoutElse_UsesDefault.verified.txt | 0
...WithFallbackReturn.DotNet10_0.verified.txt | 0
..._WithFallbackReturn.DotNet9_0.verified.txt | 0
...ithoutElse_WithFallbackReturn.verified.txt | 0
...linedMultipleTimes.DotNet10_0.verified.txt | 0
...nlinedMultipleTimes.DotNet9_0.verified.txt | 0
...eReuse_IsInlinedMultipleTimes.verified.txt | 0
...Variable_IsInlined.DotNet10_0.verified.txt | 0
...lVariable_IsInlined.DotNet9_0.verified.txt | 0
...Tests.LocalVariable_IsInlined.verified.txt | 0
...thMultiplePatterns.DotNet10_0.verified.txt | 0
...ithMultiplePatterns.DotNet9_0.verified.txt | 0
...ndSwitch_WithMultiplePatterns.verified.txt | 0
...dToNestedTernaries.DotNet10_0.verified.txt | 0
...edToNestedTernaries.DotNet9_0.verified.txt | 0
...ns_ConvertedToNestedTernaries.verified.txt | 0
...reInlinedCorrectly.DotNet10_0.verified.txt | 0
...AreInlinedCorrectly.DotNet9_0.verified.txt | 0
...Variables_AreInlinedCorrectly.verified.txt | 0
...thLogicalOperators.DotNet10_0.verified.txt | 0
...ithLogicalOperators.DotNet9_0.verified.txt | 0
...itionals_WithLogicalOperators.verified.txt | 0
...tedToNestedTernary.DotNet10_0.verified.txt | 0
...atedToNestedTernary.DotNet9_0.verified.txt | 0
...e_IsTranslatedToNestedTernary.verified.txt | 0
...nIf_WorksCorrectly.DotNet10_0.verified.txt | 0
...InIf_WorksCorrectly.DotNet9_0.verified.txt | 0
...stedSwitchInIf_WorksCorrectly.verified.txt | 0
...ary_WorksCorrectly.DotNet10_0.verified.txt | 0
...nary_WorksCorrectly.DotNet9_0.verified.txt | 0
....NestedTernary_WorksCorrectly.verified.txt | 0
...ing_WorksCorrectly.DotNet10_0.verified.txt | 0
...cing_WorksCorrectly.DotNet9_0.verified.txt | 0
...NullCoalescing_WorksCorrectly.verified.txt | 0
..._IsTranslatedToSql.DotNet10_0.verified.txt | 0
...s_IsTranslatedToSql.DotNet9_0.verified.txt | 0
...pertyAccess_IsTranslatedToSql.verified.txt | 0
..._IsTranslatedToSql.DotNet10_0.verified.txt | 0
...n_IsTranslatedToSql.DotNet9_0.verified.txt | 0
...impleReturn_IsTranslatedToSql.verified.txt | 0
...ion_WorksCorrectly.DotNet10_0.verified.txt | 0
...tion_WorksCorrectly.DotNet9_0.verified.txt | 0
...gInterpolation_WorksCorrectly.verified.txt | 0
...hExpression_Simple.DotNet10_0.verified.txt | 0
...chExpression_Simple.DotNet9_0.verified.txt | 0
...Tests.SwitchExpression_Simple.verified.txt | 0
...ession_WithDiscard.DotNet10_0.verified.txt | 0
...ression_WithDiscard.DotNet9_0.verified.txt | 0
....SwitchExpression_WithDiscard.verified.txt | 0
...chStatement_Simple.DotNet10_0.verified.txt | 0
...tchStatement_Simple.DotNet9_0.verified.txt | 0
...dTests.SwitchStatement_Simple.verified.txt | 0
..._WithMultipleCases.DotNet10_0.verified.txt | 0
...t_WithMultipleCases.DotNet9_0.verified.txt | 0
...chStatement_WithMultipleCases.verified.txt | 0
...use_WorksCorrectly.DotNet10_0.verified.txt | 0
...ause_WorksCorrectly.DotNet9_0.verified.txt | 0
...WithWhenClause_WorksCorrectly.verified.txt | 0
...ion_WorksCorrectly.DotNet10_0.verified.txt | 0
...sion_WorksCorrectly.DotNet9_0.verified.txt | 0
...naryExpression_WorksCorrectly.verified.txt | 0
.../BlockBodiedMethodTests.cs | 2 +-
.../BlockBodyProjectableCallTest.cs | 273 ++++++++++++++++++
...Method_InCondition.DotNet10_0.verified.txt | 5 +
...eMethod_InCondition.DotNet9_0.verified.txt | 5 +
...ProjectableMethod_InCondition.verified.txt | 5 +
...thod_InEarlyReturn.DotNet10_0.verified.txt | 9 +
...ethod_InEarlyReturn.DotNet9_0.verified.txt | 9 +
...ojectableMethod_InEarlyReturn.verified.txt | 9 +
...nLogicalExpression.DotNet10_0.verified.txt | 5 +
...InLogicalExpression.DotNet9_0.verified.txt | 5 +
...bleMethod_InLogicalExpression.verified.txt | 5 +
...bleMethod_InReturn.DotNet10_0.verified.txt | 2 +
...ableMethod_InReturn.DotNet9_0.verified.txt | 2 +
...ingProjectableMethod_InReturn.verified.txt | 2 +
...bleMethod_InSwitch.DotNet10_0.verified.txt | 12 +
...ableMethod_InSwitch.DotNet9_0.verified.txt | 12 +
...ingProjectableMethod_InSwitch.verified.txt | 12 +
...InSwitchExpression.DotNet10_0.verified.txt | 19 ++
..._InSwitchExpression.DotNet9_0.verified.txt | 19 ++
...ableMethod_InSwitchExpression.verified.txt | 19 ++
...leMethod_InTernary.DotNet10_0.verified.txt | 8 +
...bleMethod_InTernary.DotNet9_0.verified.txt | 8 +
...ngProjectableMethod_InTernary.verified.txt | 8 +
...bleMethod_Multiple.DotNet10_0.verified.txt | 2 +
...ableMethod_Multiple.DotNet9_0.verified.txt | 2 +
...ingProjectableMethod_Multiple.verified.txt | 2 +
...tableMethod_Nested.DotNet10_0.verified.txt | 2 +
...ctableMethod_Nested.DotNet9_0.verified.txt | 2 +
...llingProjectableMethod_Nested.verified.txt | 2 +
...tableMethod_Simple.DotNet10_0.verified.txt | 2 +
...ctableMethod_Simple.DotNet9_0.verified.txt | 2 +
...llingProjectableMethod_Simple.verified.txt | 2 +
..._WithLocalVariable.DotNet10_0.verified.txt | 2 +
...d_WithLocalVariable.DotNet9_0.verified.txt | 2 +
...tableMethod_WithLocalVariable.verified.txt | 2 +
123 files changed, 499 insertions(+), 44 deletions(-)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt (100%)
rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.cs (99%)
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index e768940..843930f 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -41,8 +41,7 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
}
// Try to convert the block statements into an expression
- var result = TryConvertStatements(block.Statements.ToList(), memberName);
- return result;
+ return TryConvertStatements(block.Statements.ToList(), memberName);
}
private ExpressionSyntax? TryConvertStatements(List statements, string memberName)
@@ -95,30 +94,6 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
return elseBody;
}
- // Check if we have a single if without else followed by a return (legacy path)
- // This is now redundant with the above logic but kept for clarity and potential optimization
- if (nonReturnStatements.Count == 1 &&
- nonReturnStatements[0] is IfStatementSyntax { Else: null } ifWithoutElse &&
- lastStatement is ReturnStatementSyntax singleFinalReturn)
- {
- // Convert: if (condition) { return x; } return y;
- // To: condition ? x : y
- var ifBody = TryConvertStatement(ifWithoutElse.Statement, memberName);
- if (ifBody == null)
- {
- return null;
- }
-
- var elseBody = TryConvertReturnStatement(singleFinalReturn, memberName);
- if (elseBody == null)
- {
- return null;
- }
-
- var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifWithoutElse.Condition);
- return SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody);
- }
-
// If we reach here, the pattern was not detected
// Process local variable declarations before the final return
foreach (var stmt in nonReturnStatements)
@@ -226,7 +201,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
return expression;
}
- private ExpressionSyntax? TryConvertIfStatement(IfStatementSyntax ifStmt, string memberName)
+ private ConditionalExpressionSyntax? TryConvertIfStatement(IfStatementSyntax ifStmt, string memberName)
{
// Convert if-else to conditional (ternary) expression
// First, rewrite the condition using the expression rewriter
@@ -371,25 +346,28 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
{
statements = statements.Take(statements.Count - 1).ToList();
}
-
- if (statements.Count == 0)
+
+ if (statements.Count != 0)
+ {
+ return TryConvertStatements(statements, memberName);
+ }
+
+ // Use the section's first label location for error reporting
+ var firstLabel = section.Labels.FirstOrDefault();
+ if (firstLabel == null)
{
- // Use the section's first label location for error reporting
- var firstLabel = section.Labels.FirstOrDefault();
- if (firstLabel != null)
- {
- var diagnostic = Diagnostic.Create(
- Diagnostics.UnsupportedStatementInBlockBody,
- firstLabel.GetLocation(),
- memberName,
- "Switch section must have at least one statement"
- );
- _context.ReportDiagnostic(diagnostic);
- }
return null;
}
-
- return TryConvertStatements(statements, memberName);
+
+ var diagnostic = Diagnostic.Create(
+ Diagnostics.UnsupportedStatementInBlockBody,
+ firstLabel.GetLocation(),
+ memberName,
+ "Switch section must have at least one statement"
+ );
+ _context.ReportDiagnostic(diagnostic);
+ return null;
+
}
private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression)
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt
similarity index 100%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs
similarity index 99%
rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs
index 37a6898..9622f34 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs
@@ -5,7 +5,7 @@
using VerifyXunit;
using Xunit;
-namespace EntityFrameworkCore.Projectables.FunctionalTests
+namespace EntityFrameworkCore.Projectables.FunctionalTests.BlockBodiedMethods
{
[UsesVerify]
public class BlockBodiedMethodTests
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs
new file mode 100644
index 0000000..4d3fa96
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs
@@ -0,0 +1,273 @@
+using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
+using Microsoft.EntityFrameworkCore;
+using System.Linq;
+using System.Threading.Tasks;
+using VerifyXunit;
+using Xunit;
+
+namespace EntityFrameworkCore.Projectables.FunctionalTests.BlockBodiedMethods
+{
+ ///
+ /// Tests for calling projectable methods from within block-bodied methods
+ ///
+ [UsesVerify]
+ public class BlockBodyProjectableCallTests
+ {
+ public record Entity
+ {
+ public int Id { get; set; }
+ public int Value { get; set; }
+ public bool IsActive { get; set; }
+ public string? Name { get; set; }
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_Simple()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.GetAdjustedWithConstant());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_InReturn()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.GetDoubledValue());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_InCondition()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.GetCategoryBasedOnAdjusted());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_Multiple()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.CombineProjectableMethods());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_InSwitch()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.GetLabelBasedOnCategory());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_InSwitchExpression()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.GetDescriptionByLevel());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_WithLocalVariable()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.CalculateUsingProjectable());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_Nested()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.GetNestedProjectableCall());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_InEarlyReturn()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.GetStatusWithProjectableCheck());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_InTernary()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.GetConditionalProjectable());
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task BlockBodyCallingProjectableMethod_InLogicalExpression()
+ {
+ using var dbContext = new SampleDbContext();
+ var query = dbContext.Set()
+ .Select(x => x.IsComplexCondition());
+ return Verifier.Verify(query.ToQueryString());
+ }
+ }
+
+ public static class ProjectableCallExtensions
+ {
+ // Base projectable methods (helper methods)
+ [Projectable]
+ public static int GetConstant(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ return 42;
+ }
+
+ [Projectable]
+ public static int GetDoubled(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ return entity.Value * 2;
+ }
+
+ [Projectable]
+ public static string GetCategory(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ if (entity.Value > 100)
+ return "High";
+ else
+ return "Low";
+ }
+
+ [Projectable]
+ public static string GetLevel(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ if (entity.Value > 100) return "Level3";
+ if (entity.Value > 50) return "Level2";
+ return "Level1";
+ }
+
+ [Projectable]
+ public static bool IsHighValue(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ return entity.Value > 100;
+ }
+
+ // Block-bodied methods calling projectable methods
+
+ [Projectable]
+ public static int GetAdjustedWithConstant(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ return entity.Value + entity.GetConstant();
+ }
+
+ [Projectable]
+ public static int GetDoubledValue(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ var doubled = entity.GetDoubled();
+ return doubled;
+ }
+
+ [Projectable]
+ public static string GetCategoryBasedOnAdjusted(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ if (entity.GetDoubled() > 200)
+ {
+ return "Very High";
+ }
+ else
+ {
+ return "Normal";
+ }
+ }
+
+ [Projectable]
+ public static int CombineProjectableMethods(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ return entity.GetDoubled() + entity.GetConstant();
+ }
+
+ [Projectable]
+ public static string GetLabelBasedOnCategory(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ switch (entity.GetCategory())
+ {
+ case "High":
+ return "Premium";
+ case "Low":
+ return "Standard";
+ default:
+ return "Unknown";
+ }
+ }
+
+ [Projectable]
+ public static string GetDescriptionByLevel(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ return entity.GetLevel() switch
+ {
+ "Level3" => "Expert",
+ "Level2" => "Intermediate",
+ "Level1" => "Beginner",
+ _ => "Unknown"
+ };
+ }
+
+ [Projectable]
+ public static int CalculateUsingProjectable(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ var doubled = entity.GetDoubled();
+ var withConstant = doubled + entity.GetConstant();
+ return withConstant * 2;
+ }
+
+ [Projectable]
+ public static int GetNestedProjectableCall(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ return entity.GetAdjustedWithConstant() + 10;
+ }
+
+ [Projectable]
+ public static string GetStatusWithProjectableCheck(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ if (entity.IsHighValue())
+ return "Premium";
+
+ if (entity.GetCategory() == "High")
+ return "Standard High";
+
+ return "Normal";
+ }
+
+ [Projectable]
+ public static string GetConditionalProjectable(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ return entity.IsActive ? entity.GetCategory() : "Inactive";
+ }
+
+ // [Projectable]
+ // public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity entity)
+ // {
+ // var doubled = entity.GetDoubled();
+ //
+ // if (doubled > 200)
+ // {
+ // return entity.GetCategory() + " Priority";
+ // }
+ //
+ // return entity.GetLevel();
+ // }
+
+ [Projectable]
+ public static bool IsComplexCondition(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ return entity.IsActive && entity.IsHighValue() || entity.GetDoubled() > 150;
+ }
+ }
+}
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet10_0.verified.txt
new file mode 100644
index 0000000..478d0ba
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[Value] * 2 > 200 THEN N'Very High'
+ ELSE N'Normal'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet9_0.verified.txt
new file mode 100644
index 0000000..478d0ba
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[Value] * 2 > 200 THEN N'Very High'
+ ELSE N'Normal'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.verified.txt
new file mode 100644
index 0000000..478d0ba
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN [e].[Value] * 2 > 200 THEN N'Very High'
+ ELSE N'Normal'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet10_0.verified.txt
new file mode 100644
index 0000000..bd650a0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet10_0.verified.txt
@@ -0,0 +1,9 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'Premium'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END = N'High' THEN N'Standard High'
+ ELSE N'Normal'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet9_0.verified.txt
new file mode 100644
index 0000000..bd650a0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet9_0.verified.txt
@@ -0,0 +1,9 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'Premium'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END = N'High' THEN N'Standard High'
+ ELSE N'Normal'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.verified.txt
new file mode 100644
index 0000000..bd650a0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.verified.txt
@@ -0,0 +1,9 @@
+SELECT CASE
+ WHEN [e].[Value] > 100 THEN N'Premium'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END = N'High' THEN N'Standard High'
+ ELSE N'Normal'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet10_0.verified.txt
new file mode 100644
index 0000000..de3373a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet10_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN ([e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100) OR [e].[Value] * 2 > 150 THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet9_0.verified.txt
new file mode 100644
index 0000000..de3373a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet9_0.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN ([e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100) OR [e].[Value] * 2 > 150 THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.verified.txt
new file mode 100644
index 0000000..de3373a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.verified.txt
@@ -0,0 +1,5 @@
+SELECT CASE
+ WHEN ([e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100) OR [e].[Value] * 2 > 150 THEN CAST(1 AS bit)
+ ELSE CAST(0 AS bit)
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet10_0.verified.txt
new file mode 100644
index 0000000..dea1914
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet9_0.verified.txt
new file mode 100644
index 0000000..dea1914
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.verified.txt
new file mode 100644
index 0000000..dea1914
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet10_0.verified.txt
new file mode 100644
index 0000000..927c6ff
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet10_0.verified.txt
@@ -0,0 +1,12 @@
+SELECT CASE
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END = N'High' THEN N'Premium'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END = N'Low' THEN N'Standard'
+ ELSE N'Unknown'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet9_0.verified.txt
new file mode 100644
index 0000000..927c6ff
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet9_0.verified.txt
@@ -0,0 +1,12 @@
+SELECT CASE
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END = N'High' THEN N'Premium'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END = N'Low' THEN N'Standard'
+ ELSE N'Unknown'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.verified.txt
new file mode 100644
index 0000000..927c6ff
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.verified.txt
@@ -0,0 +1,12 @@
+SELECT CASE
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END = N'High' THEN N'Premium'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END = N'Low' THEN N'Standard'
+ ELSE N'Unknown'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet10_0.verified.txt
new file mode 100644
index 0000000..409a445
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet10_0.verified.txt
@@ -0,0 +1,19 @@
+SELECT CASE
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'Level3'
+ WHEN [e].[Value] > 50 THEN N'Level2'
+ ELSE N'Level1'
+ END = N'Level3' THEN N'Expert'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'Level3'
+ WHEN [e].[Value] > 50 THEN N'Level2'
+ ELSE N'Level1'
+ END = N'Level2' THEN N'Intermediate'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'Level3'
+ WHEN [e].[Value] > 50 THEN N'Level2'
+ ELSE N'Level1'
+ END = N'Level1' THEN N'Beginner'
+ ELSE N'Unknown'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet9_0.verified.txt
new file mode 100644
index 0000000..409a445
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet9_0.verified.txt
@@ -0,0 +1,19 @@
+SELECT CASE
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'Level3'
+ WHEN [e].[Value] > 50 THEN N'Level2'
+ ELSE N'Level1'
+ END = N'Level3' THEN N'Expert'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'Level3'
+ WHEN [e].[Value] > 50 THEN N'Level2'
+ ELSE N'Level1'
+ END = N'Level2' THEN N'Intermediate'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'Level3'
+ WHEN [e].[Value] > 50 THEN N'Level2'
+ ELSE N'Level1'
+ END = N'Level1' THEN N'Beginner'
+ ELSE N'Unknown'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.verified.txt
new file mode 100644
index 0000000..409a445
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.verified.txt
@@ -0,0 +1,19 @@
+SELECT CASE
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'Level3'
+ WHEN [e].[Value] > 50 THEN N'Level2'
+ ELSE N'Level1'
+ END = N'Level3' THEN N'Expert'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'Level3'
+ WHEN [e].[Value] > 50 THEN N'Level2'
+ ELSE N'Level1'
+ END = N'Level2' THEN N'Intermediate'
+ WHEN CASE
+ WHEN [e].[Value] > 100 THEN N'Level3'
+ WHEN [e].[Value] > 50 THEN N'Level2'
+ ELSE N'Level1'
+ END = N'Level1' THEN N'Beginner'
+ ELSE N'Unknown'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet10_0.verified.txt
new file mode 100644
index 0000000..ad971d0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet10_0.verified.txt
@@ -0,0 +1,8 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet9_0.verified.txt
new file mode 100644
index 0000000..ad971d0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet9_0.verified.txt
@@ -0,0 +1,8 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.verified.txt
new file mode 100644
index 0000000..ad971d0
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.verified.txt
@@ -0,0 +1,8 @@
+SELECT CASE
+ WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE
+ WHEN [e].[Value] > 100 THEN N'High'
+ ELSE N'Low'
+ END
+ ELSE N'Inactive'
+END
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet10_0.verified.txt
new file mode 100644
index 0000000..69eb4b8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + 42
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet9_0.verified.txt
new file mode 100644
index 0000000..69eb4b8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + 42
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.verified.txt
new file mode 100644
index 0000000..69eb4b8
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + 42
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet10_0.verified.txt
new file mode 100644
index 0000000..72fc7ea
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] + 42 + 10
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet9_0.verified.txt
new file mode 100644
index 0000000..72fc7ea
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] + 42 + 10
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.verified.txt
new file mode 100644
index 0000000..72fc7ea
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] + 42 + 10
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet10_0.verified.txt
new file mode 100644
index 0000000..0bb6121
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] + 42
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet9_0.verified.txt
new file mode 100644
index 0000000..0bb6121
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] + 42
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.verified.txt
new file mode 100644
index 0000000..0bb6121
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] + 42
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt
new file mode 100644
index 0000000..0294ea7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + 84
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt
new file mode 100644
index 0000000..0294ea7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + 84
+FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt
new file mode 100644
index 0000000..0294ea7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt
@@ -0,0 +1,2 @@
+SELECT [e].[Value] * 2 + 84
+FROM [Entity] AS [e]
\ No newline at end of file
From ff4feb1670e7fbff8b4a9af25b7ca9eea527dfb6 Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Sun, 15 Feb 2026 22:27:12 +0100
Subject: [PATCH 12/26] Fix new case
---
.../BlockStatementConverter.cs | 56 ++++++++++++-------
.../BlockBodyProjectableCallTest.cs | 24 ++++----
2 files changed, 49 insertions(+), 31 deletions(-)
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index 843930f..2093d98 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -61,14 +61,39 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
var nonReturnStatements = statements.Take(statements.Count - 1).ToList();
var lastStatement = statements.Last();
+ // First, process any local variable declarations at the beginning
+ var localDeclStatements = new List();
+ var remainingStatements = new List();
+
+ foreach (var stmt in nonReturnStatements)
+ {
+ if (stmt is LocalDeclarationStatementSyntax localDecl)
+ {
+ localDeclStatements.Add(localDecl);
+ }
+ else
+ {
+ remainingStatements.Add(stmt);
+ }
+ }
+
+ // Process local variable declarations first
+ foreach (var localDecl in localDeclStatements)
+ {
+ if (!TryProcessLocalDeclaration(localDecl, memberName))
+ {
+ return null;
+ }
+ }
+
// Check if we have a pattern like multiple if statements without else followed by a final return:
- // if (a) return 1; if (b) return 2; return 3;
+ // var x = ...; if (a) return 1; if (b) return 2; return 3;
// This can be converted to nested ternaries: a ? 1 : (b ? 2 : 3)
if (lastStatement is ReturnStatementSyntax finalReturn &&
- nonReturnStatements.All(s => s is IfStatementSyntax { Else: null }))
+ remainingStatements.All(s => s is IfStatementSyntax { Else: null }))
{
- // All non-return statements are if statements without else
- var ifStatements = nonReturnStatements.Cast().ToList();
+ // All remaining non-return statements are if statements without else
+ var ifStatements = remainingStatements.Cast().ToList();
// Start with the final return as the base expression
var elseBody = TryConvertReturnStatement(finalReturn, memberName);
@@ -87,29 +112,22 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
return null;
}
+ // Rewrite the condition and replace any local variables
var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifStmt.Condition);
+ condition = ReplaceLocalVariables(condition);
+
elseBody = SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody);
}
return elseBody;
}
- // If we reach here, the pattern was not detected
- // Process local variable declarations before the final return
- foreach (var stmt in nonReturnStatements)
+ // If there are any remaining non-if statements, they're not supported
+ if (remainingStatements.Count > 0)
{
- if (stmt is LocalDeclarationStatementSyntax localDecl)
- {
- if (!TryProcessLocalDeclaration(localDecl, memberName))
- {
- return null;
- }
- }
- else
- {
- ReportUnsupportedStatement(stmt, memberName, "Only local variable declarations are supported before the return statement");
- return null;
- }
+ ReportUnsupportedStatement(remainingStatements[0], memberName,
+ "Only local variable declarations and if statements without else (with return) are supported before the final return statement");
+ return null;
}
// Convert the final statement (should be a return)
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs
index 4d3fa96..c09237b 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs
@@ -251,18 +251,18 @@ public static string GetConditionalProjectable(this BlockBodyProjectableCallTest
return entity.IsActive ? entity.GetCategory() : "Inactive";
}
- // [Projectable]
- // public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity entity)
- // {
- // var doubled = entity.GetDoubled();
- //
- // if (doubled > 200)
- // {
- // return entity.GetCategory() + " Priority";
- // }
- //
- // return entity.GetLevel();
- // }
+ [Projectable]
+ public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity entity)
+ {
+ var doubled = entity.GetDoubled();
+
+ if (doubled > 200)
+ {
+ return entity.GetCategory() + " Priority";
+ }
+
+ return entity.GetLevel();
+ }
[Projectable]
public static bool IsComplexCondition(this BlockBodyProjectableCallTests.Entity entity)
From 7ba8a84d5ab03529d0f550711fccd6a2bb3a8f60 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 07:35:16 +0000
Subject: [PATCH 13/26] Fix local variable replacement in conditions and switch
expressions
- Apply ReplaceLocalVariables to if statement conditions
- Apply ReplaceLocalVariables to switch expressions
- Apply ReplaceLocalVariables to case label values
- Remove double BOM character from ExpressionSyntaxRewriter.cs
- Fix documentation to match actual behavior (no multiple usage warning)
- Add tests for locals in if conditions and switch expressions
- All 201 tests passing (98 net8.0 + 103 net10.0)
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
docs/BlockBodiedMethods.md | 2 +-
.../BlockStatementConverter.cs | 12 +++-
.../ExpressionSyntaxRewriter.cs | 2 +-
...diedMethod_LocalInIfCondition.verified.txt | 17 +++++
...ethod_LocalInSwitchExpression.verified.txt | 17 +++++
.../ProjectionExpressionGeneratorTests.cs | 71 +++++++++++++++++++
6 files changed, 118 insertions(+), 3 deletions(-)
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt
diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md
index 494b5fe..fe19c69 100644
--- a/docs/BlockBodiedMethods.md
+++ b/docs/BlockBodiedMethods.md
@@ -72,7 +72,7 @@ public int CalculateComplex()
**⚠️ Important Notes:**
- Local variables are inlined at each usage point, which duplicates the initializer expression
-- If a local variable is used multiple times, the generator will emit a warning (EFP0003) as this could change semantics if the initializer has side effects
+- If a local variable is used multiple times, its initializer expression is duplicated at each usage, which can change semantics if the initializer has side effects
- Local variables can only be declared at the method body level, not inside nested blocks (if/switch/etc.)
- Variables are fully expanded transitively (variables that reference other variables are fully inlined)
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index 2093d98..5192612 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -224,6 +224,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
// Convert if-else to conditional (ternary) expression
// First, rewrite the condition using the expression rewriter
var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifStmt.Condition);
+
+ // Then replace any local variable references with their already-rewritten initializers
+ condition = ReplaceLocalVariables(condition);
var whenTrue = TryConvertStatement(ifStmt.Statement, memberName);
if (whenTrue == null)
@@ -264,6 +267,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
// Process sections in reverse order to build from the default case up
var switchExpression = (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression);
+ // Replace any local variable references in the switch expression
+ switchExpression = ReplaceLocalVariables(switchExpression);
+
ExpressionSyntax? currentExpression;
// Find default case first
@@ -317,10 +323,14 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
{
if (label is CaseSwitchLabelSyntax caseLabel)
{
+ // Rewrite and replace locals in case label value
+ var caseLabelValue = (ExpressionSyntax)_expressionRewriter.Visit(caseLabel.Value);
+ caseLabelValue = ReplaceLocalVariables(caseLabelValue);
+
var labelCondition = SyntaxFactory.BinaryExpression(
SyntaxKind.EqualsExpression,
switchExpression,
- (ExpressionSyntax)_expressionRewriter.Visit(caseLabel.Value)
+ caseLabelValue
);
condition = condition == null
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
index 2b152dd..ef46396 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
@@ -1,4 +1,4 @@
-using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt
new file mode 100644
index 0000000..e940c26
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar * 2 > 10 ? 1 : 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt
new file mode 100644
index 0000000..0a7e7da
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar * 2 == 2 ? "Two" : @this.Bar * 2 == 4 ? "Four" : "Other";
+ }
+ }
+}
\ 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 c11232b..3254530 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -2160,6 +2160,77 @@ public int Foo()
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public Task BlockBodiedMethod_LocalInIfCondition()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ var threshold = Bar * 2;
+ if (threshold > 10)
+ {
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_LocalInSwitchExpression()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public string Foo()
+ {
+ var value = Bar * 2;
+ switch (value)
+ {
+ case 2:
+ return ""Two"";
+ case 4:
+ return ""Four"";
+ default:
+ return ""Other"";
+ }
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
[Fact]
public Task BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic()
{
From b8af892b9b90d6343513a3ebdee11493ea391bbf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 07:49:19 +0000
Subject: [PATCH 14/26] Improve error reporting for side effects in
block-bodied methods
- Add specific diagnostics (EFP0004, EFP0005) for side effects
- Detect property assignments with clear error messages
- Detect compound assignments (+=, -=, etc.)
- Detect increment/decrement operators (++, --)
- Warn about non-projectable method calls
- Error messages now point to the exact problematic line
- All 209 tests passing (102 net8.0 + 107 net10.0)
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
.../AnalyzerReleases.Unshipped.md | 2 +
.../BlockStatementConverter.cs | 107 +++++++++++++++-
.../Diagnostics.cs | 16 +++
.../ExpressionSyntaxRewriter.cs | 2 +
...mpoundAssignment_ReportsError.verified.txt | 3 +
...ncrementOperator_ReportsError.verified.txt | 3 +
...ableMethodCall_ReportsWarning.verified.txt | 3 +
...opertyAssignment_ReportsError.verified.txt | 3 +
.../ProjectionExpressionGeneratorTests.cs | 117 ++++++++++++++++++
9 files changed, 251 insertions(+), 5 deletions(-)
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt
diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
index 4911eaa..c1b0078 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
@@ -4,3 +4,5 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
EFP0002 | Design | Error |
EFP0003 | Design | Warning |
+EFP0004 | Design | Error | Statement with side effects in block-bodied method
+EFP0005 | Design | Warning | Potential side effect in block-bodied method
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index 5192612..55abd92 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -122,9 +122,21 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
return elseBody;
}
- // If there are any remaining non-if statements, they're not supported
+ // If there are any remaining non-if statements, try to convert them individually
+ // This will provide better error messages for unsupported statements
if (remainingStatements.Count > 0)
{
+ // Try converting each remaining statement - this will provide specific error messages
+ foreach (var stmt in remainingStatements)
+ {
+ var converted = TryConvertStatement(stmt, memberName);
+ if (converted == null)
+ {
+ return null;
+ }
+ }
+
+ // If we got here but had non-if statements, they weren't properly handled
ReportUnsupportedStatement(remainingStatements[0], memberName,
"Only local variable declarations and if statements without else (with return) are supported before the final return statement");
return null;
@@ -186,10 +198,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
return TryConvertStatements(blockStmt.Statements.ToList(), memberName);
- case ExpressionStatementSyntax:
- // Expression statements are generally not useful in expression trees
- ReportUnsupportedStatement(statement, memberName, "Expression statements are not supported");
- return null;
+ case ExpressionStatementSyntax exprStmt:
+ // Expression statements may contain side effects - analyze them
+ return AnalyzeExpressionStatement(exprStmt, memberName);
case LocalDeclarationStatementSyntax:
// Local declarations should be handled before the return statement
@@ -405,6 +416,92 @@ private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression)
return (ExpressionSyntax)rewriter.Visit(expression);
}
+ private ExpressionSyntax? AnalyzeExpressionStatement(ExpressionStatementSyntax exprStmt, string memberName)
+ {
+ var expression = exprStmt.Expression;
+
+ // Check for specific side effects
+ switch (expression)
+ {
+ case AssignmentExpressionSyntax assignment:
+ ReportSideEffect(assignment, GetAssignmentErrorMessage(assignment));
+ return null;
+
+ case PostfixUnaryExpressionSyntax postfix when
+ postfix.IsKind(SyntaxKind.PostIncrementExpression) ||
+ postfix.IsKind(SyntaxKind.PostDecrementExpression):
+ ReportSideEffect(postfix, $"Increment/decrement operator '{postfix.OperatorToken.Text}' has side effects and cannot be used in projectable methods");
+ return null;
+
+ case PrefixUnaryExpressionSyntax prefix when
+ prefix.IsKind(SyntaxKind.PreIncrementExpression) ||
+ prefix.IsKind(SyntaxKind.PreDecrementExpression):
+ ReportSideEffect(prefix, $"Increment/decrement operator '{prefix.OperatorToken.Text}' has side effects and cannot be used in projectable methods");
+ return null;
+
+ case InvocationExpressionSyntax invocation:
+ // Check if this is a potentially impure method call
+ var symbolInfo = _expressionRewriter.GetSemanticModel().GetSymbolInfo(invocation);
+ if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
+ {
+ // Check if method has [Projectable] attribute - those are safe
+ var hasProjectableAttr = methodSymbol.GetAttributes()
+ .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute");
+
+ if (!hasProjectableAttr)
+ {
+ ReportPotentialSideEffect(invocation,
+ $"Method call '{methodSymbol.Name}' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods");
+ return null;
+ }
+ }
+ break;
+ }
+
+ // If we got here, it's an expression statement we don't support
+ ReportUnsupportedStatement(exprStmt, memberName, "Expression statements are not supported in projectable methods");
+ return null;
+ }
+
+ private string GetAssignmentErrorMessage(AssignmentExpressionSyntax assignment)
+ {
+ var operatorText = assignment.OperatorToken.Text;
+
+ if (assignment.IsKind(SyntaxKind.SimpleAssignmentExpression))
+ {
+ if (assignment.Left is MemberAccessExpressionSyntax memberAccess)
+ {
+ return $"Property assignment '{memberAccess.Name}' has side effects and cannot be used in projectable methods";
+ }
+ return $"Assignment operation has side effects and cannot be used in projectable methods";
+ }
+ else
+ {
+ // Compound assignment like +=, -=, etc.
+ return $"Compound assignment operator '{operatorText}' has side effects and cannot be used in projectable methods";
+ }
+ }
+
+ private void ReportSideEffect(SyntaxNode node, string message)
+ {
+ var diagnostic = Diagnostic.Create(
+ Diagnostics.SideEffectInBlockBody,
+ node.GetLocation(),
+ message
+ );
+ _context.ReportDiagnostic(diagnostic);
+ }
+
+ private void ReportPotentialSideEffect(SyntaxNode node, string message)
+ {
+ var diagnostic = Diagnostic.Create(
+ Diagnostics.PotentialSideEffectInBlockBody,
+ node.GetLocation(),
+ message
+ );
+ _context.ReportDiagnostic(diagnostic);
+ }
+
private void ReportUnsupportedStatement(StatementSyntax statement, string memberName, string reason)
{
var diagnostic = Diagnostic.Create(
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
index d98a1b8..6bcfaf1 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
@@ -33,5 +33,21 @@ public static class Diagnostics
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
+ public static readonly DiagnosticDescriptor SideEffectInBlockBody = new DiagnosticDescriptor(
+ id: "EFP0004",
+ title: "Statement with side effects in block-bodied method",
+ messageFormat: "{0}",
+ category: "Design",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static readonly DiagnosticDescriptor PotentialSideEffectInBlockBody = new DiagnosticDescriptor(
+ id: "EFP0005",
+ title: "Potential side effect in block-bodied method",
+ messageFormat: "{0}",
+ category: "Design",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
index ef46396..f953701 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
@@ -26,6 +26,8 @@ public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullCondition
_extensionParameterName = extensionParameterName;
}
+ public SemanticModel GetSemanticModel() => _semanticModel;
+
private SyntaxNode? VisitThisBaseExpression(CSharpSyntaxNode node)
{
// Swap out the use of this and base to @this and keep leading and trailing trivias
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt
new file mode 100644
index 0000000..a6b0b53
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt
@@ -0,0 +1,3 @@
+[
+ (11,13): error EFP0004: Compound assignment operator '+=' has side effects and cannot be used in projectable methods
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt
new file mode 100644
index 0000000..d47a3ba
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt
@@ -0,0 +1,3 @@
+[
+ (12,13): error EFP0004: Increment/decrement operator '++' has side effects and cannot be used in projectable methods
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt
new file mode 100644
index 0000000..26e6a19
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt
@@ -0,0 +1,3 @@
+[
+ (11,13): warning EFP0005: Method call 'WriteLine' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt
new file mode 100644
index 0000000..e684d40
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt
@@ -0,0 +1,3 @@
+[
+ (11,13): error EFP0004: Assignment operation has side effects and cannot be used in projectable methods
+]
\ 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 3254530..ae09af6 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -2492,6 +2492,123 @@ class C {
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public Task BlockBodiedMethod_PropertyAssignment_ReportsError()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ Bar = 10;
+ return Bar;
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ // Should have a diagnostic about side effects
+ Assert.NotEmpty(result.Diagnostics);
+ Assert.Contains(result.Diagnostics, d => d.Id == "EFP0004");
+
+ return Verifier.Verify(result.Diagnostics.Select(d => d.ToString()));
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_CompoundAssignment_ReportsError()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ Bar += 10;
+ return Bar;
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ // Should have a diagnostic about side effects
+ Assert.NotEmpty(result.Diagnostics);
+ Assert.Contains(result.Diagnostics, d => d.Id == "EFP0004");
+
+ return Verifier.Verify(result.Diagnostics.Select(d => d.ToString()));
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_IncrementOperator_ReportsError()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ var x = 5;
+ x++;
+ return x;
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ // Should have a diagnostic about side effects
+ Assert.NotEmpty(result.Diagnostics);
+ Assert.Contains(result.Diagnostics, d => d.Id == "EFP0004");
+
+ return Verifier.Verify(result.Diagnostics.Select(d => d.ToString()));
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo()
+ {
+ Console.WriteLine(""test"");
+ return Bar;
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ // Should have a diagnostic about potential side effects
+ Assert.NotEmpty(result.Diagnostics);
+ Assert.Contains(result.Diagnostics, d => d.Id == "EFP0005");
+
+ return Verifier.Verify(result.Diagnostics.Select(d => d.ToString()));
+ }
+
[Fact]
public Task MethodOverloads_WithDifferentParameterTypes()
{
From b697cd21f1f3f6b622634ddf31ca87f49ebac040 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 07:50:15 +0000
Subject: [PATCH 15/26] Add documentation for side effect detection
- Document all detected side effects with examples
- Show before/after comparison of error messages
- Explain diagnostic codes EFP0004 and EFP0005
- Provide clear guidance for developers
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
docs/SideEffectDetection.md | 110 ++++++++++++++++++++++++++++++++++++
1 file changed, 110 insertions(+)
create mode 100644 docs/SideEffectDetection.md
diff --git a/docs/SideEffectDetection.md b/docs/SideEffectDetection.md
new file mode 100644
index 0000000..891294e
--- /dev/null
+++ b/docs/SideEffectDetection.md
@@ -0,0 +1,110 @@
+# Side Effect Detection in Block-Bodied Methods
+
+This document describes the improved error reporting for side effects in block-bodied projectable methods.
+
+## Overview
+
+When using block-bodied methods with the `[Projectable]` attribute, the source generator now provides specific error messages that point to the exact line where side effects occur, making it much easier to identify and fix issues.
+
+## Detected Side Effects
+
+### 1. Property Assignments (EFP0004 - Error)
+
+**Code:**
+```csharp
+[Projectable]
+public int Foo()
+{
+ Bar = 10; // ❌ Error on this line
+ return Bar;
+}
+```
+
+**Error Message:**
+```
+(11,13): error EFP0004: Assignment operation has side effects and cannot be used in projectable methods
+```
+
+### 2. Compound Assignments (EFP0004 - Error)
+
+**Code:**
+```csharp
+[Projectable]
+public int Foo()
+{
+ Bar += 10; // ❌ Error on this line
+ return Bar;
+}
+```
+
+**Error Message:**
+```
+(11,13): error EFP0004: Compound assignment operator '+=' has side effects and cannot be used in projectable methods
+```
+
+### 3. Increment/Decrement Operators (EFP0004 - Error)
+
+**Code:**
+```csharp
+[Projectable]
+public int Foo()
+{
+ var x = 5;
+ x++; // ❌ Error on this line
+ return x;
+}
+```
+
+**Error Message:**
+```
+(12,13): error EFP0004: Increment/decrement operator '++' has side effects and cannot be used in projectable methods
+```
+
+### 4. Non-Projectable Method Calls (EFP0005 - Warning)
+
+**Code:**
+```csharp
+[Projectable]
+public int Foo()
+{
+ Console.WriteLine("test"); // ⚠️ Warning on this line
+ return Bar;
+}
+```
+
+**Warning Message:**
+```
+(11,13): warning EFP0005: Method call 'WriteLine' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods
+```
+
+## Before vs After
+
+### Before
+Generic error message at the beginning of the method:
+```
+warning EFP0003: Method 'Foo' contains an unsupported statement: Expression statements are not supported
+```
+
+### After
+Specific error message pointing to the exact problematic line:
+```
+error EFP0004: Property assignment 'Bar' has side effects and cannot be used in projectable methods
+```
+
+## Benefits
+
+1. **Precise Location**: Error messages now point to the exact line containing the side effect
+2. **Specific Messages**: Clear explanation of what kind of side effect was detected
+3. **Better Developer Experience**: Easier to identify and fix issues
+4. **Severity Levels**: Errors for definite side effects, warnings for potential ones
+5. **Actionable Guidance**: Messages explain why the code is problematic
+
+## Diagnostic Codes
+
+- **EFP0004**: Statement with side effects in block-bodied method (Error)
+- **EFP0005**: Potential side effect in block-bodied method (Warning)
+
+These are in addition to the existing:
+- **EFP0001**: Method or property should expose an expression body definition (Error)
+- **EFP0002**: Method or property is not configured to support null-conditional expressions (Error)
+- **EFP0003**: Unsupported statement in block-bodied method (Warning)
From f2a805e6311ef08b6316b050de3585baea9f30f8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 08:03:42 +0000
Subject: [PATCH 16/26] Initial exploration - understand pattern matching crash
issue
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
...iedMethod_WithPatternMatching.verified.txt | 17 +++++++++
.../ProjectionExpressionGeneratorTests.cs | 35 +++++++++++++++++++
2 files changed, 52 insertions(+)
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt
new file mode 100644
index 0000000..75eb6dd
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Extensions_GetComplexCategory_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity entity) => entity is { IsActive: true, Value: > 100 } ? "Active High" : "Other";
+ }
+ }
+}
\ 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 ae09af6..52ff32d 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -2609,6 +2609,41 @@ public int Foo()
return Verifier.Verify(result.Diagnostics.Select(d => d.ToString()));
}
+ [Fact]
+ public Task BlockBodiedMethod_WithPatternMatching()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class Entity {
+ public bool IsActive { get; set; }
+ public int Value { get; set; }
+ }
+
+ static class Extensions {
+ [Projectable]
+ public static string GetComplexCategory(this Entity entity)
+ {
+ if (entity is { IsActive: true, Value: > 100 })
+ {
+ return ""Active High"";
+ }
+ return ""Other"";
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ // The generator should not crash and should handle pattern matching
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
[Fact]
public Task MethodOverloads_WithDifferentParameterTypes()
{
From 31f42673e3a612cb7c3e2c9a023fdd4686be7ab5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 08:06:29 +0000
Subject: [PATCH 17/26] Fix pattern matching support in block-bodied methods
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add VisitIsPatternExpression to ExpressionSyntaxRewriter
- Convert pattern matching to equivalent expressions:
- RecursivePattern: entity is { Prop: value } → null check + property checks
- RelationalPattern: value is > 100 → value > 100
- ConstantPattern: entity is null → entity == null
- UnaryPattern: entity is not null → !(entity == null)
- Add comprehensive tests for all pattern types
- All 217 tests passing (106 net8.0 + 111 net10.0)
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
.../ExpressionSyntaxRewriter.cs | 128 ++++++++++++++++++
...iedMethod_WithConstantPattern.verified.txt | 17 +++
...ckBodiedMethod_WithNotPattern.verified.txt | 17 +++
...iedMethod_WithPatternMatching.verified.txt | 2 +-
...dMethod_WithRelationalPattern.verified.txt | 17 +++
.../ProjectionExpressionGeneratorTests.cs | 99 ++++++++++++++
6 files changed, 279 insertions(+), 1 deletion(-)
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
index f953701..a73ca06 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
@@ -1,3 +1,6 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -676,5 +679,130 @@ private ExpressionSyntax ReplaceVariableWithCast(ExpressionSyntax expression, De
return expression;
}
+
+ public override SyntaxNode? VisitIsPatternExpression(IsPatternExpressionSyntax node)
+ {
+ // Pattern matching is not supported in expression trees (CS8122)
+ // We need to convert patterns into equivalent expressions
+
+ var expression = (ExpressionSyntax)Visit(node.Expression);
+ var convertedPattern = ConvertPatternToExpression(node.Pattern, expression);
+
+ return convertedPattern;
+ }
+
+ private ExpressionSyntax ConvertPatternToExpression(PatternSyntax pattern, ExpressionSyntax expression)
+ {
+ switch (pattern)
+ {
+ case RecursivePatternSyntax recursivePattern:
+ return ConvertRecursivePattern(recursivePattern, expression);
+
+ case ConstantPatternSyntax constantPattern:
+ // e is null or e is 5
+ return SyntaxFactory.BinaryExpression(
+ SyntaxKind.EqualsExpression,
+ expression,
+ (ExpressionSyntax)Visit(constantPattern.Expression)
+ );
+
+ case DeclarationPatternSyntax declarationPattern:
+ // e is string s -> e is string (type check)
+ return SyntaxFactory.BinaryExpression(
+ SyntaxKind.IsExpression,
+ expression,
+ declarationPattern.Type
+ );
+
+ case RelationalPatternSyntax relationalPattern:
+ // e is > 100
+ var binaryKind = relationalPattern.OperatorToken.Kind() switch
+ {
+ SyntaxKind.LessThanToken => SyntaxKind.LessThanExpression,
+ SyntaxKind.LessThanEqualsToken => SyntaxKind.LessThanOrEqualExpression,
+ SyntaxKind.GreaterThanToken => SyntaxKind.GreaterThanExpression,
+ SyntaxKind.GreaterThanEqualsToken => SyntaxKind.GreaterThanOrEqualExpression,
+ _ => throw new NotSupportedException($"Relational operator {relationalPattern.OperatorToken} not supported")
+ };
+
+ return SyntaxFactory.BinaryExpression(
+ binaryKind,
+ expression,
+ (ExpressionSyntax)Visit(relationalPattern.Expression)
+ );
+
+ case BinaryPatternSyntax binaryPattern:
+ // e is > 10 and < 100
+ var left = ConvertPatternToExpression(binaryPattern.Left, expression);
+ var right = ConvertPatternToExpression(binaryPattern.Right, expression);
+
+ var logicalKind = binaryPattern.OperatorToken.Kind() switch
+ {
+ SyntaxKind.AndKeyword => SyntaxKind.LogicalAndExpression,
+ SyntaxKind.OrKeyword => SyntaxKind.LogicalOrExpression,
+ _ => throw new NotSupportedException($"Binary pattern operator {binaryPattern.OperatorToken} not supported")
+ };
+
+ return SyntaxFactory.BinaryExpression(logicalKind, left, right);
+
+ case UnaryPatternSyntax unaryPattern when unaryPattern.OperatorToken.IsKind(SyntaxKind.NotKeyword):
+ // e is not null
+ var innerPattern = ConvertPatternToExpression(unaryPattern.Pattern, expression);
+ return SyntaxFactory.PrefixUnaryExpression(
+ SyntaxKind.LogicalNotExpression,
+ SyntaxFactory.ParenthesizedExpression(innerPattern)
+ );
+
+ default:
+ throw new NotSupportedException($"Pattern type {pattern.GetType().Name} is not yet supported in projectable methods");
+ }
+ }
+
+ private ExpressionSyntax ConvertRecursivePattern(RecursivePatternSyntax recursivePattern, ExpressionSyntax expression)
+ {
+ // entity is { IsActive: true, Value: > 100 }
+ // Convert to: entity != null && entity.IsActive == true && entity.Value > 100
+
+ var conditions = new List();
+
+ // Add null check first (unless pattern explicitly includes null)
+ var nullCheck = SyntaxFactory.BinaryExpression(
+ SyntaxKind.NotEqualsExpression,
+ expression,
+ SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
+ );
+ conditions.Add(nullCheck);
+
+ // Handle property patterns
+ if (recursivePattern.PropertyPatternClause != null)
+ {
+ foreach (var subpattern in recursivePattern.PropertyPatternClause.Subpatterns)
+ {
+ var memberAccess = subpattern.NameColon != null
+ ? SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ expression,
+ SyntaxFactory.IdentifierName(subpattern.NameColon.Name.Identifier)
+ )
+ : expression;
+
+ var condition = ConvertPatternToExpression(subpattern.Pattern, memberAccess);
+ conditions.Add(condition);
+ }
+ }
+
+ // Combine all conditions with &&
+ var result = conditions[0];
+ for (int i = 1; i < conditions.Count; i++)
+ {
+ result = SyntaxFactory.BinaryExpression(
+ SyntaxKind.LogicalAndExpression,
+ result,
+ conditions[i]
+ );
+ }
+
+ return result;
+ }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt
new file mode 100644
index 0000000..6356921
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Extensions_IsNull_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity entity) => entity == null ? true : false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt
new file mode 100644
index 0000000..797a367
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Extensions_IsNotNull_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity entity) => !(entity == null) ? true : false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt
index 75eb6dd..a11076d 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt
@@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated
{
static global::System.Linq.Expressions.Expression> Expression()
{
- return (global::Foo.Entity entity) => entity is { IsActive: true, Value: > 100 } ? "Active High" : "Other";
+ return (global::Foo.Entity entity) => entity != null && entity.IsActive == true && entity.Value > 100 ? "Active High" : "Other";
}
}
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt
new file mode 100644
index 0000000..55dcb0a
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Extensions_GetCategory_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity entity) => entity.Value > 100 ? "High" : "Low";
+ }
+ }
+}
\ 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 52ff32d..bab5608 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -2644,6 +2644,105 @@ public static string GetComplexCategory(this Entity entity)
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public Task BlockBodiedMethod_WithRelationalPattern()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class Entity {
+ public int Value { get; set; }
+ }
+
+ static class Extensions {
+ [Projectable]
+ public static string GetCategory(this Entity entity)
+ {
+ if (entity.Value is > 100)
+ {
+ return ""High"";
+ }
+ return ""Low"";
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithConstantPattern()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class Entity {
+ public string Status { get; set; }
+ }
+
+ static class Extensions {
+ [Projectable]
+ public static bool IsNull(this Entity entity)
+ {
+ if (entity is null)
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithNotPattern()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class Entity {
+ public string Name { get; set; }
+ }
+
+ static class Extensions {
+ [Projectable]
+ public static bool IsNotNull(this Entity entity)
+ {
+ if (entity is not null)
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
+", expectedToCompile: true);
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
[Fact]
public Task MethodOverloads_WithDifferentParameterTypes()
{
From adc95f5c24892aad5c2930d3f2e97f81b20438da Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 08:07:04 +0000
Subject: [PATCH 18/26] Add documentation for pattern matching support
- Document all supported pattern types with examples
- Explain conversion logic and benefits
- Provide complex examples showing nested patterns
- Document limitations and error handling
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
docs/PatternMatchingSupport.md | 195 +++++++++++++++++++++++++++++++++
1 file changed, 195 insertions(+)
create mode 100644 docs/PatternMatchingSupport.md
diff --git a/docs/PatternMatchingSupport.md b/docs/PatternMatchingSupport.md
new file mode 100644
index 0000000..e81be3c
--- /dev/null
+++ b/docs/PatternMatchingSupport.md
@@ -0,0 +1,195 @@
+# Pattern Matching Support in Block-Bodied Methods
+
+This document describes how pattern matching is handled in block-bodied projectable methods.
+
+## Overview
+
+C# pattern matching (the `is` operator with patterns) is not supported in expression trees and will cause a CS8122 compilation error. The source generator automatically converts pattern matching syntax into equivalent boolean expressions that work in expression trees.
+
+## Supported Pattern Types
+
+### 1. Recursive Patterns (Property Patterns)
+
+**Syntax:**
+```csharp
+[Projectable]
+public static string GetCategory(this Entity entity)
+{
+ if (entity is { IsActive: true, Value: > 100 })
+ {
+ return "Active High";
+ }
+ return "Other";
+}
+```
+
+**Converted To:**
+```csharp
+entity != null && entity.IsActive == true && entity.Value > 100 ? "Active High" : "Other"
+```
+
+The pattern is converted to:
+1. Null check: `entity != null`
+2. Property checks: `entity.IsActive == true && entity.Value > 100`
+3. Combined with logical AND
+
+### 2. Relational Patterns
+
+**Syntax:**
+```csharp
+[Projectable]
+public static string GetCategory(this Entity entity)
+{
+ if (entity.Value is > 100)
+ {
+ return "High";
+ }
+ return "Low";
+}
+```
+
+**Converted To:**
+```csharp
+entity.Value > 100 ? "High" : "Low"
+```
+
+Supported relational operators:
+- `>` (greater than)
+- `>=` (greater than or equal)
+- `<` (less than)
+- `<=` (less than or equal)
+
+### 3. Constant Patterns
+
+**Syntax:**
+```csharp
+[Projectable]
+public static bool IsNull(this Entity entity)
+{
+ if (entity is null)
+ {
+ return true;
+ }
+ return false;
+}
+```
+
+**Converted To:**
+```csharp
+entity == null ? true : false
+```
+
+### 4. Unary Patterns (Not Patterns)
+
+**Syntax:**
+```csharp
+[Projectable]
+public static bool IsNotNull(this Entity entity)
+{
+ if (entity is not null)
+ {
+ return true;
+ }
+ return false;
+}
+```
+
+**Converted To:**
+```csharp
+!(entity == null) ? true : false
+```
+
+### 5. Binary Patterns (And/Or)
+
+**Syntax:**
+```csharp
+[Projectable]
+public static bool IsInRange(this Entity entity)
+{
+ if (entity.Value is > 10 and < 100)
+ {
+ return true;
+ }
+ return false;
+}
+```
+
+**Converted To:**
+```csharp
+entity.Value > 10 && entity.Value < 100 ? true : false
+```
+
+## Benefits
+
+1. **Modern C# Syntax**: Use pattern matching in block-bodied methods just like regular C# code
+2. **Automatic Conversion**: No manual rewriting needed - the generator handles it
+3. **Expression Tree Compatibility**: Generated code compiles without CS8122 errors
+4. **Semantic Equivalence**: Converted expressions maintain the same behavior as patterns
+
+## Limitations
+
+Not all pattern types are currently supported:
+- Type patterns with variable declarations may have limited support
+- List patterns are not yet supported
+- Some complex nested patterns may not be supported
+
+If you encounter an unsupported pattern, you'll receive an error message indicating which pattern type is not supported.
+
+## Examples
+
+### Complex Property Pattern
+```csharp
+[Projectable]
+public static string GetStatus(this Order order)
+{
+ if (order is { Status: "Completed", Amount: > 1000, Customer.IsVip: true })
+ {
+ return "VIP High Value Completed";
+ }
+ return "Other";
+}
+```
+
+**Generates:**
+```csharp
+order != null &&
+order.Status == "Completed" &&
+order.Amount > 1000 &&
+order.Customer.IsVip == true
+ ? "VIP High Value Completed"
+ : "Other"
+```
+
+### Range Check with Relational Patterns
+```csharp
+[Projectable]
+public static string GetRange(this Entity entity)
+{
+ if (entity.Value is >= 0 and < 50)
+ {
+ return "Low";
+ }
+ else if (entity.Value is >= 50 and < 100)
+ {
+ return "Medium";
+ }
+ return "High";
+}
+```
+
+**Generates:**
+```csharp
+entity.Value >= 0 && entity.Value < 50 ? "Low" :
+entity.Value >= 50 && entity.Value < 100 ? "Medium" :
+"High"
+```
+
+## Technical Details
+
+The conversion is implemented in `ExpressionSyntaxRewriter.VisitIsPatternExpression` which:
+1. Visits the expression being tested
+2. Converts the pattern to an equivalent expression using `ConvertPatternToExpression`
+3. Handles nested patterns recursively
+4. Combines multiple property checks with logical AND operators
+
+This ensures that all pattern matching is transformed into expression tree-compatible code before code generation.
From 5f83f40600ea160393895809f5a8f3bef90aef59 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 11:32:20 +0000
Subject: [PATCH 19/26] Revert pattern matching commits to separate feature
Pattern matching support has been moved to a separate branch/PR.
This PR now focuses solely on block-bodied method support:
- If-else statements
- Switch statements
- Local variables
- Side effect detection
Reverted commits:
- adc95f5: Add documentation for pattern matching support
- 31f4267: Fix pattern matching support in block-bodied methods
- f2a805e: Initial exploration - understand pattern matching crash issue
All 209 tests passing (102 net8.0 + 107 net10.0 generator tests, plus functional and unit tests)
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
docs/PatternMatchingSupport.md | 195 ------------------
.../ExpressionSyntaxRewriter.cs | 128 ------------
...iedMethod_WithConstantPattern.verified.txt | 17 --
...ckBodiedMethod_WithNotPattern.verified.txt | 17 --
...iedMethod_WithPatternMatching.verified.txt | 17 --
...dMethod_WithRelationalPattern.verified.txt | 17 --
.../ProjectionExpressionGeneratorTests.cs | 134 ------------
7 files changed, 525 deletions(-)
delete mode 100644 docs/PatternMatchingSupport.md
delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt
delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt
delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt
delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt
diff --git a/docs/PatternMatchingSupport.md b/docs/PatternMatchingSupport.md
deleted file mode 100644
index e81be3c..0000000
--- a/docs/PatternMatchingSupport.md
+++ /dev/null
@@ -1,195 +0,0 @@
-# Pattern Matching Support in Block-Bodied Methods
-
-This document describes how pattern matching is handled in block-bodied projectable methods.
-
-## Overview
-
-C# pattern matching (the `is` operator with patterns) is not supported in expression trees and will cause a CS8122 compilation error. The source generator automatically converts pattern matching syntax into equivalent boolean expressions that work in expression trees.
-
-## Supported Pattern Types
-
-### 1. Recursive Patterns (Property Patterns)
-
-**Syntax:**
-```csharp
-[Projectable]
-public static string GetCategory(this Entity entity)
-{
- if (entity is { IsActive: true, Value: > 100 })
- {
- return "Active High";
- }
- return "Other";
-}
-```
-
-**Converted To:**
-```csharp
-entity != null && entity.IsActive == true && entity.Value > 100 ? "Active High" : "Other"
-```
-
-The pattern is converted to:
-1. Null check: `entity != null`
-2. Property checks: `entity.IsActive == true && entity.Value > 100`
-3. Combined with logical AND
-
-### 2. Relational Patterns
-
-**Syntax:**
-```csharp
-[Projectable]
-public static string GetCategory(this Entity entity)
-{
- if (entity.Value is > 100)
- {
- return "High";
- }
- return "Low";
-}
-```
-
-**Converted To:**
-```csharp
-entity.Value > 100 ? "High" : "Low"
-```
-
-Supported relational operators:
-- `>` (greater than)
-- `>=` (greater than or equal)
-- `<` (less than)
-- `<=` (less than or equal)
-
-### 3. Constant Patterns
-
-**Syntax:**
-```csharp
-[Projectable]
-public static bool IsNull(this Entity entity)
-{
- if (entity is null)
- {
- return true;
- }
- return false;
-}
-```
-
-**Converted To:**
-```csharp
-entity == null ? true : false
-```
-
-### 4. Unary Patterns (Not Patterns)
-
-**Syntax:**
-```csharp
-[Projectable]
-public static bool IsNotNull(this Entity entity)
-{
- if (entity is not null)
- {
- return true;
- }
- return false;
-}
-```
-
-**Converted To:**
-```csharp
-!(entity == null) ? true : false
-```
-
-### 5. Binary Patterns (And/Or)
-
-**Syntax:**
-```csharp
-[Projectable]
-public static bool IsInRange(this Entity entity)
-{
- if (entity.Value is > 10 and < 100)
- {
- return true;
- }
- return false;
-}
-```
-
-**Converted To:**
-```csharp
-entity.Value > 10 && entity.Value < 100 ? true : false
-```
-
-## Benefits
-
-1. **Modern C# Syntax**: Use pattern matching in block-bodied methods just like regular C# code
-2. **Automatic Conversion**: No manual rewriting needed - the generator handles it
-3. **Expression Tree Compatibility**: Generated code compiles without CS8122 errors
-4. **Semantic Equivalence**: Converted expressions maintain the same behavior as patterns
-
-## Limitations
-
-Not all pattern types are currently supported:
-- Type patterns with variable declarations may have limited support
-- List patterns are not yet supported
-- Some complex nested patterns may not be supported
-
-If you encounter an unsupported pattern, you'll receive an error message indicating which pattern type is not supported.
-
-## Examples
-
-### Complex Property Pattern
-```csharp
-[Projectable]
-public static string GetStatus(this Order order)
-{
- if (order is { Status: "Completed", Amount: > 1000, Customer.IsVip: true })
- {
- return "VIP High Value Completed";
- }
- return "Other";
-}
-```
-
-**Generates:**
-```csharp
-order != null &&
-order.Status == "Completed" &&
-order.Amount > 1000 &&
-order.Customer.IsVip == true
- ? "VIP High Value Completed"
- : "Other"
-```
-
-### Range Check with Relational Patterns
-```csharp
-[Projectable]
-public static string GetRange(this Entity entity)
-{
- if (entity.Value is >= 0 and < 50)
- {
- return "Low";
- }
- else if (entity.Value is >= 50 and < 100)
- {
- return "Medium";
- }
- return "High";
-}
-```
-
-**Generates:**
-```csharp
-entity.Value >= 0 && entity.Value < 50 ? "Low" :
-entity.Value >= 50 && entity.Value < 100 ? "Medium" :
-"High"
-```
-
-## Technical Details
-
-The conversion is implemented in `ExpressionSyntaxRewriter.VisitIsPatternExpression` which:
-1. Visits the expression being tested
-2. Converts the pattern to an equivalent expression using `ConvertPatternToExpression`
-3. Handles nested patterns recursively
-4. Combines multiple property checks with logical AND operators
-
-This ensures that all pattern matching is transformed into expression tree-compatible code before code generation.
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
index a73ca06..f953701 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -679,130 +676,5 @@ private ExpressionSyntax ReplaceVariableWithCast(ExpressionSyntax expression, De
return expression;
}
-
- public override SyntaxNode? VisitIsPatternExpression(IsPatternExpressionSyntax node)
- {
- // Pattern matching is not supported in expression trees (CS8122)
- // We need to convert patterns into equivalent expressions
-
- var expression = (ExpressionSyntax)Visit(node.Expression);
- var convertedPattern = ConvertPatternToExpression(node.Pattern, expression);
-
- return convertedPattern;
- }
-
- private ExpressionSyntax ConvertPatternToExpression(PatternSyntax pattern, ExpressionSyntax expression)
- {
- switch (pattern)
- {
- case RecursivePatternSyntax recursivePattern:
- return ConvertRecursivePattern(recursivePattern, expression);
-
- case ConstantPatternSyntax constantPattern:
- // e is null or e is 5
- return SyntaxFactory.BinaryExpression(
- SyntaxKind.EqualsExpression,
- expression,
- (ExpressionSyntax)Visit(constantPattern.Expression)
- );
-
- case DeclarationPatternSyntax declarationPattern:
- // e is string s -> e is string (type check)
- return SyntaxFactory.BinaryExpression(
- SyntaxKind.IsExpression,
- expression,
- declarationPattern.Type
- );
-
- case RelationalPatternSyntax relationalPattern:
- // e is > 100
- var binaryKind = relationalPattern.OperatorToken.Kind() switch
- {
- SyntaxKind.LessThanToken => SyntaxKind.LessThanExpression,
- SyntaxKind.LessThanEqualsToken => SyntaxKind.LessThanOrEqualExpression,
- SyntaxKind.GreaterThanToken => SyntaxKind.GreaterThanExpression,
- SyntaxKind.GreaterThanEqualsToken => SyntaxKind.GreaterThanOrEqualExpression,
- _ => throw new NotSupportedException($"Relational operator {relationalPattern.OperatorToken} not supported")
- };
-
- return SyntaxFactory.BinaryExpression(
- binaryKind,
- expression,
- (ExpressionSyntax)Visit(relationalPattern.Expression)
- );
-
- case BinaryPatternSyntax binaryPattern:
- // e is > 10 and < 100
- var left = ConvertPatternToExpression(binaryPattern.Left, expression);
- var right = ConvertPatternToExpression(binaryPattern.Right, expression);
-
- var logicalKind = binaryPattern.OperatorToken.Kind() switch
- {
- SyntaxKind.AndKeyword => SyntaxKind.LogicalAndExpression,
- SyntaxKind.OrKeyword => SyntaxKind.LogicalOrExpression,
- _ => throw new NotSupportedException($"Binary pattern operator {binaryPattern.OperatorToken} not supported")
- };
-
- return SyntaxFactory.BinaryExpression(logicalKind, left, right);
-
- case UnaryPatternSyntax unaryPattern when unaryPattern.OperatorToken.IsKind(SyntaxKind.NotKeyword):
- // e is not null
- var innerPattern = ConvertPatternToExpression(unaryPattern.Pattern, expression);
- return SyntaxFactory.PrefixUnaryExpression(
- SyntaxKind.LogicalNotExpression,
- SyntaxFactory.ParenthesizedExpression(innerPattern)
- );
-
- default:
- throw new NotSupportedException($"Pattern type {pattern.GetType().Name} is not yet supported in projectable methods");
- }
- }
-
- private ExpressionSyntax ConvertRecursivePattern(RecursivePatternSyntax recursivePattern, ExpressionSyntax expression)
- {
- // entity is { IsActive: true, Value: > 100 }
- // Convert to: entity != null && entity.IsActive == true && entity.Value > 100
-
- var conditions = new List();
-
- // Add null check first (unless pattern explicitly includes null)
- var nullCheck = SyntaxFactory.BinaryExpression(
- SyntaxKind.NotEqualsExpression,
- expression,
- SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
- );
- conditions.Add(nullCheck);
-
- // Handle property patterns
- if (recursivePattern.PropertyPatternClause != null)
- {
- foreach (var subpattern in recursivePattern.PropertyPatternClause.Subpatterns)
- {
- var memberAccess = subpattern.NameColon != null
- ? SyntaxFactory.MemberAccessExpression(
- SyntaxKind.SimpleMemberAccessExpression,
- expression,
- SyntaxFactory.IdentifierName(subpattern.NameColon.Name.Identifier)
- )
- : expression;
-
- var condition = ConvertPatternToExpression(subpattern.Pattern, memberAccess);
- conditions.Add(condition);
- }
- }
-
- // Combine all conditions with &&
- var result = conditions[0];
- for (int i = 1; i < conditions.Count; i++)
- {
- result = SyntaxFactory.BinaryExpression(
- SyntaxKind.LogicalAndExpression,
- result,
- conditions[i]
- );
- }
-
- return result;
- }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt
deleted file mode 100644
index 6356921..0000000
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-#nullable disable
-using System;
-using EntityFrameworkCore.Projectables;
-using Foo;
-
-namespace EntityFrameworkCore.Projectables.Generated
-{
- [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- static class Foo_Extensions_IsNull_P0_Foo_Entity
- {
- static global::System.Linq.Expressions.Expression> Expression()
- {
- return (global::Foo.Entity entity) => entity == null ? true : false;
- }
- }
-}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt
deleted file mode 100644
index 797a367..0000000
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-#nullable disable
-using System;
-using EntityFrameworkCore.Projectables;
-using Foo;
-
-namespace EntityFrameworkCore.Projectables.Generated
-{
- [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- static class Foo_Extensions_IsNotNull_P0_Foo_Entity
- {
- static global::System.Linq.Expressions.Expression> Expression()
- {
- return (global::Foo.Entity entity) => !(entity == null) ? true : false;
- }
- }
-}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt
deleted file mode 100644
index a11076d..0000000
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-#nullable disable
-using System;
-using EntityFrameworkCore.Projectables;
-using Foo;
-
-namespace EntityFrameworkCore.Projectables.Generated
-{
- [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- static class Foo_Extensions_GetComplexCategory_P0_Foo_Entity
- {
- static global::System.Linq.Expressions.Expression> Expression()
- {
- return (global::Foo.Entity entity) => entity != null && entity.IsActive == true && entity.Value > 100 ? "Active High" : "Other";
- }
- }
-}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt
deleted file mode 100644
index 55dcb0a..0000000
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-#nullable disable
-using System;
-using EntityFrameworkCore.Projectables;
-using Foo;
-
-namespace EntityFrameworkCore.Projectables.Generated
-{
- [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- static class Foo_Extensions_GetCategory_P0_Foo_Entity
- {
- static global::System.Linq.Expressions.Expression> Expression()
- {
- return (global::Foo.Entity entity) => entity.Value > 100 ? "High" : "Low";
- }
- }
-}
\ 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 bab5608..ae09af6 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -2609,140 +2609,6 @@ public int Foo()
return Verifier.Verify(result.Diagnostics.Select(d => d.ToString()));
}
- [Fact]
- public Task BlockBodiedMethod_WithPatternMatching()
- {
- var compilation = CreateCompilation(@"
-using System;
-using EntityFrameworkCore.Projectables;
-namespace Foo {
- class Entity {
- public bool IsActive { get; set; }
- public int Value { get; set; }
- }
-
- static class Extensions {
- [Projectable]
- public static string GetComplexCategory(this Entity entity)
- {
- if (entity is { IsActive: true, Value: > 100 })
- {
- return ""Active High"";
- }
- return ""Other"";
- }
- }
-}
-", expectedToCompile: true);
-
- var result = RunGenerator(compilation);
-
- // The generator should not crash and should handle pattern matching
- Assert.Empty(result.Diagnostics);
- Assert.Single(result.GeneratedTrees);
-
- return Verifier.Verify(result.GeneratedTrees[0].ToString());
- }
-
- [Fact]
- public Task BlockBodiedMethod_WithRelationalPattern()
- {
- var compilation = CreateCompilation(@"
-using System;
-using EntityFrameworkCore.Projectables;
-namespace Foo {
- class Entity {
- public int Value { get; set; }
- }
-
- static class Extensions {
- [Projectable]
- public static string GetCategory(this Entity entity)
- {
- if (entity.Value is > 100)
- {
- return ""High"";
- }
- return ""Low"";
- }
- }
-}
-", expectedToCompile: true);
-
- var result = RunGenerator(compilation);
-
- Assert.Empty(result.Diagnostics);
- Assert.Single(result.GeneratedTrees);
-
- return Verifier.Verify(result.GeneratedTrees[0].ToString());
- }
-
- [Fact]
- public Task BlockBodiedMethod_WithConstantPattern()
- {
- var compilation = CreateCompilation(@"
-using System;
-using EntityFrameworkCore.Projectables;
-namespace Foo {
- class Entity {
- public string Status { get; set; }
- }
-
- static class Extensions {
- [Projectable]
- public static bool IsNull(this Entity entity)
- {
- if (entity is null)
- {
- return true;
- }
- return false;
- }
- }
-}
-", expectedToCompile: true);
-
- var result = RunGenerator(compilation);
-
- Assert.Empty(result.Diagnostics);
- Assert.Single(result.GeneratedTrees);
-
- return Verifier.Verify(result.GeneratedTrees[0].ToString());
- }
-
- [Fact]
- public Task BlockBodiedMethod_WithNotPattern()
- {
- var compilation = CreateCompilation(@"
-using System;
-using EntityFrameworkCore.Projectables;
-namespace Foo {
- class Entity {
- public string Name { get; set; }
- }
-
- static class Extensions {
- [Projectable]
- public static bool IsNotNull(this Entity entity)
- {
- if (entity is not null)
- {
- return true;
- }
- return false;
- }
- }
-}
-", expectedToCompile: true);
-
- var result = RunGenerator(compilation);
-
- Assert.Empty(result.Diagnostics);
- Assert.Single(result.GeneratedTrees);
-
- return Verifier.Verify(result.GeneratedTrees[0].ToString());
- }
-
[Fact]
public Task MethodOverloads_WithDifferentParameterTypes()
{
From 8b144b5f416a2aaecc15a04dda500035f7d464d1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 16 Feb 2026 20:51:47 +0000
Subject: [PATCH 20/26] Address code review suggestions and update
documentation
Code Review Fixes:
- Use proper type resolution for [Projectable] attribute check (SymbolEqualityComparer)
- Add parentheses when inlining local variables to preserve operator precedence
- Restrict multiple if-without-else pattern to simple return bodies
Documentation Updates:
- Add block-bodied methods FAQ section to README with examples
- Merge SideEffectDetection.md content into BlockBodiedMethods.md
- Remove standalone SideEffectDetection.md file
- Link to BlockBodiedMethods.md from README
All 209 tests passing
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
README.md | 30 +++++
docs/BlockBodiedMethods.md | 82 +++++++++++++
docs/SideEffectDetection.md | 110 ------------------
.../BlockStatementConverter.cs | 66 ++++++++++-
4 files changed, 174 insertions(+), 114 deletions(-)
delete mode 100644 docs/SideEffectDetection.md
diff --git a/README.md b/README.md
index ab688bf..d61a2ee 100644
--- a/README.md
+++ b/README.md
@@ -159,6 +159,36 @@ GROUP BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '')
ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '')
```
+#### Can I use block-bodied methods instead of expression-bodied methods?
+Yes! As of version 3.x, you can now use traditional block-bodied methods with `[Projectable]`. This makes code more readable when dealing with complex conditional logic:
+
+```csharp
+// Expression-bodied (still supported)
+[Projectable]
+public string Level() => Value > 100 ? "High" : Value > 50 ? "Medium" : "Low";
+
+// Block-bodied (now also supported!)
+[Projectable]
+public string Level()
+{
+ if (Value > 100)
+ return "High";
+ else if (Value > 50)
+ return "Medium";
+ else
+ return "Low";
+}
+```
+
+Both generate identical SQL. Block-bodied methods support:
+- If-else statements (converted to ternary/CASE expressions)
+- Switch statements
+- Local variables (automatically inlined)
+- Simple return statements
+
+The generator will also detect and report side effects (assignments, method calls to non-projectable methods, etc.) with precise error messages. See [Block-Bodied Methods Documentation](docs/BlockBodiedMethods.md) for complete details.
+
+
#### How do I expand enum extension methods?
When you have an enum property and want to call an extension method on it (like getting a display name from a `[Display]` attribute), you can use the `ExpandEnumMethods` property on the `[Projectable]` attribute. This will expand the enum method call into a chain of ternary expressions for each enum value, allowing EF Core to translate it to SQL CASE expressions.
diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md
index fe19c69..12c0022 100644
--- a/docs/BlockBodiedMethods.md
+++ b/docs/BlockBodiedMethods.md
@@ -299,3 +299,85 @@ SELECT CASE
END
FROM [Entity] AS [e]
```
+
+## Side Effect Detection
+
+The generator provides specific error reporting for side effects in block-bodied methods, helping you identify and fix issues quickly.
+
+### Detected Side Effects
+
+#### 1. Property Assignments (EFP0004 - Error)
+
+Property assignments modify state and are not allowed:
+
+```csharp
+[Projectable]
+public int Foo()
+{
+ Bar = 10; // ❌ Error: Assignment operation has side effects
+ return Bar;
+}
+```
+
+#### 2. Compound Assignments (EFP0004 - Error)
+
+Compound assignment operators like `+=`, `-=`, `*=`, etc. are not allowed:
+
+```csharp
+[Projectable]
+public int Foo()
+{
+ Bar += 10; // ❌ Error: Compound assignment operator '+=' has side effects
+ return Bar;
+}
+```
+
+#### 3. Increment/Decrement Operators (EFP0004 - Error)
+
+Pre and post increment/decrement operators are not allowed:
+
+```csharp
+[Projectable]
+public int Foo()
+{
+ var x = 5;
+ x++; // ❌ Error: Increment/decrement operator '++' has side effects
+ return x;
+}
+```
+
+#### 4. Non-Projectable Method Calls (EFP0005 - Warning)
+
+Calls to methods not marked with `[Projectable]` may have side effects:
+
+```csharp
+[Projectable]
+public int Foo()
+{
+ Console.WriteLine("test"); // ⚠️ Warning: Method call 'WriteLine' may have side effects
+ return Bar;
+}
+```
+
+### Diagnostic Codes
+
+- **EFP0003**: Unsupported statement in block-bodied method (Warning)
+- **EFP0004**: Statement with side effects in block-bodied method (Error)
+- **EFP0005**: Potential side effect in block-bodied method (Warning)
+
+### Error Message Improvements
+
+Instead of generic error messages, you now get precise, actionable feedback:
+
+**Before:**
+```
+warning EFP0003: Method 'Foo' contains an unsupported statement: Expression statements are not supported
+```
+
+**After:**
+```
+error EFP0004: Property assignment 'Bar' has side effects and cannot be used in projectable methods
+```
+
+The error message points to the exact line with the problematic code, making it much easier to identify and fix issues.
+
diff --git a/docs/SideEffectDetection.md b/docs/SideEffectDetection.md
deleted file mode 100644
index 891294e..0000000
--- a/docs/SideEffectDetection.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# Side Effect Detection in Block-Bodied Methods
-
-This document describes the improved error reporting for side effects in block-bodied projectable methods.
-
-## Overview
-
-When using block-bodied methods with the `[Projectable]` attribute, the source generator now provides specific error messages that point to the exact line where side effects occur, making it much easier to identify and fix issues.
-
-## Detected Side Effects
-
-### 1. Property Assignments (EFP0004 - Error)
-
-**Code:**
-```csharp
-[Projectable]
-public int Foo()
-{
- Bar = 10; // ❌ Error on this line
- return Bar;
-}
-```
-
-**Error Message:**
-```
-(11,13): error EFP0004: Assignment operation has side effects and cannot be used in projectable methods
-```
-
-### 2. Compound Assignments (EFP0004 - Error)
-
-**Code:**
-```csharp
-[Projectable]
-public int Foo()
-{
- Bar += 10; // ❌ Error on this line
- return Bar;
-}
-```
-
-**Error Message:**
-```
-(11,13): error EFP0004: Compound assignment operator '+=' has side effects and cannot be used in projectable methods
-```
-
-### 3. Increment/Decrement Operators (EFP0004 - Error)
-
-**Code:**
-```csharp
-[Projectable]
-public int Foo()
-{
- var x = 5;
- x++; // ❌ Error on this line
- return x;
-}
-```
-
-**Error Message:**
-```
-(12,13): error EFP0004: Increment/decrement operator '++' has side effects and cannot be used in projectable methods
-```
-
-### 4. Non-Projectable Method Calls (EFP0005 - Warning)
-
-**Code:**
-```csharp
-[Projectable]
-public int Foo()
-{
- Console.WriteLine("test"); // ⚠️ Warning on this line
- return Bar;
-}
-```
-
-**Warning Message:**
-```
-(11,13): warning EFP0005: Method call 'WriteLine' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods
-```
-
-## Before vs After
-
-### Before
-Generic error message at the beginning of the method:
-```
-warning EFP0003: Method 'Foo' contains an unsupported statement: Expression statements are not supported
-```
-
-### After
-Specific error message pointing to the exact problematic line:
-```
-error EFP0004: Property assignment 'Bar' has side effects and cannot be used in projectable methods
-```
-
-## Benefits
-
-1. **Precise Location**: Error messages now point to the exact line containing the side effect
-2. **Specific Messages**: Clear explanation of what kind of side effect was detected
-3. **Better Developer Experience**: Easier to identify and fix issues
-4. **Severity Levels**: Errors for definite side effects, warnings for potential ones
-5. **Actionable Guidance**: Messages explain why the code is problematic
-
-## Diagnostic Codes
-
-- **EFP0004**: Statement with side effects in block-bodied method (Error)
-- **EFP0005**: Potential side effect in block-bodied method (Warning)
-
-These are in addition to the existing:
-- **EFP0001**: Method or property should expose an expression body definition (Error)
-- **EFP0002**: Method or property is not configured to support null-conditional expressions (Error)
-- **EFP0003**: Unsupported statement in block-bodied method (Warning)
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index 55abd92..d392c9b 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -87,14 +87,27 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
}
// Check if we have a pattern like multiple if statements without else followed by a final return:
- // var x = ...; if (a) return 1; if (b) return 2; return 3;
+ // if (a) return 1; if (b) return 2; return 3;
// This can be converted to nested ternaries: a ? 1 : (b ? 2 : 3)
+ // Each if statement must have a body that is a simple return statement
if (lastStatement is ReturnStatementSyntax finalReturn &&
remainingStatements.All(s => s is IfStatementSyntax { Else: null }))
{
// All remaining non-return statements are if statements without else
var ifStatements = remainingStatements.Cast().ToList();
+ // Validate each if statement has a simple return statement body
+ foreach (var ifStmt in ifStatements)
+ {
+ if (!IsSimpleReturnBody(ifStmt.Statement))
+ {
+ ReportUnsupportedStatement(ifStmt, memberName,
+ "Multiple if statements without else clauses require each if body to be a simple return statement. " +
+ "Complex if bodies are not supported in this pattern.");
+ return null;
+ }
+ }
+
// Start with the final return as the base expression
var elseBody = TryConvertReturnStatement(finalReturn, memberName);
if (elseBody == null)
@@ -146,6 +159,19 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
return TryConvertStatement(lastStatement, memberName);
}
+ ///
+ /// Checks if a statement is a simple return statement or a block containing only a return statement.
+ ///
+ private static bool IsSimpleReturnBody(StatementSyntax statement)
+ {
+ return statement switch
+ {
+ ReturnStatementSyntax => true,
+ BlockSyntax block => block.Statements.Count == 1 && block.Statements[0] is ReturnStatementSyntax,
+ _ => false
+ };
+ }
+
private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDecl, string memberName)
{
foreach (var variable in localDecl.Declaration.Variables)
@@ -445,8 +471,12 @@ private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression)
if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
{
// Check if method has [Projectable] attribute - those are safe
+ // Use proper type resolution to avoid false positives
+ var semanticModel = _expressionRewriter.GetSemanticModel();
+ var projectableAttributeType = semanticModel.Compilation.GetTypeByMetadataName("EntityFrameworkCore.Projectables.ProjectableAttribute");
+
var hasProjectableAttr = methodSymbol.GetAttributes()
- .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute");
+ .Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, projectableAttributeType));
if (!hasProjectableAttr)
{
@@ -528,12 +558,40 @@ public LocalVariableReplacer(Dictionary localVariables
var identifier = node.Identifier.Text;
if (_localVariables.TryGetValue(identifier, out var replacement))
{
- // Replace the identifier with the expression it was initialized with
- return replacement.WithTriviaFrom(node);
+ // Replace the identifier with the expression it was initialized with.
+ // Wrap non-trivial expressions in parentheses to preserve operator precedence.
+ var replacementWithTrivia = replacement.WithTriviaFrom(node);
+
+ if (RequiresParentheses(replacementWithTrivia))
+ {
+ return SyntaxFactory.ParenthesizedExpression(replacementWithTrivia);
+ }
+
+ return replacementWithTrivia;
}
return base.VisitIdentifierName(node);
}
+
+ private static bool RequiresParentheses(ExpressionSyntax replacement)
+ {
+ // Simple expressions do not need parentheses when inlined.
+ if (replacement is IdentifierNameSyntax
+ or ThisExpressionSyntax
+ or BaseExpressionSyntax
+ or LiteralExpressionSyntax
+ or ParenthesizedExpressionSyntax
+ or MemberAccessExpressionSyntax
+ or InvocationExpressionSyntax
+ or ElementAccessExpressionSyntax)
+ {
+ return false;
+ }
+
+ // For all other (potentially non-trivial) expressions, use parentheses
+ // to avoid changing semantics due to operator precedence.
+ return true;
+ }
}
}
}
From 2b70d43f3f43577ac2c5fbd86963955de47e0726 Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Mon, 16 Feb 2026 21:55:12 +0100
Subject: [PATCH 21/26] Update block bodied release number
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d61a2ee..2f06978 100644
--- a/README.md
+++ b/README.md
@@ -160,7 +160,7 @@ ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '')
```
#### Can I use block-bodied methods instead of expression-bodied methods?
-Yes! As of version 3.x, you can now use traditional block-bodied methods with `[Projectable]`. This makes code more readable when dealing with complex conditional logic:
+Yes! As of version 6.x, you can now use traditional block-bodied methods with `[Projectable]`. This makes code more readable when dealing with complex conditional logic:
```csharp
// Expression-bodied (still supported)
From b4989af1910bdb62ea5109ea717d7493641667a5 Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Mon, 16 Feb 2026 22:03:44 +0100
Subject: [PATCH 22/26] Reveret change about if with single return
---
.../BlockStatementConverter.cs | 66 ++-----------------
1 file changed, 4 insertions(+), 62 deletions(-)
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index d392c9b..55abd92 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -87,27 +87,14 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
}
// Check if we have a pattern like multiple if statements without else followed by a final return:
- // if (a) return 1; if (b) return 2; return 3;
+ // var x = ...; if (a) return 1; if (b) return 2; return 3;
// This can be converted to nested ternaries: a ? 1 : (b ? 2 : 3)
- // Each if statement must have a body that is a simple return statement
if (lastStatement is ReturnStatementSyntax finalReturn &&
remainingStatements.All(s => s is IfStatementSyntax { Else: null }))
{
// All remaining non-return statements are if statements without else
var ifStatements = remainingStatements.Cast().ToList();
- // Validate each if statement has a simple return statement body
- foreach (var ifStmt in ifStatements)
- {
- if (!IsSimpleReturnBody(ifStmt.Statement))
- {
- ReportUnsupportedStatement(ifStmt, memberName,
- "Multiple if statements without else clauses require each if body to be a simple return statement. " +
- "Complex if bodies are not supported in this pattern.");
- return null;
- }
- }
-
// Start with the final return as the base expression
var elseBody = TryConvertReturnStatement(finalReturn, memberName);
if (elseBody == null)
@@ -159,19 +146,6 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
return TryConvertStatement(lastStatement, memberName);
}
- ///
- /// Checks if a statement is a simple return statement or a block containing only a return statement.
- ///
- private static bool IsSimpleReturnBody(StatementSyntax statement)
- {
- return statement switch
- {
- ReturnStatementSyntax => true,
- BlockSyntax block => block.Statements.Count == 1 && block.Statements[0] is ReturnStatementSyntax,
- _ => false
- };
- }
-
private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDecl, string memberName)
{
foreach (var variable in localDecl.Declaration.Variables)
@@ -471,12 +445,8 @@ private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression)
if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
{
// Check if method has [Projectable] attribute - those are safe
- // Use proper type resolution to avoid false positives
- var semanticModel = _expressionRewriter.GetSemanticModel();
- var projectableAttributeType = semanticModel.Compilation.GetTypeByMetadataName("EntityFrameworkCore.Projectables.ProjectableAttribute");
-
var hasProjectableAttr = methodSymbol.GetAttributes()
- .Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, projectableAttributeType));
+ .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute");
if (!hasProjectableAttr)
{
@@ -558,40 +528,12 @@ public LocalVariableReplacer(Dictionary localVariables
var identifier = node.Identifier.Text;
if (_localVariables.TryGetValue(identifier, out var replacement))
{
- // Replace the identifier with the expression it was initialized with.
- // Wrap non-trivial expressions in parentheses to preserve operator precedence.
- var replacementWithTrivia = replacement.WithTriviaFrom(node);
-
- if (RequiresParentheses(replacementWithTrivia))
- {
- return SyntaxFactory.ParenthesizedExpression(replacementWithTrivia);
- }
-
- return replacementWithTrivia;
+ // Replace the identifier with the expression it was initialized with
+ return replacement.WithTriviaFrom(node);
}
return base.VisitIdentifierName(node);
}
-
- private static bool RequiresParentheses(ExpressionSyntax replacement)
- {
- // Simple expressions do not need parentheses when inlined.
- if (replacement is IdentifierNameSyntax
- or ThisExpressionSyntax
- or BaseExpressionSyntax
- or LiteralExpressionSyntax
- or ParenthesizedExpressionSyntax
- or MemberAccessExpressionSyntax
- or InvocationExpressionSyntax
- or ElementAccessExpressionSyntax)
- {
- return false;
- }
-
- // For all other (potentially non-trivial) expressions, use parentheses
- // to avoid changing semantics due to operator precedence.
- return true;
- }
}
}
}
From a741a2770f14642c922e0aba6845d7ada4428a22 Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Wed, 18 Feb 2026 12:14:40 +0100
Subject: [PATCH 23/26] Mark this new feature as experimental and allow
explicit getters for properties
---
README.md | 13 +-
...BodiedMethods.md => BlockBodiedMembers.md} | 29 +-
.../ProjectableAttribute.cs | 9 +
.../AnalyzerReleases.Shipped.md | 24 +-
.../AnalyzerReleases.Unshipped.md | 9 +-
.../Diagnostics.cs | 23 +-
.../ProjectableInterpreter.cs | 89 +++++-
.../BlockBodiedMethodTests.cs | 60 ++--
.../BlockBodyProjectableCallTest.cs | 34 +--
...yWithExplicitExpressionGetter.verified.txt | 17 ++
...opertyWithExplicitBlockGetter.verified.txt | 17 ++
...yWithExplicitExpressionGetter.verified.txt | 17 ++
.../ProjectionExpressionGeneratorTests.cs | 289 +++++++++++++++---
13 files changed, 501 insertions(+), 129 deletions(-)
rename docs/{BlockBodiedMethods.md => BlockBodiedMembers.md} (89%)
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt
diff --git a/README.md b/README.md
index 2f06978..471c6f5 100644
--- a/README.md
+++ b/README.md
@@ -159,8 +159,9 @@ GROUP BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '')
ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '')
```
-#### Can I use block-bodied methods instead of expression-bodied methods?
-Yes! As of version 6.x, you can now use traditional block-bodied methods with `[Projectable]`. This makes code more readable when dealing with complex conditional logic:
+#### Can I use block-bodied members instead of expression-bodied members?
+
+Yes! As of version 6.x, you can now use traditional block-bodied members with `[Projectable]`. This makes code more readable when dealing with complex conditional logic:
```csharp
// Expression-bodied (still supported)
@@ -168,7 +169,7 @@ Yes! As of version 6.x, you can now use traditional block-bodied methods with `[
public string Level() => Value > 100 ? "High" : Value > 50 ? "Medium" : "Low";
// Block-bodied (now also supported!)
-[Projectable]
+[Projectable(AllowBlockBody = true)] // Note: AllowBlockBody is required to remove the warning for experimental feature usage
public string Level()
{
if (Value > 100)
@@ -180,13 +181,15 @@ public string Level()
}
```
-Both generate identical SQL. Block-bodied methods support:
+> This is an experimental feature and may have some limitations. Please refer to the documentation for details.
+
+Both generate identical SQL. Block-bodied members support:
- If-else statements (converted to ternary/CASE expressions)
- Switch statements
- Local variables (automatically inlined)
- Simple return statements
-The generator will also detect and report side effects (assignments, method calls to non-projectable methods, etc.) with precise error messages. See [Block-Bodied Methods Documentation](docs/BlockBodiedMethods.md) for complete details.
+The generator will also detect and report side effects (assignments, method calls to non-projectable members, etc.) with precise error messages. See [Block-Bodied Members Documentation](docs/BlockBodiedMembers.md) for complete details.
#### How do I expand enum extension methods?
diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMembers.md
similarity index 89%
rename from docs/BlockBodiedMethods.md
rename to docs/BlockBodiedMembers.md
index 12c0022..63be89f 100644
--- a/docs/BlockBodiedMethods.md
+++ b/docs/BlockBodiedMembers.md
@@ -1,6 +1,33 @@
# Block-Bodied Methods Support
-As of this version, EntityFrameworkCore.Projectables now supports "classic" block-bodied methods decorated with `[Projectable]`, in addition to expression-bodied methods.
+EntityFrameworkCore.Projectables now supports "classic" block-bodied members (methods and properties) decorated with `[Projectable]`, in addition to expression-bodied members.
+
+## ⚠️ Experimental Feature
+
+Block-bodied members support is currently **experimental**. By default, using a block-bodied member with `[Projectable]` will emit a warning:
+
+```
+EFP0001: Block-bodied member 'MethodName' is using an experimental feature. Set AllowBlockBody = true on the Projectable attribute to suppress this warning.
+```
+
+To acknowledge that you're using an experimental feature and suppress the warning, set `AllowBlockBody = true`:
+
+```csharp
+[Projectable(AllowBlockBody = true)]
+public string GetCategory()
+{
+ if (Value > 100)
+ {
+ return "High";
+ }
+ else
+ {
+ return "Low";
+ }
+}
+```
+
+This requirement will be removed in a future version once the feature is considered stable.
## What's Supported
diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs
index 30af683..94b63b2 100644
--- a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs
+++ b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs
@@ -38,5 +38,14 @@ public sealed class ProjectableAttribute : Attribute
///
///
public bool ExpandEnumMethods { get; set; }
+
+ ///
+ /// Get or set whether to allow block-bodied members (experimental feature).
+ ///
+ ///
+ /// Block-bodied method support is experimental and may have limitations.
+ /// Set this to true to suppress the experimental feature warning.
+ ///
+ public bool AllowBlockBody { get; set; }
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
index 586c754..7bd9067 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
@@ -1,7 +1,25 @@
-## Release 5.0
+## Release 6.0
### New Rules
Rule ID | Category | Severity | Notes
---------|----------|----------|--------------------
-EFP0001 | Design | Error |
+--------|----------|----------|---------------------------------------------------------------------------
+EFP0003 | Design | Warning | Unsupported statement in block-bodied method
+EFP0004 | Design | Error | Statement with side effects in block-bodied method
+EFP0005 | Design | Warning | Potential side effect in block-bodied method
+EFP0006 | Design | Error | Method or property should expose an body definition (block or expression)
+
+### Changed Rules
+
+Rule ID | New Category | New Severity | Old Category | Old Severity | Notes";
+--------|--------------|--------------|--------------|--------------|-----------------------------------------------------------------
+EFP0001 | Design | Warning | Design | Error | Changed to warning for experimental block-bodied members support
+
+## Release 5.0
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|------------------------------------------------------------------------------
+EFP0001 | Design | Error | Method or property should expose an expression body definition
+EFP0002 | Design | Error | Method or property is not configured to support null-conditional expressions
diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
index c1b0078..5f28270 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
@@ -1,8 +1 @@
-### New Rules
-
-Rule ID | Category | Severity | Notes
---------|----------|----------|--------------------
-EFP0002 | Design | Error |
-EFP0003 | Design | Warning |
-EFP0004 | Design | Error | Statement with side effects in block-bodied method
-EFP0005 | Design | Warning | Potential side effect in block-bodied method
+
\ No newline at end of file
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
index 6bcfaf1..70e2964 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
@@ -1,20 +1,15 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis;
namespace EntityFrameworkCore.Projectables.Generator
{
public static class Diagnostics
{
- public static readonly DiagnosticDescriptor RequiresExpressionBodyDefinition = new DiagnosticDescriptor(
+ public static readonly DiagnosticDescriptor BlockBodyExperimental = new DiagnosticDescriptor(
id: "EFP0001",
- title: "Method or property should expose an expression body definition",
- messageFormat: "Method or property '{0}' should expose an expression body definition",
+ title: "Block-bodied member support is experimental",
+ messageFormat: "Block-bodied member '{0}' is using an experimental feature. Set AllowBlockBody = true on the Projectable attribute to suppress this warning.",
category: "Design",
- DiagnosticSeverity.Error,
+ DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public static readonly DiagnosticDescriptor NullConditionalRewriteUnsupported = new DiagnosticDescriptor(
@@ -49,5 +44,13 @@ public static class Diagnostics
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
+ public static readonly DiagnosticDescriptor RequiresBodyDefinition = new DiagnosticDescriptor(
+ id: "EFP0006",
+ title: "Method or property should expose a body definition",
+ messageFormat: "Method or property '{0}' should expose a body definition (e.g. an expression-bodied member or a block-bodied method) to be used as the source for the generated expression tree.",
+ category: "Design",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
index 5006081..356420b 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
@@ -80,6 +80,11 @@ static IEnumerable GetNestedInClassPathForExtensionMember(ITypeSymbol ex
.Select(x => x.Value.Value is bool b && b)
.FirstOrDefault();
+ var allowBlockBody = projectableAttributeClass.NamedArguments
+ .Where(x => x.Key == "AllowBlockBody")
+ .Select(x => x.Value.Value is bool b && b)
+ .FirstOrDefault();
+
var memberBody = member;
if (useMemberBody is not null)
@@ -124,18 +129,33 @@ x is IPropertySymbol xProperty &&
{
return true;
}
- else if (x is PropertyDeclarationSyntax xProperty &&
- xProperty.ExpressionBody is not null)
- {
- return true;
- }
- else
+ else if (x is PropertyDeclarationSyntax xProperty)
{
- return false;
+ // Support expression-bodied properties: int Prop => value;
+ if (xProperty.ExpressionBody is not null)
+ {
+ return true;
+ }
+
+ // Support properties with explicit getters: int Prop { get => value; } or { get { return value; } }
+ if (xProperty.AccessorList is not null)
+ {
+ var getter = xProperty.AccessorList.Accessors
+ .FirstOrDefault(a => a.IsKind(SyntaxKind.GetAccessorDeclaration));
+ if (getter?.ExpressionBody is not null || getter?.Body is not null)
+ {
+ return true;
+ }
+ }
}
+
+ return false;
});
- if (memberBody is null) return null;
+ if (memberBody is null)
+ {
+ return null;
+ }
}
// Check if this member is inside a C# 14 extension block
@@ -300,6 +320,7 @@ x is IPropertySymbol xProperty &&
descriptor.TargetNestedInClassNames = descriptor.NestedInClassNames;
}
+ // Projectable methods
if (memberBody is MethodDeclarationSyntax methodDeclarationSyntax)
{
ExpressionSyntax? bodyExpression = null;
@@ -312,6 +333,14 @@ x is IPropertySymbol xProperty &&
else if (methodDeclarationSyntax.Body is not null)
{
// Block-bodied method (e.g., int Foo() { return 1; })
+
+ // Emit warning if AllowBlockBody is not set to true
+ if (!allowBlockBody)
+ {
+ var diagnostic = Diagnostic.Create(Diagnostics.BlockBodyExperimental, methodDeclarationSyntax.GetLocation(), memberSymbol.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+
var blockConverter = new BlockStatementConverter(context, expressionSyntaxRewriter);
bodyExpression = blockConverter.TryConvertBlock(methodDeclarationSyntax.Body, memberSymbol.Name);
@@ -325,7 +354,7 @@ x is IPropertySymbol xProperty &&
}
else
{
- var diagnostic = Diagnostic.Create(Diagnostics.RequiresExpressionBodyDefinition, methodDeclarationSyntax.GetLocation(), memberSymbol.Name);
+ var diagnostic = Diagnostic.Create(Diagnostics.RequiresBodyDefinition, methodDeclarationSyntax.GetLocation(), memberSymbol.Name);
context.ReportDiagnostic(diagnostic);
return null;
}
@@ -360,11 +389,47 @@ x is IPropertySymbol xProperty &&
);
}
}
+
+ // Projectable properties
else if (memberBody is PropertyDeclarationSyntax propertyDeclarationSyntax)
{
- if (propertyDeclarationSyntax.ExpressionBody is null)
+ ExpressionSyntax? bodyExpression = null;
+
+ // Expression-bodied property: int Prop => value;
+ if (propertyDeclarationSyntax.ExpressionBody is not null)
+ {
+
+ bodyExpression = propertyDeclarationSyntax.ExpressionBody.Expression;
+ }
+ else if (propertyDeclarationSyntax.AccessorList is not null)
+ {
+ // Property with explicit getter
+ var getter = propertyDeclarationSyntax.AccessorList.Accessors
+ .FirstOrDefault(a => a.IsKind(SyntaxKind.GetAccessorDeclaration));
+
+ if (getter?.ExpressionBody is not null)
+ {
+ // get => expression;
+ bodyExpression = getter.ExpressionBody.Expression;
+ }
+ else if (getter?.Body is not null)
+ {
+ // get { return expression; }
+ // Emit warning if AllowBlockBody is not set to true
+ if (!allowBlockBody)
+ {
+ var diagnostic = Diagnostic.Create(Diagnostics.BlockBodyExperimental, propertyDeclarationSyntax.GetLocation(), memberSymbol.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+
+ var blockConverter = new BlockStatementConverter(context, expressionSyntaxRewriter);
+ bodyExpression = blockConverter.TryConvertBlock(getter.Body, memberSymbol.Name);
+ }
+ }
+
+ if (bodyExpression is null)
{
- var diagnostic = Diagnostic.Create(Diagnostics.RequiresExpressionBodyDefinition, propertyDeclarationSyntax.GetLocation(), memberSymbol.Name);
+ var diagnostic = Diagnostic.Create(Diagnostics.RequiresBodyDefinition, propertyDeclarationSyntax.GetLocation(), memberSymbol.Name);
context.ReportDiagnostic(diagnostic);
return null;
}
@@ -372,7 +437,7 @@ x is IPropertySymbol xProperty &&
var returnType = declarationSyntaxRewriter.Visit(propertyDeclarationSyntax.Type);
descriptor.ReturnTypeName = returnType.ToString();
- descriptor.ExpressionBody = (ExpressionSyntax)expressionSyntaxRewriter.Visit(propertyDeclarationSyntax.ExpressionBody.Expression);
+ descriptor.ExpressionBody = (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression);
}
else
{
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs
index 9622f34..98320b3 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs
@@ -1,4 +1,4 @@
-using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
+using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
@@ -340,19 +340,19 @@ public Task ArithmeticInReturn_WorksCorrectly()
public static class EntityExtensions
{
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int GetConstant(this BlockBodiedMethodTests.Entity entity)
{
return 42;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int GetValuePlusTen(this BlockBodiedMethodTests.Entity entity)
{
return entity.Value + 10;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetCategory(this BlockBodiedMethodTests.Entity entity)
{
if (entity.Value > 100)
@@ -365,7 +365,7 @@ public static string GetCategory(this BlockBodiedMethodTests.Entity entity)
}
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetLevel(this BlockBodiedMethodTests.Entity entity)
{
if (entity.Value > 100)
@@ -382,14 +382,14 @@ public static string GetLevel(this BlockBodiedMethodTests.Entity entity)
}
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int CalculateDouble(this BlockBodiedMethodTests.Entity entity)
{
var doubled = entity.Value * 2;
return doubled + 5;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int GetAdjustedValue(this BlockBodiedMethodTests.Entity entity)
{
if (entity.IsActive && entity.Value > 0)
@@ -402,13 +402,13 @@ public static int GetAdjustedValue(this BlockBodiedMethodTests.Entity entity)
}
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int Add(this BlockBodiedMethodTests.Entity entity, int a, int b)
{
return a + b;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int? GetPremiumIfActive(this BlockBodiedMethodTests.Entity entity)
{
if (entity.IsActive)
@@ -418,7 +418,7 @@ public static int Add(this BlockBodiedMethodTests.Entity entity, int a, int b)
return null;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetStatus(this BlockBodiedMethodTests.Entity entity)
{
if (entity.IsActive)
@@ -428,7 +428,7 @@ public static string GetStatus(this BlockBodiedMethodTests.Entity entity)
return "Inactive";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetValueLabel(this BlockBodiedMethodTests.Entity entity)
{
switch (entity.Value)
@@ -444,7 +444,7 @@ public static string GetValueLabel(this BlockBodiedMethodTests.Entity entity)
}
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetPriority(this BlockBodiedMethodTests.Entity entity)
{
switch (entity.Value)
@@ -465,7 +465,7 @@ public static string GetPriority(this BlockBodiedMethodTests.Entity entity)
}
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetValueCategory(this BlockBodiedMethodTests.Entity entity)
{
if (entity.Value > 100)
@@ -486,19 +486,19 @@ public static string GetValueCategory(this BlockBodiedMethodTests.Entity entity)
return "Low";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetNameOrDefault(this BlockBodiedMethodTests.Entity entity)
{
return entity.Name ?? "Unknown";
}
- [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)]
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite, AllowBlockBody = true)]
public static int? GetNameLength(this BlockBodiedMethodTests.Entity entity)
{
return entity.Name?.Length;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetValueLabelModern(this BlockBodiedMethodTests.Entity entity)
{
return entity.Value switch
@@ -510,7 +510,7 @@ public static string GetValueLabelModern(this BlockBodiedMethodTests.Entity enti
};
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetPriorityModern(this BlockBodiedMethodTests.Entity entity)
{
return entity.Value switch
@@ -522,7 +522,7 @@ public static string GetPriorityModern(this BlockBodiedMethodTests.Entity entity
};
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int CalculateComplex(this BlockBodiedMethodTests.Entity entity)
{
var doubled = entity.Value * 2;
@@ -531,7 +531,7 @@ public static int CalculateComplex(this BlockBodiedMethodTests.Entity entity)
return sum + 10;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetComplexCategory(this BlockBodiedMethodTests.Entity entity)
{
if (entity.IsActive && entity.Value > 100)
@@ -552,7 +552,7 @@ public static string GetComplexCategory(this BlockBodiedMethodTests.Entity entit
return "Other";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int GetGuardedValue(this BlockBodiedMethodTests.Entity entity)
{
if (!entity.IsActive)
@@ -568,7 +568,7 @@ public static int GetGuardedValue(this BlockBodiedMethodTests.Entity entity)
return entity.Value * 2;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetCombinedLogic(this BlockBodiedMethodTests.Entity entity)
{
if (entity.IsActive)
@@ -587,19 +587,19 @@ public static string GetCombinedLogic(this BlockBodiedMethodTests.Entity entity)
return "Inactive";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetValueUsingTernary(this BlockBodiedMethodTests.Entity entity)
{
return entity.IsActive ? "Active" : "Inactive";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetNestedTernary(this BlockBodiedMethodTests.Entity entity)
{
return entity.Value > 100 ? "High" : entity.Value > 50 ? "Medium" : "Low";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetComplexMix(this BlockBodiedMethodTests.Entity entity)
{
if (entity.IsActive)
@@ -615,7 +615,7 @@ public static string GetComplexMix(this BlockBodiedMethodTests.Entity entity)
return "Inactive";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetValueWithCondition(this BlockBodiedMethodTests.Entity entity)
{
return entity.Value switch
@@ -627,14 +627,14 @@ public static string GetValueWithCondition(this BlockBodiedMethodTests.Entity en
};
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int CalculateWithReuse(this BlockBodiedMethodTests.Entity entity)
{
var doubled = entity.Value * 2;
return doubled + doubled;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static bool IsHighValue(this BlockBodiedMethodTests.Entity entity)
{
if (entity.Value > 100)
@@ -644,7 +644,7 @@ public static bool IsHighValue(this BlockBodiedMethodTests.Entity entity)
return false;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetInactiveStatus(this BlockBodiedMethodTests.Entity entity)
{
if (!entity.IsActive)
@@ -657,13 +657,13 @@ public static string GetInactiveStatus(this BlockBodiedMethodTests.Entity entity
}
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetFormattedValue(this BlockBodiedMethodTests.Entity entity)
{
return $"Value: {entity.Value}";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static double CalculatePercentage(this BlockBodiedMethodTests.Entity entity)
{
return (double)entity.Value / 100.0 * 50.0;
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs
index c09237b..99cbb3e 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs
@@ -124,19 +124,19 @@ public Task BlockBodyCallingProjectableMethod_InLogicalExpression()
public static class ProjectableCallExtensions
{
// Base projectable methods (helper methods)
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int GetConstant(this BlockBodyProjectableCallTests.Entity entity)
{
return 42;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int GetDoubled(this BlockBodyProjectableCallTests.Entity entity)
{
return entity.Value * 2;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetCategory(this BlockBodyProjectableCallTests.Entity entity)
{
if (entity.Value > 100)
@@ -145,7 +145,7 @@ public static string GetCategory(this BlockBodyProjectableCallTests.Entity entit
return "Low";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetLevel(this BlockBodyProjectableCallTests.Entity entity)
{
if (entity.Value > 100) return "Level3";
@@ -153,7 +153,7 @@ public static string GetLevel(this BlockBodyProjectableCallTests.Entity entity)
return "Level1";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static bool IsHighValue(this BlockBodyProjectableCallTests.Entity entity)
{
return entity.Value > 100;
@@ -161,20 +161,20 @@ public static bool IsHighValue(this BlockBodyProjectableCallTests.Entity entity)
// Block-bodied methods calling projectable methods
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int GetAdjustedWithConstant(this BlockBodyProjectableCallTests.Entity entity)
{
return entity.Value + entity.GetConstant();
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int GetDoubledValue(this BlockBodyProjectableCallTests.Entity entity)
{
var doubled = entity.GetDoubled();
return doubled;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetCategoryBasedOnAdjusted(this BlockBodyProjectableCallTests.Entity entity)
{
if (entity.GetDoubled() > 200)
@@ -187,13 +187,13 @@ public static string GetCategoryBasedOnAdjusted(this BlockBodyProjectableCallTes
}
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int CombineProjectableMethods(this BlockBodyProjectableCallTests.Entity entity)
{
return entity.GetDoubled() + entity.GetConstant();
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetLabelBasedOnCategory(this BlockBodyProjectableCallTests.Entity entity)
{
switch (entity.GetCategory())
@@ -207,7 +207,7 @@ public static string GetLabelBasedOnCategory(this BlockBodyProjectableCallTests.
}
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetDescriptionByLevel(this BlockBodyProjectableCallTests.Entity entity)
{
return entity.GetLevel() switch
@@ -219,7 +219,7 @@ public static string GetDescriptionByLevel(this BlockBodyProjectableCallTests.En
};
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int CalculateUsingProjectable(this BlockBodyProjectableCallTests.Entity entity)
{
var doubled = entity.GetDoubled();
@@ -227,13 +227,13 @@ public static int CalculateUsingProjectable(this BlockBodyProjectableCallTests.E
return withConstant * 2;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static int GetNestedProjectableCall(this BlockBodyProjectableCallTests.Entity entity)
{
return entity.GetAdjustedWithConstant() + 10;
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetStatusWithProjectableCheck(this BlockBodyProjectableCallTests.Entity entity)
{
if (entity.IsHighValue())
@@ -245,13 +245,13 @@ public static string GetStatusWithProjectableCheck(this BlockBodyProjectableCall
return "Normal";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetConditionalProjectable(this BlockBodyProjectableCallTests.Entity entity)
{
return entity.IsActive ? entity.GetCategory() : "Inactive";
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity entity)
{
var doubled = entity.GetDoubled();
@@ -264,7 +264,7 @@ public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity
return entity.GetLevel();
}
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public static bool IsComplexCondition(this BlockBodyProjectableCallTests.Entity entity)
{
return entity.IsActive && entity.IsHighValue() || entity.GetDoubled() > 150;
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt
new file mode 100644
index 0000000..1614e52
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar + 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt
new file mode 100644
index 0000000..c9f2bbb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt
new file mode 100644
index 0000000..c9f2bbb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => 1;
+ }
+ }
+}
\ 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 ae09af6..b459e43 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -201,6 +201,179 @@ class C {
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public Task ProjectablePropertyWithExplicitExpressionGetter()
+ {
+ // Tests explicit getter with expression body: { get => expression; }
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ [Projectable]
+ public int Foo { get => 1; }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectablePropertyWithExplicitBlockGetter()
+ {
+ // Tests explicit getter with block body: { get { return expression; } }
+ // Requires AllowBlockBody = true
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ [Projectable(AllowBlockBody = true)]
+ public int Foo { get { return 1; } }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableComputedPropertyWithExplicitExpressionGetter()
+ {
+ // Tests explicit getter with expression body accessing other properties
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable]
+ public int Foo { get => Bar + 1; }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+// [Fact]
+// public Task ProjectableComputedPropertyWithExplicitBlockGetter()
+// {
+// // Tests explicit getter with block body accessing other properties
+// // Requires AllowBlockBody = true
+// var compilation = CreateCompilation(@"
+// using System;
+// using EntityFrameworkCore.Projectables;
+// namespace Foo {
+// class C {
+// public int Bar { get; set; }
+//
+// [Projectable(AllowBlockBody = true)]
+// public int Foo { get { return Bar + 1; } }
+// }
+// }
+// ");
+//
+// var result = RunGenerator(compilation);
+//
+// Assert.Empty(result.Diagnostics);
+// Assert.Single(result.GeneratedTrees);
+//
+// return Verifier.Verify(result.GeneratedTrees[0].ToString());
+// }
+
+// [Fact]
+// public Task ProjectablePropertyWithExplicitBlockGetterUsingThis()
+// {
+// // Tests explicit getter with block body using 'this' qualifier
+// // Requires AllowBlockBody = true
+// var compilation = CreateCompilation(@"
+// using System;
+// using EntityFrameworkCore.Projectables;
+// namespace Foo {
+// class C {
+// public int Bar { get; set; }
+//
+// [Projectable(AllowBlockBody = true)]
+// public int Foo { get { return this.Bar; } }
+// }
+// }
+// ");
+//
+// var result = RunGenerator(compilation);
+//
+// Assert.Empty(result.Diagnostics);
+// Assert.Single(result.GeneratedTrees);
+//
+// return Verifier.Verify(result.GeneratedTrees[0].ToString());
+// }
+
+// [Fact]
+// public Task ProjectablePropertyWithExplicitBlockGetterAndMethodCall()
+// {
+// // Tests explicit getter with block body calling other methods
+// // Requires AllowBlockBody = true
+// var compilation = CreateCompilation(@"
+// using System;
+// using EntityFrameworkCore.Projectables;
+// namespace Foo {
+// class C {
+// public int Bar() => 1;
+//
+// [Projectable(AllowBlockBody = true)]
+// public int Foo { get { return Bar(); } }
+// }
+// }
+// ");
+//
+// var result = RunGenerator(compilation);
+//
+// Assert.Empty(result.Diagnostics);
+// Assert.Single(result.GeneratedTrees);
+//
+// return Verifier.Verify(result.GeneratedTrees[0].ToString());
+// }
+
+ [Fact]
+ public void ProjectablePropertyWithExplicitBlockGetter_WithoutAllowBlockBody_EmitsWarning()
+ {
+ // Tests that block-bodied property getter without AllowBlockBody = true emits a warning
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ [Projectable]
+ public int Foo { get { return 1; } }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ // Should have a warning about experimental feature
+ var diagnostic = Assert.Single(result.Diagnostics);
+ Assert.Equal("EFP0001", diagnostic.Id);
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ }
+
[Fact]
public Task MoreComplexProjectableComputedProperty()
@@ -470,28 +643,6 @@ static class C {
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
- [Fact]
- public void BlockBodiedMember_RaisesDiagnostics()
- {
- var compilation = CreateCompilation(@"
-using System;
-using EntityFrameworkCore.Projectables;
-namespace Foo {
- class C {
- [Projectable]
- public int Foo
- {
- get => 1;
- }
- }
-}
-");
-
- var result = RunGenerator(compilation);
-
- Assert.Single(result.Diagnostics);
- }
-
[Fact]
public void BlockBodiedMethod_NoLongerRaisesDiagnostics()
{
@@ -500,7 +651,7 @@ public void BlockBodiedMethod_NoLongerRaisesDiagnostics()
using EntityFrameworkCore.Projectables;
namespace Foo {
class C {
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
return 1;
@@ -1987,7 +2138,7 @@ public Task BlockBodiedMethod_SimpleReturn()
using EntityFrameworkCore.Projectables;
namespace Foo {
class C {
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
return 42;
@@ -2014,7 +2165,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
return Bar + 10;
@@ -2041,7 +2192,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
if (Bar > 10)
@@ -2075,7 +2226,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public string Foo()
{
if (Bar > 10)
@@ -2113,7 +2264,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
var temp = Bar * 2;
@@ -2141,7 +2292,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
var a = Bar * 2;
@@ -2170,7 +2321,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
var threshold = Bar * 2;
@@ -2205,7 +2356,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public string Foo()
{
var value = Bar * 2;
@@ -2241,7 +2392,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
if (Bar > 10)
@@ -2272,7 +2423,7 @@ public Task BlockBodiedMethod_WithMultipleParameters()
using EntityFrameworkCore.Projectables;
namespace Foo {
class C {
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Add(int a, int b)
{
return a + b;
@@ -2300,7 +2451,7 @@ class C {
public int Bar { get; set; }
public bool IsActive { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
if (IsActive && Bar > 0)
@@ -2335,7 +2486,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
if (Bar > 10)
@@ -2366,7 +2517,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int? Foo()
{
if (Bar > 10)
@@ -2396,7 +2547,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public string Foo()
{
switch (Bar)
@@ -2431,7 +2582,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public string Foo()
{
switch (Bar)
@@ -2469,7 +2620,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public string? Foo()
{
switch (Bar)
@@ -2502,7 +2653,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
Bar = 10;
@@ -2531,7 +2682,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
Bar += 10;
@@ -2560,7 +2711,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
var x = 5;
@@ -2590,7 +2741,7 @@ namespace Foo {
class C {
public int Bar { get; set; }
- [Projectable]
+ [Projectable(AllowBlockBody = true)]
public int Foo()
{
Console.WriteLine(""test"");
@@ -3170,6 +3321,58 @@ public record Entity
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public void BlockBodiedMethod_WithoutAllowFlag_EmitsWarning()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class C {
+ public int Value { get; set; }
+
+ [Projectable]
+ public int GetDouble()
+ {
+ return Value * 2;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ // Should have a warning about experimental feature
+ var diagnostic = Assert.Single(result.Diagnostics);
+ Assert.Equal("EFP0001", diagnostic.Id);
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ }
+
+ [Fact]
+ public void BlockBodiedMethod_WithAllowFlag_NoWarning()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class C {
+ public int Value { get; set; }
+
+ [Projectable(AllowBlockBody = true)]
+ public int GetDouble()
+ {
+ return Value * 2;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ // Should have no warnings
+ Assert.Empty(result.Diagnostics);
+ }
+
#region Helpers
Compilation CreateCompilation(string source, bool expectedToCompile = true)
From 3788729afd29dec42d6e978b0ffd1eaa6548a300 Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Wed, 18 Feb 2026 12:58:35 +0100
Subject: [PATCH 24/26] Fix block bodied properties
---
.../ProjectableInterpreter.cs | 17 +-
...opertyWithExplicitBlockGetter.verified.txt | 17 ++
...licitBlockGetterAndMethodCall.verified.txt | 17 ++
...hExplicitBlockGetterUsingThis.verified.txt | 17 ++
.../ProjectionExpressionGeneratorTests.cs | 154 +++++++++---------
5 files changed, 143 insertions(+), 79 deletions(-)
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
index 356420b..3037fb1 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs
@@ -394,11 +394,11 @@ x is IPropertySymbol xProperty &&
else if (memberBody is PropertyDeclarationSyntax propertyDeclarationSyntax)
{
ExpressionSyntax? bodyExpression = null;
+ var isBlockBodiedGetter = false;
// Expression-bodied property: int Prop => value;
if (propertyDeclarationSyntax.ExpressionBody is not null)
{
-
bodyExpression = propertyDeclarationSyntax.ExpressionBody.Expression;
}
else if (propertyDeclarationSyntax.AccessorList is not null)
@@ -424,6 +424,15 @@ x is IPropertySymbol xProperty &&
var blockConverter = new BlockStatementConverter(context, expressionSyntaxRewriter);
bodyExpression = blockConverter.TryConvertBlock(getter.Body, memberSymbol.Name);
+ isBlockBodiedGetter = true;
+
+ if (bodyExpression is null)
+ {
+ // Diagnostics already reported by BlockStatementConverter
+ return null;
+ }
+
+ // The expression has already been rewritten by BlockStatementConverter, so we don't rewrite it again
}
}
@@ -437,7 +446,11 @@ x is IPropertySymbol xProperty &&
var returnType = declarationSyntaxRewriter.Visit(propertyDeclarationSyntax.Type);
descriptor.ReturnTypeName = returnType.ToString();
- descriptor.ExpressionBody = (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression);
+
+ // Only rewrite expression-bodied properties, block-bodied getters are already rewritten
+ descriptor.ExpressionBody = isBlockBodiedGetter
+ ? bodyExpression
+ : (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression);
}
else
{
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt
new file mode 100644
index 0000000..1614e52
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar + 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt
new file mode 100644
index 0000000..fb4be05
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt
new file mode 100644
index 0000000..3ad21d6
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_C_Foo
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.C @this) => @this.Bar;
+ }
+ }
+}
\ 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 b459e43..ea03112 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -273,83 +273,83 @@ class C {
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
-// [Fact]
-// public Task ProjectableComputedPropertyWithExplicitBlockGetter()
-// {
-// // Tests explicit getter with block body accessing other properties
-// // Requires AllowBlockBody = true
-// var compilation = CreateCompilation(@"
-// using System;
-// using EntityFrameworkCore.Projectables;
-// namespace Foo {
-// class C {
-// public int Bar { get; set; }
-//
-// [Projectable(AllowBlockBody = true)]
-// public int Foo { get { return Bar + 1; } }
-// }
-// }
-// ");
-//
-// var result = RunGenerator(compilation);
-//
-// Assert.Empty(result.Diagnostics);
-// Assert.Single(result.GeneratedTrees);
-//
-// return Verifier.Verify(result.GeneratedTrees[0].ToString());
-// }
-
-// [Fact]
-// public Task ProjectablePropertyWithExplicitBlockGetterUsingThis()
-// {
-// // Tests explicit getter with block body using 'this' qualifier
-// // Requires AllowBlockBody = true
-// var compilation = CreateCompilation(@"
-// using System;
-// using EntityFrameworkCore.Projectables;
-// namespace Foo {
-// class C {
-// public int Bar { get; set; }
-//
-// [Projectable(AllowBlockBody = true)]
-// public int Foo { get { return this.Bar; } }
-// }
-// }
-// ");
-//
-// var result = RunGenerator(compilation);
-//
-// Assert.Empty(result.Diagnostics);
-// Assert.Single(result.GeneratedTrees);
-//
-// return Verifier.Verify(result.GeneratedTrees[0].ToString());
-// }
-
-// [Fact]
-// public Task ProjectablePropertyWithExplicitBlockGetterAndMethodCall()
-// {
-// // Tests explicit getter with block body calling other methods
-// // Requires AllowBlockBody = true
-// var compilation = CreateCompilation(@"
-// using System;
-// using EntityFrameworkCore.Projectables;
-// namespace Foo {
-// class C {
-// public int Bar() => 1;
-//
-// [Projectable(AllowBlockBody = true)]
-// public int Foo { get { return Bar(); } }
-// }
-// }
-// ");
-//
-// var result = RunGenerator(compilation);
-//
-// Assert.Empty(result.Diagnostics);
-// Assert.Single(result.GeneratedTrees);
-//
-// return Verifier.Verify(result.GeneratedTrees[0].ToString());
-// }
+ [Fact]
+ public Task ProjectableComputedPropertyWithExplicitBlockGetter()
+ {
+ // Tests explicit getter with block body accessing other properties
+ // Requires AllowBlockBody = true
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable(AllowBlockBody = true)]
+ public int Foo { get { return Bar + 1; } }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectablePropertyWithExplicitBlockGetterUsingThis()
+ {
+ // Tests explicit getter with block body using 'this' qualifier
+ // Requires AllowBlockBody = true
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar { get; set; }
+
+ [Projectable(AllowBlockBody = true)]
+ public int Foo { get { return this.Bar; } }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectablePropertyWithExplicitBlockGetterAndMethodCall()
+ {
+ // Tests explicit getter with block body calling other methods
+ // Requires AllowBlockBody = true
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Bar() => 1;
+
+ [Projectable(AllowBlockBody = true)]
+ public int Foo { get { return Bar(); } }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
[Fact]
public void ProjectablePropertyWithExplicitBlockGetter_WithoutAllowBlockBody_EmitsWarning()
From f2d0ec8974e5978ae3fdb37eff9b4a539e183853 Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Wed, 18 Feb 2026 13:37:34 +0100
Subject: [PATCH 25/26] Simplify code and add xmldocs
---
.../BlockStatementConverter.cs | 153 ++++++++++++------
1 file changed, 101 insertions(+), 52 deletions(-)
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index 55abd92..41a5b4c 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -44,6 +44,9 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
return TryConvertStatements(block.Statements.ToList(), memberName);
}
+ ///
+ /// Tries to convert a list of statements into a single expression. This is used for the body of the method or property.
+ ///
private ExpressionSyntax? TryConvertStatements(List statements, string memberName)
{
if (statements.Count == 0)
@@ -146,6 +149,9 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax
return TryConvertStatement(lastStatement, memberName);
}
+ ///
+ /// Processes a local variable declaration statement, rewriting the initializer and storing it in the local variables dictionary.
+ ///
private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDecl, string memberName)
{
foreach (var variable in localDecl.Declaration.Variables)
@@ -171,6 +177,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
return true;
}
+ ///
+ /// Tries to convert a single statement into an expression. This is used for return statements, if statements, and switch statements.
+ ///
private ExpressionSyntax? TryConvertStatement(StatementSyntax statement, string memberName)
{
switch (statement)
@@ -213,6 +222,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
}
}
+ ///
+ /// Converts a return statement to its expression, after rewriting it and replacing any local variable references.
+ ///
private ExpressionSyntax? TryConvertReturnStatement(ReturnStatementSyntax returnStmt, string memberName)
{
if (returnStmt.Expression == null)
@@ -230,6 +242,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
return expression;
}
+ ///
+ /// Converts an if statement (with optional else) to a conditional expression.
+ ///
private ConditionalExpressionSyntax? TryConvertIfStatement(IfStatementSyntax ifStmt, string memberName)
{
// Convert if-else to conditional (ternary) expression
@@ -272,12 +287,16 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
);
}
+ ///
+ /// Converts a switch statement to nested conditional expressions.
+ ///
private ExpressionSyntax? TryConvertSwitchStatement(SwitchStatementSyntax switchStmt, string memberName)
{
// Convert switch statement to nested conditional expressions
// Process sections in reverse order to build from the default case up
- var switchExpression = (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression);
+ var switchExpression =
+ (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression);
// Replace any local variable references in the switch expression
switchExpression = ReplaceLocalVariables(switchExpression);
@@ -374,10 +393,12 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
return currentExpression;
}
+ ///
+ /// Converts a switch section to an expression. This assumes the section has already been validated to only contain supported statements.
+ ///
private ExpressionSyntax? ConvertSwitchSection(SwitchSectionSyntax section, string memberName)
{
// Convert the statements in the switch section
- // Most switch sections end with break, return, or throw
var statements = section.Statements.ToList();
// Remove trailing break statements as they're not needed in expressions
@@ -386,21 +407,18 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
statements = statements.Take(statements.Count - 1).ToList();
}
- if (statements.Count != 0)
+ if (statements.Count > 0)
{
return TryConvertStatements(statements, memberName);
}
- // Use the section's first label location for error reporting
+ // Empty section - report diagnostic
var firstLabel = section.Labels.FirstOrDefault();
- if (firstLabel == null)
- {
- return null;
- }
-
+ var location = firstLabel?.GetLocation() ?? section.GetLocation();
+
var diagnostic = Diagnostic.Create(
Diagnostics.UnsupportedStatementInBlockBody,
- firstLabel.GetLocation(),
+ location,
memberName,
"Switch section must have at least one statement"
);
@@ -409,60 +427,94 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec
}
+ ///
+ /// Replaces references to local variables in the given expression with their initializer expressions.
+ ///
private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression)
{
// Use a rewriter to replace local variable references with their initializer expressions
var rewriter = new LocalVariableReplacer(_localVariables);
return (ExpressionSyntax)rewriter.Visit(expression);
}
-
+
+ ///
+ /// Analyzes an expression statement for side effects. If it has side effects, reports a diagnostic and returns null.
+ ///
private ExpressionSyntax? AnalyzeExpressionStatement(ExpressionStatementSyntax exprStmt, string memberName)
{
var expression = exprStmt.Expression;
- // Check for specific side effects
- switch (expression)
+ // Check for specific side effects that are always errors
+ if (HasSideEffects(expression, out var errorMessage))
{
- case AssignmentExpressionSyntax assignment:
- ReportSideEffect(assignment, GetAssignmentErrorMessage(assignment));
+ ReportSideEffect(expression, errorMessage);
+ return null;
+ }
+
+ // Check for potentially impure method calls
+ if (expression is InvocationExpressionSyntax invocation)
+ {
+ if (!IsProjectableMethodCall(invocation, out var warningMessage))
+ {
+ ReportPotentialSideEffect(invocation, warningMessage);
return null;
-
- case PostfixUnaryExpressionSyntax postfix when
+ }
+ }
+
+ // Expression statements without side effects are still not supported in the current design
+ ReportUnsupportedStatement(exprStmt, memberName,
+ "Expression statements are not supported in projectable methods. Consider removing this statement or converting it to a return statement.");
+ return null;
+ }
+
+ ///
+ /// Checks if an expression has side effects.
+ ///
+ private bool HasSideEffects(ExpressionSyntax expression, out string errorMessage)
+ {
+ return expression switch
+ {
+ AssignmentExpressionSyntax assignment => (errorMessage = GetAssignmentErrorMessage(assignment)) != null,
+
+ PostfixUnaryExpressionSyntax postfix when
postfix.IsKind(SyntaxKind.PostIncrementExpression) ||
- postfix.IsKind(SyntaxKind.PostDecrementExpression):
- ReportSideEffect(postfix, $"Increment/decrement operator '{postfix.OperatorToken.Text}' has side effects and cannot be used in projectable methods");
- return null;
-
- case PrefixUnaryExpressionSyntax prefix when
+ postfix.IsKind(SyntaxKind.PostDecrementExpression)
+ => (errorMessage = $"Increment/decrement operator '{postfix.OperatorToken.Text}' has side effects and cannot be used in projectable methods") != null,
+
+ PrefixUnaryExpressionSyntax prefix when
prefix.IsKind(SyntaxKind.PreIncrementExpression) ||
- prefix.IsKind(SyntaxKind.PreDecrementExpression):
- ReportSideEffect(prefix, $"Increment/decrement operator '{prefix.OperatorToken.Text}' has side effects and cannot be used in projectable methods");
- return null;
+ prefix.IsKind(SyntaxKind.PreDecrementExpression)
+ => (errorMessage = $"Increment/decrement operator '{prefix.OperatorToken.Text}' has side effects and cannot be used in projectable methods") != null,
+
+ _ => (errorMessage = string.Empty) == null
+ };
+ }
+
+ ///
+ /// Checks if a method invocation is to a projectable method.
+ ///
+ private bool IsProjectableMethodCall(InvocationExpressionSyntax invocation, out string warningMessage)
+ {
+ var symbolInfo = _expressionRewriter.GetSemanticModel().GetSymbolInfo(invocation);
+ if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
+ {
+ var hasProjectableAttr = methodSymbol.GetAttributes()
+ .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute");
- case InvocationExpressionSyntax invocation:
- // Check if this is a potentially impure method call
- var symbolInfo = _expressionRewriter.GetSemanticModel().GetSymbolInfo(invocation);
- if (symbolInfo.Symbol is IMethodSymbol methodSymbol)
- {
- // Check if method has [Projectable] attribute - those are safe
- var hasProjectableAttr = methodSymbol.GetAttributes()
- .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute");
-
- if (!hasProjectableAttr)
- {
- ReportPotentialSideEffect(invocation,
- $"Method call '{methodSymbol.Name}' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods");
- return null;
- }
- }
- break;
+ if (!hasProjectableAttr)
+ {
+ warningMessage = $"Method call '{methodSymbol.Name}' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods";
+ return false;
+ }
}
- // If we got here, it's an expression statement we don't support
- ReportUnsupportedStatement(exprStmt, memberName, "Expression statements are not supported in projectable methods");
- return null;
+ warningMessage = string.Empty;
+ return true;
}
+ ///
+ /// Generates an error message for an assignment expression, indicating that it has side effects and cannot be used in projectable methods.
+ ///
private string GetAssignmentErrorMessage(AssignmentExpressionSyntax assignment)
{
var operatorText = assignment.OperatorToken.Text;
@@ -473,13 +525,11 @@ private string GetAssignmentErrorMessage(AssignmentExpressionSyntax assignment)
{
return $"Property assignment '{memberAccess.Name}' has side effects and cannot be used in projectable methods";
}
- return $"Assignment operation has side effects and cannot be used in projectable methods";
- }
- else
- {
- // Compound assignment like +=, -=, etc.
- return $"Compound assignment operator '{operatorText}' has side effects and cannot be used in projectable methods";
+ return "Assignment operation has side effects and cannot be used in projectable methods";
}
+
+ // Compound assignment like +=, -=, etc.
+ return $"Compound assignment operator '{operatorText}' has side effects and cannot be used in projectable methods";
}
private void ReportSideEffect(SyntaxNode node, string message)
@@ -513,7 +563,6 @@ private void ReportUnsupportedStatement(StatementSyntax statement, string member
_context.ReportDiagnostic(diagnostic);
}
-
private class LocalVariableReplacer : CSharpSyntaxRewriter
{
private readonly Dictionary _localVariables;
From 9b7e61a9a760d108eb2738b845c27324b075238c Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Wed, 18 Feb 2026 15:05:04 +0100
Subject: [PATCH 26/26] Handle code review suggestions and fix an operator
precedence issue with parenthesis
---
.../AnalyzerReleases.Shipped.md | 12 ++++++------
.../BlockStatementConverter.cs | 6 ++++--
...eMethod_WithLocalVariable.DotNet10_0.verified.txt | 2 +-
...leMethod_WithLocalVariable.DotNet9_0.verified.txt | 2 +-
...gProjectableMethod_WithLocalVariable.verified.txt | 2 +-
...BlockBodiedMethod_LocalInIfCondition.verified.txt | 2 +-
...BodiedMethod_LocalInSwitchExpression.verified.txt | 2 +-
....BlockBodiedMethod_WithLocalVariable.verified.txt | 2 +-
...dMethod_WithTransitiveLocalVariables.verified.txt | 2 +-
9 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
index 7bd9067..253db78 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md
@@ -3,15 +3,15 @@
### New Rules
Rule ID | Category | Severity | Notes
---------|----------|----------|---------------------------------------------------------------------------
-EFP0003 | Design | Warning | Unsupported statement in block-bodied method
-EFP0004 | Design | Error | Statement with side effects in block-bodied method
-EFP0005 | Design | Warning | Potential side effect in block-bodied method
-EFP0006 | Design | Error | Method or property should expose an body definition (block or expression)
+--------|----------|----------|-------------------------------------------------------------------------
+EFP0003 | Design | Warning | Unsupported statement in block-bodied method
+EFP0004 | Design | Error | Statement with side effects in block-bodied method
+EFP0005 | Design | Warning | Potential side effect in block-bodied method
+EFP0006 | Design | Error | Method or property should expose a body definition (block or expression)
### Changed Rules
-Rule ID | New Category | New Severity | Old Category | Old Severity | Notes";
+Rule ID | New Category | New Severity | Old Category | Old Severity | Notes
--------|--------------|--------------|--------------|--------------|-----------------------------------------------------------------
EFP0001 | Design | Warning | Design | Error | Changed to warning for experimental block-bodied members support
diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
index 41a5b4c..16f7fae 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs
@@ -577,8 +577,10 @@ public LocalVariableReplacer(Dictionary localVariables
var identifier = node.Identifier.Text;
if (_localVariables.TryGetValue(identifier, out var replacement))
{
- // Replace the identifier with the expression it was initialized with
- return replacement.WithTriviaFrom(node);
+ // Replace the identifier with the expression it was initialized with,
+ // wrapping in parentheses to preserve operator precedence.
+ var parenthesized = SyntaxFactory.ParenthesizedExpression(replacement.WithoutTrivia());
+ return parenthesized.WithTriviaFrom(node);
}
return base.VisitIdentifierName(node);
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt
index 0294ea7..ae5ad93 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt
@@ -1,2 +1,2 @@
-SELECT [e].[Value] * 2 + 84
+SELECT ([e].[Value] * 2 + 42) * 2
FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt
index 0294ea7..ae5ad93 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt
@@ -1,2 +1,2 @@
-SELECT [e].[Value] * 2 + 84
+SELECT ([e].[Value] * 2 + 42) * 2
FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt
index 0294ea7..ae5ad93 100644
--- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt
@@ -1,2 +1,2 @@
-SELECT [e].[Value] * 2 + 84
+SELECT ([e].[Value] * 2 + 42) * 2
FROM [Entity] AS [e]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt
index e940c26..47b44c4 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt
@@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated
{
static global::System.Linq.Expressions.Expression> Expression()
{
- return (global::Foo.C @this) => @this.Bar * 2 > 10 ? 1 : 0;
+ return (global::Foo.C @this) => (@this.Bar * 2) > 10 ? 1 : 0;
}
}
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt
index 0a7e7da..ce11b5b 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt
@@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated
{
static global::System.Linq.Expressions.Expression> Expression()
{
- return (global::Foo.C @this) => @this.Bar * 2 == 2 ? "Two" : @this.Bar * 2 == 4 ? "Four" : "Other";
+ return (global::Foo.C @this) => (@this.Bar * 2) == 2 ? "Two" : (@this.Bar * 2) == 4 ? "Four" : "Other";
}
}
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt
index d863659..44c2e0f 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt
@@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated
{
static global::System.Linq.Expressions.Expression> Expression()
{
- return (global::Foo.C @this) => @this.Bar * 2 + 5;
+ return (global::Foo.C @this) => (@this.Bar * 2) + 5;
}
}
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt
index 24ae821..3e8b98c 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt
@@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated
{
static global::System.Linq.Expressions.Expression> Expression()
{
- return (global::Foo.C @this) => @this.Bar * 2 + 5 + 10;
+ return (global::Foo.C @this) => ((@this.Bar * 2) + 5) + 10;
}
}
}
\ No newline at end of file