diff --git a/README.md b/README.md
index fd2f4edd..e7ad0d39 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ There are two components: a **Roslyn source generator** that emits companion `Ex
| `UseMemberBody` | [Reference →](https://efnext.github.io/reference/use-member-body) |
| Roslyn analyzers & code fixes (EFP0001–EFP0012) | [Reference →](https://efnext.github.io/reference/diagnostics) |
| Limited/Full compatibility mode | [Reference →](https://efnext.github.io/reference/compatibility-mode) |
+| Polymorphic dispatch (hierarchies) | [Advanced →](https://efnext.github.io/advanced/polymorphic-dispatch) |
## FAQ
diff --git a/docs/advanced/polymorphic-dispatch.md b/docs/advanced/polymorphic-dispatch.md
new file mode 100644
index 00000000..9a4a9f93
--- /dev/null
+++ b/docs/advanced/polymorphic-dispatch.md
@@ -0,0 +1,87 @@
+# Polymorphic Dispatch (Hierarchies)
+
+EF Core Projectables supports abstract/virtual/overwritten properties and methods decorated with `[Projectable]`, and can generate expression trees to mimic virtual calls.
+
+## Runtime
+
+Virtual members are invoked for the most-specific type of the instance.
+
+```csharp
+public class Foo{
+ public virtual string Name() => "Foo";
+}
+
+public class Bar : Foo{
+ public override string Name() => "Bar";
+}
+
+
+Foo bar = new Bar();
+bar.Name(); // "Bar"
+```
+
+## Expressions
+
+Expressions are compiled and cannot know which type the provided instance will be, so the only solution is a type test chain, which gets automatically created.
+
+```csharp
+public class Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual string Name() => "Foo";
+ // Converted to: @this is Bar ? "Bar" : "Foo"
+}
+
+public class Bar : Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public override string Name() => "Bar";
+ // Converted to: "Bar" as it has no derived types
+}
+```
+
+## Abstract Members
+
+Members can also be abstract, in which case the last branch of the type test chain will just be the last type itself.
+
+```csharp
+public abstract class Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public abstract string Name();
+ // Converted to: @this is Bar ? "Bar" : "Baz"
+}
+
+public class Bar : Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public override string Name() => "Bar";
+ // Converted to: "Bar" as it has no derived types
+}
+
+public class Baz : Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public override string Name() => "Baz";
+ // Converted to: "Baz" as it has no derived types
+}
+```
+
+## Base Invocations
+
+You can also use base in your derived types to invoke the base method/property.
+
+```csharp
+public class Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual string Name() => "Foo";
+ // Converted to: @this is Bar ? (((Bar)@this).MyProp ? "Bar" : "Foo") : "Foo"
+}
+
+public class Bar : Foo{
+ public bool MyProp { get; set; }
+
+ [Projectable(PolymorphicDispatch = true)]
+ public override string Name() => MyProp ? "Bar" : base.Name();
+ // Converted to: @this.MyProp ? "Bar" : "Foo" as it has no derived types
+}
+```
+
+## Enabling Polymorphic Dispatch
+
+Add `PolymorphicDispatch = true` to the Projectables
diff --git a/docs/guide/projectable-methods.md b/docs/guide/projectable-methods.md
index b818aafc..7450c336 100644
--- a/docs/guide/projectable-methods.md
+++ b/docs/guide/projectable-methods.md
@@ -105,6 +105,24 @@ public string GetStatus(decimal threshold)
See [Block-Bodied Members](/advanced/block-bodied-members) for full details.
+## Polymorphic Dispatch (Hierarchies)
+
+```csharp
+public class Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual string Name() => "Foo";
+ // Converted to: @this is Bar ? "Bar" : "Foo"
+}
+
+public class Bar : Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public override string Name() => "Bar";
+ // Converted to: "Bar" as it has no derived types
+}
+```
+
+See [Polymorphic Dispatch](/advanced/polymorphic-dispatch) for full details.
+
## Important Rules
- Methods must be **expression-bodied** (`=>`) unless `AllowBlockBody = true`.
diff --git a/docs/guide/projectable-properties.md b/docs/guide/projectable-properties.md
index 166eb499..7aeb9c08 100644
--- a/docs/guide/projectable-properties.md
+++ b/docs/guide/projectable-properties.md
@@ -121,6 +121,24 @@ public string Category
See [Block-Bodied Members](/advanced/block-bodied-members) for the full feature documentation.
+## Polymorphic Dispatch (Hierarchies)
+
+```csharp
+public class Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual string Name => "Foo";
+ // Converted to: @this is Bar ? "Bar" : "Foo"
+}
+
+public class Bar : Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public override string Name => "Bar";
+ // Converted to: "Bar" as it has no derived types
+}
+```
+
+See [Polymorphic Dispatch](/advanced/polymorphic-dispatch) for full details.
+
## Important Rules
- The property **must be expression-bodied** (using `=>`) unless `AllowBlockBody = true` is set.
diff --git a/docs/reference/projectable-attribute.md b/docs/reference/projectable-attribute.md
index 308d77db..ac2838db 100644
--- a/docs/reference/projectable-attribute.md
+++ b/docs/reference/projectable-attribute.md
@@ -107,6 +107,31 @@ See [Block-Bodied Members](/advanced/block-bodied-members) for full details.
---
+### `PolymorphicDispatch`
+
+**Type:** `bool`
+**Default:** `false`
+
+Enables **polymorphic dispatch** support for abstract/virtual/overwritten members.
+
+```csharp
+public class Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual string Name() => "Foo";
+ // Converted to: @this is Bar ? "Bar" : "Foo"
+}
+
+public class Bar : Foo{
+ [Projectable(PolymorphicDispatch = true)]
+ public override string Name() => "Bar";
+ // Converted to: "Bar" as it has no derived types
+}
+```
+
+See [Polymorphic Dispatch](/advanced/polymorphic-dispatch) for full details.
+
+---
+
## Complete Example
```csharp
@@ -144,6 +169,17 @@ public class Order
return "Normal";
}
}
+
+ // Polymorphic Dispatch
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual string Currency() => "EUR";
+ // Converted to: @this is Preorder ? "USD" : "EUR"
+}
+
+public class Preorder : Order{
+ [Projectable(PolymorphicDispatch = true)]
+ public override string Currency() => "USD";
+ // Converted to: "USD" as it has no derived types
}
```
diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs
index 6edbee04..f0289811 100644
--- a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs
+++ b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs
@@ -47,5 +47,10 @@ public sealed class ProjectableAttribute : Attribute
/// Set this to true to suppress the experimental feature warning.
///
public bool AllowBlockBody { get; set; }
+
+ ///
+ /// Get or set whether to inline derived types overrides of the member.
+ ///
+ public bool PolymorphicDispatch { get; set; }
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/build/EntityFrameworkCore.Projectables.Abstractions.props b/src/EntityFrameworkCore.Projectables.Abstractions/build/EntityFrameworkCore.Projectables.Abstractions.props
index 39ed2b66..32109bc4 100644
--- a/src/EntityFrameworkCore.Projectables.Abstractions/build/EntityFrameworkCore.Projectables.Abstractions.props
+++ b/src/EntityFrameworkCore.Projectables.Abstractions/build/EntityFrameworkCore.Projectables.Abstractions.props
@@ -8,10 +8,13 @@
Condition="'$(Projectables_ExpandEnumMethods)' == ''" />
+
+
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs
index 9fab2f28..21ee2136 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs
@@ -47,7 +47,7 @@ static internal class Diagnostics
public readonly static 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.",
+ 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. If the member is abstract you can use the InlineHierarchy property on the Projectable attribute to allow an empty body which will be replaced by the derived classes' implementations.",
category: "Design",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs
index 60cdc32f..9ffb3eb2 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs
@@ -16,6 +16,7 @@ static internal partial class ProjectableInterpreter
private static bool TryApplyMethodBody(
MethodDeclarationSyntax methodDeclarationSyntax,
bool allowBlockBody,
+ bool polymorphicDispatch,
ISymbol memberSymbol,
ExpressionSyntaxRewriter expressionSyntaxRewriter,
DeclarationSyntaxRewriter declarationSyntaxRewriter,
@@ -48,7 +49,7 @@ private static bool TryApplyMethodBody(
return false; // diagnostics already reported by BlockStatementConverter
}
}
- else
+ else if (!polymorphicDispatch)
{
return ReportRequiresBodyAndFail(context, methodDeclarationSyntax, memberSymbol.Name);
}
@@ -57,7 +58,7 @@ private static bool TryApplyMethodBody(
descriptor.ReturnTypeName = returnType.ToString();
// Only rewrite expression-bodied methods; block-bodied methods are already rewritten
- descriptor.ExpressionBody = isExpressionBodied
+ descriptor.ExpressionBody = isExpressionBodied && bodyExpression != null
? (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression)
: bodyExpression;
@@ -87,6 +88,7 @@ private static bool TryApplyExpressionPropertyBody(
MethodDeclarationSyntax originalMethodDecl,
PropertyDeclarationSyntax exprPropDecl,
SemanticModel semanticModel,
+ bool polymorphicDispatch,
MemberDeclarationSyntax member,
ISymbol memberSymbol,
ExpressionSyntaxRewriter expressionSyntaxRewriter,
@@ -99,7 +101,7 @@ private static bool TryApplyExpressionPropertyBody(
? TryExtractLambdaBodyAndParams(rawExpr, semanticModel, member.SyntaxTree)
: (null, []);
- if (innerBody is null)
+ if (innerBody is null && !polymorphicDispatch)
{
return ReportRequiresBodyAndFail(context, exprPropDecl, memberSymbol.Name);
}
@@ -112,77 +114,80 @@ private static bool TryApplyExpressionPropertyBody(
// For cross-tree expression properties the rewriter's SemanticModel cannot resolve
// nodes from the other file — skip rewriting in that case (simple lambda bodies need
// no rewrites; advanced features like null-conditional rewriting are unsupported cross-file).
- var visitedBody = exprPropDecl.SyntaxTree == member.SyntaxTree
+ var visitedBody = exprPropDecl.SyntaxTree == member.SyntaxTree && innerBody != null
? (ExpressionSyntax)expressionSyntaxRewriter.Visit(innerBody)
: innerBody;
- // For instance methods and C#14 extension members, BuildBaseDescriptor adds an
- // implicit @this receiver parameter. If the expression property lambda uses a
- // different parameter name (e.g. c => c.Value > 0), rename it so the generated
- // code references @this instead of an undefined identifier.
+ if (visitedBody != null)
+ {
+ // For instance methods and C#14 extension members, BuildBaseDescriptor adds an
+ // implicit @this receiver parameter. If the expression property lambda uses a
+ // different parameter name (e.g. c => c.Value > 0), rename it so the generated
+ // code references @this instead of an undefined identifier.
#if ROSLYN_5_0_OR_LATER
- var isExtensionMember = memberSymbol.ContainingType is { IsExtension: true };
+ var isExtensionMember = memberSymbol.ContainingType is { IsExtension: true };
#else
- var isExtensionMember = false;
+ var isExtensionMember = false;
#endif
- var hasImplicitReceiver = isExtensionMember
- || !originalMethodDecl.Modifiers.Any(SyntaxKind.StaticKeyword);
+ var hasImplicitReceiver = isExtensionMember
+ || !originalMethodDecl.Modifiers.Any(SyntaxKind.StaticKeyword);
- // Collect (lambdaParamName → methodParamName) rename pairs to apply in a
- // single multi-variable pass, avoiding cascading renames when names overlap.
- var renames = new List<(string From, string To)>();
+ // Collect (lambdaParamName → methodParamName) rename pairs to apply in a
+ // single multi-variable pass, avoiding cascading renames when names overlap.
+ var renames = new List<(string From, string To)>();
- var lambdaOffset = 0;
- if (hasImplicitReceiver)
- {
- if (lambdaParamNames.Count > 0 && lambdaParamNames[0] != "@this")
+ var lambdaOffset = 0;
+ if (hasImplicitReceiver)
{
- renames.Add((lambdaParamNames[0], "@this"));
- }
-
- lambdaOffset = 1;
- }
+ if (lambdaParamNames.Count > 0 && lambdaParamNames[0] != "@this")
+ {
+ renames.Add((lambdaParamNames[0], "@this"));
+ }
- // Rename each explicit method parameter from its lambda counterpart name.
- var methodParams = originalMethodDecl.ParameterList.Parameters;
- for (var i = 0; i < methodParams.Count; i++)
- {
- var lambdaIdx = lambdaOffset + i;
- if (lambdaIdx >= lambdaParamNames.Count)
- {
- break;
+ lambdaOffset = 1;
}
- var lambdaName = lambdaParamNames[lambdaIdx];
- var methodName = methodParams[i].Identifier.ValueText;
- if (lambdaName != methodName)
+ // Rename each explicit method parameter from its lambda counterpart name.
+ var methodParams = originalMethodDecl.ParameterList.Parameters;
+ for (var i = 0; i < methodParams.Count; i++)
{
- renames.Add((lambdaName, methodName));
- }
- }
+ var lambdaIdx = lambdaOffset + i;
+ if (lambdaIdx >= lambdaParamNames.Count)
+ {
+ break;
+ }
- // Apply all renames. To avoid cascading substitutions when names overlap
- // (e.g. swapped parameter names), use a unique sentinel prefix for each
- // intermediate name, then replace sentinels with the final names.
- if (renames.Count > 0)
- {
- // Phase 1: rename each source name to a collision-free sentinel.
- var sentinels = new List<(string Sentinel, string To)>(renames.Count);
- for (var i = 0; i < renames.Count; i++)
- {
- var sentinel = $"__rename_sentinel_{i}__";
- visitedBody = (ExpressionSyntax)new VariableReplacementRewriter(
- renames[i].From,
- SyntaxFactory.IdentifierName(sentinel)).Visit(visitedBody);
- sentinels.Add((sentinel, renames[i].To));
+ var lambdaName = lambdaParamNames[lambdaIdx];
+ var methodName = methodParams[i].Identifier.ValueText;
+ if (lambdaName != methodName)
+ {
+ renames.Add((lambdaName, methodName));
+ }
}
- // Phase 2: replace each sentinel with the final target name.
- foreach (var (sentinel, to) in sentinels)
+ // Apply all renames. To avoid cascading substitutions when names overlap
+ // (e.g. swapped parameter names), use a unique sentinel prefix for each
+ // intermediate name, then replace sentinels with the final names.
+ if (renames.Count > 0)
{
- visitedBody = (ExpressionSyntax)new VariableReplacementRewriter(
- sentinel,
- SyntaxFactory.IdentifierName(to)).Visit(visitedBody);
+ // Phase 1: rename each source name to a collision-free sentinel.
+ var sentinels = new List<(string Sentinel, string To)>(renames.Count);
+ for (var i = 0; i < renames.Count; i++)
+ {
+ var sentinel = $"__rename_sentinel_{i}__";
+ visitedBody = (ExpressionSyntax)new VariableReplacementRewriter(
+ renames[i].From,
+ SyntaxFactory.IdentifierName(sentinel)).Visit(visitedBody);
+ sentinels.Add((sentinel, renames[i].To));
+ }
+
+ // Phase 2: replace each sentinel with the final target name.
+ foreach (var (sentinel, to) in sentinels)
+ {
+ visitedBody = (ExpressionSyntax)new VariableReplacementRewriter(
+ sentinel,
+ SyntaxFactory.IdentifierName(to)).Visit(visitedBody);
+ }
}
}
@@ -206,6 +211,7 @@ private static bool TryApplyExpressionPropertyBodyForProperty(
PropertyDeclarationSyntax originalPropertyDecl,
PropertyDeclarationSyntax exprPropDecl,
SemanticModel semanticModel,
+ bool polymorphicDispatch,
MemberDeclarationSyntax member,
ISymbol memberSymbol,
ExpressionSyntaxRewriter expressionSyntaxRewriter,
@@ -218,7 +224,7 @@ private static bool TryApplyExpressionPropertyBodyForProperty(
? TryExtractLambdaBodyAndFirstParam(rawExpr, semanticModel, member.SyntaxTree)
: (null, null);
- if (innerBody is null)
+ if (innerBody is null && !polymorphicDispatch)
{
return ReportRequiresBodyAndFail(context, exprPropDecl, memberSymbol.Name);
}
@@ -229,10 +235,10 @@ private static bool TryApplyExpressionPropertyBodyForProperty(
// uses the semantic model which requires the original (pre-rename) syntax nodes.
// For cross-tree expression properties the rewriter's SemanticModel cannot resolve
// nodes from the other file — skip rewriting in that case.
- var visitedBody = exprPropDecl.SyntaxTree == member.SyntaxTree
+ var visitedBody = exprPropDecl.SyntaxTree == member.SyntaxTree && innerBody != null
? (ExpressionSyntax)expressionSyntaxRewriter.Visit(innerBody)
: innerBody;
- if (firstParamName is not null && firstParamName != "@this")
+ if (visitedBody != null && firstParamName != null && firstParamName != "@this")
{
visitedBody = (ExpressionSyntax)new VariableReplacementRewriter(
firstParamName,
@@ -253,6 +259,7 @@ private static bool TryApplyExpressionPropertyBodyForProperty(
private static bool TryApplyPropertyBody(
PropertyDeclarationSyntax propertyDeclarationSyntax,
bool allowBlockBody,
+ bool polymorphicDispatch,
ISymbol memberSymbol,
ExpressionSyntaxRewriter expressionSyntaxRewriter,
DeclarationSyntaxRewriter declarationSyntaxRewriter,
@@ -299,7 +306,7 @@ private static bool TryApplyPropertyBody(
}
}
- if (bodyExpression is null)
+ if (bodyExpression is null && !polymorphicDispatch)
{
return ReportRequiresBodyAndFail(context, propertyDeclarationSyntax, memberSymbol.Name);
}
@@ -308,7 +315,7 @@ private static bool TryApplyPropertyBody(
descriptor.ReturnTypeName = returnType.ToString();
// Only rewrite expression-bodied properties; block-bodied getters are already rewritten
- descriptor.ExpressionBody = isBlockBodiedGetter
+ descriptor.ExpressionBody = isBlockBodiedGetter || bodyExpression == null
? bodyExpression
: (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression);
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs
index 23db5a47..dae7bae5 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs
@@ -25,6 +25,8 @@ static internal partial class ProjectableInterpreter
projectableAttribute.ExpandEnumMethods ?? globalOptions.ExpandEnumMethods ?? false;
var allowBlockBody =
projectableAttribute.AllowBlockBody ?? globalOptions.AllowBlockBody ?? false;
+ var polymorphicDispatch =
+ projectableAttribute.PolymorphicDispatch ?? globalOptions.PolymorphicDispatch ?? false;
// 1. Resolve the member body (handles UseMemberBody redirection)
var memberBody = TryResolveMemberBody(member, memberSymbol, useMemberBody, context);
@@ -75,25 +77,25 @@ static internal partial class ProjectableInterpreter
{
// Projectable method
(_, MethodDeclarationSyntax methodDecl) =>
- TryApplyMethodBody(methodDecl, allowBlockBody, memberSymbol,
+ TryApplyMethodBody(methodDecl, allowBlockBody, polymorphicDispatch, memberSymbol,
expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor),
// Projectable method whose body is an Expression property
(MethodDeclarationSyntax originalMethodDecl, PropertyDeclarationSyntax exprPropDecl) =>
TryApplyExpressionPropertyBody(originalMethodDecl, exprPropDecl,
- semanticModel, member, memberSymbol,
+ semanticModel, polymorphicDispatch, member, memberSymbol,
expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor),
// Projectable property whose body is an Expression property
(PropertyDeclarationSyntax originalPropertyDecl, PropertyDeclarationSyntax exprPropDecl)
when IsExpressionDelegatePropertyDecl(exprPropDecl, semanticModel) =>
TryApplyExpressionPropertyBodyForProperty(originalPropertyDecl, exprPropDecl,
- semanticModel, member, memberSymbol,
+ semanticModel, polymorphicDispatch, member, memberSymbol,
expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor),
// Projectable property
(_, PropertyDeclarationSyntax propDecl) =>
- TryApplyPropertyBody(propDecl, allowBlockBody, memberSymbol,
+ TryApplyPropertyBody(propDecl, allowBlockBody, polymorphicDispatch, memberSymbol,
expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor),
// Projectable constructor
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableAttributeData.cs b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableAttributeData.cs
index 9541decd..e49613a6 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableAttributeData.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableAttributeData.cs
@@ -13,6 +13,7 @@ readonly internal record struct ProjectableAttributeData
public string? UseMemberBody { get; }
public bool? ExpandEnumMethods { get; }
public bool? AllowBlockBody { get; }
+ public bool? PolymorphicDispatch { get; }
public ProjectableAttributeData(AttributeData attribute)
{
@@ -20,6 +21,7 @@ public ProjectableAttributeData(AttributeData attribute)
string? useMemberBody = null;
bool? expandEnumMethods = null;
bool? allowBlockBody = null;
+ bool? polymorphicDispatch = null;
foreach (var namedArgument in attribute.NamedArguments)
{
@@ -53,6 +55,12 @@ value.Value is not null &&
allowBlockBody = allow;
}
break;
+ case nameof(PolymorphicDispatch):
+ if (value.Value is bool dispatch)
+ {
+ polymorphicDispatch = dispatch;
+ }
+ break;
}
}
@@ -60,5 +68,6 @@ value.Value is not null &&
UseMemberBody = useMemberBody;
ExpandEnumMethods = expandEnumMethods;
AllowBlockBody = allowBlockBody;
+ PolymorphicDispatch = polymorphicDispatch;
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableGlobalOptions.cs b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableGlobalOptions.cs
index 28e66498..211317eb 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableGlobalOptions.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableGlobalOptions.cs
@@ -12,6 +12,7 @@ readonly internal record struct ProjectableGlobalOptions
public NullConditionalRewriteSupport? NullConditionalRewriteSupport { get; }
public bool? ExpandEnumMethods { get; }
public bool? AllowBlockBody { get; }
+ public bool? PolymorphicDispatch { get; }
public ProjectableGlobalOptions(AnalyzerConfigOptions globalOptions)
{
@@ -33,5 +34,11 @@ public ProjectableGlobalOptions(AnalyzerConfigOptions globalOptions)
{
AllowBlockBody = allow;
}
+
+ if (globalOptions.TryGetValue("build_property.Projectables_PolymorphicDispatch", out var dispatchStr)
+ && bool.TryParse(dispatchStr, out var dispatch))
+ {
+ PolymorphicDispatch = dispatch;
+ }
}
}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
index 36d68bed..753a1dee 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
@@ -7,12 +7,12 @@
using System.Text;
using EntityFrameworkCore.Projectables.CodeFixes;
using EntityFrameworkCore.Projectables.Generator.Comparers;
-using EntityFrameworkCore.Projectables.Generator.Infrastructure;
using EntityFrameworkCore.Projectables.Generator.Interpretation;
using EntityFrameworkCore.Projectables.Generator.Models;
using EntityFrameworkCore.Projectables.Generator.Registry;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using System.CodeDom.Compiler;
namespace EntityFrameworkCore.Projectables.Generator;
@@ -94,8 +94,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
});
// Build the projection registry: collect all entries and emit a single registry file
- var registryEntries = compilationAndMemberPairs.Select(
- static (source, cancellationToken) => {
+ var registryEntries = compilationAndMemberPairs
+ .Where(source => !source.Item1.Member.Modifiers.Any(m => m.IsKind(SyntaxKind.AbstractKeyword)))
+ .Select(static (source, cancellationToken) => {
var ((member, _, _), compilation) = source;
#if !ROSLYN_5_0_OR_LATER
@@ -122,6 +123,32 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.RegisterImplementationSourceOutput(
registryEntries.Collect(),
static (spc, entries) => ProjectionRegistryEmitter.Emit(entries, spc));
+
+ context.RegisterImplementationSourceOutput(globalOptions, (spc, options) => {
+ var sw = new StringWriter();
+ var writer = new IndentedTextWriter(sw, " ");
+
+ writer.WriteLine("// ");
+ writer.WriteLine("#nullable disable");
+ writer.WriteLine();
+ writer.WriteLine("namespace EntityFrameworkCore.Projectables.Generated");
+ writer.WriteLine("{");
+ writer.Indent++;
+
+ writer.WriteLine("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");
+ writer.WriteLine("internal static class ProjectableGlobalOptions");
+ writer.WriteLine("{");
+ writer.Indent++;
+
+ writer.WriteLine($"public static bool? PolymorphicDispatch => { (options.PolymorphicDispatch == null ? "null" : (options.PolymorphicDispatch.Value ? "true" : "false")) };");
+
+ writer.Indent--;
+ writer.WriteLine("}");
+ writer.Indent--;
+ writer.WriteLine("}");
+
+ spc.AddSource("ProjectableGlobalOptions.g.cs", SourceText.From(sw.ToString(), Encoding.UTF8));
+ });
}
private static SyntaxTriviaList BuildSourceDocComment(ConstructorDeclarationSyntax ctor, Compilation compilation)
@@ -209,7 +236,7 @@ private static void Execute(
var projectable = ProjectableInterpreter.GetDescriptor(
semanticModel, member, memberSymbol, projectableAttribute, globalOptions, context, compilation);
- if (projectable is null)
+ if (projectable is null || projectable.ExpressionBody is null)
{
return;
}
@@ -304,6 +331,7 @@ private static void Execute(
context.AddSource(generatedFileName, SourceText.From(compilationUnit.NormalizeWhitespace().ToFullString(), Encoding.UTF8));
+
static TypeArgumentListSyntax GetLambdaTypeArgumentListSyntax(ProjectableDescriptor projectable)
{
var lambdaTypeArguments = TypeArgumentList(
diff --git a/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs
index 619a90f9..a7528f02 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs
@@ -26,14 +26,6 @@ public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullCondition
}
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
- return SyntaxFactory.IdentifierName("@this")
- .WithLeadingTrivia(node.GetLeadingTrivia())
- .WithTrailingTrivia(node.GetTrailingTrivia());
- }
public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
{
@@ -110,18 +102,30 @@ public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullCondition
public override SyntaxNode? VisitThisExpression(ThisExpressionSyntax node)
{
- // Swap out the use of this to @this
- return VisitThisBaseExpression(node);
+ // Swap out the use of this and base to @this and keep leading and trailing trivias
+ return SyntaxFactory.IdentifierName("@this")
+ .WithLeadingTrivia(node.GetLeadingTrivia())
+ .WithTrailingTrivia(node.GetTrailingTrivia());
}
public override SyntaxNode? VisitBaseExpression(BaseExpressionSyntax node)
{
- // Swap out the use of this to @this
- return VisitThisBaseExpression(node);
+ // Swap out the use of this to @this and cast it to the base type and keep leading and trailing trivias
+ return SyntaxFactory.ParenthesizedExpression(
+ SyntaxFactory.CastExpression(
+ SyntaxFactory.ParseTypeName(_semanticModel.GetTypeInfo(node).Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
+ SyntaxFactory.IdentifierName("@this")))
+ .WithLeadingTrivia(node.GetLeadingTrivia())
+ .WithTrailingTrivia(node.GetTrailingTrivia());
}
public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
{
+ if (node.Identifier.Text == "@this")
+ {
+ return node;
+ }
+
// Handle C# 14 extension parameter replacement (e.g., `e` in `extension(Entity e)` becomes `@this`)
#if ROSLYN_5_0_OR_LATER
if (_extensionParameterName is not null && node.Identifier.Text == _extensionParameterName)
diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
index 72bc0f8a..d99e69ee 100644
--- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
+++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
@@ -21,6 +21,13 @@ public sealed class ProjectableExpressionReplacer : ExpressionVisitor
private readonly bool _trackingByDefault;
private IEntityType? _entityType;
+ private readonly static bool _polymorphicDispatchGlobal = ((bool?)AppDomain.CurrentDomain.GetAssemblies()
+ .SelectMany(a => a.GetTypes())
+ .FirstOrDefault(t => t.FullName == "EntityFrameworkCore.Projectables.Generated.ProjectableGlobalOptions")
+ ?.GetProperty("PolymorphicDispatch", BindingFlags.Public | BindingFlags.Static)
+ ?.GetValue(null))
+ ?? false;
+
// Extract MethodInfo via expression trees (trim-safe; computed once per AppDomain)
private readonly static MethodInfo _select =
((MethodCallExpression)((Expression, IQueryable