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..55c0ee57 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
@@ -7,7 +7,6 @@
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;
@@ -94,8 +93,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 +122,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.RegisterImplementationSourceOutput(
registryEntries.Collect(),
static (spc, entries) => ProjectionRegistryEmitter.Emit(entries, spc));
+
+ // DEV: create a runtime global config for ProjectableAttribute.PolymorphicDispatch
}
private static SyntaxTriviaList BuildSourceDocComment(ConstructorDeclarationSyntax ctor, Compilation compilation)
@@ -209,7 +211,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 +306,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..9d1e946b 100644
--- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
+++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
@@ -19,6 +19,7 @@ public sealed class ProjectableExpressionReplacer : ExpressionVisitor
private IQueryProvider? _currentQueryProvider;
private bool _disableRootRewrite = false;
private readonly bool _trackingByDefault;
+ private readonly bool _polymorphicDispatchGlobal;
private IEntityType? _entityType;
// Extract MethodInfo via expression trees (trim-safe; computed once per AppDomain)
@@ -42,6 +43,7 @@ public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExp
{
_trackingByDefault = trackByDefault;
_resolver = projectionExpressionResolver;
+ _polymorphicDispatchGlobal = false; // DEV: retrieve from global config
}
bool TryGetReflectedExpression(MemberInfo memberInfo, [NotNullWhen(true)] out LambdaExpression? reflectedExpression)
@@ -185,34 +187,175 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
_disableRootRewrite = false;
}
- if (TryGetReflectedExpression(methodInfo, out var reflectedExpression))
+ // Check if we are rewriting a base invocation ((BaseType)@this).MyMethod(...) or ((BaseBaseType)(BaseType)@this).MyMethod(...)
+ // We are only checking for a type cast from a type to its immediate parent,
+ // unwrapping nested casts, because the original parameter might have been replaced
+ var isBase = (node.Object is UnaryExpression u && UnwrapUnaryConvert(u) != u);
+
+ var polymorphicDispatch = !isBase && IsPolymorphic(methodInfo) &&
+ methodInfo.GetCustomAttribute() is ProjectableAttribute projectable &&
+ (projectable.PolymorphicDispatch || _polymorphicDispatchGlobal);
+
+ if ((TryGetReflectedExpression(methodInfo, out var reflectedExpression) && reflectedExpression != null) || polymorphicDispatch)
{
- for (var parameterIndex = 0; parameterIndex < reflectedExpression.Parameters.Count; parameterIndex++)
+ if (polymorphicDispatch)
{
- var parameterExpression = reflectedExpression.Parameters[parameterIndex];
- var mappedArgumentExpression = (parameterIndex, node.Object) switch {
- (0, not null) => node.Object,
- (_, not null) => node.Arguments[parameterIndex - 1],
- (_, null) => node.Arguments.Count > parameterIndex ? node.Arguments[parameterIndex] : null
- };
-
- if (mappedArgumentExpression is not null)
+ var derivedTypes = RetrieveTypes(methodInfo.DeclaringType!, methodInfo);
+ if (derivedTypes.Count > 0)
{
- _expressionArgumentReplacer.ParameterArgumentMapping.Add(parameterExpression, mappedArgumentExpression);
+ var arguments = node.Arguments.ToArray();
+
+ // Check if the method has an implementation or if it is abstract, if it is not abstract it will be added
+ // as the last result in the if/else if/else chain, otherwise the last type will be used instead
+ Expression body;
+ if (reflectedExpression != null)
+ {
+ // @this is Type1 ? ((Type1)@this).Method(...) : ...
+ // ... ? ... :
+ // @this is TypeN ? ((TypeN)@this).Method(...) : ...
+ // virtualImplementation
+ body = derivedTypes.AsEnumerable()
+ .Reverse()
+ .Aggregate(reflectedExpression.Body, AggregateTypes);
+ }
+ else
+ {
+ // DEV: handle generic types
+ var lastType = derivedTypes[derivedTypes.Count - 1];
+
+ // @this is Type1 ? ((Type1)@this).Method(...) : ...
+ // ... ? ... :
+ // ((TypeN)@this).Method(...)
+ body = derivedTypes.AsEnumerable()
+ .Reverse()
+ .Skip(1)
+ .Aggregate((Expression)Expression.Call(Expression.Convert(node.Object!, lastType), methodInfo.Name, null, arguments), AggregateTypes);
+ }
+
+ return Visit(body);
+
+
+ Expression AggregateTypes(Expression expr, Type type)
+ {
+ return Expression.Condition(
+ Expression.TypeIs(node.Object!, type),
+ Expression.Call(Expression.Convert(node.Object!, type), methodInfo.Name, null, arguments),
+ expr);
+ }
}
}
- var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body);
- _expressionArgumentReplacer.ParameterArgumentMapping.Clear();
+ if (reflectedExpression != null)
+ {
+ for (var parameterIndex = 0; parameterIndex < reflectedExpression.Parameters.Count; parameterIndex++)
+ {
+ var parameterExpression = reflectedExpression.Parameters[parameterIndex];
+ var mappedArgumentExpression = (parameterIndex, node.Object) switch {
+ (0, not null) => node.Object,
+ (_, not null) => node.Arguments[parameterIndex - 1],
+ (_, null) => node.Arguments.Count > parameterIndex ? node.Arguments[parameterIndex] : null
+ };
+
+ if (mappedArgumentExpression is not null)
+ {
+ // If the type is different in case of a base call we re-cast it
+ if (isBase && mappedArgumentExpression.Type != parameterExpression.Type &&
+ mappedArgumentExpression.Type.IsAssignableTo(parameterExpression.Type) &&
+ mappedArgumentExpression is UnaryExpression u2)
+ {
+ var unwrapped = UnwrapUnaryConvert(u2);
+ if (unwrapped != u2)
+ {
+ mappedArgumentExpression = Expression.Convert(unwrapped, parameterExpression.Type);
+ }
+ }
+
+ _expressionArgumentReplacer.ParameterArgumentMapping.Add(parameterExpression, mappedArgumentExpression);
+ }
+ }
- return base.Visit(
- updatedBody
- );
+ var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body);
+ _expressionArgumentReplacer.ParameterArgumentMapping.Clear();
+
+ return Visit(updatedBody);
+ }
}
return base.VisitMethodCall(node);
}
+ private static bool IsPolymorphic(MethodInfo? method)
+ {
+ return method != null && (method.IsAbstract || method.IsVirtual || method.GetBaseDefinition() != method);
+ }
+
+ private static Expression UnwrapUnaryConvert(UnaryExpression node)
+ {
+ if (node.NodeType != ExpressionType.Convert || node.Type != node.Operand.Type.BaseType)
+ {
+ return node;
+ }
+
+ if (node.Operand is UnaryExpression u)
+ {
+ return UnwrapUnaryConvert(u);
+ }
+ else
+ {
+ return node.Operand;
+ }
+ }
+
+ private static List RetrieveTypes(Type baseType, MemberInfo member)
+ {
+ Func memberGetter;
+ if (member is MethodInfo method)
+ {
+ var parameters = method.GetParameters()
+ .Select(p => p.ParameterType)
+ .ToArray();
+ memberGetter = t => t.GetMethod(member.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, parameters);
+ }
+ else
+ {
+ memberGetter = t => t.GetProperty(member.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, ((PropertyInfo)member).PropertyType, Array.Empty(), null);
+ }
+
+ // Retrieve all the derived types which have an override of the member
+ var types = AppDomain.CurrentDomain.GetAssemblies()
+ .SelectMany(a => a.GetTypes())
+ .Where(t => t != baseType && t.IsAssignableTo(baseType) && memberGetter.Invoke(t) != null)
+ .OrderByDescending(GetDepth) // More specific types first
+ .ThenBy(t => t.Name)
+ .ToList();
+
+ // Remove types which are derived from another type in the list which has the declared symbol
+ // with the Projectable attribute (generation will be delegated to them)
+ var typesToRemove = types.Where(t => types.Any(tt => t != tt && t.IsAssignableTo(tt) &&
+ memberGetter.Invoke(t)?.GetCustomAttribute() != null))
+ .ToList();
+
+ foreach (var type in typesToRemove)
+ {
+ types.Remove(type);
+ }
+
+ return types;
+
+
+ static int GetDepth(Type type)
+ {
+ var depth = 0;
+ while (type.BaseType != null)
+ {
+ depth++;
+ type = type.BaseType;
+ }
+
+ return depth;
+ }
+ }
+
protected override Expression VisitNew(NewExpression node)
{
var constructor = node.Constructor;
@@ -300,18 +443,89 @@ PropertyInfo property when nodeExpression is not null
_ => node.Member
};
- if (TryGetReflectedExpression(nodeMember, out var reflectedExpression))
+ // Check if we are rewriting a base property ((BaseType)@this).MyProp or ((BaseBaseType)(BaseType)@this).MyProp
+ // We are only checking for a type cast from a type to its immediate parent,
+ // unwrapping nested casts, because the original parameter might have been replaced
+ var isBase = (node.Expression is UnaryExpression u && UnwrapUnaryConvert(u) != u);
+
+ // If we don't have an expression we might have an abstract property with Projectable attribute
+ // and PolymorphicDispatch set to true, so we check that
+ var polymorphicDispatch = !isBase && nodeMember is PropertyInfo p && IsPolymorphic(p.GetGetMethod()) &&
+ nodeMember.GetCustomAttribute() is ProjectableAttribute projectable &&
+ (projectable.PolymorphicDispatch || _polymorphicDispatchGlobal);
+
+ if ((TryGetReflectedExpression(nodeMember, out var reflectedExpression) && reflectedExpression != null) || polymorphicDispatch)
{
- if (nodeExpression is not null)
+ if (polymorphicDispatch)
{
- _expressionArgumentReplacer.ParameterArgumentMapping.Add(reflectedExpression.Parameters[0], nodeExpression);
- var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body);
- _expressionArgumentReplacer.ParameterArgumentMapping.Clear();
+ var derivedTypes = RetrieveTypes(nodeMember.DeclaringType!, nodeMember);
+ if (derivedTypes.Count > 0)
+ {
+ // Check if the method has an implementation or if it is abstract, if it is not abstract it will be added
+ // as the last result in the if/else if/else chain, otherwise the last type will be used instead
+ Expression body;
+ if (reflectedExpression != null)
+ {
+ // @this is Type1 ? ((Type1)@this).Property : ...
+ // ... ? ... :
+ // @this is TypeN ? ((TypeN)@this).Property : ...
+ // virtualImplementation
+ body = derivedTypes.AsEnumerable()
+ .Reverse()
+ .Aggregate(reflectedExpression.Body, AggregateTypes);
+ }
+ else
+ {
+ // DEV: handle generic types
+ var lastType = derivedTypes[derivedTypes.Count - 1];
+
+ // @this is Type1 ? ((Type1)@this).Property : ...
+ // ... ? ... :
+ // ((TypeN)@this).Property
+ body = derivedTypes.AsEnumerable()
+ .Reverse()
+ .Skip(1)
+ .Aggregate((Expression)Expression.Property(Expression.Convert(node.Expression!, lastType), nodeMember.Name), AggregateTypes);
+ }
- return base.Visit(updatedBody);
+ return Visit(body);
+
+
+ Expression AggregateTypes(Expression expr, Type type)
+ {
+ return Expression.Condition(
+ Expression.TypeIs(node.Expression!, type),
+ Expression.Property(Expression.Convert(node.Expression!, type), nodeMember.Name),
+ expr);
+ }
+ }
}
- return base.Visit(reflectedExpression.Body);
+ if (reflectedExpression != null)
+ {
+ if (nodeExpression is not null)
+ {
+ // If the type is different in case of a base call we re-cast it
+ if (isBase && nodeExpression.Type != reflectedExpression.Parameters[0].Type &&
+ nodeExpression.Type.IsAssignableTo(reflectedExpression.Parameters[0].Type) &&
+ nodeExpression is UnaryExpression u2)
+ {
+ var unwrapped = UnwrapUnaryConvert(u2);
+ if (unwrapped != u2)
+ {
+ nodeExpression = Expression.Convert(unwrapped, reflectedExpression.Parameters[0].Type);
+ }
+ }
+
+ _expressionArgumentReplacer.ParameterArgumentMapping.Add(reflectedExpression.Parameters[0], nodeExpression);
+ var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body);
+ _expressionArgumentReplacer.ParameterArgumentMapping.Clear();
+
+ return Visit(updatedBody);
+ }
+
+ return Visit(reflectedExpression.Body);
+ }
}
return base.VisitMember(node);
diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
index 21da9a46..5b78eaee 100644
--- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
+++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
@@ -82,7 +82,7 @@ public LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo
projectableAttribute);
private static LambdaExpression ResolveExpressionCore(MemberInfo projectableMemberInfo,
- ProjectableAttribute? projectableAttribute = null)
+ ProjectableAttribute? projectableAttribute)
{
projectableAttribute ??= projectableMemberInfo.GetCustomAttribute()
?? throw new InvalidOperationException("Expected member to have a Projectable attribute. None found");
@@ -117,7 +117,7 @@ private static LambdaExpression ResolveExpressionCore(MemberInfo projectableMemb
// caches it as a delegate; subsequent calls use the cached delegate for an O(1) dictionary lookup.
var registry = GetAssemblyRegistry(declaringType.Assembly);
var registeredExpr = registry?.Invoke(projectableMemberInfo);
-
+
return registeredExpr ??
// Slow path: reflection fallback for open-generic class members and generic methods
// that are not yet in the registry.
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs
index 2ea9fe0b..024c56eb 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs
@@ -1,8 +1,4 @@
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.CodeAnalysis;
-using VerifyXunit;
-using Xunit;
+using Microsoft.CodeAnalysis;
namespace EntityFrameworkCore.Projectables.Generator.Tests;
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.BaseMemberExplicitReference.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.BaseMemberExplicitReference.verified.txt
index 6544cf94..cd002e78 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.BaseMemberExplicitReference.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.BaseMemberExplicitReference.verified.txt
@@ -10,7 +10,7 @@ namespace EntityFrameworkCore.Projectables.Generated
{
static global::System.Linq.Expressions.Expression> Expression()
{
- return (global::Projectables.Repro.Derived @this) => @this.Foo;
+ return (global::Projectables.Repro.Derived @this) => ((global::Projectables.Repro.Base)@this).Foo;
}
}
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.BaseMethodExplicitReference.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.BaseMethodExplicitReference.verified.txt
index 56ee3532..72dbe3b3 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.BaseMethodExplicitReference.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.BaseMethodExplicitReference.verified.txt
@@ -10,7 +10,7 @@ namespace EntityFrameworkCore.Projectables.Generated
{
static global::System.Linq.Expressions.Expression> Expression()
{
- return (global::Projectables.Repro.Derived @this) => @this.Foo();
+ return (global::Projectables.Repro.Derived @this) => ((global::Projectables.Repro.Base)@this).Foo();
}
}
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs
index cc5e1ed1..ce18da6f 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs
@@ -1,8 +1,4 @@
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.CodeAnalysis;
-using VerifyXunit;
-using Xunit;
+using Microsoft.CodeAnalysis;
namespace EntityFrameworkCore.Projectables.Generator.Tests;
@@ -799,4 +795,27 @@ class Bar {
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+
+ [Fact]
+ public void AbstractWithPolymorphicDispatch()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ public abstract class Foo {
+ [Projectable(PolymorphicDispatch = true)]
+ public abstract int Id();
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Empty(result.GeneratedTrees);
+ }
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/PropertyTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/PropertyTests.cs
index 6b218c2b..5bcd02f2 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/PropertyTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/PropertyTests.cs
@@ -497,4 +497,27 @@ public int Foo
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+
+ [Fact]
+ public void AbstractWithPolymorphicDispatch()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ public abstract class Foo {
+ [Projectable(PolymorphicDispatch = true)]
+ public abstract int Id { get; }
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Empty(result.GeneratedTrees);
+ }
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs
index 19d58391..b2081ab3 100644
--- a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs
@@ -209,5 +209,306 @@ private sealed class FakeClosureWithIQueryableProperty
{
public IQueryable? Items { get; set; }
}
+
+ public class ProjectableExpressionResolverStubBase : IProjectionExpressionResolver
+ {
+ readonly Func _implementation;
+
+ public ProjectableExpressionResolverStubBase(Func implementation)
+ {
+ _implementation = implementation;
+ }
+
+ public LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo,
+ ProjectableAttribute? projectableAttribute = null) => _implementation(projectableMemberInfo, projectableAttribute);
+ }
+
+ class Foo
+ {
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual int VirtualProperty => 1;
+
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual int VirtualMethod() => 1;
+ }
+
+ class Bar : Foo
+ {
+ [Projectable]
+ override public int VirtualProperty => 2;
+
+ [Projectable]
+ override public int VirtualMethod() => 2;
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicProperty()
+ {
+ Expression> input = x => x.VirtualProperty;
+ Expression> expectedFoo = x => 1;
+ Expression> expectedBar = x => 2;
+ Expression> expected = x => x is Bar ? 2 : 1;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo) ? expectedFoo : expectedBar
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicMethod()
+ {
+ Expression> input = x => x.VirtualMethod();
+ Expression> expectedFoo = x => 1;
+ Expression> expectedBar = x => 2;
+ Expression> expected = x => x is Bar ? 2 : 1;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo) ? expectedFoo : expectedBar
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
+
+ class Foo1
+ {
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual int VirtualProperty => 1;
+
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual int VirtualMethod() => 1;
+ }
+
+ class Bar1 : Foo1
+ {
+ [Projectable]
+ override public int VirtualProperty => 2;
+
+ [Projectable]
+ override public int VirtualMethod() => 2;
+ }
+
+ class Baz1 : Foo1
+ {
+ [Projectable]
+ override public int VirtualProperty => 3;
+
+ [Projectable]
+ override public int VirtualMethod() => 3;
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicPropertyMultiple()
+ {
+ Expression> input = x => x.VirtualProperty;
+ Expression> expectedFoo = x => 1;
+ Expression> expectedBar = x => 2;
+ Expression> expectedBaz = x => 3;
+ Expression> expected = x => x is Bar1 ? 2 : x is Baz1 ? 3 : 1;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo1) ? expectedFoo : (x.DeclaringType == typeof(Bar1) ? expectedBar : expectedBaz)
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicMethodMultiple()
+ {
+ Expression> input = x => x.VirtualMethod();
+ Expression> expectedFoo = x => 1;
+ Expression> expectedBar = x => 2;
+ Expression> expectedBaz = x => 3;
+ Expression> expected = x => x is Bar1 ? 2 : x is Baz1 ? 3 : 1;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo1) ? expectedFoo : (x.DeclaringType == typeof(Bar1) ? expectedBar : expectedBaz)
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
+
+ class Foo2
+ {
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual int VirtualProperty => 1;
+
+ [Projectable(PolymorphicDispatch = true)]
+ public virtual int VirtualMethod() => 1;
+ }
+
+ class Bar2 : Foo2
+ {
+ [Projectable(PolymorphicDispatch = true)]
+ override public int VirtualProperty => true ? 2 : base.VirtualProperty;
+
+ [Projectable(PolymorphicDispatch = true)]
+ override public int VirtualMethod() => true ? 2 : base.VirtualProperty;
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicBaseProperty()
+ {
+ Expression> input = x => x.VirtualProperty;
+ Expression> expectedFoo = x => 1;
+ Expression> expectedBar = x => true ? 2 : ((Foo2)x).VirtualProperty;
+ Expression> expected = x => x is Bar2 ? true ? 2 : 1 : 1;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo2) ? expectedFoo : expectedBar
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicBaseMethod()
+ {
+ Expression> input = x => x.VirtualMethod();
+ Expression> expectedFoo = x => 1;
+ Expression> expectedBar = x => true ? 2 : ((Foo2)x).VirtualMethod();
+ Expression> expected = x => x is Bar2 ? true ? 2 : 1 : 1;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo2) ? expectedFoo : expectedBar
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
+
+ abstract class Foo3
+ {
+ [Projectable(PolymorphicDispatch = true)]
+ public abstract int VirtualProperty { get; }
+
+ [Projectable(PolymorphicDispatch = true)]
+ public abstract int VirtualMethod();
+ }
+
+ class Bar3 : Foo3
+ {
+ [Projectable(PolymorphicDispatch = true)]
+ override public int VirtualProperty => 2;
+
+ [Projectable(PolymorphicDispatch = true)]
+ override public int VirtualMethod() => 2;
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicPropertyAbstract()
+ {
+ Expression> input = x => x.VirtualProperty;
+ Expression> expectedBar = x => 2;
+ Expression> expected = x => 2;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo3) ? null! : expectedBar
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicMethodAbstract()
+ {
+ Expression> input = x => x.VirtualMethod();
+ Expression> expectedBar = x => 2;
+ Expression> expected = x => 2;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo3) ? null! : expectedBar
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
+
+ abstract class Foo4
+ {
+ [Projectable(PolymorphicDispatch = true)]
+ public abstract int VirtualProperty { get; }
+
+ [Projectable(PolymorphicDispatch = true)]
+ public abstract int VirtualMethod();
+ }
+
+ class Bar4 : Foo4
+ {
+ [Projectable]
+ override public int VirtualProperty => 2;
+
+ [Projectable]
+ override public int VirtualMethod() => 2;
+ }
+
+ class Baz4 : Foo4
+ {
+ [Projectable]
+ override public int VirtualProperty => 3;
+
+ [Projectable]
+ override public int VirtualMethod() => 3;
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicPropertyAbstractMultiple()
+ {
+ Expression> input = x => x.VirtualProperty;
+ Expression> expectedBar = x => 2;
+ Expression> expectedBaz = x => 3;
+ Expression> expected = x => x is Bar4 ? 2 : 3;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo4) ? null! : (x.DeclaringType == typeof(Bar4) ? expectedBar : expectedBaz)
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
+
+ [Fact]
+ public void VisitMember_PolymorphicMethodAbstractMultiple()
+ {
+ Expression> input = x => x.VirtualMethod();
+ Expression> expectedBar = x => 2;
+ Expression> expectedBaz = x => 3;
+ Expression> expected = x => x is Bar4 ? 2 : 3;
+
+ var resolver = new ProjectableExpressionResolverStubBase(
+ (x, a) => x.DeclaringType == typeof(Foo4) ? null! : (x.DeclaringType == typeof(Bar4) ? expectedBar : expectedBaz)
+ );
+ var subject = new ProjectableExpressionReplacer(resolver);
+
+ var actual = subject.Replace(input);
+
+ Assert.Equal(expected.ToString(), actual.ToString());
+ }
}
}