diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
index 5f28270..5f7773d 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
@@ -1 +1,6 @@
-
\ No newline at end of file
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-----------------------------------------------
+EFP0007 | Design | Error | Unsupported pattern in projectable expression
+
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
index 70e2964..9b968a3 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs
@@ -52,5 +52,13 @@ public static class Diagnostics
DiagnosticSeverity.Error,
isEnabledByDefault: true);
+ public static readonly DiagnosticDescriptor UnsupportedPatternInExpression = new DiagnosticDescriptor(
+ id: "EFP0007",
+ title: "Unsupported pattern in projectable expression",
+ messageFormat: "The pattern '{0}' cannot be rewritten into an expression tree. Simplify the pattern or restructure the projectable member body.",
+ category: "Design",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
index f953701..ae58790 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs
@@ -309,130 +309,56 @@ private ExpressionSyntax CreateMethodCallOnEnumValue(IMethodSymbol methodSymbol,
public override SyntaxNode? VisitSwitchExpression(SwitchExpressionSyntax node)
{
- // Reverse arms order to start from the default value
var arms = node.Arms.Reverse();
-
+ var visitedGoverning = (ExpressionSyntax)Visit(node.GoverningExpression);
ExpressionSyntax? currentExpression = null;
foreach (var arm in arms)
{
var armExpression = (ExpressionSyntax)Visit(arm.Expression);
-
- // Handle fallback value
+
if (currentExpression == null)
{
- currentExpression = arm.Pattern is DiscardPatternSyntax
+ currentExpression = arm.Pattern is DiscardPatternSyntax
? armExpression
: SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression);
-
continue;
}
-
- // Handle each arm, only if it's a constant expression
- if (arm.Pattern is ConstantPatternSyntax constant)
- {
- ExpressionSyntax expression = SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression, (ExpressionSyntax)Visit(node.GoverningExpression), constant.Expression);
-
- // Add the when clause as a AND expression
- if (arm.WhenClause != null)
- {
- expression = SyntaxFactory.BinaryExpression(
- SyntaxKind.LogicalAndExpression,
- expression,
- (ExpressionSyntax)Visit(arm.WhenClause.Condition)
- );
- }
-
- currentExpression = SyntaxFactory.ConditionalExpression(
- expression,
- armExpression,
- currentExpression
- );
- continue;
- }
+ ExpressionSyntax? condition;
- if (arm.Pattern is DeclarationPatternSyntax declaration)
+ // DeclarationPattern with a named variable requires replacing the variable with a cast in the arm body
+ if (arm.Pattern is DeclarationPatternSyntax declaration && declaration.Designation is SingleVariableDesignationSyntax)
{
- var getTypeExpression = SyntaxFactory.MemberAccessExpression(
- SyntaxKind.SimpleMemberAccessExpression,
- (ExpressionSyntax)Visit(node.GoverningExpression),
- SyntaxFactory.IdentifierName("GetType")
- );
-
- var getTypeCall = SyntaxFactory.InvocationExpression(getTypeExpression);
- var typeofExpression = SyntaxFactory.TypeOfExpression(declaration.Type);
- var equalsExpression = SyntaxFactory.BinaryExpression(
- SyntaxKind.EqualsExpression,
- getTypeCall,
- typeofExpression
- );
-
- ExpressionSyntax condition = equalsExpression;
- if (arm.WhenClause != null)
- {
- condition = SyntaxFactory.BinaryExpression(
- SyntaxKind.LogicalAndExpression,
- equalsExpression,
- (ExpressionSyntax)Visit(arm.WhenClause.Condition)
- );
- }
-
- var modifiedArmExpression = ReplaceVariableWithCast(armExpression, declaration, node.GoverningExpression);
- currentExpression = SyntaxFactory.ConditionalExpression(
- condition,
- modifiedArmExpression,
- currentExpression
- );
-
- continue;
+ condition = SyntaxFactory.BinaryExpression(SyntaxKind.IsExpression, visitedGoverning, declaration.Type);
+ armExpression = ReplaceVariableWithCast(armExpression, declaration, visitedGoverning);
}
-
- // Handle relational patterns (<=, <, >=, >)
- if (arm.Pattern is RelationalPatternSyntax relational)
+ else
{
- // Map the pattern operator token to a binary expression kind
- var binaryKind = relational.OperatorToken.Kind() switch
+ condition = ConvertPatternToExpression(arm.Pattern, visitedGoverning);
+ if (condition is null)
{
- 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)
- );
+ // A diagnostic (EFP0007) has already been reported for this arm.
+ // Skip it instead of falling back to base.VisitSwitchExpression which
+ // would leave an unsupported switch expression in the generated lambda and
+ // produce unrelated compiler errors. The best-effort ternary chain built
+ // so far is still emitted so the output remains valid C#.
+ continue;
}
+ }
- currentExpression = SyntaxFactory.ConditionalExpression(
+ if (arm.WhenClause != null)
+ {
+ condition = SyntaxFactory.BinaryExpression(
+ SyntaxKind.LogicalAndExpression,
condition,
- armExpression,
- currentExpression
+ (ExpressionSyntax)Visit(arm.WhenClause.Condition)
);
-
- continue;
}
- throw new InvalidOperationException(
- $"Switch expressions rewriting supports constant values, relational patterns (<=, <, >=, >), and declaration patterns (Type var). " +
- $"Unsupported pattern: {arm.Pattern.GetType().Name}"
- );
+ currentExpression = SyntaxFactory.ConditionalExpression(condition, armExpression, currentExpression);
}
-
+
return currentExpression;
}
@@ -676,5 +602,281 @@ 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);
+
+ // ConvertPatternToExpression returns null when the pattern cannot be rewritten and has
+ // already reported a diagnostic (EFP0007). Return a 'false' literal placeholder so
+ // the generated lambda stays syntactically valid and no additional CS8122 errors are
+ // triggered by leaving raw pattern-matching syntax inside an expression tree.
+ return ConvertPatternToExpression(node.Pattern, expression)
+ ?? SyntaxFactory.LiteralExpression(SyntaxKind.FalseLiteralExpression);
+ }
+
+ ///
+ /// Returns true when can be compared to null.
+ /// Accepts a pre-resolved symbol so synthesized (unbound) expression nodes can bypass
+ /// semantic-model lookup, which would return null for synthesized nodes and cause
+ /// the method to conservatively (and incorrectly) emit a null-check for value-type properties.
+ ///
+ private static bool TypeRequiresNullCheck(ITypeSymbol? type)
+ {
+ if (type is null)
+ {
+ return true; // conservative: unknown type → assume nullable
+ }
+
+ // Nullable is a value type whose OriginalDefinition is System.Nullable
+ if (type.IsValueType &&
+ type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T)
+ {
+ return false; // plain struct / record struct — null check would not compile
+ }
+
+ return true;
+ }
+
+ ///
+ /// Attempts to convert into an ordinary expression that is valid
+ /// inside an expression tree. Returns null and reports a diagnostic when the pattern
+ /// cannot be rewritten.
+ ///
+ /// The pattern syntax to convert.
+ /// The expression being tested against the pattern.
+ ///
+ /// Pre-resolved type of . When the expression is a synthesized
+ /// node (not present in the original source) Roslyn cannot bind it, so callers that know the
+ /// type should pass it here to avoid falling back to the conservative "assume nullable" path.
+ ///
+ private ExpressionSyntax? ConvertPatternToExpression(PatternSyntax pattern, ExpressionSyntax expression, ITypeSymbol? expressionType = null)
+ {
+ switch (pattern)
+ {
+ case RecursivePatternSyntax recursivePattern:
+ return ConvertRecursivePattern(recursivePattern, expression, expressionType);
+
+ case ConstantPatternSyntax constantPattern:
+ // e is null / e is 5
+ return SyntaxFactory.BinaryExpression(
+ SyntaxKind.EqualsExpression,
+ expression,
+ (ExpressionSyntax)Visit(constantPattern.Expression)
+ );
+
+ case DeclarationPatternSyntax declarationPattern:
+ // e is string _ → type-check only (discard is fine)
+ // e is string s → we cannot safely rewrite because references to 's' in
+ // the surrounding expression are outside this node's scope.
+ if (declarationPattern.Designation is SingleVariableDesignationSyntax)
+ {
+ _context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.UnsupportedPatternInExpression,
+ pattern.GetLocation(),
+ pattern.ToString()));
+ return null;
+ }
+
+ return SyntaxFactory.BinaryExpression(
+ SyntaxKind.IsExpression,
+ expression,
+ declarationPattern.Type
+ );
+
+ case RelationalPatternSyntax relationalPattern:
+ {
+ // e is > 100
+ SyntaxKind? binaryKind = relationalPattern.OperatorToken.Kind() switch
+ {
+ SyntaxKind.LessThanToken => SyntaxKind.LessThanExpression,
+ SyntaxKind.LessThanEqualsToken => SyntaxKind.LessThanOrEqualExpression,
+ SyntaxKind.GreaterThanToken => SyntaxKind.GreaterThanExpression,
+ SyntaxKind.GreaterThanEqualsToken => SyntaxKind.GreaterThanOrEqualExpression,
+ _ => null
+ };
+
+ if (binaryKind is null)
+ {
+ _context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.UnsupportedPatternInExpression,
+ pattern.GetLocation(),
+ pattern.ToString()));
+ return null;
+ }
+
+ return SyntaxFactory.BinaryExpression(
+ binaryKind.Value,
+ 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);
+
+ // Propagate failures from either side
+ if (left is null || right is null)
+ {
+ return null;
+ }
+
+ SyntaxKind? logicalKind = binaryPattern.OperatorToken.Kind() switch
+ {
+ SyntaxKind.AndKeyword => SyntaxKind.LogicalAndExpression,
+ SyntaxKind.OrKeyword => SyntaxKind.LogicalOrExpression,
+ _ => null
+ };
+
+ if (logicalKind is null)
+ {
+ _context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.UnsupportedPatternInExpression,
+ pattern.GetLocation(),
+ pattern.ToString()));
+ return null;
+ }
+
+ return SyntaxFactory.BinaryExpression(logicalKind.Value, left, right);
+ }
+
+ case UnaryPatternSyntax unaryPattern when unaryPattern.OperatorToken.IsKind(SyntaxKind.NotKeyword):
+ {
+ // e is not null
+ var inner = ConvertPatternToExpression(unaryPattern.Pattern, expression);
+ if (inner is null)
+ {
+ return null;
+ }
+
+ return SyntaxFactory.PrefixUnaryExpression(
+ SyntaxKind.LogicalNotExpression,
+ SyntaxFactory.ParenthesizedExpression(inner)
+ );
+ }
+
+ default:
+ _context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.UnsupportedPatternInExpression,
+ pattern.GetLocation(),
+ pattern.ToString()));
+ return null;
+ }
+ }
+
+ private ExpressionSyntax? ConvertRecursivePattern(RecursivePatternSyntax recursivePattern, ExpressionSyntax expression, ITypeSymbol? expressionType = null)
+ {
+ // Positional / deconstruct patterns (e.g. obj is Point(1, 2)) cannot be rewritten
+ // into a plain expression tree. Report a diagnostic and bail out.
+ if (recursivePattern.PositionalPatternClause != null)
+ {
+ _context.ReportDiagnostic(Diagnostic.Create(
+ Diagnostics.UnsupportedPatternInExpression,
+ recursivePattern.GetLocation(),
+ recursivePattern.ToString()));
+ return null;
+ }
+
+ var conditions = new List();
+
+ // Null check: only legal (and only necessary) for reference types and nullable value types.
+ // Emitting "x != null" for a plain struct / record struct would not compile.
+ // Use the pre-resolved expressionType when available so synthesized nodes (which Roslyn
+ // cannot bind) are handled correctly instead of falling back to the conservative path.
+ var typeForNullCheck = expressionType ?? _semanticModel.GetTypeInfo(expression).Type;
+ if (TypeRequiresNullCheck(typeForNullCheck))
+ {
+ conditions.Add(SyntaxFactory.BinaryExpression(
+ SyntaxKind.NotEqualsExpression,
+ expression,
+ SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
+ ));
+ }
+
+ // Type check: "obj is SomeType { ... }" — add "expression is SomeType" guard.
+ TypeSyntax? visitedType = null;
+ if (recursivePattern.Type != null)
+ {
+ visitedType = (TypeSyntax)Visit(recursivePattern.Type);
+ conditions.Add(SyntaxFactory.BinaryExpression(
+ SyntaxKind.IsExpression,
+ expression,
+ visitedType
+ ));
+ }
+
+ // When a concrete type is known, member accesses on sub-patterns must go through a
+ // cast so the generated code compiles correctly (e.g. ((SomeType)expression).Prop).
+ var memberBase = visitedType != null
+ ? SyntaxFactory.ParenthesizedExpression(
+ SyntaxFactory.CastExpression(visitedType, expression))
+ : expression;
+
+ // Handle property sub-patterns: { Prop: value, ... }
+ if (recursivePattern.PropertyPatternClause != null)
+ {
+ foreach (var subpattern in recursivePattern.PropertyPatternClause.Subpatterns)
+ {
+ ExpressionSyntax propExpression;
+ ITypeSymbol? propType = null;
+
+ if (subpattern.NameColon != null)
+ {
+ // Look up the property/field type from the original source binding so that
+ // when the recursive ConvertPatternToExpression call checks TypeRequiresNullCheck
+ // on the synthesized propExpression it receives the real symbol instead of null.
+ var memberSymbol = _semanticModel.GetSymbolInfo(subpattern.NameColon.Name).Symbol;
+ propType = memberSymbol switch
+ {
+ IPropertySymbol prop => prop.Type,
+ IFieldSymbol field => field.Type,
+ _ => null
+ };
+
+ propExpression = SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ memberBase,
+ SyntaxFactory.IdentifierName(subpattern.NameColon.Name.Identifier));
+ }
+ else
+ {
+ propExpression = memberBase;
+ }
+
+ // Pass propType so nested recursive patterns don't misidentify value-type
+ // properties as nullable when Roslyn can't bind the synthesized node.
+ var condition = ConvertPatternToExpression(subpattern.Pattern, propExpression, propType);
+ if (condition is null)
+ {
+ return null; // diagnostic already emitted
+ }
+
+ conditions.Add(condition);
+ }
+ }
+
+ if (conditions.Count == 0)
+ {
+ return SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression);
+ }
+
+ // Combine all conditions with &&
+ var result = conditions[0];
+ for (var 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_WithAndPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithAndPattern.verified.txt
new file mode 100644
index 0000000..da5e306
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithAndPattern.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_IsInRange_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity entity) => entity.Value >= 1 && entity.Value <= 100 ? true : false;
+ }
+ }
+}
\ No newline at end of file
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_WithOrPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithOrPattern.verified.txt
new file mode 100644
index 0000000..b5e5f65
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithOrPattern.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_IsTerminal_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity entity) => entity.Status == "Cancelled" || entity.Status == "Completed" ? 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
new file mode 100644
index 0000000..a11076d
--- /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 != 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.ExpressionBodied_IsPattern_WithAndPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithAndPattern.verified.txt
new file mode 100644
index 0000000..5ee1641
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithAndPattern.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Entity_IsInRange
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => @this.Value >= 1 && @this.Value <= 100;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithNotNullPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithNotNullPattern.verified.txt
new file mode 100644
index 0000000..dba7ad6
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithNotNullPattern.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Entity_HasName
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => !(@this.Name == null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithOrPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithOrPattern.verified.txt
new file mode 100644
index 0000000..04942c7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithOrPattern.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Entity_IsOutOfRange
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => @this.Value == 0 || @this.Value > 100;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithPropertyPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithPropertyPattern.verified.txt
new file mode 100644
index 0000000..cc2201f
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithPropertyPattern.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Extensions_IsActiveAndPositive_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity entity) => entity != null && entity.IsActive == true && entity.Value > 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.DotNet10_0.verified.txt
new file mode 100644
index 0000000..a16c95c
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.DotNet10_0.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_IEntityExtensions_Label
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.IEntity @this) => @this.Id + ": " + @this.Name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.verified.txt
new file mode 100644
index 0000000..a16c95c
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.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_IEntityExtensions_Label
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.IEntity @this) => @this.Id + ": " + @this.Name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.DotNet10_0.verified.txt
new file mode 100644
index 0000000..70c9c10
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.DotNet10_0.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_EntityExtensions_GetStatus_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => @this.IsActive && @this.Value > 0 ? "Active" : "Inactive";
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.verified.txt
new file mode 100644
index 0000000..70c9c10
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.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_EntityExtensions_GetStatus_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => @this.IsActive && @this.Value > 0 ? "Active" : "Inactive";
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.DotNet10_0.verified.txt
new file mode 100644
index 0000000..758dab3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.DotNet10_0.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_EntityExtensions_IsHighValue
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => @this.Value > 100;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.verified.txt
new file mode 100644
index 0000000..758dab3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.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_EntityExtensions_IsHighValue
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => @this.Value > 100;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.DotNet10_0.verified.txt
new file mode 100644
index 0000000..2bbeb71
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.DotNet10_0.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_EntityExtensions_GetGrade_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => @this.Score >= 90 ? "A" : @this.Score >= 80 ? "B" : @this.Score >= 70 ? "C" : "F";
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.verified.txt
new file mode 100644
index 0000000..2bbeb71
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.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_EntityExtensions_GetGrade_P0_Foo_Entity
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => @this.Score >= 90 ? "A" : @this.Score >= 80 ? "B" : @this.Score >= 70 ? "C" : "F";
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpressionWithTypePattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpressionWithTypePattern.verified.txt
index c9efd3c..e611e59 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpressionWithTypePattern.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpressionWithTypePattern.verified.txt
@@ -9,7 +9,7 @@ namespace EntityFrameworkCore.Projectables.Generated
{
static global::System.Linq.Expressions.Expression> Expression()
{
- return (global::Item item) => item.GetType() == typeof(GroupItem) ? new global::GroupData(((GroupItem)item).Id, ((GroupItem)item).Name, ((GroupItem)item).Description) : item.GetType() == typeof(DocumentItem) ? new global::DocumentData(((DocumentItem)item).Id, ((DocumentItem)item).Name, ((DocumentItem)item).Priority) : null !;
+ return (global::Item item) => item is GroupItem ? new global::GroupData(((GroupItem)item).Id, ((GroupItem)item).Name, ((GroupItem)item).Description) : item is DocumentItem ? new global::DocumentData(((DocumentItem)item).Id, ((DocumentItem)item).Name, ((DocumentItem)item).Priority) : null !;
}
}
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern.verified.txt
new file mode 100644
index 0000000..6b1ef59
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_Entity_GetGrade
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Entity @this) => @this.Score >= 90 ? "A" : @this.Score >= 80 ? "B" : @this.Score >= 70 ? "C" : "F";
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern_OnExtensionMethod.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern_OnExtensionMethod.verified.txt
new file mode 100644
index 0000000..2de6000
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern_OnExtensionMethod.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_OrderExtensions_GetTier_P0_Foo_Order
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.Order order) => order.Amount >= 1000 ? "Platinum" : order.Amount >= 500 ? "Gold" : order.Amount >= 100 ? "Silver" : "Bronze";
+ }
+ }
+}
\ 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 9ca78b1..57c06cf 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs
@@ -2011,6 +2011,236 @@ public static ItemData ToData(this Item item) =>
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public Task SwitchExpression_WithRelationalPattern()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public int Score { get; set; }
+
+ [Projectable]
+ public string GetGrade() => Score switch
+ {
+ >= 90 => ""A"",
+ >= 80 => ""B"",
+ >= 70 => ""C"",
+ _ => ""F"",
+ };
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task SwitchExpression_WithRelationalPattern_OnExtensionMethod()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Order {
+ public decimal Amount { get; set; }
+ }
+
+ static class OrderExtensions {
+ [Projectable]
+ public static string GetTier(this Order order) => order.Amount switch
+ {
+ >= 1000 => ""Platinum"",
+ >= 500 => ""Gold"",
+ >= 100 => ""Silver"",
+ _ => ""Bronze"",
+ };
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ExpressionBodied_IsPattern_WithAndPattern()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public int Value { get; set; }
+
+ [Projectable]
+ public bool IsInRange => Value is >= 1 and <= 100;
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ExpressionBodied_IsPattern_WithOrPattern()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public int Value { get; set; }
+
+ [Projectable]
+ public bool IsOutOfRange => Value is 0 or > 100;
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ExpressionBodied_IsPattern_WithPropertyPattern()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public bool IsActive { get; set; }
+ public int Value { get; set; }
+ }
+
+ static class Extensions {
+ [Projectable]
+ public static bool IsActiveAndPositive(this Entity entity) =>
+ entity is { IsActive: true, Value: > 0 };
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ExpressionBodied_IsPattern_WithNotNullPattern()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public string? Name { get; set; }
+
+ [Projectable]
+ public bool HasName => Name is not null;
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithAndPattern()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public int Value { get; set; }
+ }
+
+ static class Extensions {
+ [Projectable(AllowBlockBody = true)]
+ public static bool IsInRange(this Entity entity)
+ {
+ if (entity.Value is >= 1 and <= 100)
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task BlockBodiedMethod_WithOrPattern()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public string Status { get; set; }
+ }
+
+ static class Extensions {
+ [Projectable(AllowBlockBody = true)]
+ public static bool IsTerminal(this Entity entity)
+ {
+ if (entity.Status is ""Cancelled"" or ""Completed"")
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
[Fact]
public Task GenericTypes()
{
@@ -2967,6 +3197,137 @@ static class EntityExtensions {
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+
+ [Fact]
+ public Task ExtensionMemberWithBlockBody()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public int Value { get; set; }
+ public bool IsActive { get; set; }
+ }
+
+ static class EntityExtensions {
+ extension(Entity e) {
+ [Projectable(AllowBlockBody = true)]
+ public string GetStatus()
+ {
+ if (e.IsActive && e.Value > 0)
+ {
+ return ""Active"";
+ }
+ return ""Inactive"";
+ }
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ExtensionMemberWithSwitchExpression()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public int Score { get; set; }
+ }
+
+ static class EntityExtensions {
+ extension(Entity e) {
+ [Projectable]
+ public string GetGrade() => e.Score switch
+ {
+ >= 90 => ""A"",
+ >= 80 => ""B"",
+ >= 70 => ""C"",
+ _ => ""F"",
+ };
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ExtensionMemberOnInterface()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ interface IEntity {
+ int Id { get; }
+ string Name { get; }
+ }
+
+ static class IEntityExtensions {
+ extension(IEntity e) {
+ [Projectable]
+ public string Label => e.Id + "": "" + e.Name;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ExtensionMemberWithIsPatternExpression()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Entity {
+ public int Value { get; set; }
+ }
+
+ static class EntityExtensions {
+ extension(Entity e) {
+ [Projectable]
+ public bool IsHighValue => e.Value is > 100;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
#endif
[Fact]
@@ -3506,6 +3867,140 @@ public int GetDouble()
// Should have no warnings
Assert.Empty(result.Diagnostics);
}
+
+ [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(AllowBlockBody = true)]
+ public static string GetComplexCategory(this Entity entity)
+ {
+ if (entity is { IsActive: true, Value: > 100 })
+ {
+ return ""Active High"";
+ }
+ return ""Other"";
+ }
+ }
+}
+");
+
+ 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(AllowBlockBody = true)]
+ public static string GetCategory(this Entity entity)
+ {
+ if (entity.Value is > 100)
+ {
+ return ""High"";
+ }
+ 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_WithConstantPattern()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class Entity {
+ public string Status { get; set; }
+ }
+
+ static class Extensions {
+ [Projectable(AllowBlockBody = true)]
+ public static bool IsNull(this Entity entity)
+ {
+ if (entity is null)
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
+");
+
+ 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(AllowBlockBody = true)]
+ public static bool IsNotNull(this Entity entity)
+ {
+ if (entity is not null)
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
#region Helpers