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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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 From 37c1bb945533c7c24b938ba2c6833cec78008cd3 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sat, 21 Feb 2026 10:22:46 +0100 Subject: [PATCH 27/30] Add comprehensive docs --- .gitignore | 4 + README.md | 298 +-- docs/.vitepress/config.mts | 86 + docs/BlockBodiedMembers.md | 410 ---- docs/advanced/block-bodied-members.md | 324 +++ docs/advanced/how-it-works.md | 176 ++ docs/advanced/limitations.md | 156 ++ docs/advanced/query-compiler-pipeline.md | 158 ++ docs/guide/extension-methods.md | 124 ++ docs/guide/installation.md | 102 + docs/guide/introduction.md | 95 + docs/guide/projectable-methods.md | 118 + docs/guide/projectable-properties.md | 136 ++ docs/guide/quickstart.md | 151 ++ docs/index.md | 72 + docs/package-lock.json | 2313 ++++++++++++++++++++ docs/package.json | 10 + docs/recipes/computed-properties.md | 157 ++ docs/recipes/enum-display-names.md | 172 ++ docs/recipes/nullable-navigation.md | 159 ++ docs/recipes/reusable-query-filters.md | 167 ++ docs/reference/compatibility-mode.md | 111 + docs/reference/diagnostics.md | 296 +++ docs/reference/expand-enum-methods.md | 169 ++ docs/reference/null-conditional-rewrite.md | 152 ++ docs/reference/projectable-attribute.md | 149 ++ docs/reference/use-member-body.md | 106 + 27 files changed, 5703 insertions(+), 668 deletions(-) create mode 100644 docs/.vitepress/config.mts delete mode 100644 docs/BlockBodiedMembers.md create mode 100644 docs/advanced/block-bodied-members.md create mode 100644 docs/advanced/how-it-works.md create mode 100644 docs/advanced/limitations.md create mode 100644 docs/advanced/query-compiler-pipeline.md create mode 100644 docs/guide/extension-methods.md create mode 100644 docs/guide/installation.md create mode 100644 docs/guide/introduction.md create mode 100644 docs/guide/projectable-methods.md create mode 100644 docs/guide/projectable-properties.md create mode 100644 docs/guide/quickstart.md create mode 100644 docs/index.md create mode 100644 docs/package-lock.json create mode 100644 docs/package.json create mode 100644 docs/recipes/computed-properties.md create mode 100644 docs/recipes/enum-display-names.md create mode 100644 docs/recipes/nullable-navigation.md create mode 100644 docs/recipes/reusable-query-filters.md create mode 100644 docs/reference/compatibility-mode.md create mode 100644 docs/reference/diagnostics.md create mode 100644 docs/reference/expand-enum-methods.md create mode 100644 docs/reference/null-conditional-rewrite.md create mode 100644 docs/reference/projectable-attribute.md create mode 100644 docs/reference/use-member-body.md diff --git a/.gitignore b/.gitignore index 4e57e37..a5b1eb7 100644 --- a/.gitignore +++ b/.gitignore @@ -366,3 +366,7 @@ FodyWeavers.xsd *.received.* .idea + +# Docs +/docs/.vitepress/cache/ +/docs/.vitepress/dist/ diff --git a/README.md b/README.md index 471c6f5..53e693d 100644 --- a/README.md +++ b/README.md @@ -4,31 +4,38 @@ Flexible projection magic for EF Core [![NuGet version (EntityFrameworkCore.Projectables)](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) [![.NET](https://github.com/koenbeuk/EntityFrameworkCore.Projectables/actions/workflows/build.yml/badge.svg)](https://github.com/koenbeuk/EntityFrameworkCore.Projectables/actions/workflows/build.yml) +Write properties and methods once — use them anywhere in your LINQ queries, translated to efficient SQL automatically. + +📖 **[Full documentation → projectables.github.io](https://projectables.github.io)** + ## NuGet packages -- EntityFrameworkCore.Projectables.Abstractions [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) [![NuGet](https://img.shields.io/nuget/dt/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) -- EntityFrameworkCore.Projectables [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) [![NuGet](https://img.shields.io/nuget/dt/EntityFrameworkCore.Projectables.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) -> Starting with V2 of this project we're binding against **EF Core 6**. If you're targeting **EF Core 5** or **EF Core 3.1** then you can use the latest v1 release. These are functionally equivalent. +| Package | | +|---|---| +| `EntityFrameworkCore.Projectables.Abstractions` | [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) [![Downloads](https://img.shields.io/nuget/dt/EntityFrameworkCore.Projectables.Abstractions.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) | +| `EntityFrameworkCore.Projectables` | [![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.Projectables.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) [![Downloads](https://img.shields.io/nuget/dt/EntityFrameworkCore.Projectables.svg?style=flat-square)](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) | +> **EF Core compatibility:** v1.x → EF Core 3.1 / 5 · v2.x+ → EF Core 6+ -## Getting started -1. Install the package from [NuGet](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) -2. Enable Projectables in your DbContext by adding: `dbContextOptions.UseProjectables()` -3. Implement projectable properties and methods, marking them with the `[Projectable]` attribute. -4. Explore our [samples](https://github.com/koenbeuk/EntityFrameworkCore.Projectables/tree/master/samples) and checkout our [Blog Post](https://onthedrift.com/posts/efcore-projectables/) for further guidance. +## Quick start -### Example -Assuming this sample: +```bash +dotnet add package EntityFrameworkCore.Projectables.Abstractions +dotnet add package EntityFrameworkCore.Projectables +``` + +Enable Projectables on your `DbContext`: ```csharp -class Order { - public int Id { get; set; } - public int UserId { get; set; } - public DateTime CreatedDate { get; set; } +options.UseSqlServer(connectionString) + .UseProjectables(); +``` + +Mark properties and methods with `[Projectable]`: +```csharp +class Order { public decimal TaxRate { get; set; } - - public User User { get; set; } public ICollection Items { get; set; } [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); @@ -38,261 +45,36 @@ class Order { public static class UserExtensions { [Projectable] - public static Order GetMostRecentOrderForUser(this User user, DateTime? cutoffDate) => + public static Order GetMostRecentOrder(this User user, DateTime? cutoffDate) => user.Orders .Where(x => cutoffDate == null || x.CreatedDate >= cutoffDate) .OrderByDescending(x => x.CreatedDate) .FirstOrDefault(); } - -var result = _dbContext.Users - .Where(x => x.UserName == "Jon") - .Select(x => new { - x.GetMostRecentOrderForUser(DateTime.UtcNow.AddDays(-30)).GrandTotal - }); - .FirstOrDefault(); -``` - -The following query gets generated (assuming SQL Server as a database provider) -```sql -DECLARE @__sampleUser_UserName_0 nvarchar(4000) = N'Jon'; - -SELECT ( - SELECT COALESCE(SUM([p].[ListPrice] * CAST([o].[Quantity] AS decimal(18,2))), 0.0) - FROM [OrderItem] AS [o] - INNER JOIN [Products] AS [p] ON [o].[ProductId] = [p].[Id] - WHERE ( - SELECT TOP(1) [o0].[Id] - FROM [Orders] AS [o0] - WHERE [u].[Id] = [o0].[UserId] AND [o0].[FulfilledDate] IS NOT NULL - ORDER BY [o0].[CreatedDate] DESC) IS NOT NULL AND ( - SELECT TOP(1) [o1].[Id] - FROM [Orders] AS [o1] - WHERE [u].[Id] = [o1].[UserId] AND [o1].[FulfilledDate] IS NOT NULL - ORDER BY [o1].[CreatedDate] DESC) = [o].[OrderId]) * ( - SELECT TOP(1) [o2].[TaxRate] - FROM [Orders] AS [o2] - WHERE [u].[Id] = [o2].[UserId] AND [o2].[FulfilledDate] IS NOT NULL - ORDER BY [o2].[CreatedDate] DESC) AS [GrandTotal] -FROM [Users] AS [u] -WHERE [u].[UserName] = @__sampleUser_UserName_0 -``` - -Projectable properties and methods have been inlined! the generated SQL could be improved but this is what EF Core (v8) gives us. - -### How it works -Essentially, there are two components: We have a source generator that can write companion expressions for properties and methods marked with the Projectable attribute. Then, we have a runtime component that intercepts any query and translates any call to a property or method marked with the Projectable attribute, translating the query to use the generated expression instead. - -### FAQ - -#### Are there currently any known limitations? -Currently, there is no support for overloaded methods. Each method name needs to be unique within a given type. - -#### Is this specific to a database provider? -No, the runtime component injects itself into the EFCore query compilation pipeline, thus having no impact on the database provider used. Of course, you're still limited to whatever your database provider can do. - -#### Are there performance implications that I should be aware of? -There are two compatibility modes: Limited and Full (Default). Most of the time, limited compatibility mode is sufficient. However, if you are running into issues with failed query compilation, then you may want to stick with Full compatibility mode. With Full compatibility mode, each query will first be expanded (any calls to Projectable properties and methods will be replaced by their respective expression) before being handed off to EFCore. (This is similar to how LinqKit/LinqExpander/Expressionify works.) Because of this additional step, there is a small performance impact. Limited compatibility mode is smart about things and only expands the query after it has been accepted by EF. The expanded query will then be stored in the Query Cache. With Limited compatibility, you will likely see increased performance over EFCore without projectables. - -#### Can I call additional properties and methods from my Projectable properties and methods? -Yes, you can! Any projectable property/method can call into other properties and methods as long as those properties/methods are native to EFCore or marked with a Projectable attribute. - -#### Can I use projectable extensions methods on non-entity types? -Yes you can. It's perfectly acceptable to have the following code: -```csharp -[Projectable] -public static int Squared(this int i) => i * i; -``` -Any call to squared given any int will perfectly translate to SQL. - -#### How do I deal with nullable properties -Expressions and Lamdas are different and not equal. Expressions can only express a subset of valid CSharp statements that are allowed in lambda's and arrow functions. One obvious limitation is the null-conditional operator. Consider the following example: -```csharp -[Projectable] -public static string? GetFullAddress(this User? user) => user?.Location?.AddressLine1 + " " + user?.Location.AddressLine2; -``` -This is a perfectly valid arrow function but it can't be translated directly to an expression tree. This Project will generate an error by default and suggest 2 solutions: Either you rewrite the function to explicitly check for nullables or you let the generator do that for you! - -Starting from the official release of V2, we can now hint the generator in how to translate this arrow function to an expression tree. We can say: -```csharp -[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] -``` -which will simply generate an expression tree that ignores the null-conditional operator. This generates: -```csharp -user.Location.AddressLine1 + " " + user.Location.AddressLine2 -``` -This is perfect for a database like SQL Server where nullability is implicit and if any of the arguments were to be null, the resulting value will be null. If you are dealing with CosmosDB (which may result to client-side evaluation) or want to be explicit about things. You can configure your projectable as such: -```csharp -[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] -``` -This will rewrite your expression to explicitly check for nullables. In the former example, this will be rewritten to: -```csharp -(user != null ? user.Location != null ? user.Location?.AddressLine1 + (user != null ? user.Location != null ? user.Location.AddressLine2 : null) : null) -``` -Note that using rewrite (not ignore) may increase the actual SQL query complexity being generated with some database providers such as SQL Server - -#### Can I use projectables in any part of my query? -Certainly, consider the following example: -```csharp -public class User -{ - public int Id { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - - [Projectable] - public string FullName => FirstName + " " + LastName; -} - -var query = dbContext.Users - .Where(x => x.FullName.Contains("Jon")) - .GroupBy(x => x.FullName) - .OrderBy(x => x.Key) - .Select(x => x.Key); -``` -Which generates the following SQL (SQLite syntax) -```sql -SELECT (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') -FROM "Users" AS "u" -WHERE ('Jon' = '') OR (instr((COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", ''), 'Jon') > 0) -GROUP BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') -ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') -``` - -#### 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) -[Projectable] -public string Level() => Value > 100 ? "High" : Value > 50 ? "Medium" : "Low"; - -// Block-bodied (now also supported!) -[Projectable(AllowBlockBody = true)] // Note: AllowBlockBody is required to remove the warning for experimental feature usage -public string Level() -{ - if (Value > 100) - return "High"; - else if (Value > 50) - return "Medium"; - else - return "Low"; -} ``` -> 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 members, etc.) with precise error messages. See [Block-Bodied Members Documentation](docs/BlockBodiedMembers.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. +Use them anywhere in your queries — they are **inlined into SQL automatically**: ```csharp -public enum OrderStatus -{ - [Display(Name = "Pending Review")] - Pending, - - [Display(Name = "Approved")] - Approved, - - [Display(Name = "Rejected")] - Rejected -} - -public static class EnumExtensions -{ - public static string GetDisplayName(this OrderStatus value) - { - // Your implementation here - return value.ToString(); - } - - public static bool IsApproved(this OrderStatus value) - { - return value == OrderStatus.Approved; - } - - public static int GetSortOrder(this OrderStatus value) - { - return (int)value; - } - - public static string Format(this OrderStatus value, string prefix) - { - return prefix + value.ToString(); - } -} - -public class Order -{ - public int Id { get; set; } - public OrderStatus Status { get; set; } - - [Projectable(ExpandEnumMethods = true)] - public string StatusName => Status.GetDisplayName(); - - [Projectable(ExpandEnumMethods = true)] - public bool IsStatusApproved => Status.IsApproved(); - - [Projectable(ExpandEnumMethods = true)] - public int StatusOrder => Status.GetSortOrder(); - - [Projectable(ExpandEnumMethods = true)] - public string FormattedStatus => Status.Format("Status: "); -} -``` - -This generates expression trees equivalent to: -```csharp -// For StatusName -@this.Status == OrderStatus.Pending ? GetDisplayName(OrderStatus.Pending) - : @this.Status == OrderStatus.Approved ? GetDisplayName(OrderStatus.Approved) - : @this.Status == OrderStatus.Rejected ? GetDisplayName(OrderStatus.Rejected) - : null - -// For IsStatusApproved (boolean) -@this.Status == OrderStatus.Pending ? false - : @this.Status == OrderStatus.Approved ? true - : @this.Status == OrderStatus.Rejected ? false - : default(bool) +var result = dbContext.Users + .Where(x => x.UserName == "Jon") + .Select(x => new { + x.GetMostRecentOrder(DateTime.UtcNow.AddDays(-30)).GrandTotal + }) + .FirstOrDefault(); ``` -Which EF Core translates to SQL CASE expressions: -```sql -SELECT CASE - WHEN [o].[Status] = 0 THEN N'Pending Review' - WHEN [o].[Status] = 1 THEN N'Approved' - WHEN [o].[Status] = 2 THEN N'Rejected' -END AS [StatusName] -FROM [Orders] AS [o] -``` +No client-side evaluation. No duplicated expressions. Just clean, efficient SQL. -The `ExpandEnumMethods` feature supports: -- **String return types** - returns `null` as the default fallback -- **Boolean return types** - returns `default(bool)` (false) as the default fallback -- **Integer return types** - returns `default(int)` (0) as the default fallback -- **Other value types** - returns `default(T)` as the default fallback -- **Nullable enum types** - wraps the expansion in a null check -- **Methods with parameters** - parameters are passed through to each enum value call -- **Enum properties on navigation properties** - works with nested navigation +## Documentation -#### How does this relate to [Expressionify](https://github.com/ClaveConsulting/Expressionify)? -Expressionify is a project that was launched before this project. It has some overlapping features and uses similar approaches. When I first published this project, I was not aware of its existence, so shame on me. Currently, Expressionify targets a more focused scope of what this project is doing, and thereby it seems to be more limiting in its capabilities. Check them out though! +The full documentation is hosted at **[projectables.github.io](https://projectables.github.io)** and covers: -#### How does this relate to LinqKit/LinqExpander/...? -There are a few projects like [LinqKit](https://github.com/scottksmith95/LINQKit) that were created before we had source generators in .NET. These are great options if you're stuck with classical EF or don't want to rely on code generation. Otherwise, I would suggest that EntityFrameworkCore.Projectables and Expressionify are superior approaches as they can rely on SourceGenerators to do most of the hard work. +- [Getting Started](https://projectables.github.io/guide/introduction) — installation, quick start, core concepts +- [Reference](https://projectables.github.io/reference/projectable-attribute) — `[Projectable]` attribute options, compatibility mode, diagnostics +- [Advanced](https://projectables.github.io/advanced/how-it-works) — internals, query compiler pipeline, block-bodied members +- [Recipes](https://projectables.github.io/recipes/computed-properties) — computed properties, enum display names, reusable query filters -#### Is the available for EFCore 3.1, 5 and 6? -V1 is targeting EF Core 5 and 3.1. V2 and V3 are targeting EF Core 6 and are compatible with EF Core 7. You can upgrade/downgrade between these versions based on your EF Core version requirements. +## License -#### What is next for this project? -TBD... However, one thing I'd like to improve is our expression generation logic as it's currently making a few assumptions (have yet to experience it breaking). Community contributions are very welcome! +MIT diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..fad18b1 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,86 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "EF Core Projectables", + description: "Flexible projection magic for EF Core — use properties and methods directly in your LINQ queries", + themeConfig: { + logo: '/logo.svg', + nav: [ + { text: 'Home', link: '/' }, + { text: 'Guide', link: '/guide/introduction' }, + { text: 'Reference', link: '/reference/projectable-attribute' }, + { text: 'Advanced', link: '/advanced/how-it-works' }, + { text: 'Recipes', link: '/recipes/computed-properties' }, + ], + + sidebar: { + '/guide/': [ + { + text: 'Getting Started', + items: [ + { text: 'Introduction', link: '/guide/introduction' }, + { text: 'Installation', link: '/guide/installation' }, + { text: 'Quick Start', link: '/guide/quickstart' }, + ] + }, + { + text: 'Core Concepts', + items: [ + { text: 'Projectable Properties', link: '/guide/projectable-properties' }, + { text: 'Projectable Methods', link: '/guide/projectable-methods' }, + { text: 'Extension Methods', link: '/guide/extension-methods' }, + ] + } + ], + '/reference/': [ + { + text: 'Reference', + items: [ + { text: '[Projectable] Attribute', link: '/reference/projectable-attribute' }, + { text: 'Compatibility Mode', link: '/reference/compatibility-mode' }, + { text: 'Null-Conditional Rewrite', link: '/reference/null-conditional-rewrite' }, + { text: 'Expand Enum Methods', link: '/reference/expand-enum-methods' }, + { text: 'Use Member Body', link: '/reference/use-member-body' }, + { text: 'Diagnostics', link: '/reference/diagnostics' }, + ] + } + ], + '/advanced/': [ + { + text: 'Advanced', + items: [ + { text: 'How It Works', link: '/advanced/how-it-works' }, + { text: 'Query Compiler Pipeline', link: '/advanced/query-compiler-pipeline' }, + { text: 'Block-Bodied Members', link: '/advanced/block-bodied-members' }, + { text: 'Limitations', link: '/advanced/limitations' }, + ] + } + ], + '/recipes/': [ + { + text: 'Recipes', + items: [ + { text: 'Computed Entity Properties', link: '/recipes/computed-properties' }, + { text: 'Enum Display Names', link: '/recipes/enum-display-names' }, + { text: 'Nullable Navigation Properties', link: '/recipes/nullable-navigation' }, + { text: 'Reusable Query Filters', link: '/recipes/reusable-query-filters' }, + ] + } + ], + }, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/koenbeuk/EntityFrameworkCore.Projectables' } + ], + + search: { + provider: 'local' + }, + + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © EntityFrameworkCore.Projectables Contributors' + } + } +}) diff --git a/docs/BlockBodiedMembers.md b/docs/BlockBodiedMembers.md deleted file mode 100644 index 63be89f..0000000 --- a/docs/BlockBodiedMembers.md +++ /dev/null @@ -1,410 +0,0 @@ -# Block-Bodied Methods Support - -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 - -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; -} - -// 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, 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) - -### 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 -// Pattern 1: Explicit null return -[Projectable] -public int? GetPremiumIfActive() -{ - if (IsActive) - { - return Value * 2; - } - return null; // Explicit return for all code paths -} - -// Pattern 2: Explicit fallback return -[Projectable] -public string GetStatus() -{ - if (IsActive) - { - return "Active"; - } - return "Inactive"; // Explicit fallback -} -``` - -### 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: - -### Unsupported Statements: -- While, for, foreach loops -- Try-catch-finally blocks -- Throw statements -- New object instantiation in statement position - -### Example of Unsupported Pattern: -```csharp -[Projectable] -public int GetValue() -{ - for (int i = 0; i < 10; i++) // ❌ Loops not supported - { - // ... - } - return 0; -} -``` - -Supported patterns: -```csharp -[Projectable] -public int GetValue() -{ - if (IsActive) // ✅ If without else is now supported! - { - return Value; - } - else - { - return 0; - } -} -``` - -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] -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. 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 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] -``` - -### If-Else 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] -``` - -## 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/advanced/block-bodied-members.md b/docs/advanced/block-bodied-members.md new file mode 100644 index 0000000..fd93202 --- /dev/null +++ b/docs/advanced/block-bodied-members.md @@ -0,0 +1,324 @@ +# Block-Bodied Members + +As of v6.x, EF Core Projectables supports **block-bodied** properties and methods decorated with `[Projectable]`, in addition to expression-bodied members (`=>`). + +::: warning Experimental Feature +Block-bodied member support is currently **experimental**. Set `AllowBlockBody = true` on the attribute to acknowledge this and suppress warning EFP0001. +::: + +## Why Block Bodies? + +Expression-bodied members are concise but can become hard to read with complex conditional logic: + +```csharp +// Hard to read as a nested ternary +[Projectable] +public string Level() => Value > 100 ? "High" : Value > 50 ? "Medium" : "Low"; + +// Much easier to read as a block body +[Projectable(AllowBlockBody = true)] +public string Level() +{ + if (Value > 100) + return "High"; + else if (Value > 50) + return "Medium"; + else + return "Low"; +} +``` + +Both generate **identical SQL** — the block body is converted to a ternary expression internally. + +## Enabling Block Bodies + +Add `AllowBlockBody = true` to suppress the experimental warning: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetCategory() +{ + if (Value > 100) + return "High"; + else + return "Low"; +} +``` + +## Supported Constructs + +### Simple Return Statements + +```csharp +[Projectable(AllowBlockBody = true)] +public int GetConstant() +{ + return 42; +} +``` + +--- + +### If-Else Statements + +If-else chains are converted to ternary (`? :`) expressions: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetCategory() +{ + if (Value > 100) + return "High"; + else if (Value > 50) + return "Medium"; + else + return "Low"; +} +// Converted to: Value > 100 ? "High" : Value > 50 ? "Medium" : "Low" +``` + +--- + +### If Without Else (Fallback Return) + +An `if` statement without an `else` is supported when followed by a fallback `return`: + +```csharp +// Pattern 1: explicit fallback return +[Projectable(AllowBlockBody = true)] +public string GetStatus() +{ + if (IsActive) + return "Active"; + return "Inactive"; // Fallback +} + +// Pattern 2: explicit null return +[Projectable(AllowBlockBody = true)] +public int? GetPremium() +{ + if (IsActive) + return Value * 2; + return null; +} +``` + +--- + +### Multiple Early Returns + +Multiple independent early-return `if` statements are converted to a nested ternary chain: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetValueCategory() +{ + if (Value > 100) return "Very High"; + if (Value > 50) return "High"; + if (Value > 10) return "Medium"; + return "Low"; +} +// → Value > 100 ? "Very High" : (Value > 50 ? "High" : (Value > 10 ? "Medium" : "Low")) +``` + +--- + +### Switch Statements + +Switch statements are converted to nested ternary expressions: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetValueLabel() +{ + switch (Value) + { + case 1: return "One"; + case 2: return "Two"; + case 3: return "Three"; + default: return "Many"; + } +} +``` + +Multiple cases mapping to the same result are collapsed: + +```csharp +switch (Value) +{ + case 1: + case 2: + return "Low"; + case 3: + case 4: + case 5: + return "Medium"; + default: + return "High"; +} +``` + +Generated 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] +``` + +--- + +### Local Variables + +Local variables declared at the method body level are **inlined** at each usage point: + +```csharp +[Projectable(AllowBlockBody = true)] +public int CalculateDouble() +{ + var doubled = Value * 2; + return doubled + 5; +} +// → (Value * 2) + 5 +``` + +Transitive inlining is supported: + +```csharp +[Projectable(AllowBlockBody = true)] +public int CalculateComplex() +{ + var a = Value * 2; + var b = a + 5; + return b + 10; +} +// → ((Value * 2) + 5) + 10 +``` + +::: warning Variable Duplication +If a local variable is referenced **multiple times**, its initializer is duplicated at each reference point. This can affect performance (and semantics if the initializer has side effects): + +```csharp +[Projectable(AllowBlockBody = true)] +public int Foo() +{ + var x = ExpensiveComputation(); // Inlined at each use + return x + x; // → ExpensiveComputation() + ExpensiveComputation() +} +``` +::: + +**Local variables are only supported at the method body level** — not inside nested blocks (inside `if`, `switch`, etc.). + +## SQL Output Examples + +### If-Else → CASE WHEN + +```csharp +public record Entity +{ + public int Value { get; set; } + public bool IsActive { get; set; } + + [Projectable(AllowBlockBody = true)] + public int GetAdjustedValue() + { + if (IsActive && Value > 0) + return Value * 2; + else + return 0; + } +} +``` + +Generated SQL: +```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] +``` + +### Switch → CASE WHEN IN + +```csharp +[Projectable(AllowBlockBody = true)] +public string Category +{ + get + { + switch (Status) + { + case 1: case 2: return "Low"; + case 3: case 4: case 5: return "Medium"; + default: return "High"; + } + } +} +``` + +Generated SQL: +```sql +SELECT CASE + WHEN [e].[Status] IN (1, 2) THEN N'Low' + WHEN [e].[Status] IN (3, 4, 5) THEN N'Medium' + ELSE N'High' +END +FROM [Entity] AS [e] +``` + +## Limitations and Unsupported Constructs + +The following statement types produce **warning EFP0003** and are not supported: + +| Construct | Reason | +|---|---| +| `while` / `for` / `foreach` loops | Cannot be represented as expression trees | +| `try` / `catch` / `finally` | Cannot be represented as expression trees | +| `throw` statements | Cannot be represented as expression trees | +| `new MyClass()` in statement position | Object instantiation not supported in this context | + +```csharp +// ❌ Warning EFP0003 — loops are not supported +[Projectable(AllowBlockBody = true)] +public int SumItems() +{ + int total = 0; + foreach (var item in Items) // EFP0003 + total += item.Price; + return total; +} + +// ✅ Use LINQ instead +[Projectable] +public int SumItems() => Items.Sum(i => i.Price); +``` + +## Side Effect Detection + +The generator actively detects statements with side effects and reports them as errors (EFP0004) or warnings (EFP0005). See [Diagnostics](/reference/diagnostics) for the full list. + +| Code | Diagnostic | +|---|---| +| `Bar = 10;` | ❌ EFP0004 — property assignment | +| `Bar += 10;` | ❌ EFP0004 — compound assignment | +| `Bar++;` | ❌ EFP0004 — increment/decrement | +| `Console.WriteLine("x");` | ⚠️ EFP0005 — non-projectable method call | + +## How the Conversion Works + +The `BlockStatementConverter` class in the source generator: + +1. Collects all local variable declarations at the method body level. +2. Identifies the `return` statements and their conditions. +3. Converts `if`/`else` chains into ternary expression syntax nodes. +4. Converts `switch` statements into nested ternary expressions (or `case IN (...)` optimized forms). +5. Substitutes local variable references with their initializer expressions (via `VariableReplacementRewriter`). +6. Passes the resulting expression syntax to the standard expression rewriter pipeline. + +The output is equivalent to what would have been produced by an expression-bodied member with the same logic. + diff --git a/docs/advanced/how-it-works.md b/docs/advanced/how-it-works.md new file mode 100644 index 0000000..d6682ed --- /dev/null +++ b/docs/advanced/how-it-works.md @@ -0,0 +1,176 @@ +# How It Works + +Understanding the internals of EF Core Projectables helps you use it effectively and debug issues when they arise. The library has two main components: a **build-time source generator** and a **runtime query interceptor**. + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────┐ +│ BUILD TIME │ +│ │ +│ Your C# code with [Projectable] members │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────┐ │ +│ │ Roslyn Source Generator │ │ +│ │ (ProjectionExpressionGenerator) │ +│ │ - Scans for [Projectable] │ │ +│ │ - Parses member bodies │ │ +│ │ - Generates Expression<> │ │ +│ │ companion classes │ │ +│ └─────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ Auto-generated *.g.cs files with Expression<> trees │ +└─────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────┐ +│ RUNTIME │ +│ │ +│ LINQ query using projectable member │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────┐ │ +│ │ ProjectableExpressionReplacer (ExpressionVisitor) │ +│ │ - Walks the LINQ expression tree │ +│ │ - Detects calls to [Projectable] members │ +│ │ - Loads generated Expression<> via reflection │ +│ │ - Substitutes the call with the expression │ +│ └─────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ Expanded expression tree (no [Projectable] calls) │ +│ │ │ +│ ▼ │ +│ Standard EF Core SQL translation → SQL query │ +└─────────────────────────────────────────────────────────┘ +``` + +## Build Time: The Source Generator + +### `ProjectionExpressionGenerator` + +This is the entry point for the Roslyn incremental source generator. It implements `IIncrementalGenerator` for high-performance, incremental code generation. + +**Pipeline:** +1. **Filter** — Uses `ForAttributeWithMetadataName` to efficiently find all `MemberDeclarationSyntax` nodes decorated with `[ProjectableAttribute]`. +2. **Interpret** — Calls `ProjectableInterpreter.GetDescriptor()` to extract all the information needed to generate code. +3. **Generate** — Produces a static class with an `Expression>` factory method. + +### `ProjectableInterpreter` + +Reads the attribute arguments, resolves the member's type information (namespace, generic parameters, containing classes), and extracts the expression body. + +**Key tasks:** +- Resolves `NullConditionalRewriteSupport`, `UseMemberBody`, `ExpandEnumMethods`, and `AllowBlockBody` from the attribute. +- Determines the correct parameter list for the generated lambda (including the implicit `@this` parameter for instance members and extension methods). +- Dispatches to `BlockStatementConverter` for block-bodied members. + +### `BlockStatementConverter` + +Converts block-bodied method statements into expression-tree-compatible forms: + +| Statement | Converted to | +|---|---| +| `if (cond) return A; else return B;` | `cond ? A : B` | +| `switch (x) { case 1: return "a"; }` | `x == 1 ? "a" : ...` | +| `var v = expr; return v + 1;` | Inline substitution: `expr + 1` | +| Multiple early `return` | Nested ternary chain | + +### Expression Rewriters + +After the body is extracted, several rewriters transform the expression syntax: + +| Rewriter | Purpose | +|---|---| +| `ExpressionSyntaxRewriter` | Rewrites `?.` operators based on `NullConditionalRewriteSupport` | +| `DeclarationSyntaxRewriter` | Adjusts member declarations for the generated class | +| `VariableReplacementRewriter` | Inlines local variables into the return expression | + +### Generated Code + +For a property like: + +```csharp +public class Order +{ + [Projectable] + public decimal GrandTotal => Subtotal + Tax; +} +``` + +The generator produces something like: + +```csharp +// Auto-generated — not visible in IntelliSense +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class Order__GrandTotal +{ + public static Expression> Expression() + => @this => @this.Subtotal + @this.Tax; +} +``` + +The class name is deterministic, based on namespace + class name + member name. + +### `ProjectionExpressionClassNameGenerator` + +Generates a stable, unique class name for each projectable member. Handles generics, overloads (via parameter type names), and nested classes. + +## Runtime: The Query Interceptor + +### How Queries Are Intercepted + +When `UseProjectables()` is called, the library registers custom implementations of EF Core's internal query infrastructure. Depending on the [Compatibility Mode](/reference/compatibility-mode): + +**Full mode** — registers a `CustomQueryCompiler` that wraps EF Core's default compiler. Before compiling any query, it calls `ProjectableExpressionReplacer.Replace()` on the raw LINQ expression. + +**Limited mode** — registers a `CustomQueryTranslationPreprocessor` (via `CustomQueryTranslationPreprocessorFactory`). This runs inside EF Core's own query pipeline after the query is accepted, so the expanded query benefits from EF Core's query cache. + +### `ProjectableExpressionReplacer` + +Inherits from `ExpressionVisitor`. Its `Visit` method walks the LINQ expression tree and looks for: + +- **Property accesses** that correspond to `[Projectable]` properties. +- **Method calls** that correspond to `[Projectable]` methods. + +For each hit, it: +1. Calls `ProjectionExpressionResolver.FindGeneratedExpression()` to locate the auto-generated expression class via reflection. +2. Uses `ExpressionArgumentReplacer` to substitute the lambda parameters with the actual arguments from the call site. +3. Replaces the original call node with the inlined expression body. + +The replacement is done recursively — if the inlined expression itself contains projectable calls, they are also expanded. + +### `ProjectionExpressionResolver` + +Discovers the auto-generated companion class by constructing the expected class name (using the same naming logic as the generator) and reflecting into the assembly. + +```csharp +// Roughly equivalent to: +var type = assembly.GetType("Order__GrandTotal"); +var method = type.GetMethod("Expression"); +var expression = (LambdaExpression)method.Invoke(null, null); +``` + +### `ExpressionArgumentReplacer` + +Replaces the `@this` parameter (and any method arguments) in the retrieved lambda with the actual expressions from the call site. This is standard expression tree parameter substitution. + +## Tracking Behavior Handling + +The replacer also manages EF Core's tracking behavior. When a projectable member is used in a `Select` projection, the replacer wraps the expanded query in a `AsNoTracking()` call if necessary, ensuring consistent behavior with and without projectables. + +## Summary + +| Phase | Component | Responsibility | +|---|---|---| +| Build | `ProjectionExpressionGenerator` | Source gen entry point, orchestration | +| Build | `ProjectableInterpreter` | Extract descriptor from attribute + syntax | +| Build | `BlockStatementConverter` | Block body → expression conversion | +| Build | `ExpressionSyntaxRewriter` | `?.` handling, null-conditional rewrite | +| Runtime | `CustomQueryCompiler` | Full mode: expand before EF Core | +| Runtime | `CustomQueryTranslationPreprocessor` | Limited mode: expand inside EF Core pipeline | +| Runtime | `ProjectableExpressionReplacer` | Walk and replace projectable calls | +| Runtime | `ProjectionExpressionResolver` | Locate generated expression via reflection | +| Runtime | `ExpressionArgumentReplacer` | Substitute parameters in lambda | + diff --git a/docs/advanced/limitations.md b/docs/advanced/limitations.md new file mode 100644 index 0000000..ff7bdee --- /dev/null +++ b/docs/advanced/limitations.md @@ -0,0 +1,156 @@ +# Limitations & Known Issues + +This page documents the current limitations of EF Core Projectables and guidance on how to work around them. + +## Method Overloading Is Not Supported + +Each projectable method name must be **unique** within its declaring type. You cannot have two projectable methods with the same name but different parameter lists. + +```csharp +// ❌ Not supported — two methods named "GetTotal" +public class Order +{ + [Projectable] + public decimal GetTotal() => Subtotal; + + [Projectable] + public decimal GetTotal(decimal discountRate) => Subtotal * (1 - discountRate); // ❌ +} + +// ✅ Workaround — use distinct method names +public class Order +{ + [Projectable] + public decimal GetTotal() => Subtotal; + + [Projectable] + public decimal GetDiscountedTotal(decimal discountRate) => Subtotal * (1 - discountRate); +} +``` + +## Members Must Have a Body + +A `[Projectable]` member must have an **expression body** or a **block body** (with `AllowBlockBody = true`). Abstract members, interface declarations, and auto-properties without accessors are not supported and produce error EFP0006. + +```csharp +// ❌ Error EFP0006 — no body +[Projectable] +public string FullName { get; set; } + +// ✅ Expression-bodied property +[Projectable] +public string FullName => FirstName + " " + LastName; +``` + +Use [`UseMemberBody`](/reference/use-member-body) to delegate to another member if the projectable itself can't have a body. + +## Null-Conditional Operators Require Configuration + +The null-conditional operator (`?.`) cannot be used in projectable members unless `NullConditionalRewriteSupport` is set. The default (`None`) produces error EFP0002. + +```csharp +// ❌ Error EFP0002 +[Projectable] +public string? City => Address?.City; + +// ✅ Configured +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public string? City => Address?.City; +``` + +See [Null-Conditional Rewrite](/reference/null-conditional-rewrite). + +## Block Body Restrictions + +When using block-bodied members (experimental), the following constructs are **not supported**: + +- `while`, `for`, `foreach` loops (EFP0003) +- `try` / `catch` / `finally` blocks (EFP0003) +- `throw` statements (EFP0003) +- Local variables inside nested blocks (only top-level variable declarations are supported) + +```csharp +// ❌ Not supported +[Projectable(AllowBlockBody = true)] +public int Process() +{ + for (int i = 0; i < 10; i++) { ... } // EFP0003 + return result; +} + +// ✅ Use LINQ +[Projectable] +public int Process() => Items.Take(10).Sum(i => i.Value); +``` + +## Local Variables Are Inlined (No De-duplication) + +In block-bodied members, local variables are **inlined at every usage point**. If a variable is used multiple times, the initializer expression is duplicated. This can: + +- Increase SQL complexity. +- Change semantics if the initializer has observable side effects (though side effects are detected as EFP0004/EFP0005). + +```csharp +// The initializer "Value * 2" appears twice in the generated expression +[Projectable(AllowBlockBody = true)] +public int Foo() +{ + var doubled = Value * 2; + return doubled + doubled; // → (Value * 2) + (Value * 2) +} +``` + +## Expression Tree Restrictions Apply + +Since projectable members are ultimately compiled to expression trees, all standard expression tree limitations apply: + +- **No `dynamic` typing** — expression trees must be statically typed. +- **No `ref` or `out` parameters**. +- **No named/optional parameters in LINQ** — parameters must be passed positionally in query expressions. +- **No multi-statement lambdas** — expression-bodied members must be single expressions (block bodies go through the converter, but with the limitations above). +- **Only EF Core-translatable operations** — the generated expression will ultimately be translated to SQL by EF Core. Any operation that EF Core cannot translate (e.g., calling a .NET method that has no SQL equivalent) will cause a runtime query translation error. + +## EF Core Translatable Operations Only + +The body of a projectable member can only use: + +- Mapped entity properties and navigation properties. +- Other `[Projectable]` members (transitively expanded). +- EF Core built-in functions (e.g., `EF.Functions.Like(...)`, `DateTime.Now`, string methods EF Core knows). +- LINQ methods EF Core supports (`Where`, `Sum`, `Any`, `Select`, etc.). + +```csharp +// ❌ Path.Combine has no SQL equivalent — runtime error +[Projectable] +public string FilePath => Path.Combine(Directory, FileName); + +// ✅ String concatenation — translated by EF Core +[Projectable] +public string FilePath => Directory + "/" + FileName; +``` + +## Limited Compatibility Mode and Dynamic State + +[Limited mode](/reference/compatibility-mode) caches the expanded query after the first execution. If a projectable member's expansion depends on external state that changes between calls (not through standard EF Core query parameters), the cached expansion may be stale. + +## No Support for Generic Type Parameters on Methods + +Generic method parameters are not supported on projectable methods: + +```csharp +// ❌ Not supported +[Projectable] +public T GetValue() => ...; +``` + +Generic **class** type parameters (on the containing entity) are supported. + +## Performance: First-Execution Overhead + +Both compatibility modes have a one-time cost on first execution: + +- **Full mode:** Expression walking + expansion on every execution. +- **Limited mode:** Expression walking + expansion on first execution; subsequent calls use EF Core's query cache. + +For performance-critical code paths, consider Limited mode to amortize this cost. + diff --git a/docs/advanced/query-compiler-pipeline.md b/docs/advanced/query-compiler-pipeline.md new file mode 100644 index 0000000..d7f294d --- /dev/null +++ b/docs/advanced/query-compiler-pipeline.md @@ -0,0 +1,158 @@ +# Query Compiler Pipeline + +This page explains how EF Core Projectables integrates with EF Core's internal query compilation pipeline, and the differences between Full and Limited compatibility modes. + +## EF Core's Query Pipeline (Background) + +When you execute a LINQ query against a `DbContext`, EF Core runs it through a multi-stage pipeline: + +``` +LINQ Expression (IQueryable) + ↓ +QueryCompiler.Execute() + ↓ +Query Translation Preprocessor + ↓ +Query Translator (LINQ → SQL model) + ↓ +SQL Generator + ↓ +SQL + Parameters → Database +``` + +Projectables hooks into this pipeline at different points depending on the selected compatibility mode. + +## Full Compatibility Mode + +In Full mode, expansion happens **before** the query reaches EF Core's pipeline: + +``` +LINQ Expression + ↓ +CustomQueryCompiler.Execute() / CreateCompiledQuery() + ↓ ← [Projectables expansion happens HERE] +ProjectableExpressionReplacer.Replace() + ↓ +Expanded LINQ Expression + ↓ +(Delegated to the original EF Core QueryCompiler) + ↓ +Standard EF Core pipeline... + ↓ +SQL +``` + +### `CustomQueryCompiler` + +The `CustomQueryCompiler` class wraps EF Core's default `QueryCompiler`. It overrides all execution entry points: + +```csharp +public override TResult Execute(Expression query) + => _decoratedQueryCompiler.Execute(Expand(query)); + +public override TResult ExecuteAsync(Expression query, CancellationToken cancellationToken) + => _decoratedQueryCompiler.ExecuteAsync(Expand(query), cancellationToken); + +public override Func CreateCompiledQuery(Expression query) + => _decoratedQueryCompiler.CreateCompiledQuery(Expand(query)); +``` + +The `Expand()` method calls `ProjectableExpressionReplacer.Replace()` on the raw expression before passing it downstream. + +### Query Cache Implications + +Because expansion happens before EF Core sees the query, the expanded expression is what gets compiled and cached. This means: + +- EF Core's query cache works on the **expanded** expression. +- Two queries that differ only in which projectable member they call will produce **different cache keys**, even if the expanded SQL is the same. +- Each unique LINQ query shape goes through expansion on **every execution** — there is no caching of the expansion step itself. + +## Limited Compatibility Mode + +In Limited mode, expansion happens **inside** EF Core's query translation preprocessor: + +``` +LINQ Expression + ↓ +EF Core QueryCompiler (default) + ↓ +CustomQueryTranslationPreprocessor.Process() + ↓ ← [Projectables expansion happens HERE] +ProjectableExpressionReplacer (via ExpandProjectables() extension) + ↓ +Expanded expression (now stored in EF Core's query cache) + ↓ +Standard EF Core query translator... + ↓ +SQL +``` + +### `CustomQueryTranslationPreprocessor` + +This class wraps EF Core's default `QueryTranslationPreprocessor` and overrides the `Process()` method: + +```csharp +public override Expression Process(Expression query) + => _decoratedPreprocessor.Process(query.ExpandProjectables()); +``` + +`ExpandProjectables()` is an extension method on `Expression` that runs the `ProjectableExpressionReplacer` over the expression tree. + +### Query Cache Benefits + +Because the expansion happens **inside** EF Core's own preprocessing step, EF Core compiles the resulting expanded expression and stores it in its query cache. On subsequent executions with the same query shape: + +1. EF Core computes the cache key from the original (unexpanded) query. +2. It finds the cached compiled query. +3. It executes the cached query directly — **no expansion needed**. + +This is why Limited mode can outperform both Full mode and vanilla EF Core for repeated queries. + +### Dynamic Parameter Caveat + +The downside of Limited mode is that EF Core's query cache key is based on the **original** LINQ expression. If your projectable member captures external state (a closure variable that changes between calls), the cache may not distinguish between calls with different values. + +**Safe with Limited mode:** +```csharp +// The threshold is a query parameter — EF Core handles it correctly +dbContext.Orders.Where(o => o.ExceedsThreshold(threshold)) +``` + +**Potentially unsafe with Limited mode:** +```csharp +// If GetCurrentUserRegion() returns a different value per call +// and the result is baked into the expression tree at expansion time +// (not captured as a standard EF Core parameter), this may be stale. +dbContext.Orders.Where(o => o.Region == GetCurrentUserRegion()) +``` + +## How Expansion Works + +In both modes, the core expansion logic is in `ProjectableExpressionReplacer`: + +1. **Visit the expression tree** — The replacer inherits from `ExpressionVisitor` and recursively visits every node. +2. **Detect projectable calls** — For each `MemberExpression` (property access) or `MethodCallExpression`, it checks if the member has a `[ProjectableAttribute]`. +3. **Load the generated expression** — Uses `ProjectionExpressionResolver` to find the auto-generated companion class and invoke its `Expression()` factory method via reflection. +4. **Cache the resolved expression** — The resolved `LambdaExpression` is cached in a per-replacer dictionary to avoid redundant reflection calls within the same query expansion. +5. **Substitute arguments** — Uses `ExpressionArgumentReplacer` to replace the lambda's parameters with the actual arguments from the call site. +6. **Recurse** — The substituted expression body is itself visited, expanding any nested projectable calls. + +## Registering the Infrastructure + +Both modes use the same EF Core extension mechanism. `ProjectionOptionsExtension` implements `IDbContextOptionsExtension` and registers the appropriate services: + +```csharp +// Full mode — registers CustomQueryCompiler +services.AddScoped(); + +// Limited mode — registers CustomQueryTranslationPreprocessorFactory +services.AddScoped(); +``` + +The `CustomConventionSetPlugin` also registers the `ProjectablePropertiesNotMappedConvention`, which ensures EF Core's model builder ignores `[Projectable]` properties (they are computed — not mapped to database columns). + +## Query Filters + +The `ProjectablesExpandQueryFiltersConvention` handles the case where global query filters reference projectable members. It ensures that query filters are also expanded when Projectables is active. + diff --git a/docs/guide/extension-methods.md b/docs/guide/extension-methods.md new file mode 100644 index 0000000..d1bee9e --- /dev/null +++ b/docs/guide/extension-methods.md @@ -0,0 +1,124 @@ +# Extension Methods + +Projectable extension methods let you define query logic outside of your entity classes — useful for keeping entities clean, applying logic to types you don't own, or grouping related query helpers. + +## Defining a Projectable Extension Method + +Add `[Projectable]` to any extension method in a **static class**: + +```csharp +using EntityFrameworkCore.Projectables; + +public static class UserExtensions +{ + [Projectable] + public static Order GetMostRecentOrder(this User user, DateTime? cutoffDate) => + user.Orders + .Where(x => cutoffDate == null || x.CreatedDate >= cutoffDate) + .OrderByDescending(x => x.CreatedDate) + .FirstOrDefault(); +} +``` + +## Using Extension Methods in Queries + +```csharp +var result = dbContext.Users + .Where(u => u.UserName == "Jon") + .Select(u => new { + GrandTotal = u.GetMostRecentOrder(DateTime.UtcNow.AddDays(-30)).GrandTotal + }) + .FirstOrDefault(); +``` + +The extension method is fully inlined — including any nested projectable members like `GrandTotal`. + +## Extension Methods on Non-Entity Types + +You don't need to restrict projectable extension methods to entity types. They work on **any type** that EF Core can work with in queries: + +```csharp +// On int +public static class IntExtensions +{ + [Projectable] + public static int Squared(this int i) => i * i; +} + +// On string +public static class StringExtensions +{ + [Projectable] + public static bool ContainsIgnoreCase(this string source, string value) => + source.ToLower().Contains(value.ToLower()); +} +``` + +Usage in queries: + +```csharp +var squaredScores = dbContext.Players + .Select(p => new { p.Name, SquaredScore = p.Score.Squared() }) + .ToList(); + +var results = dbContext.Products + .Where(p => p.Name.ContainsIgnoreCase("widget")) + .ToList(); +``` + +## Extension Methods with Multiple Parameters + +```csharp +public static class OrderExtensions +{ + [Projectable] + public static bool IsHighValueOrder(this Order order, decimal threshold, bool includeTax = false) => + (includeTax ? order.GrandTotal : order.Subtotal) > threshold; +} + +var highValue = dbContext.Orders + .Where(o => o.IsHighValueOrder(500, includeTax: true)) + .ToList(); +``` + +## Chaining Extension Methods + +Extension methods can call other projectable members (properties, methods, or other extension methods): + +```csharp +public static class UserExtensions +{ + [Projectable] + public static decimal TotalSpentThisMonth(this User user) => + user.Orders + .Where(o => o.CreatedDate.Month == DateTime.UtcNow.Month) + .Sum(o => o.GrandTotal); // GrandTotal is [Projectable] on Order + + [Projectable] + public static bool IsVipCustomer(this User user) => + user.TotalSpentThisMonth() > 1000; // Calls another [Projectable] extension +} +``` + +## Extension Methods on Nullable Types + +Extension methods on nullable entity types work naturally: + +```csharp +public static class UserExtensions +{ + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public static string GetFullAddress(this User? user) => + user?.Location?.AddressLine1 + " " + user?.Location?.AddressLine2; +} +``` + +See [Null-Conditional Rewrite](/reference/null-conditional-rewrite) for details on handling nullable navigation. + +## Important Rules + +- Extension methods **must be in a static class**. +- The `this` parameter represents the entity instance in the generated expression. +- **Method overloading is not supported** — each method name must be unique within its declaring static class. +- Default parameter values are supported but the caller must explicitly provide all arguments in LINQ queries (EF Core does not support optional parameters in expression trees). + diff --git a/docs/guide/installation.md b/docs/guide/installation.md new file mode 100644 index 0000000..146438b --- /dev/null +++ b/docs/guide/installation.md @@ -0,0 +1,102 @@ +# Installation + +Projectables is split into two NuGet packages. You will typically need both. + +## Packages + +### `EntityFrameworkCore.Projectables.Abstractions` + +Contains the `[Projectable]` attribute and the Roslyn **source generator**. This package must be referenced by the project that **defines** your entities and projectable members. + +### `EntityFrameworkCore.Projectables` + +Contains the EF Core **runtime extension** that intercepts queries and expands projectable members into SQL. This package must be referenced by the project that configures your `DbContext`. + +In most single-project setups, you reference both packages in the same project. + +## Install via .NET CLI + +```bash +dotnet add package EntityFrameworkCore.Projectables.Abstractions +dotnet add package EntityFrameworkCore.Projectables +``` + +## Install via Package Manager Console + +```powershell +Install-Package EntityFrameworkCore.Projectables.Abstractions +Install-Package EntityFrameworkCore.Projectables +``` + +## Install via PackageReference (csproj) + +```xml + + + + +``` + +> **Tip:** Replace `*` with the latest stable version from [NuGet](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/). + +## Enable in Your DbContext + +After installing the packages, call `UseProjectables()` when configuring your `DbContextOptions`: + +```csharp +services.AddDbContext(options => + options.UseSqlServer(connectionString) + .UseProjectables()); // 👈 Add this +``` + +Or in `OnConfiguring`: + +```csharp +protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +{ + optionsBuilder + .UseSqlServer(connectionString) + .UseProjectables(); +} +``` + +That's it — you're ready to start using `[Projectable]`! + +## Optional Configuration + +`UseProjectables()` accepts an optional callback to configure advanced options: + +```csharp +options.UseProjectables(projectables => + projectables.CompatibilityMode(CompatibilityMode.Limited)); +``` + +See [Compatibility Mode](/reference/compatibility-mode) for details. + +## Verifying the Installation + +The source generator runs at compile time. You can verify it is working by: + +1. Adding `[Projectable]` to a property in your entity class. +2. Building the project — no errors should appear. +3. Using the property in a LINQ query and checking that the generated SQL reflects the inlined logic (e.g., via `ToQueryString()` or EF Core logging). + +## Multi-Project Solutions + +In solutions where entities are in a separate class library: + +``` +MyApp.Domain → references Abstractions (has [Projectable] attributes) +MyApp.Data → references Projectables runtime + Domain +MyApp.Web → references Data +``` + +```xml + + + + + + +``` + diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md new file mode 100644 index 0000000..a9c6e6e --- /dev/null +++ b/docs/guide/introduction.md @@ -0,0 +1,95 @@ +# Introduction + +**EntityFrameworkCore.Projectables** is a library that lets you write C# properties and methods — decorated with a simple `[Projectable]` attribute — and use them directly inside any EF Core LINQ query. The library takes care of translating those members into the SQL query, keeping your codebase DRY and your queries efficient. + +## The Problem It Solves + +When using EF Core, you often need to express the same business logic in two places: + +1. **In-memory** — as a regular C# property or method on your entity. +2. **In queries** — duplicated inline as a LINQ expression so EF Core can translate it to SQL. + +```csharp +// ❌ Without Projectables — logic duplicated +class Order { + // C# property (in-memory use) + public decimal GrandTotal => Subtotal + Tax; + + // Must be duplicated inline in every LINQ query +} + +var totals = dbContext.Orders + .Select(o => new { + GrandTotal = o.Items.Sum(i => i.Price) + (o.Items.Sum(i => i.Price) * o.TaxRate) + }) + .ToList(); +``` + +With Projectables, you write the logic once: + +```csharp +// ✅ With Projectables — write once, use everywhere +class Order { + [Projectable] public decimal Subtotal => Items.Sum(i => i.Price); + [Projectable] public decimal Tax => Subtotal * TaxRate; + [Projectable] public decimal GrandTotal => Subtotal + Tax; +} + +var totals = dbContext.Orders + .Select(o => new { o.GrandTotal }) // Inlined into SQL automatically + .ToList(); +``` + +## How It Works + +Projectables has two components that work together: + +### 1. Source Generator (build time) + +When you compile your project, a Roslyn source generator scans for members decorated with `[Projectable]` and generates a **companion expression tree** for each one. For example, the `GrandTotal` property above generates something like: + +```csharp +// Auto-generated — hidden from IntelliSense +public static Expression> GrandTotal_Expression() + => @this => @this.Items.Sum(i => i.Price) + (@this.Items.Sum(i => i.Price) * @this.TaxRate); +``` + +### 2. Runtime Interceptor (query time) + +At query execution time, a custom EF Core query pre-processor walks your LINQ expression tree. Whenever it encounters a call to a `[Projectable]` member, it **replaces it with the generated expression tree**, substituting the actual parameters. The resulting expanded expression tree is then handed off to EF Core for normal SQL translation. + +``` +LINQ query + → [Projectables interceptor replaces member calls with expressions] + → Expanded expression tree + → EF Core SQL translation + → SQL query +``` + +## Comparison with Similar Libraries + +| Feature | Projectables | Expressionify | LinqKit | +|---|---|---|---| +| Source generator based | ✅ | ✅ | ❌ | +| Works with entity methods | ✅ | ✅ | Partial | +| Works with extension methods | ✅ | ✅ | ✅ | +| Composable projectables | ✅ | ❌ | Partial | +| Block-bodied members | ✅ (experimental) | ❌ | ❌ | +| Enum method expansion | ✅ | ❌ | ❌ | +| Null-conditional rewriting | ✅ | ❌ | ❌ | +| Limited/cached mode | ✅ | ❌ | ❌ | + +## EF Core Version Compatibility + +| Library Version | EF Core Version | +|---|---| +| v1.x | EF Core 3.1, 5 | +| v2.x, v3.x | EF Core 6, 7 | +| v6.x+ | EF Core 6+ (block-bodied members added) | + +## Next Steps + +- [Install the packages →](/guide/installation) +- [Follow the Quick Start →](/guide/quickstart) +- [Learn how it works internally →](/advanced/how-it-works) + diff --git a/docs/guide/projectable-methods.md b/docs/guide/projectable-methods.md new file mode 100644 index 0000000..b818aaf --- /dev/null +++ b/docs/guide/projectable-methods.md @@ -0,0 +1,118 @@ +# Projectable Methods + +Projectable methods work like projectable properties but accept parameters, making them ideal for reusable query fragments that vary based on runtime values. + +## Defining a Projectable Method + +Add `[Projectable]` to any **expression-bodied method** on an entity: + +```csharp +public class Order +{ + public int Id { get; set; } + public DateTime CreatedDate { get; set; } + public bool IsFulfilled { get; set; } + public decimal TaxRate { get; set; } + public ICollection Items { get; set; } + + [Projectable] + public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + + [Projectable] + public bool IsRecentOrder(int days) => + CreatedDate >= DateTime.UtcNow.AddDays(-days) && IsFulfilled; +} +``` + +## Using Projectable Methods in Queries + +```csharp +// Pass runtime values as arguments +var recentOrders = dbContext.Orders + .Where(o => o.IsRecentOrder(30)) + .ToList(); + +// Use in Select +var summary = dbContext.Orders + .Select(o => new { + o.Id, + IsRecent = o.IsRecentOrder(7), + o.Subtotal + }) + .ToList(); +``` + +The method argument (`30` or `7`) is captured and translated into the generated SQL expression. + +## Methods with Multiple Parameters + +```csharp +public class Product +{ + public decimal ListPrice { get; set; } + public decimal DiscountRate { get; set; } + + [Projectable] + public decimal DiscountedPrice(decimal additionalDiscount, int quantity) => + ListPrice * (1 - DiscountRate - additionalDiscount) * quantity; +} + +// Usage +var prices = dbContext.Products + .Select(p => new { + p.Id, + FinalPrice = p.DiscountedPrice(0.05m, 10) + }) + .ToList(); +``` + +## Composing Methods and Properties + +Projectable methods can call projectable properties and vice versa: + +```csharp +public class Order +{ + [Projectable] public decimal Subtotal => Items.Sum(i => i.Price); + [Projectable] public decimal Tax => Subtotal * TaxRate; + + // Method calling projectable properties + [Projectable] + public bool ExceedsThreshold(decimal threshold) => (Subtotal + Tax) > threshold; +} + +var highValue = dbContext.Orders + .Where(o => o.ExceedsThreshold(500)) + .ToList(); +``` + +## Block-Bodied Methods (Experimental) + +Methods can also use traditional block bodies when `AllowBlockBody = true`: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetStatus(decimal threshold) +{ + if (GrandTotal > threshold) + return "High Value"; + else if (GrandTotal > threshold / 2) + return "Medium Value"; + else + return "Standard"; +} +``` + +See [Block-Bodied Members](/advanced/block-bodied-members) for full details. + +## Important Rules + +- Methods must be **expression-bodied** (`=>`) unless `AllowBlockBody = true`. +- **Method overloading is not supported** — each method name must be unique within its type. +- Parameters are passed through to the generated expression as closures and resolved at query time. +- Parameter types must be supported by EF Core (primitive types, enums, and other EF-translatable types). + +## Difference from Extension Methods + +Instance methods are defined directly on the entity. For query logic that doesn't belong on the entity, or that applies to types you don't own, use [Extension Methods](/guide/extension-methods) instead. + diff --git a/docs/guide/projectable-properties.md b/docs/guide/projectable-properties.md new file mode 100644 index 0000000..b4cd785 --- /dev/null +++ b/docs/guide/projectable-properties.md @@ -0,0 +1,136 @@ +# Projectable Properties + +Projectable properties let you define computed values on your entities using standard C# expression-bodied properties, and have those computations automatically translated into SQL when used in LINQ queries. + +## Defining a Projectable Property + +Add `[Projectable]` to any **expression-bodied property**: + +```csharp +using EntityFrameworkCore.Projectables; + +public class User +{ + public string FirstName { get; set; } + public string LastName { get; set; } + + [Projectable] + public string FullName => FirstName + " " + LastName; +} +``` + +> **Note:** The `using EntityFrameworkCore.Projectables;` namespace is required for the `[Projectable]` attribute. + +## Using Projectable Properties in Queries + +Once defined, projectable properties can be used in **any part of a LINQ query**: + +### In `Select` + +```csharp +var names = dbContext.Users + .Select(u => u.FullName) + .ToList(); +``` + +Generated SQL (SQLite): +```sql +SELECT (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') +FROM "Users" AS "u" +``` + +### In `Where` + +```csharp +var users = dbContext.Users + .Where(u => u.FullName.Contains("Jon")) + .ToList(); +``` + +### In `GroupBy` + +```csharp +var grouped = dbContext.Users + .GroupBy(u => u.FullName) + .Select(g => new { Name = g.Key, Count = g.Count() }) + .ToList(); +``` + +### In `OrderBy` + +```csharp +var sorted = dbContext.Users + .OrderBy(u => u.FullName) + .ToList(); +``` + +### In multiple clauses at once + +```csharp +var query = dbContext.Users + .Where(u => u.FullName.Contains("Jon")) + .GroupBy(u => u.FullName) + .OrderBy(u => u.Key) + .Select(u => u.Key); +``` + +Generated SQL (SQLite): +```sql +SELECT (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') +FROM "Users" AS "u" +WHERE ('Jon' = '') OR (instr((COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", ''), 'Jon') > 0) +GROUP BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') +ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') +``` + +## Composing Projectable Properties + +Projectable properties can reference **other projectable properties**. The entire chain is expanded into the final SQL: + +```csharp +public class Order +{ + public decimal TaxRate { get; set; } + public ICollection Items { get; set; } + + [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + [Projectable] public decimal Tax => Subtotal * TaxRate; // uses Subtotal + [Projectable] public decimal GrandTotal => Subtotal + Tax; // uses Subtotal + Tax +} +``` + +All three properties are inlined transitively in the generated SQL. + +## Block-Bodied Properties (Experimental) + +In addition to expression-bodied properties (`=>`), you can use **block-bodied properties** with `AllowBlockBody = true`: + +```csharp +[Projectable(AllowBlockBody = true)] +public string Category +{ + get + { + if (Score > 90) + return "Excellent"; + else if (Score > 70) + return "Good"; + else + return "Average"; + } +} +``` + +See [Block-Bodied Members](/advanced/block-bodied-members) for the full feature documentation. + +## Important Rules + +- The property **must be expression-bodied** (using `=>`) unless `AllowBlockBody = true` is set. +- The expression must be translatable by EF Core — it can only use members that EF Core understands (mapped columns, navigation properties, and other `[Projectable]` members). +- Properties **cannot be overloaded** — each property name must be unique within its type. +- The property body has access to `this` (the entity instance) and its navigation properties. + +## Nullable Properties + +If your expression uses the null-conditional operator (`?.`), you need to configure `NullConditionalRewriteSupport`. See [Null-Conditional Rewrite](/reference/null-conditional-rewrite) for details. + diff --git a/docs/guide/quickstart.md b/docs/guide/quickstart.md new file mode 100644 index 0000000..f94d4c0 --- /dev/null +++ b/docs/guide/quickstart.md @@ -0,0 +1,151 @@ +# Quick Start + +This guide walks you through a complete end-to-end example — from defining entities with projectable members to seeing the generated SQL. + +## Step 1 — Define Your Entities + +```csharp +public class User +{ + public int Id { get; set; } + public string UserName { get; set; } + public ICollection Orders { get; set; } +} + +public class Order +{ + public int Id { get; set; } + public int UserId { get; set; } + public DateTime CreatedDate { get; set; } + public decimal TaxRate { get; set; } + + public User User { get; set; } + public ICollection Items { get; set; } + + // Mark computed properties with [Projectable] + [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + [Projectable] public decimal Tax => Subtotal * TaxRate; + [Projectable] public decimal GrandTotal => Subtotal + Tax; +} + +public class OrderItem +{ + public int Id { get; set; } + public int OrderId { get; set; } + public int Quantity { get; set; } + public Product Product { get; set; } +} + +public class Product +{ + public int Id { get; set; } + public decimal ListPrice { get; set; } +} +``` + +## Step 2 — Enable Projectables on Your DbContext + +```csharp +public class AppDbContext : DbContext +{ + public DbSet Users { get; set; } + public DbSet Orders { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseSqlServer("your-connection-string") + .UseProjectables(); // Enable the runtime interceptor + } +} +``` + +## Step 3 — Use Projectable Members in Queries + +Now you can use `GrandTotal`, `Subtotal`, and `Tax` **directly in any LINQ query**: + +```csharp +// In a Select projection +var orderSummaries = dbContext.Orders + .Select(o => new { + o.Id, + o.Subtotal, + o.Tax, + o.GrandTotal + }) + .ToList(); + +// In a Where clause +var highValueOrders = dbContext.Orders + .Where(o => o.GrandTotal > 1000) + .ToList(); + +// In an OrderBy +var sortedOrders = dbContext.Orders + .OrderByDescending(o => o.GrandTotal) + .ToList(); +``` + +## Step 4 — Check the Generated SQL + +Use `ToQueryString()` to inspect the SQL EF Core generates: + +```csharp +var query = dbContext.Orders + .Where(o => o.GrandTotal > 1000) + .OrderByDescending(o => o.GrandTotal); + +Console.WriteLine(query.ToQueryString()); +``` + +The `GrandTotal` property — which itself uses `Subtotal` (which is also `[Projectable]`) — is fully inlined: + +```sql +SELECT [o].[Id], [o].[UserId], [o].[CreatedDate], [o].[TaxRate] +FROM [Orders] AS [o] +WHERE ( + COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) + + COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) * [o].[TaxRate] +) > 1000.0 +ORDER BY ( + COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) + + COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) * [o].[TaxRate] +) DESC +``` + +All computation happens in the database — no data is loaded into memory for filtering or sorting. + +## Adding Extension Methods + +You can also define projectable extension methods — useful for logic that doesn't belong on the entity itself: + +```csharp +public static class UserExtensions +{ + [Projectable] + public static Order GetMostRecentOrder(this User user, DateTime? cutoffDate = null) => + user.Orders + .Where(x => cutoffDate == null || x.CreatedDate >= cutoffDate) + .OrderByDescending(x => x.CreatedDate) + .FirstOrDefault(); +} +``` + +Use it in a query just like any regular method: + +```csharp +var result = dbContext.Users + .Where(u => u.UserName == "Jon") + .Select(u => new { + GrandTotal = u.GetMostRecentOrder(DateTime.UtcNow.AddDays(-30)).GrandTotal + }) + .FirstOrDefault(); +``` + +## Next Steps + +- [Projectable Properties in depth →](/guide/projectable-properties) +- [Projectable Methods →](/guide/projectable-methods) +- [Extension Methods →](/guide/extension-methods) +- [Full [Projectable] attribute reference →](/reference/projectable-attribute) + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..f55400a --- /dev/null +++ b/docs/index.md @@ -0,0 +1,72 @@ +--- +layout: home + +hero: + name: "EF Core Projectables" + text: "Flexible projection magic for EF Core" + tagline: Write properties and methods once — use them anywhere in your LINQ queries, translated to efficient SQL automatically. + actions: + - theme: brand + text: Get Started + link: /guide/introduction + - theme: alt + text: Quick Start + link: /guide/quickstart + - theme: alt + text: View on GitHub + link: https://github.com/koenbeuk/EntityFrameworkCore.Projectables + +features: + - icon: 🏷️ + title: Just Add [Projectable] + details: Decorate any property or method with [Projectable] and the source generator does the rest — no boilerplate, no manual expression trees. + + - icon: 🔌 + title: Works with Any EF Core Provider + details: Provider-agnostic. SQL Server, PostgreSQL, SQLite, Cosmos DB — Projectables hooks into the EF Core query pipeline regardless of your database. + + - icon: ⚡ + title: Performance-First Design + details: Limited compatibility mode expands and caches queries after their first execution. Subsequent calls skip the expansion step entirely, often outperforming native EF Core. + + - icon: 🔗 + title: Composable by Design + details: Projectable members can call other projectable members. Build a library of reusable query fragments and compose them freely in any query. + + - icon: 🛡️ + title: Null-Conditional Rewriting + details: Working with nullable navigation properties? Configure NullConditionalRewriteSupport to automatically handle the ?. operator in generated expressions. + + - icon: 🔢 + title: Enum Method Expansion + details: Use ExpandEnumMethods to translate enum extension methods (like display names from [Display] attributes) into SQL CASE expressions automatically. +--- + +## At a Glance + +```csharp +class Order { + public decimal TaxRate { get; set; } + public ICollection Items { get; set; } + + [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + [Projectable] public decimal Tax => Subtotal * TaxRate; + [Projectable] public decimal GrandTotal => Subtotal + Tax; +} + +// Use it anywhere in your queries — translated to SQL automatically +var result = dbContext.Users + .Where(u => u.UserName == "Jon") + .Select(u => new { u.GetMostRecentOrder().GrandTotal }) + .FirstOrDefault(); +``` + +The properties are **inlined into the SQL** — no client-side evaluation, no N+1. + +## NuGet Packages + +| Package | Description | +|---|---| +| [`EntityFrameworkCore.Projectables.Abstractions`](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) | The `[Projectable]` attribute and source generator | +| [`EntityFrameworkCore.Projectables`](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) | The EF Core runtime extension | + diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000..1765a25 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,2313 @@ +{ + "name": "docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "vitepress": "^2.0.0-alpha.16" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.0.tgz", + "integrity": "sha512-YlcAimkXclvqta47g47efzCM5CFxDwv2ClkDfEs/fC/Ak0OxPH2b3czwa4o8O1TRBf+ujFF2RiUwszz2fPVNJQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.6.0.tgz", + "integrity": "sha512-9/rbgkm/BgTq46cwxIohvSAz3koOFjnPpg0mwkJItAfzKbQIj+310PvwtgUY1YITDuGCag6yOL50GW2DBkaaBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/sidepanel-js": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.6.0.tgz", + "integrity": "sha512-lFT5KLwlzUmpoGArCScNoK41l9a22JYsEPwBzMrz+/ILVR5Ax87UphCuiyDFQWEvEmbwzn/kJx5W/O5BUlN1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.71", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.71.tgz", + "integrity": "sha512-rNoDFbq1fAYiEexBvrw613/xiUOPEu5MKVV/X8lI64AgdTzLQUUemr9f9fplxUMPoxCBP2rWzlhOEeTHk/Sf0Q==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.58.0.tgz", + "integrity": "sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.58.0.tgz", + "integrity": "sha512-+s++dbp+/RTte62mQD9wLSbiMTV+xr/PeRJEc/sFZFSBRlHPNPVaf5FXlzAL77Mr8FtSfQqCN+I598M8U41ccQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.58.0.tgz", + "integrity": "sha512-MFWBwTcYs0jZbINQBXHfSrpSQJq3IUOakcKPzfeSznONop14Pxuqa0Kg19GD0rNBMPQI2tFtu3UzapZpH0Uc1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.58.0.tgz", + "integrity": "sha512-yiKJY7pj9c9JwzuKYLFaDZw5gma3fI9bkPEIyofvVfsPqjCWPglSHdpdwXpKGvDeYDms3Qal8qGMEHZ1M/4Udg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.58.0.tgz", + "integrity": "sha512-x97kCoBh5MOevpn/CNK9W1x8BEzO238541BGWBc315uOlN0AD/ifZ1msg+ZQB05Ux+VF6EcYqpiagfLJ8U3LvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.58.0.tgz", + "integrity": "sha512-Aa8jPoZ6IQAG2eIrcXPpjRcMjROMFxCt1UYPZZtCxRV68WkuSigYtQ/7Zwrcr2IvtNJo7T2JfDXyMLxq5L4Jlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.58.0.tgz", + "integrity": "sha512-Ob8YgT5kD/lSIYW2Rcngs5kNB/44Q2RzBSPz9brf2WEtcGR7/f/E9HeHn1wYaAwKBni+bdXEwgHvUd0x12lQSA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.58.0.tgz", + "integrity": "sha512-K+RI5oP1ceqoadvNt1FecL17Qtw/n9BgRSzxif3rTL2QlIu88ccvY+Y9nnHe/cmT5zbH9+bpiJuG1mGHRVwF4Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.58.0.tgz", + "integrity": "sha512-T+17JAsCKUjmbopcKepJjHWHXSjeW7O5PL7lEFaeQmiVyw4kkc5/lyYKzrv6ElWRX/MrEWfPiJWqbTvfIvjM1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.58.0.tgz", + "integrity": "sha512-cCePktb9+6R9itIJdeCFF9txPU7pQeEHB5AbHu/MKsfH/k70ZtOeq1k4YAtBv9Z7mmKI5/wOLYjQ+B9QdxR6LA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.58.0.tgz", + "integrity": "sha512-iekUaLkfliAsDl4/xSdoCJ1gnnIXvoNz85C8U8+ZxknM5pBStfZjeXgB8lXobDQvvPRCN8FPmmuTtH+z95HTmg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.58.0.tgz", + "integrity": "sha512-68ofRgJNl/jYJbxFjCKE7IwhbfxOl1muPN4KbIqAIe32lm22KmU7E8OPvyy68HTNkI2iV/c8y2kSPSm2mW/Q9Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.58.0.tgz", + "integrity": "sha512-dpz8vT0i+JqUKuSNPCP5SYyIV2Lh0sNL1+FhM7eLC457d5B9/BC3kDPp5BBftMmTNsBarcPcoz5UGSsnCiw4XQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.58.0.tgz", + "integrity": "sha512-4gdkkf9UJ7tafnweBCR/mk4jf3Jfl0cKX9Np80t5i78kjIH0ZdezUv/JDI2VtruE5lunfACqftJ8dIMGN4oHew==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.58.0.tgz", + "integrity": "sha512-YFS4vPnOkDTD/JriUeeZurFYoJhPf9GQQEF/v4lltp3mVcBmnsAdjEWhr2cjUCZzZNzxCG0HZOvJU44UGHSdzw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.58.0.tgz", + "integrity": "sha512-x2xgZlFne+QVNKV8b4wwaCS8pwq3y14zedZ5DqLzjdRITvreBk//4Knbcvm7+lWmms9V9qFp60MtUd0/t/PXPw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.58.0.tgz", + "integrity": "sha512-jIhrujyn4UnWF8S+DHSkAkDEO3hLX0cjzxJZPLF80xFyzyUIYgSMRcYQ3+uqEoyDD2beGq7Dj7edi8OnJcS/hg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.58.0.tgz", + "integrity": "sha512-+410Srdoh78MKSJxTQ+hZ/Mx+ajd6RjjPwBPNd0R3J9FtL6ZA0GqiiyNjCO9In0IzZkCNrpGymSfn+kgyPQocg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.58.0.tgz", + "integrity": "sha512-ZjMyby5SICi227y1MTR3VYBpFTdZs823Rs/hpakufleBoufoOIB6jtm9FEoxn/cgO7l6PM2rCEl5Kre5vX0QrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.58.0.tgz", + "integrity": "sha512-ds4iwfYkSQ0k1nb8LTcyXw//ToHOnNTJtceySpL3fa7tc/AsE+UpUFphW126A6fKBGJD5dhRvg8zw1rvoGFxmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.58.0.tgz", + "integrity": "sha512-fd/zpJniln4ICdPkjWFhZYeY/bpnaN9pGa6ko+5WD38I0tTqk9lXMgXZg09MNdhpARngmxiCg0B0XUamNw/5BQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.58.0.tgz", + "integrity": "sha512-YpG8dUOip7DCz3nr/JUfPbIUo+2d/dy++5bFzgi4ugOGBIox+qMbbqt/JoORwvI/C9Kn2tz6+Bieoqd5+B1CjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.58.0.tgz", + "integrity": "sha512-b9DI8jpFQVh4hIXFr0/+N/TzLdpBIoPzjt0Rt4xJbW3mzguV3mduR9cNgiuFcuL/TeORejJhCWiAXe3E/6PxWA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.58.0.tgz", + "integrity": "sha512-CSrVpmoRJFN06LL9xhkitkwUcTZtIotYAF5p6XOR2zW0Zz5mzb3IPpcoPhB02frzMHFNo1reQ9xSF5fFm3hUsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.58.0.tgz", + "integrity": "sha512-QFsBgQNTnh5K0t/sBsjJLq24YVqEIVkGpfN2VHsnN90soZyhaiA9UUHufcctVNL4ypJY0wrwad0wslx2KJQ1/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.22.0.tgz", + "integrity": "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.22.0.tgz", + "integrity": "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.22.0.tgz", + "integrity": "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.22.0.tgz", + "integrity": "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.22.0.tgz", + "integrity": "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.22.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.22.0.tgz", + "integrity": "sha512-E7eRV7mwDBjueLF6852n2oYeJYxBq3NSsDk+uyruYAXONv4U8holGmIrT+mPRJQ1J1SNOH6L8G19KRzmBawrFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.22.0", + "@shikijs/types": "3.22.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.22.0.tgz", + "integrity": "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", + "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.28", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", + "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", + "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.28", + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", + "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.6.tgz", + "integrity": "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.0.6" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.6.tgz", + "integrity": "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.6", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^2.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.6.tgz", + "integrity": "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", + "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", + "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/shared": "3.5.28" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", + "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.28", + "@vue/runtime-core": "3.5.28", + "@vue/shared": "3.5.28", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", + "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "vue": "3.5.28" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", + "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.2.1.tgz", + "integrity": "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7 || ^8", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", + "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.58.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.58.0.tgz", + "integrity": "sha512-wbT0mBmWbIvvq8NeEYWWvevvxnOyhKChir47S66WCxw1SXqhw7ssIYejnQEVt7XYQpsj2y8F9PM+Cr3SNEa0gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.58.0", + "@rollup/rollup-android-arm64": "4.58.0", + "@rollup/rollup-darwin-arm64": "4.58.0", + "@rollup/rollup-darwin-x64": "4.58.0", + "@rollup/rollup-freebsd-arm64": "4.58.0", + "@rollup/rollup-freebsd-x64": "4.58.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.58.0", + "@rollup/rollup-linux-arm-musleabihf": "4.58.0", + "@rollup/rollup-linux-arm64-gnu": "4.58.0", + "@rollup/rollup-linux-arm64-musl": "4.58.0", + "@rollup/rollup-linux-loong64-gnu": "4.58.0", + "@rollup/rollup-linux-loong64-musl": "4.58.0", + "@rollup/rollup-linux-ppc64-gnu": "4.58.0", + "@rollup/rollup-linux-ppc64-musl": "4.58.0", + "@rollup/rollup-linux-riscv64-gnu": "4.58.0", + "@rollup/rollup-linux-riscv64-musl": "4.58.0", + "@rollup/rollup-linux-s390x-gnu": "4.58.0", + "@rollup/rollup-linux-x64-gnu": "4.58.0", + "@rollup/rollup-linux-x64-musl": "4.58.0", + "@rollup/rollup-openbsd-x64": "4.58.0", + "@rollup/rollup-openharmony-arm64": "4.58.0", + "@rollup/rollup-win32-arm64-msvc": "4.58.0", + "@rollup/rollup-win32-ia32-msvc": "4.58.0", + "@rollup/rollup-win32-x64-gnu": "4.58.0", + "@rollup/rollup-win32-x64-msvc": "4.58.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/shiki": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.22.0.tgz", + "integrity": "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.22.0", + "@shikijs/engine-javascript": "3.22.0", + "@shikijs/engine-oniguruma": "3.22.0", + "@shikijs/langs": "3.22.0", + "@shikijs/themes": "3.22.0", + "@shikijs/types": "3.22.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "2.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.16.tgz", + "integrity": "sha512-w1nwsefDVIsje7BZr2tsKxkZutDGjG0YoQ2yxO7+a9tvYVqfljYbwj5LMYkPy8Tb7YbPwa22HtIhk62jbrvuEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "^4.5.3", + "@docsearch/js": "^4.5.3", + "@docsearch/sidepanel-js": "^4.5.3", + "@iconify-json/simple-icons": "^1.2.68", + "@shikijs/core": "^3.21.0", + "@shikijs/transformers": "^3.21.0", + "@shikijs/types": "^3.21.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^6.0.3", + "@vue/devtools-api": "^8.0.5", + "@vue/shared": "^3.5.27", + "@vueuse/core": "^14.1.0", + "@vueuse/integrations": "^14.1.0", + "focus-trap": "^7.8.0", + "mark.js": "8.11.1", + "minisearch": "^7.2.0", + "shiki": "^3.21.0", + "vite": "^7.3.1", + "vue": "^3.5.27" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "oxc-minify": "*", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "oxc-minify": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.28", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", + "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.28", + "@vue/compiler-sfc": "3.5.28", + "@vue/runtime-dom": "3.5.28", + "@vue/server-renderer": "3.5.28", + "@vue/shared": "3.5.28" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000..cb2ecb9 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "^2.0.0-alpha.16" + } +} diff --git a/docs/recipes/computed-properties.md b/docs/recipes/computed-properties.md new file mode 100644 index 0000000..695841e --- /dev/null +++ b/docs/recipes/computed-properties.md @@ -0,0 +1,157 @@ +# Computed Entity Properties + +This recipe shows how to define reusable computed properties on your entities and use them across multiple query operations — all translated to SQL without any duplication. + +## The Pattern + +Define computed values as `[Projectable]` properties directly on your entity. These properties can then be used in `Select`, `Where`, `GroupBy`, `OrderBy`, and any combination thereof. + +## Example: Order Totals + +```csharp +public class Order +{ + public int Id { get; set; } + public decimal TaxRate { get; set; } + public DateTime CreatedDate { get; set; } + public ICollection Items { get; set; } + + // Building blocks + [Projectable] + public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + + [Projectable] + public decimal Tax => Subtotal * TaxRate; + + // Composed from other projectables + [Projectable] + public decimal GrandTotal => Subtotal + Tax; +} +``` + +### Use in Select + +```csharp +var summaries = dbContext.Orders + .Select(o => new OrderSummaryDto + { + Id = o.Id, + Subtotal = o.Subtotal, // ✅ Inlined into SQL + Tax = o.Tax, // ✅ Inlined into SQL + GrandTotal = o.GrandTotal // ✅ Inlined into SQL + }) + .ToList(); +``` + +### Use in Where + +```csharp +// Only load high-value orders +var highValue = dbContext.Orders + .Where(o => o.GrandTotal > 1000) + .ToList(); +``` + +### Use in OrderBy + +```csharp +// Sort by computed value +var ranked = dbContext.Orders + .OrderByDescending(o => o.GrandTotal) + .Take(10) + .ToList(); +``` + +### All Together + +```csharp +var report = dbContext.Orders + .Where(o => o.GrandTotal > 500) + .OrderByDescending(o => o.GrandTotal) + .GroupBy(o => o.CreatedDate.Year) + .Select(g => new + { + Year = g.Key, + Count = g.Count(), + TotalRevenue = g.Sum(o => o.GrandTotal) + }) + .ToList(); +``` + +All computed values are evaluated **in the database** — no data is fetched to memory for filtering or aggregation. + +## Example: User Profile + +```csharp +public class User +{ + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime BirthDate { get; set; } + public DateTime? LastLoginDate { get; set; } + + [Projectable] + public string FullName => FirstName + " " + LastName; + + [Projectable] + public int Age => DateTime.Today.Year - BirthDate.Year + - (DateTime.Today.DayOfYear < BirthDate.DayOfYear ? 1 : 0); + + [Projectable] + public bool IsActive => LastLoginDate != null + && LastLoginDate >= DateTime.UtcNow.AddDays(-30); +} +``` + +```csharp +// Find active adult users, sorted by name +var results = dbContext.Users + .Where(u => u.IsActive && u.Age >= 18) + .OrderBy(u => u.FullName) + .Select(u => new { u.FullName, u.Age }) + .ToList(); +``` + +## Example: Product Catalog + +```csharp +public class Product +{ + public decimal ListPrice { get; set; } + public decimal DiscountRate { get; set; } + public int StockQuantity { get; set; } + public int ReorderPoint { get; set; } + + [Projectable] + public decimal SalePrice => ListPrice * (1 - DiscountRate); + + [Projectable] + public decimal SavingsAmount => ListPrice - SalePrice; + + [Projectable] + public bool NeedsReorder => StockQuantity <= ReorderPoint; +} +``` + +```csharp +// Products on sale that need restocking +var reorder = dbContext.Products + .Where(p => p.NeedsReorder && p.SalePrice < 50) + .OrderBy(p => p.StockQuantity) + .Select(p => new + { + p.Id, + p.SalePrice, + p.SavingsAmount, + p.StockQuantity + }) + .ToList(); +``` + +## Tips + +- **Compose freely** — projectables can call other projectables. Build from simple to complex. +- **Use Limited mode** in production for repeated queries — computed properties are cached after the first execution. +- **Keep it pure** — projectable properties should be pure computations (no side effects). Everything must be translatable to SQL. +- **Avoid N+1** — if a projectable property references navigation properties, make sure to structure your queries so EF Core can generate a single efficient query. + diff --git a/docs/recipes/enum-display-names.md b/docs/recipes/enum-display-names.md new file mode 100644 index 0000000..5cf2cf7 --- /dev/null +++ b/docs/recipes/enum-display-names.md @@ -0,0 +1,172 @@ +# Enum Display Names in Queries + +This recipe shows how to project human-readable labels from enum values — such as names from `[Display]` attributes — directly into SQL queries using `ExpandEnumMethods`. + +## The Problem + +You have an enum with display-friendly labels: + +```csharp +public enum OrderStatus +{ + [Display(Name = "Pending Review")] + Pending = 0, + + [Display(Name = "Approved & Processing")] + Approved = 1, + + [Display(Name = "Rejected")] + Rejected = 2, + + [Display(Name = "Shipped")] + Shipped = 3 +} +``` + +And a helper extension method: + +```csharp +public static class OrderStatusExtensions +{ + public static string GetDisplayName(this OrderStatus status) + { + var field = typeof(OrderStatus).GetField(status.ToString()); + var attr = field?.GetCustomAttribute(); + return attr?.Name ?? status.ToString(); + } +} +``` + +The problem: `GetDisplayName` uses reflection — EF Core cannot translate this to SQL. + +## The Solution with `ExpandEnumMethods` + +Use `ExpandEnumMethods = true` on the projectable member that calls `GetDisplayName`: + +```csharp +public class Order +{ + public int Id { get; set; } + public OrderStatus Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusLabel => Status.GetDisplayName(); +} +``` + +The source generator evaluates `GetDisplayName` for each enum value at **compile time** and bakes the results into the expression tree as string constants: + +```csharp +// Generated expression equivalent: +Status == OrderStatus.Pending ? "Pending Review" : +Status == OrderStatus.Approved ? "Approved & Processing" : +Status == OrderStatus.Rejected ? "Rejected" : +Status == OrderStatus.Shipped ? "Shipped" : +null +``` + +Which translates to: + +```sql +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved & Processing' + WHEN [o].[Status] = 2 THEN N'Rejected' + WHEN [o].[Status] = 3 THEN N'Shipped' +END AS [StatusLabel] +FROM [Orders] AS [o] +``` + +## Using StatusLabel in Queries + +```csharp +// Project enum labels into a DTO +var orders = dbContext.Orders + .Select(o => new OrderDto + { + Id = o.Id, + StatusLabel = o.StatusLabel // Translated to CASE in SQL + }) + .ToList(); + +// Group by display name +var statusCounts = dbContext.Orders + .GroupBy(o => o.StatusLabel) + .Select(g => new { Status = g.Key, Count = g.Count() }) + .ToList(); + +// Filter on the computed label (less efficient — prefer filtering on the enum value directly) +var pending = dbContext.Orders + .Where(o => o.StatusLabel == "Pending Review") + .ToList(); +``` + +## Adding More Computed Properties + +```csharp +public class Order +{ + public OrderStatus Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusLabel => Status.GetDisplayName(); + + [Projectable(ExpandEnumMethods = true)] + public bool IsProcessing => Status.IsInProgress(); // Custom bool extension + + [Projectable(ExpandEnumMethods = true)] + public int StatusSortOrder => Status.GetSortOrder(); +} + +public static class OrderStatusExtensions +{ + public static string GetDisplayName(this OrderStatus status) { /* ... */ } + + public static bool IsInProgress(this OrderStatus status) => + status is OrderStatus.Approved or OrderStatus.Shipped; + + public static int GetSortOrder(this OrderStatus status) => + status switch { + OrderStatus.Pending => 1, + OrderStatus.Approved => 2, + OrderStatus.Shipped => 3, + OrderStatus.Rejected => 99, + _ => 0 + }; +} +``` + +## Nullable Enum Properties + +If the enum property is nullable, wrap the call in a null-conditional and configure the rewrite: + +```csharp +public class Order +{ + public OrderStatus? OptionalStatus { get; set; } + + [Projectable( + ExpandEnumMethods = true, + NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public string? OptionalStatusLabel => OptionalStatus?.GetDisplayName(); +} +``` + +## Enum on Navigation Property + +```csharp +public class Order +{ + public Customer Customer { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string CustomerTierLabel => Customer.Tier.GetDisplayName(); +} +``` + +## Best Practices + +- **Filter on the enum value** (not the label) for best SQL performance: `Where(o => o.Status == OrderStatus.Pending)`. +- **Use labels only for projection** (`Select`) — translating `WHERE StatusLabel = 'Pending Review'` is less efficient than `WHERE Status = 0`. +- If your enum changes frequently, regenerate — the display name values are baked in at compile time. + diff --git a/docs/recipes/nullable-navigation.md b/docs/recipes/nullable-navigation.md new file mode 100644 index 0000000..13a310a --- /dev/null +++ b/docs/recipes/nullable-navigation.md @@ -0,0 +1,159 @@ +# Nullable Navigation Properties + +This recipe covers how to work with optional (nullable) navigation properties in projectable members, using `NullConditionalRewriteSupport` to safely handle `?.` operators. + +## The Challenge + +Navigation properties can be nullable — either because the relationship is optional, or because the related entity isn't loaded. Using `?.` in a projectable body without configuration produces **error EFP0002**, because expression trees cannot represent the null-conditional operator directly. + +## Choosing a Strategy + +| Strategy | Best For | +|---|---| +| `Ignore` | SQL Server / databases with implicit null propagation; navigation is usually present | +| `Rewrite` | Cosmos DB; client-side evaluation scenarios; maximum correctness | +| Manual null check | Complex multi-level nullable chains where you want full control | + +## Strategy 1: `Ignore` + +Strips the `?.` — `A?.B` becomes `A.B`. In SQL, NULL propagates implicitly in most expressions. + +```csharp +public class User +{ + public Address? Address { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] + public string? CityName => Address?.City; +} +``` + +Generated expression: `Address.City` + +Generated SQL (SQL Server): +```sql +SELECT [a].[City] +FROM [Users] AS [u] +LEFT JOIN [Addresses] AS [a] ON [u].[AddressId] = [a].[Id] +``` + +If `Address` is `NULL`, SQL returns `NULL` for `City` — which matches the expected C# behavior. + +**Use when:** You're on SQL Server (or a database with implicit null propagation), and you're confident that `NULL` will propagate correctly for your use case. + +## Strategy 2: `Rewrite` + +Rewrites `A?.B` as `A != null ? A.B : null` — generates explicit null checks in the expression. + +```csharp +public class User +{ + public Address? Address { get; set; } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public string? CityName => Address?.City; +} +``` + +Generated expression: `Address != null ? Address.City : null` + +Generated SQL (SQL Server): +```sql +SELECT CASE WHEN [a].[Id] IS NOT NULL THEN [a].[City] END +FROM [Users] AS [u] +LEFT JOIN [Addresses] AS [a] ON [u].[AddressId] = [a].[Id] +``` + +**Use when:** You need explicit null handling, you're targeting Cosmos DB, or you want maximum semantic equivalence to C# code. + +## Multi-Level Nullable Chains + +For deeply nested nullable navigation: + +```csharp +public class User +{ + public Address? Address { get; set; } +} + +public class Address +{ + public City? City { get; set; } +} + +public class City +{ + public string? PostalCode { get; set; } +} + +// Ignore: strips all ?. +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public string? PostalCode => Address?.City?.PostalCode; +// → Address.City.PostalCode + +// Rewrite: explicit null check at each level +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public string? PostalCode => Address?.City?.PostalCode; +// → Address != null +// ? Address.City != null +// ? Address.City.PostalCode +// : null +// : null +``` + +## Strategy 3: Manual Null Checks + +For maximum control, write the null check explicitly — no `NullConditionalRewriteSupport` needed: + +```csharp +[Projectable] +public string? CityName => + Address != null ? Address.City : null; + +[Projectable] +public string? PostalCode => + Address != null && Address.City != null + ? Address.City.PostalCode + : null; +``` + +This approach is verbose but gives you precise control over the generated expression. + +## Extension Methods on Nullable Entity Parameters + +When an extension method's `this` parameter is nullable: + +```csharp +public static class UserExtensions +{ + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static string? GetFullAddress(this User? user) => + user?.Address?.AddressLine1 + ", " + user?.Address?.City; +} +``` + +## Combining with Other Options + +Null-conditional rewrite is compatible with other `[Projectable]` options: + +```csharp +[Projectable( + NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore, + ExpandEnumMethods = true)] +public string? ShippingStatusLabel => + ShippingInfo?.Status.GetDisplayName(); +``` + +## Practical Recommendation + +``` +Is the property on SQL Server? + → Yes, and null propagation is acceptable: use Ignore (simpler SQL) + → Yes, but you need explicit null behavior: use Rewrite + → No (Cosmos DB, in-memory, or client-side eval): use Rewrite or manual check +``` + +::: tip +Start with `Ignore` for SQL Server projects. Switch to `Rewrite` if you observe unexpected nullability behavior in query results. +::: + diff --git a/docs/recipes/reusable-query-filters.md b/docs/recipes/reusable-query-filters.md new file mode 100644 index 0000000..cec5100 --- /dev/null +++ b/docs/recipes/reusable-query-filters.md @@ -0,0 +1,167 @@ +# Reusable Query Filters + +This recipe shows how to define reusable filtering logic as projectable extension methods or properties, and compose them across multiple queries without duplicating LINQ expressions. + +## The Pattern + +Define your filtering criteria as `[Projectable]` members that return `bool`. Use them in `Where()` clauses exactly as you would any other property. EF Core translates the expanded expression to a SQL `WHERE` clause. + +## Example: Active Entity Filter + +```csharp +public class User +{ + public bool IsDeleted { get; set; } + public DateTime? LastLoginDate { get; set; } + public DateTime? EmailVerifiedDate { get; set; } + + [Projectable] + public bool IsActive => + !IsDeleted + && EmailVerifiedDate != null + && LastLoginDate >= DateTime.UtcNow.AddDays(-90); +} +``` + +```csharp +// Reuse everywhere +var activeUsers = dbContext.Users.Where(u => u.IsActive).ToList(); +var activeAdmins = dbContext.Users.Where(u => u.IsActive && u.IsAdmin).ToList(); +var activeCount = dbContext.Users.Count(u => u.IsActive); +``` + +Generated SQL (simplified): +```sql +SELECT * FROM [Users] +WHERE [IsDeleted] = 0 + AND [EmailVerifiedDate] IS NOT NULL + AND [LastLoginDate] >= DATEADD(day, -90, GETUTCDATE()) +``` + +## Example: Parameterized Filter as Extension Method + +Extension methods are ideal for filters that accept parameters: + +```csharp +public static class OrderExtensions +{ + [Projectable] + public static bool IsWithinDateRange(this Order order, DateTime from, DateTime to) => + order.CreatedDate >= from && order.CreatedDate <= to; + + [Projectable] + public static bool IsHighValue(this Order order, decimal threshold) => + order.GrandTotal >= threshold; + + [Projectable] + public static bool BelongsToRegion(this Order order, string region) => + order.ShippingAddress != null && order.ShippingAddress.Region == region; +} +``` + +```csharp +var from = DateTime.UtcNow.AddMonths(-1); +var to = DateTime.UtcNow; + +var recentHighValueOrders = dbContext.Orders + .Where(o => o.IsWithinDateRange(from, to)) + .Where(o => o.IsHighValue(500m)) + .ToList(); +``` + +## Example: Composing Multiple Filters + +Build complex filters by composing simpler ones: + +```csharp +public class Order +{ + [Projectable] + public bool IsFulfilled => FulfilledDate != null; + + [Projectable] + public bool IsRecent => CreatedDate >= DateTime.UtcNow.AddDays(-30); + + // Composed from simpler projectables + [Projectable] + public bool IsRecentFulfilledOrder => IsFulfilled && IsRecent; +} + +public static class OrderExtensions +{ + [Projectable] + public static bool IsEligibleForReturn(this Order order) => + order.IsFulfilled + && order.FulfilledDate >= DateTime.UtcNow.AddDays(-30) + && !order.HasOpenReturnRequest; +} +``` + +## Example: Global Query Filters + +Projectable properties work in EF Core's global query filters (configured in `OnModelCreating`): + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + // Soft-delete global filter using a projectable property + modelBuilder.Entity() + .HasQueryFilter(o => !o.IsDeleted); + + // Tenant isolation filter + modelBuilder.Entity() + .HasQueryFilter(o => o.TenantId == _currentTenantId); +} +``` + +::: info +When using global query filters with Projectables, ensure that `UseProjectables()` is configured on your `DbContext`. The library includes a convention (`ProjectablesExpandQueryFiltersConvention`) that ensures global filters referencing projectable members are also expanded correctly. +::: + +## Example: Specification Pattern + +Projectables pair naturally with the Specification pattern: + +```csharp +public static class OrderSpecifications +{ + [Projectable] + public static bool IsActive(this Order order) => + !order.IsCancelled && !order.IsDeleted; + + [Projectable] + public static bool IsOverdue(this Order order) => + order.IsActive() + && order.DueDate < DateTime.UtcNow + && !order.IsFulfilled; + + [Projectable] + public static bool RequiresAttention(this Order order) => + order.IsOverdue() + || order.HasOpenDispute + || order.PaymentStatus == PaymentStatus.Failed; +} +``` + +```csharp +// Dashboard: count orders requiring attention +var attentionCount = await dbContext.Orders + .Where(o => o.RequiresAttention()) + .CountAsync(); + +// Alert users with overdue orders +var overdueUserIds = await dbContext.Orders + .Where(o => o.IsOverdue()) + .Select(o => o.UserId) + .Distinct() + .ToListAsync(); +``` + +## Tips + +- **Keep filters pure** — filter projectables should only read data, never modify it. +- **Compose at the projectable level** — compose filters inside projectable members rather than chaining multiple `.Where()` calls for more reusable building blocks. +- **Name clearly** — use names that express business intent (`IsEligibleForRefund`) rather than technical details (`HasRefundDateNullAndStatusIsComplete`). +- **Prefer entity-level properties for entity-specific filters**, and extension methods for cross-entity or parameterized filters. +- **Use Limited mode** — parameterized filter methods are a perfect use case for [Limited compatibility mode](/reference/compatibility-mode), which caches the expanded query after the first execution. + diff --git a/docs/reference/compatibility-mode.md b/docs/reference/compatibility-mode.md new file mode 100644 index 0000000..2ea2ea8 --- /dev/null +++ b/docs/reference/compatibility-mode.md @@ -0,0 +1,111 @@ +# Compatibility Mode + +Compatibility mode controls **when** and **how** EF Core Projectables expands your projectable members during query execution. The choice affects both performance and query caching behavior. + +## Configuration + +Set the compatibility mode when registering Projectables: + +```csharp +options.UseProjectables(projectables => + projectables.CompatibilityMode(CompatibilityMode.Limited)); +``` + +## Modes + +### `Full` (Default) + +```csharp +options.UseProjectables(); // Full is the default + +// Or explicitly: +options.UseProjectables(p => p.CompatibilityMode(CompatibilityMode.Full)); +``` + +In Full mode, the expression tree is **expanded on every individual query invocation**, before being passed to EF Core. This is similar to how libraries like LinqKit work. + +**Flow:** +``` +LINQ query + → [Projectables expands all member calls] + → Expanded query sent to EF Core compiler + → SQL generated and executed +``` + +**Characteristics:** +- ✅ Works with **dynamic parameters** — captures fresh parameter values on each execution. +- ✅ Maximum compatibility — works in all EF Core scenarios. +- ⚠️ Slight overhead per query invocation (expression tree walking + expansion). +- ⚠️ EF Core's query cache key changes with expanded expressions, so the compiled query cache may be less effective. + +**When to use Full:** +- When you're running into query compilation errors with Limited mode. +- When your projectable members depend on dynamic expressions that change between calls. +- As a safe default while getting started. + +--- + +### `Limited` + +```csharp +options.UseProjectables(p => p.CompatibilityMode(CompatibilityMode.Limited)); +``` + +In Limited mode, expansion happens inside **EF Core's query translation preprocessor** — after EF Core accepts the query and before it compiles it. The expanded query is then stored in EF Core's query cache. Subsequent executions with the same query shape skip the expansion step entirely. + +**Flow:** +``` +LINQ query + → EF Core query preprocessor + → [Projectables expands member calls here] + → Expanded query compiled and stored in query cache + → SQL generated and executed + +Second execution with same query shape: + → EF Core query cache hit + → Compiled query reused directly (no expansion needed) +``` + +**Characteristics:** +- ✅ **Better performance** — after the first execution, cached queries bypass expansion entirely. +- ✅ Often **outperforms vanilla EF Core** for repeated queries. +- ⚠️ Dynamic parameters captured as closures may not work correctly — the expanded query is cached with the parameter values from the first execution. +- ⚠️ If a projectable member uses external runtime state (not EF Core query parameters), the cached expansion may be stale. + +**When to use Limited:** +- When all your projectable members' logic is deterministic given the query parameters. +- In production environments where query performance is critical. +- When queries are executed many times with the same shape. + +## Performance Comparison + +| Scenario | Full | Limited | Vanilla EF Core | +|---|---|---|---| +| First query execution | Slower (expansion overhead) | Slower (expansion + compile) | Baseline | +| Subsequent executions | Slower (expansion overhead) | **Faster** (cache hit, no expansion) | Baseline | +| Dynamic projectable parameters | ✅ Correct | ⚠️ May be stale | N/A | + +## Choosing a Mode + +``` +Start with Full (default) + ↓ +Is performance critical? + → No: Stay on Full + → Yes: Try Limited + ↓ + Do your queries produce correct results with Limited? + → Yes: Use Limited + → No: Stay on Full +``` + +## Troubleshooting + +### Queries returning wrong results in Limited mode + +If you're using projectable members that depend on values computed at runtime (outside of EF Core's parameter system), Limited mode may cache the wrong expansion. Switch to Full mode. + +### Query compilation errors in Full mode + +If Full mode causes compilation errors related to expression tree translation, check that your projectable members only use EF Core-translatable expressions. Refer to [Limitations](/advanced/limitations). + diff --git a/docs/reference/diagnostics.md b/docs/reference/diagnostics.md new file mode 100644 index 0000000..a7bc293 --- /dev/null +++ b/docs/reference/diagnostics.md @@ -0,0 +1,296 @@ +# Diagnostics Reference + +The Projectables source generator emits diagnostics (warnings and errors) during compilation to help you identify and fix issues with your projectable members. + +## Overview + +| ID | Severity | Title | +|---|---|---| +| [EFP0001](#efp0001) | ⚠️ Warning | Block-bodied member support is experimental | +| [EFP0002](#efp0002) | ❌ Error | Null-conditional expression not configured | +| [EFP0003](#efp0003) | ⚠️ Warning | Unsupported statement in block-bodied method | +| [EFP0004](#efp0004) | ❌ Error | Statement with side effects in block-bodied method | +| [EFP0005](#efp0005) | ⚠️ Warning | Potential side effect in block-bodied method | +| [EFP0006](#efp0006) | ❌ Error | Method or property requires a body definition | + +--- + +## EFP0001 — Block-bodied member support is experimental {#efp0001} + +**Severity:** Warning +**Category:** Design + +### Message + +``` +Block-bodied member '{0}' is using an experimental feature. +Set AllowBlockBody = true on the Projectable attribute to suppress this warning. +``` + +### Cause + +A `[Projectable]` member uses a block body (`{ ... }`) instead of an expression body (`=>`), which is an experimental feature. + +### Fix + +Suppress the warning by setting `AllowBlockBody = true`: + +```csharp +// Before (warning) +[Projectable] +public string GetCategory() +{ + if (Value > 100) return "High"; + return "Low"; +} + +// After (warning suppressed) +[Projectable(AllowBlockBody = true)] +public string GetCategory() +{ + if (Value > 100) return "High"; + return "Low"; +} +``` + +Or convert to an expression-bodied member: + +```csharp +[Projectable] +public string GetCategory() => Value > 100 ? "High" : "Low"; +``` + +--- + +## EFP0002 — Null-conditional expression not configured {#efp0002} + +**Severity:** Error +**Category:** Design + +### Message + +``` +'{0}' has a null-conditional expression exposed but is not configured to rewrite this +(Consider configuring a strategy using the NullConditionalRewriteSupport property +on the Projectable attribute) +``` + +### Cause + +The projectable member's body contains a null-conditional operator (`?.`) but `NullConditionalRewriteSupport` is not configured (defaults to `None`). + +### Fix + +Configure how the `?.` operator should be handled: + +```csharp +// ❌ Error +[Projectable] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; + +// ✅ Option 1: Ignore (strips the ?. — safe for SQL Server) +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; + +// ✅ Option 2: Rewrite (explicit null checks) +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; + +// ✅ Option 3: Rewrite the expression manually +[Projectable] +public string? FullAddress => + Location != null ? Location.AddressLine1 + " " + Location.City : null; +``` + +See [Null-Conditional Rewrite](/reference/null-conditional-rewrite) for details. + +--- + +## EFP0003 — Unsupported statement in block-bodied method {#efp0003} + +**Severity:** Warning +**Category:** Design + +### Message + +``` +Method '{0}' contains an unsupported statement: {1} +``` + +### Cause + +A block-bodied `[Projectable]` member contains a statement type that cannot be converted to an expression tree (e.g., loops, try-catch, throw, new object instantiation in statement position). + +### Unsupported Statements + +- `while`, `for`, `foreach` loops +- `try`/`catch`/`finally` blocks +- `throw` statements +- Object instantiation as a statement (not in a `return`) + +### Fix + +Refactor to use only supported constructs (`if`/`else`, `switch`, local variables, `return`), or convert to an expression-bodied member: + +```csharp +// ❌ Warning: loops are not supported +[Projectable(AllowBlockBody = true)] +public int SumItems() +{ + int total = 0; + foreach (var item in Items) // EFP0003 + total += item.Price; + return total; +} + +// ✅ Use LINQ instead +[Projectable] +public int SumItems() => Items.Sum(i => i.Price); +``` + +--- + +## EFP0004 — Statement with side effects in block-bodied method {#efp0004} + +**Severity:** Error +**Category:** Design + +### Message + +Context-specific — one of: + +- `Property assignment '{0}' has side effects and cannot be used in projectable methods` +- `Compound assignment operator '{0}' has side effects` +- `Increment/decrement operator '{0}' has side effects` + +### Cause + +A block-bodied projectable member modifies state. Expression trees cannot represent side effects. + +### Triggers + +```csharp +// ❌ Property assignment +Bar = 10; + +// ❌ Compound assignment +Bar += 10; + +// ❌ Increment / Decrement +Bar++; +--Count; +``` + +### Fix + +Remove the side-effecting statement. Projectable members must be **pure functions** — they can only read data and return a value. + +```csharp +// ❌ Error: has side effects +[Projectable(AllowBlockBody = true)] +public int Foo() +{ + Bar = 10; // EFP0004 + return Bar; +} + +// ✅ Read-only computation +[Projectable] +public int Foo() => Bar + 10; +``` + +--- + +## EFP0005 — Potential side effect in block-bodied method {#efp0005} + +**Severity:** Warning +**Category:** Design + +### Message + +``` +Method call '{0}' may have side effects and cannot be guaranteed to be safe in projectable methods +``` + +### Cause + +A block-bodied projectable member calls a method that is **not** itself marked with `[Projectable]`. Such calls may have side effects that cannot be represented in an expression tree. + +### Example + +```csharp +[Projectable(AllowBlockBody = true)] +public int Foo() +{ + Console.WriteLine("test"); // ⚠️ EFP0005 — may have side effects + return Bar; +} +``` + +### Fix + +- Remove the method call if it is not needed in a query context. +- If the method is safe to use in queries, mark it with `[Projectable]`. + +--- + +## EFP0006 — Method or property requires a body definition {#efp0006} + +**Severity:** Error +**Category:** Design + +### Message + +``` +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. +``` + +### Cause + +A `[Projectable]` member has no body — it is abstract, an interface declaration, or an auto-property without an expression. + +### Fix + +Provide a body, or use [`UseMemberBody`](/reference/use-member-body) to delegate to another member: + +```csharp +// ❌ Error: no body +[Projectable] +public string FullName { get; set; } + +// ✅ Expression-bodied property +[Projectable] +public string FullName => FirstName + " " + LastName; + +// ✅ Delegate to another member +[Projectable(UseMemberBody = nameof(ComputeFullName))] +public string FullName => ComputeFullName(); +private string ComputeFullName() => FirstName + " " + LastName; +``` + +--- + +## Suppressing Diagnostics + +Individual warnings can be suppressed with standard C# pragma directives: + +```csharp +#pragma warning disable EFP0001 +[Projectable] +public string GetValue() +{ + if (IsActive) return "Active"; + return "Inactive"; +} +#pragma warning restore EFP0001 +``` + +Or via `.editorconfig` / `Directory.Build.props`: + +```xml + + $(NoWarn);EFP0001;EFP0003 + +``` + diff --git a/docs/reference/expand-enum-methods.md b/docs/reference/expand-enum-methods.md new file mode 100644 index 0000000..a293e22 --- /dev/null +++ b/docs/reference/expand-enum-methods.md @@ -0,0 +1,169 @@ +# Expand Enum Methods + +The `ExpandEnumMethods` option allows you to call ordinary C# methods on enum values inside a projectable member and have those calls translated to SQL `CASE` expressions. Without this option, calling a non-projectable method on an enum value would fail SQL translation. + +## The Problem + +You have an enum with a helper method: + +```csharp +public enum OrderStatus { Pending, Approved, Rejected } + +public static class OrderStatusExtensions +{ + public static string GetDisplayName(this OrderStatus status) => + status switch { + OrderStatus.Pending => "Pending Review", + OrderStatus.Approved => "Approved", + OrderStatus.Rejected => "Rejected", + _ => status.ToString() + }; +} +``` + +If you try to use `GetDisplayName()` inside a projectable member without `ExpandEnumMethods`, the generator cannot produce a valid expression tree because `GetDisplayName` is not a database function. + +## The Solution + +Set `ExpandEnumMethods = true` on the projectable member that calls the enum method: + +```csharp +public class Order +{ + public OrderStatus Status { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string StatusName => Status.GetDisplayName(); +} +``` + +The generator enumerates all values of `OrderStatus` and produces a chain of ternary expressions: + +```csharp +// Generated expression equivalent +Status == OrderStatus.Pending ? GetDisplayName(OrderStatus.Pending) : +Status == OrderStatus.Approved ? GetDisplayName(OrderStatus.Approved) : +Status == OrderStatus.Rejected ? GetDisplayName(OrderStatus.Rejected) : +null +``` + +EF Core then translates this to a SQL `CASE` expression: + +```sql +SELECT CASE + WHEN [o].[Status] = 0 THEN N'Pending Review' + WHEN [o].[Status] = 1 THEN N'Approved' + WHEN [o].[Status] = 2 THEN N'Rejected' +END AS [StatusName] +FROM [Orders] AS [o] +``` + +## Supported Return Types + +| Return type | Default fallback value | +|---|---| +| `string` | `null` | +| `bool` | `default(bool)` → `false` | +| `int` | `default(int)` → `0` | +| Other value types | `default(T)` | +| Nullable types | `null` | + +## Examples + +### Boolean Return + +```csharp +public static bool IsApproved(this OrderStatus status) => + status == OrderStatus.Approved; + +[Projectable(ExpandEnumMethods = true)] +public bool IsStatusApproved => Status.IsApproved(); +``` + +Generated SQL: +```sql +SELECT CASE + WHEN [o].[Status] = 0 THEN CAST(0 AS bit) + WHEN [o].[Status] = 1 THEN CAST(1 AS bit) + WHEN [o].[Status] = 2 THEN CAST(0 AS bit) + ELSE CAST(0 AS bit) +END AS [IsStatusApproved] +FROM [Orders] AS [o] +``` + +### Integer Return + +```csharp +public static int GetSortOrder(this OrderStatus status) => (int)status; + +[Projectable(ExpandEnumMethods = true)] +public int StatusSortOrder => Status.GetSortOrder(); +``` + +Generated SQL: +```sql +SELECT CASE + WHEN [o].[Status] = 0 THEN 0 + WHEN [o].[Status] = 1 THEN 1 + WHEN [o].[Status] = 2 THEN 2 + ELSE 0 +END AS [StatusSortOrder] +FROM [Orders] AS [o] +``` + +### Methods with Additional Parameters + +Additional parameters are passed through to each branch of the expanded ternary: + +```csharp +public static string Format(this OrderStatus status, string prefix) => + prefix + status.ToString(); + +[Projectable(ExpandEnumMethods = true)] +public string FormattedStatus => Status.Format("Status: "); +``` + +### Nullable Enum Types + +If the enum property is nullable, the expansion is wrapped in a null check: + +```csharp +public class Order +{ + public OrderStatus? OptionalStatus { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string? OptionalStatusName => OptionalStatus?.GetDisplayName(); +} +``` + +### Enum on Navigation Properties + +```csharp +public class Order +{ + public Customer Customer { get; set; } + + [Projectable(ExpandEnumMethods = true)] + public string CustomerTierName => Customer.Tier.GetDisplayName(); +} +``` + +## Limitations + +- The method being expanded **must be deterministic** — it will be evaluated at code-generation time for each enum value. +- All enum values must produce valid SQL-translatable results. +- The enum type must be known at compile time (no dynamic enum types). +- Only the outermost enum method call on the enum property is expanded; nested calls may require multiple projectable members. + +## Comparison with `[Projectable]` on the Extension Method + +You might wonder: why not just put `[Projectable]` on `GetDisplayName` itself? + +| Approach | When to use | +|---|---| +| `[Projectable]` on the extension method | The method body is a simple expression EF Core can translate (e.g., `== OrderStatus.Approved`). | +| `ExpandEnumMethods = true` | The method body is complex or references non-EF-translatable code (e.g., reads a `[Display]` attribute via reflection). | + +`ExpandEnumMethods` evaluates the method at **compile time** for each enum value and bakes the results into the expression tree, so the method body doesn't need to be translatable at all. + diff --git a/docs/reference/null-conditional-rewrite.md b/docs/reference/null-conditional-rewrite.md new file mode 100644 index 0000000..9cfc7ca --- /dev/null +++ b/docs/reference/null-conditional-rewrite.md @@ -0,0 +1,152 @@ +# Null-Conditional Rewrite + +Expression trees — the representation EF Core uses internally — cannot directly express the null-conditional operator (`?.`). If your projectable member contains `?.`, the source generator needs to know how to handle it. + +## The Problem + +Consider this projectable property: + +```csharp +[Projectable] +public string? FullAddress => + Location?.AddressLine1 + " " + Location?.City; +``` + +This is valid C# code, but it **cannot be converted to an expression tree as-is**. The null-conditional operator is syntactic sugar that cannot be represented directly in an `Expression>`. + +By default (with `NullConditionalRewriteSupport.None`), the generator will report **error EFP0002** and refuse to generate code. + +## The `NullConditionalRewriteSupport` Options + +Configure the strategy on the `[Projectable]` attribute: + +```csharp +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +``` + +--- + +### `None` (Default) + +```csharp +[Projectable] // NullConditionalRewriteSupport.None is the default +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; +``` + +The generator **rejects** any use of `?.`. This produces error EFP0002: + +``` +error EFP0002: 'FullAddress' has a null-conditional expression exposed but is not configured +to rewrite this (Consider configuring a strategy using the NullConditionalRewriteSupport +property on the Projectable attribute) +``` + +**Use when:** Your projectable members never use `?.` — this is the safest default that prevents accidental misuse. + +--- + +### `Ignore` + +```csharp +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; +``` + +The null-conditional operators are **stripped** — `A?.B` becomes `A.B`. + +Generated expression is equivalent to: +```csharp +Location.AddressLine1 + " " + Location.City +``` + +**Behavior in SQL:** The result is `NULL` if any operand is `NULL`, because SQL's null propagation works implicitly in most expressions. This is consistent with how most SQL databases handle null values. + +**Use when:** +- You're using SQL Server or another database where null propagation in expressions works as expected. +- You know the navigation property will not be null in practice (or null is an acceptable result when it is). +- You want simpler, shorter SQL output. + +**Generated SQL example (SQL Server):** +```sql +SELECT ([u].[AddressLine1] + N' ') + [u].[City] +FROM [Users] AS [u] +``` + +--- + +### `Rewrite` + +```csharp +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; +``` + +The null-conditional operators are **rewritten as explicit null checks** — `A?.B` becomes `A != null ? A.B : null`. + +Generated expression is equivalent to: +```csharp +(Location != null ? Location.AddressLine1 : null) ++ " " + +(Location != null ? Location.City : null) +``` + +**Use when:** +- You need **explicit null handling** in the generated expression. +- You're targeting Cosmos DB or another provider that evaluates expressions client-side. +- You want the expression to behave identically to the original C# code. + +**Trade-off:** The generated SQL can become significantly more complex, especially with deeply nested null-conditional chains. + +**Generated SQL example (SQL Server):** +```sql +SELECT + CASE WHEN [u].[LocationId] IS NOT NULL THEN [l].[AddressLine1] ELSE NULL END + + N' ' + + CASE WHEN [u].[LocationId] IS NOT NULL THEN [l].[City] ELSE NULL END +FROM [Users] AS [u] +LEFT JOIN [Locations] AS [l] ON [u].[LocationId] = [l].[Id] +``` + +## Comparison Table + +| Option | `?.` allowed | Expression generated | SQL complexity | +|---|---|---|---| +| `None` | ❌ (error EFP0002) | — | — | +| `Ignore` | ✅ | `A.B` | Simple | +| `Rewrite` | ✅ | `A != null ? A.B : null` | Higher | + +## Practical Recommendation + +- **SQL Server + navigations you control:** Use `Ignore` — SQL Server's null semantics match C#'s null-conditional in most cases. +- **Cosmos DB or client-side evaluation:** Use `Rewrite` — you need explicit null checks. +- **Unsure:** Start with `Rewrite` for correctness, optimize to `Ignore` if SQL complexity is an issue. + +## Example: Navigation Property Chain + +```csharp +public class User +{ + public Address? Location { get; set; } +} + +public class Address +{ + public string? AddressLine1 { get; set; } + public string? City { get; set; } +} + +// Option 1: Ignore (simpler SQL) +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)] +public static string? GetFullAddress(this User? user) => + user?.Location?.AddressLine1 + " " + user?.Location?.City; +// → user.Location.AddressLine1 + " " + user.Location.City + +// Option 2: Rewrite (explicit null checks, safer) +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public static string? GetFullAddress(this User? user) => + user?.Location?.AddressLine1 + " " + user?.Location?.City; +// → (user != null ? (user.Location != null ? user.Location.AddressLine1 : null) : null) +// + " " + +// (user != null ? (user.Location != null ? user.Location.City : null) : null) +``` + diff --git a/docs/reference/projectable-attribute.md b/docs/reference/projectable-attribute.md new file mode 100644 index 0000000..1b09deb --- /dev/null +++ b/docs/reference/projectable-attribute.md @@ -0,0 +1,149 @@ +# `[Projectable]` Attribute + +The `ProjectableAttribute` is the entry point for this library. Place it on any property or method to tell the source generator to produce a companion expression tree for it. + +## Namespace + +```csharp +using EntityFrameworkCore.Projectables; +``` + +## Target + +| Target | Supported | +|---|---| +| Properties | ✅ | +| Methods | ✅ | +| Extension methods | ✅ | +| Constructors | ❌ | +| Indexers | ❌ | + +The attribute can be inherited by derived types (`Inherited = true`). + +## Properties + +### `NullConditionalRewriteSupport` + +**Type:** `NullConditionalRewriteSupport` +**Default:** `NullConditionalRewriteSupport.None` + +Controls how null-conditional operators (`?.`) in the member body are handled by the source generator. + +```csharp +[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] +public string? FullAddress => Location?.AddressLine1 + " " + Location?.City; +``` + +| Value | Behavior | +|---|---| +| `None` (default) | Null-conditional operators are **not allowed** — the generator raises error EFP0002. | +| `Ignore` | Null-conditional operators are **stripped** — `A?.B` becomes `A.B`. Safe for databases where null propagates implicitly (SQL Server). | +| `Rewrite` | Null-conditional operators are **rewritten** as explicit null checks — `A?.B` becomes `A != null ? A.B : null`. Safer but may increase SQL complexity. | + +See [Null-Conditional Rewrite](/reference/null-conditional-rewrite) for full details. + +--- + +### `UseMemberBody` + +**Type:** `string?` +**Default:** `null` + +Tells the generator to use a **different member's body** as the source for the generated expression tree. Useful when the projectable member's body is not directly available (e.g., interface implementation, abstract member). + +```csharp +public class Order +{ + // The actual computation is defined here + private decimal ComputeGrandTotal() => Subtotal + Tax; + + // The projectable member delegates to it + [Projectable(UseMemberBody = nameof(ComputeGrandTotal))] + public decimal GrandTotal => ComputeGrandTotal(); +} +``` + +See [Use Member Body](/reference/use-member-body) for full details. + +--- + +### `ExpandEnumMethods` + +**Type:** `bool` +**Default:** `false` + +When set to `true`, method calls on enum values inside this projectable member are expanded into a **chain of ternary expressions** — one branch per enum value. This allows enum helper methods (like display name lookups) to be translated to SQL `CASE` expressions. + +```csharp +[Projectable(ExpandEnumMethods = true)] +public string StatusName => Status.GetDisplayName(); +``` + +See [Expand Enum Methods](/reference/expand-enum-methods) for full details. + +--- + +### `AllowBlockBody` + +**Type:** `bool` +**Default:** `false` + +Enables **block-bodied member** support (experimental). Without this flag, using a block body with `[Projectable]` produces warning EFP0001. Setting this to `true` suppresses the warning. + +```csharp +[Projectable(AllowBlockBody = true)] +public string Category +{ + get + { + if (Score > 100) return "High"; + else if (Score > 50) return "Medium"; + else return "Low"; + } +} +``` + +See [Block-Bodied Members](/advanced/block-bodied-members) for full details. + +--- + +## Complete Example + +```csharp +public class Order +{ + public OrderStatus Status { get; set; } + public decimal TaxRate { get; set; } + public Address? ShippingAddress { get; set; } + public ICollection Items { get; set; } + + // Simple computed property + [Projectable] + public decimal Subtotal => Items.Sum(i => i.Price * i.Quantity); + + // Composing projectables + [Projectable] + public decimal GrandTotal => Subtotal * (1 + TaxRate); + + // Handling nullable navigation + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public string? ShippingLine => ShippingAddress?.AddressLine1 + ", " + ShippingAddress?.City; + + // Enum expansion + [Projectable(ExpandEnumMethods = true)] + public string StatusLabel => Status.GetDisplayName(); + + // Block-bodied (experimental) + [Projectable(AllowBlockBody = true)] + public string Priority + { + get + { + if (GrandTotal > 1000) return "High"; + if (GrandTotal > 500) return "Medium"; + return "Normal"; + } + } +} +``` + diff --git a/docs/reference/use-member-body.md b/docs/reference/use-member-body.md new file mode 100644 index 0000000..a781faf --- /dev/null +++ b/docs/reference/use-member-body.md @@ -0,0 +1,106 @@ +# Use Member Body + +The `UseMemberBody` option tells the source generator to use a **different member's body** as the source expression for the generated expression tree. This is useful when the projectable member itself cannot have a body. + +## Basic Usage + +```csharp +[Projectable(UseMemberBody = nameof(ComputeFullName))] +public string FullName => ComputeFullName(); + +private string ComputeFullName() => FirstName + " " + LastName; +``` + +The generator reads the body of `ComputeFullName` and generates an expression tree from it, even though `FullName` is marked as the projectable. + +## Use Cases + +### Interface Members + +Interface members cannot have bodies. Use `UseMemberBody` to delegate to a default implementation or a helper: + +```csharp +public interface IOrderSummary +{ + decimal GrandTotal { get; } +} + +public class Order : IOrderSummary +{ + public decimal TaxRate { get; set; } + public ICollection Items { get; set; } + + // The actual computation + private decimal ComputeGrandTotal() => + Items.Sum(i => i.Price * i.Quantity) * (1 + TaxRate); + + // Marks the interface member as projectable, delegates body + [Projectable(UseMemberBody = nameof(ComputeGrandTotal))] + public decimal GrandTotal => ComputeGrandTotal(); +} +``` + +### Separating Declaration from Implementation + +Keep the entity class clean by delegating computation to private helpers: + +```csharp +public class Customer +{ + public DateTime BirthDate { get; set; } + public DateTime LastOrderDate { get; set; } + + [Projectable(UseMemberBody = nameof(ComputeAge))] + public int Age => ComputeAge(); + + [Projectable(UseMemberBody = nameof(ComputeDaysSinceLastOrder))] + public int DaysSinceLastOrder => ComputeDaysSinceLastOrder(); + + // Implementation details hidden from the projectable declarations + private int ComputeAge() => + DateTime.Today.Year - BirthDate.Year - (DateTime.Today.DayOfYear < BirthDate.DayOfYear ? 1 : 0); + + private int ComputeDaysSinceLastOrder() => + (DateTime.Today - LastOrderDate).Days; +} +``` + +### Reusing Bodies Across Multiple Members + +The same body can power multiple projectable members: + +```csharp +public class Order +{ + private bool IsEligibleForDiscount() => + Items.Count > 5 && TotalValue > 100; + + // Both members share the same expression body + [Projectable(UseMemberBody = nameof(IsEligibleForDiscount))] + public bool CanApplyDiscount => IsEligibleForDiscount(); + + [Projectable(UseMemberBody = nameof(IsEligibleForDiscount))] + public bool ShowDiscountBadge => IsEligibleForDiscount(); +} +``` + +## Rules + +- The referenced member (via `UseMemberBody`) must exist in the **same class** as the projectable member. +- The referenced member must have a **compatible return type**. +- The referenced member must be an **expression-bodied method or property** (it doesn't need `[Projectable]` itself). +- The referenced member must have a **compatible parameter list** — if the projectable is a method with parameters, the referenced member must have matching parameters. + +## Method with Parameters + +```csharp +public class Order +{ + [Projectable(UseMemberBody = nameof(ComputeDiscountedTotal))] + public decimal GetDiscountedTotal(decimal discountRate) => ComputeDiscountedTotal(discountRate); + + private decimal ComputeDiscountedTotal(decimal discountRate) => + GrandTotal * (1 - discountRate); +} +``` + From 0ac1c2a8a4c0ba4b6e4eb112a7f184abb6b87384 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sat, 21 Feb 2026 10:36:22 +0100 Subject: [PATCH 28/30] Improve formatting --- docs/.vitepress/config.mts | 7 ++ docs/advanced/block-bodied-members.md | 10 +-- docs/advanced/how-it-works.md | 98 +++++++++++++-------------- docs/guide/introduction.md | 30 ++++---- docs/index.md | 20 ++++-- docs/public/logo.svg | 59 ++++++++++++++++ docs/public/social.svg | 98 +++++++++++++++++++++++++++ 7 files changed, 246 insertions(+), 76 deletions(-) create mode 100644 docs/public/logo.svg create mode 100644 docs/public/social.svg diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index fad18b1..572b6cf 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -4,6 +4,13 @@ import { defineConfig } from 'vitepress' export default defineConfig({ title: "EF Core Projectables", description: "Flexible projection magic for EF Core — use properties and methods directly in your LINQ queries", + head: [ + ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }], + ['meta', { property: 'og:image', content: 'https://projectables.github.io/social.svg' }], + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { name: 'twitter:card', content: 'summary_large_image' }], + ['meta', { name: 'twitter:image', content: 'https://projectables.github.io/social.svg' }], + ], themeConfig: { logo: '/logo.svg', nav: [ diff --git a/docs/advanced/block-bodied-members.md b/docs/advanced/block-bodied-members.md index fd93202..7ce378c 100644 --- a/docs/advanced/block-bodied-members.md +++ b/docs/advanced/block-bodied-members.md @@ -302,11 +302,11 @@ public int SumItems() => Items.Sum(i => i.Price); The generator actively detects statements with side effects and reports them as errors (EFP0004) or warnings (EFP0005). See [Diagnostics](/reference/diagnostics) for the full list. -| Code | Diagnostic | -|---|---| -| `Bar = 10;` | ❌ EFP0004 — property assignment | -| `Bar += 10;` | ❌ EFP0004 — compound assignment | -| `Bar++;` | ❌ EFP0004 — increment/decrement | +| Code | Diagnostic | +|---------------------------|------------------------------------------| +| `Bar = 10;` | ❌ EFP0004 — property assignment | +| `Bar += 10;` | ❌ EFP0004 — compound assignment | +| `Bar++;` | ❌ EFP0004 — increment/decrement | | `Console.WriteLine("x");` | ⚠️ EFP0005 — non-projectable method call | ## How the Conversion Works diff --git a/docs/advanced/how-it-works.md b/docs/advanced/how-it-works.md index d6682ed..25fc4a0 100644 --- a/docs/advanced/how-it-works.md +++ b/docs/advanced/how-it-works.md @@ -11,39 +11,39 @@ Understanding the internals of EF Core Projectables helps you use it effectively │ Your C# code with [Projectable] members │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────┐ │ -│ │ Roslyn Source Generator │ │ -│ │ (ProjectionExpressionGenerator) │ -│ │ - Scans for [Projectable] │ │ -│ │ - Parses member bodies │ │ -│ │ - Generates Expression<> │ │ -│ │ companion classes │ │ -│ └─────────────────────────────┘ │ +│ ┌───────────────────────────────────┐ │ +│ │ Roslyn Source Generator │ │ +│ │ (ProjectionExpressionGenerator) │ │ +│ │ - Scans for [Projectable] │ │ +│ │ - Parses member bodies │ │ +│ │ - Generates Expression<> │ │ +│ │ companion classes │ │ +│ └───────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Auto-generated *.g.cs files with Expression<> trees │ └─────────────────────────────────────────────────────────┘ -┌─────────────────────────────────────────────────────────┐ -│ RUNTIME │ -│ │ -│ LINQ query using projectable member │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────┐ │ -│ │ ProjectableExpressionReplacer (ExpressionVisitor) │ -│ │ - Walks the LINQ expression tree │ -│ │ - Detects calls to [Projectable] members │ -│ │ - Loads generated Expression<> via reflection │ -│ │ - Substitutes the call with the expression │ -│ └─────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ Expanded expression tree (no [Projectable] calls) │ -│ │ │ -│ ▼ │ -│ Standard EF Core SQL translation → SQL query │ -└─────────────────────────────────────────────────────────┘ +┌───────────────────────────────────────────────────────────┐ +│ RUNTIME │ +│ │ +│ LINQ query using projectable member │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ ProjectableExpressionReplacer (ExpressionVisitor) │ │ +│ │ - Walks the LINQ expression tree │ │ +│ │ - Detects calls to [Projectable] members │ │ +│ │ - Loads generated Expression<> via reflection │ │ +│ │ - Substitutes the call with the expression │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ Expanded expression tree (no [Projectable] calls) │ +│ │ │ +│ ▼ │ +│ Standard EF Core SQL translation → SQL query │ +└───────────────────────────────────────────────────────────┘ ``` ## Build Time: The Source Generator @@ -70,22 +70,22 @@ Reads the attribute arguments, resolves the member's type information (namespace Converts block-bodied method statements into expression-tree-compatible forms: -| Statement | Converted to | -|---|---| -| `if (cond) return A; else return B;` | `cond ? A : B` | -| `switch (x) { case 1: return "a"; }` | `x == 1 ? "a" : ...` | -| `var v = expr; return v + 1;` | Inline substitution: `expr + 1` | -| Multiple early `return` | Nested ternary chain | +| Statement | Converted to | +|--------------------------------------|---------------------------------| +| `if (cond) return A; else return B;` | `cond ? A : B` | +| `switch (x) { case 1: return "a"; }` | `x == 1 ? "a" : ...` | +| `var v = expr; return v + 1;` | Inline substitution: `expr + 1` | +| Multiple early `return` | Nested ternary chain | ### Expression Rewriters After the body is extracted, several rewriters transform the expression syntax: -| Rewriter | Purpose | -|---|---| -| `ExpressionSyntaxRewriter` | Rewrites `?.` operators based on `NullConditionalRewriteSupport` | -| `DeclarationSyntaxRewriter` | Adjusts member declarations for the generated class | -| `VariableReplacementRewriter` | Inlines local variables into the return expression | +| Rewriter | Purpose | +|-------------------------------|------------------------------------------------------------------| +| `ExpressionSyntaxRewriter` | Rewrites `?.` operators based on `NullConditionalRewriteSupport` | +| `DeclarationSyntaxRewriter` | Adjusts member declarations for the generated class | +| `VariableReplacementRewriter` | Inlines local variables into the return expression | ### Generated Code @@ -162,15 +162,15 @@ The replacer also manages EF Core's tracking behavior. When a projectable member ## Summary -| Phase | Component | Responsibility | -|---|---|---| -| Build | `ProjectionExpressionGenerator` | Source gen entry point, orchestration | -| Build | `ProjectableInterpreter` | Extract descriptor from attribute + syntax | -| Build | `BlockStatementConverter` | Block body → expression conversion | -| Build | `ExpressionSyntaxRewriter` | `?.` handling, null-conditional rewrite | -| Runtime | `CustomQueryCompiler` | Full mode: expand before EF Core | +| Phase | Component | Responsibility | +|---------|--------------------------------------|----------------------------------------------| +| Build | `ProjectionExpressionGenerator` | Source gen entry point, orchestration | +| Build | `ProjectableInterpreter` | Extract descriptor from attribute + syntax | +| Build | `BlockStatementConverter` | Block body → expression conversion | +| Build | `ExpressionSyntaxRewriter` | `?.` handling, null-conditional rewrite | +| Runtime | `CustomQueryCompiler` | Full mode: expand before EF Core | | Runtime | `CustomQueryTranslationPreprocessor` | Limited mode: expand inside EF Core pipeline | -| Runtime | `ProjectableExpressionReplacer` | Walk and replace projectable calls | -| Runtime | `ProjectionExpressionResolver` | Locate generated expression via reflection | -| Runtime | `ExpressionArgumentReplacer` | Substitute parameters in lambda | +| Runtime | `ProjectableExpressionReplacer` | Walk and replace projectable calls | +| Runtime | `ProjectionExpressionResolver` | Locate generated expression via reflection | +| Runtime | `ExpressionArgumentReplacer` | Substitute parameters in lambda | diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md index a9c6e6e..2df35da 100644 --- a/docs/guide/introduction.md +++ b/docs/guide/introduction.md @@ -68,24 +68,24 @@ LINQ query ## Comparison with Similar Libraries -| Feature | Projectables | Expressionify | LinqKit | -|---|---|---|---| -| Source generator based | ✅ | ✅ | ❌ | -| Works with entity methods | ✅ | ✅ | Partial | -| Works with extension methods | ✅ | ✅ | ✅ | -| Composable projectables | ✅ | ❌ | Partial | -| Block-bodied members | ✅ (experimental) | ❌ | ❌ | -| Enum method expansion | ✅ | ❌ | ❌ | -| Null-conditional rewriting | ✅ | ❌ | ❌ | -| Limited/cached mode | ✅ | ❌ | ❌ | +| Feature | Projectables | Expressionify | LinqKit | +|------------------------------|------------------|---------------|---------| +| Source generator based | ✅ | ✅ | ❌ | +| Works with entity methods | ✅ | ✅ | Partial | +| Works with extension methods | ✅ | ✅ | ✅ | +| Composable projectables | ✅ | ❌ | Partial | +| Block-bodied members | ✅ (experimental) | ❌ | ❌ | +| Enum method expansion | ✅ | ❌ | ❌ | +| Null-conditional rewriting | ✅ | ❌ | ❌ | +| Limited/cached mode | ✅ | ❌ | ❌ | ## EF Core Version Compatibility -| Library Version | EF Core Version | -|---|---| -| v1.x | EF Core 3.1, 5 | -| v2.x, v3.x | EF Core 6, 7 | -| v6.x+ | EF Core 6+ (block-bodied members added) | +| Library Version | EF Core Version | +|-----------------|-----------------------------------------| +| v1.x | EF Core 3.1, 5 | +| v2.x, v3.x | EF Core 6, 7 | +| v6.x+ | EF Core 6+ (block-bodied members added) | ## Next Steps diff --git a/docs/index.md b/docs/index.md index f55400a..502a6fa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -45,13 +45,19 @@ features: ## At a Glance ```csharp -class Order { +class Order +{ public decimal TaxRate { get; set; } public ICollection Items { get; set; } - [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); - [Projectable] public decimal Tax => Subtotal * TaxRate; - [Projectable] public decimal GrandTotal => Subtotal + Tax; + [Projectable] + public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity); + + [Projectable] + public decimal Tax => Subtotal * TaxRate; + + [Projectable] + public decimal GrandTotal => Subtotal + Tax; } // Use it anywhere in your queries — translated to SQL automatically @@ -65,8 +71,8 @@ The properties are **inlined into the SQL** — no client-side evaluation, no N+ ## NuGet Packages -| Package | Description | -|---|---| +| Package | Description | +|----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------| | [`EntityFrameworkCore.Projectables.Abstractions`](https://www.nuget.org/packages/EntityFrameworkCore.Projectables.Abstractions/) | The `[Projectable]` attribute and source generator | -| [`EntityFrameworkCore.Projectables`](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) | The EF Core runtime extension | +| [`EntityFrameworkCore.Projectables`](https://www.nuget.org/packages/EntityFrameworkCore.Projectables/) | The EF Core runtime extension | diff --git a/docs/public/logo.svg b/docs/public/logo.svg new file mode 100644 index 0000000..ece1394 --- /dev/null +++ b/docs/public/logo.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/public/social.svg b/docs/public/social.svg new file mode 100644 index 0000000..a11bf63 --- /dev/null +++ b/docs/public/social.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EF Core + + + Projectables + + + + + Flexible projection magic for EF Core + + + + + + [Projectable] + + + + + EF Core 6+ + + + From f4d56d800f85be4814810bb7ae54050299492f39 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sat, 21 Feb 2026 22:32:36 +0100 Subject: [PATCH 29/30] Add GitHub Actions workflow for deploying documentation to GitHub Pages --- .github/workflows/docs.yml | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..a56c9d8 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,68 @@ +name: Deploy Docs to GitHub Pages + +on: + push: + branches: + - main + paths: + - 'docs/**' + - '.github/workflows/docs.yml' + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Build VitePress site + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Needed for lastUpdated feature + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: docs/package-lock.json + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Install dependencies + working-directory: docs + run: npm ci + + - name: Build VitePress site + working-directory: docs + run: npm run build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + name: Deploy to GitHub Pages + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + From cf37a4adea55038fee8b295f31ec72a6e0c6a7bb Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sat, 21 Feb 2026 22:33:42 +0100 Subject: [PATCH 30/30] Fix branch name --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a56c9d8..16ef920 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ on: push: branches: - - main + - master paths: - 'docs/**' - '.github/workflows/docs.yml'