Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 301 additions & 0 deletions docs/BlockBodiedMethods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# 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;
}

// 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
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs claim the generator will emit warning EFP0003 when a local variable is used multiple times (to highlight potential semantic changes), but the current block-body conversion code doesn’t implement any reference counting or warning emission for that scenario. Either implement this warning behavior (and add a generator test) or adjust the documentation to match the actual behavior.

Suggested change
- 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

Copilot uses AI. Check for mistakes.
- 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]
```
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
EFP0002 | Design | Error |
EFP0003 | Design | Warning |
Loading