From 8152ab432f9dd38e3bab6096cdd075cc497ef83b Mon Sep 17 00:00:00 2001 From: R Hudylko Date: Mon, 15 Jun 2026 12:27:05 +0200 Subject: [PATCH 1/5] Hierarchy implementation --- .../Infrastructure/Diagnostics.cs | 4 +- .../ProjectableInterpreter.BodyProcessors.cs | 171 ++++++++----- .../ProjectableInterpreter.Helpers.cs | 94 +++++++ .../Interpretation/ProjectableInterpreter.cs | 12 +- .../ProjectionExpressionGenerator.cs | 1 - .../HierarchyMembersConverter.cs | 142 +++++++++++ ...s.BlockBodiedMethod_Hierarchy.verified.txt | 19 ++ ...odiedMethod_HierarchyMultiple.verified.txt | 19 ++ ...erarchyNestedWithoutAttribute.verified.txt | 19 ++ ...erarchyNotNestedWithAttribute.verified.txt | 41 +++ .../BlockBodyTests.cs | 151 +++++++++++ .../MethodTests.Hierarchy.verified.txt | 19 ++ ...MethodTests.HierarchyAbstract.verified.txt | 19 ++ ...sts.HierarchyAbstractMultiple.verified.txt | 19 ++ ...MethodTests.HierarchyMultiple.verified.txt | 19 ++ ...erarchyNestedWithoutAttribute.verified.txt | 19 ++ ...erarchyNotNestedWithAttribute.verified.txt | 41 +++ .../MethodTests.cs | 241 ++++++++++++++++++ ...ressionPropertyBody_Hierarchy.verified.txt | 18 ++ ...thod_UsesMethodBody_Hierarchy.verified.txt | 17 ++ ...ressionPropertyBody_Hierarchy.verified.txt | 18 ++ ...ty_UsesPropertyBody_Hierarchy.verified.txt | 17 ++ .../UseMemberBodyTests.cs | 110 ++++++++ 23 files changed, 1159 insertions(+), 71 deletions(-) create mode 100644 src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/HierarchyMembersConverter.cs create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstract.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstractMultiple.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs index 9fab2f28..bd44b763 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs @@ -46,8 +46,8 @@ 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.", + title: "Method or property should expose a body definition if not overwritten in classes derived from the declaring class", + 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 not overwritten in at least one class derived from the class where the method or property is declared.", 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..a3ca1ddd 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs @@ -14,17 +14,23 @@ static internal partial class ProjectableInterpreter /// Returns false and reports diagnostics on failure. /// private static bool TryApplyMethodBody( + MemberDeclarationSyntax originalMemberDeclarationSyntax, MethodDeclarationSyntax methodDeclarationSyntax, + SemanticModel semanticModel, bool allowBlockBody, ISymbol memberSymbol, ExpressionSyntaxRewriter expressionSyntaxRewriter, DeclarationSyntaxRewriter declarationSyntaxRewriter, SourceProductionContext context, + Compilation? compilation, ProjectableDescriptor descriptor) { ExpressionSyntax? bodyExpression = null; var isExpressionBodied = false; + var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMemberDeclarationSyntax), compilation, semanticModel); + var isHierarchy = derivedTypes?.Count > 0; + if (methodDeclarationSyntax.ExpressionBody is not null) { bodyExpression = methodDeclarationSyntax.ExpressionBody.Expression; @@ -48,7 +54,7 @@ private static bool TryApplyMethodBody( return false; // diagnostics already reported by BlockStatementConverter } } - else + else if (!isHierarchy) { return ReportRequiresBodyAndFail(context, methodDeclarationSyntax, memberSymbol.Name); } @@ -57,7 +63,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; @@ -73,6 +79,12 @@ private static bool TryApplyMethodBody( ApplyExtensionBlockTypeParameters(memberSymbol, descriptor); } + // If we are rewriting a hierarchy method we need to invoke the derived types' overrides + if(isHierarchy) + { + descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicateMethodExpression(derivedTypes!, descriptor); + } + return true; } @@ -92,14 +104,18 @@ private static bool TryApplyExpressionPropertyBody( ExpressionSyntaxRewriter expressionSyntaxRewriter, DeclarationSyntaxRewriter declarationSyntaxRewriter, SourceProductionContext context, + Compilation? compilation, ProjectableDescriptor descriptor) { + var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMethodDecl), compilation, semanticModel); + var isHierarchy = derivedTypes?.Count > 0; + var rawExpr = TryGetPropertyGetterExpression(exprPropDecl); var (innerBody, lambdaParamNames) = rawExpr is not null ? TryExtractLambdaBodyAndParams(rawExpr, semanticModel, member.SyntaxTree) : (null, []); - if (innerBody is null) + if (innerBody is null && !isHierarchy) { return ReportRequiresBodyAndFail(context, exprPropDecl, memberSymbol.Name); } @@ -112,77 +128,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); + } } } @@ -191,6 +210,12 @@ private static bool TryApplyExpressionPropertyBody( ApplyParameterList(originalMethodDecl.ParameterList, declarationSyntaxRewriter, descriptor); ApplyTypeParameters(originalMethodDecl, declarationSyntaxRewriter, descriptor); + // If we are rewriting a hierarchy method we need to invoke the derived types' overrides + if (isHierarchy) + { + descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicateMethodExpression(derivedTypes!, descriptor); + } + return true; } @@ -211,14 +236,18 @@ private static bool TryApplyExpressionPropertyBodyForProperty( ExpressionSyntaxRewriter expressionSyntaxRewriter, DeclarationSyntaxRewriter declarationSyntaxRewriter, SourceProductionContext context, + Compilation? compilation, ProjectableDescriptor descriptor) { + var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalPropertyDecl), compilation, semanticModel); + var isHierarchy = derivedTypes?.Count > 0; + var rawExpr = TryGetPropertyGetterExpression(exprPropDecl); var (innerBody, firstParamName) = rawExpr is not null ? TryExtractLambdaBodyAndFirstParam(rawExpr, semanticModel, member.SyntaxTree) : (null, null); - if (innerBody is null) + if (innerBody is null && !isHierarchy) { return ReportRequiresBodyAndFail(context, exprPropDecl, memberSymbol.Name); } @@ -229,10 +258,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, @@ -243,6 +272,12 @@ private static bool TryApplyExpressionPropertyBodyForProperty( descriptor.ReturnTypeName = returnType.ToString(); descriptor.ExpressionBody = visitedBody; + // If we are rewriting a hierarchy method we need to invoke the derived types' overrides + if (isHierarchy) + { + descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicatePropertyExpression(derivedTypes!, descriptor); + } + return true; } @@ -251,14 +286,20 @@ private static bool TryApplyExpressionPropertyBodyForProperty( /// Returns false and reports diagnostics on failure. /// private static bool TryApplyPropertyBody( + MemberDeclarationSyntax originalMemberDeclarationSyntax, PropertyDeclarationSyntax propertyDeclarationSyntax, + SemanticModel semanticModel, bool allowBlockBody, ISymbol memberSymbol, ExpressionSyntaxRewriter expressionSyntaxRewriter, DeclarationSyntaxRewriter declarationSyntaxRewriter, SourceProductionContext context, + Compilation? compilation, ProjectableDescriptor descriptor) { + var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMemberDeclarationSyntax), compilation, semanticModel); + var isHierarchy = derivedTypes?.Count > 0; + ExpressionSyntax? bodyExpression = null; var isBlockBodiedGetter = false; @@ -299,7 +340,7 @@ private static bool TryApplyPropertyBody( } } - if (bodyExpression is null) + if (bodyExpression is null && !isHierarchy) { return ReportRequiresBodyAndFail(context, propertyDeclarationSyntax, memberSymbol.Name); } @@ -308,10 +349,16 @@ 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); + // If we are rewriting a hierarchy method we need to invoke the derived types' overrides + if (isHierarchy) + { + descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicatePropertyExpression(derivedTypes!, descriptor); + } + return true; } diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs index ec6c25f8..1d184a90 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs @@ -183,5 +183,99 @@ private static bool ReportRequiresBodyAndFail( memberName)); return false; } + + private static IEnumerable GetAllTypes(INamespaceSymbol namespaceSymbol) + { + foreach (var type in namespaceSymbol.GetTypeMembers()) + { + yield return type; + } + + foreach (var nestedNamespace in namespaceSymbol.GetNamespaceMembers()) + { + foreach (var type in GetAllTypes(nestedNamespace)) + { + yield return type; + } + } + } + + private static IList GetDerivedTypes(ISymbol? symbol, Compilation? compilation, SemanticModel semanticModel) + { + if (symbol != null && compilation != null && (symbol.IsAbstract || symbol.IsVirtual || symbol.IsOverride)) + { + var types = GetAllTypes(compilation.GlobalNamespace) + .Where(t => IsDerivedFrom(t, symbol.ContainingType) && + t.DeclaringSyntaxReferences.Any(s => ((ClassDeclarationSyntax)s.GetSyntax()).Members.Any(m => { + var ss = semanticModel.GetDeclaredSymbol(m); + return (ss != null && ss.IsOverride && ss.Kind == symbol.Kind && ss.Name == symbol.Name); + }))) + .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 => IsDerivedFrom(t, tt) && + tt.DeclaringSyntaxReferences.Any(s => ((ClassDeclarationSyntax)s.GetSyntax()).Members.First(m => { + var ss = semanticModel.GetDeclaredSymbol(m); + return (ss != null && ss.IsOverride && ss.Kind == symbol.Kind && ss.Name == symbol.Name); + }).AttributeLists.Any(a => a.Attributes.Any(aa => { + var attributeSymbol = semanticModel.GetSymbolInfo(aa).Symbol; + + INamedTypeSymbol attributeTypeSymbol; + if (attributeSymbol is IMethodSymbol methodSymbol) + { + attributeTypeSymbol = methodSymbol.ContainingType; + } + else + { + attributeTypeSymbol = ((INamedTypeSymbol)attributeSymbol!); + } + + return attributeTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::EntityFrameworkCore.Projectables.ProjectableAttribute"; + }))))).ToList(); + + foreach(var type in typesToRemove) + { + types.Remove(type); + } + + return types; + } + else + { + return Array.Empty(); + } + } + + private static bool IsDerivedFrom(INamedTypeSymbol candidate, INamedTypeSymbol baseClass) + { + var current = candidate.BaseType; + + while (current != null) + { + // SymbolEqualityComparer ensures we compare symbols accurately across compilation boundaries + if (SymbolEqualityComparer.Default.Equals(current, baseClass)) + { + return true; + } + current = current.BaseType; + } + + return false; + } + + private static int GetDepth(INamedTypeSymbol type) + { + var depth = 0; + while(type.BaseType != null) + { + depth++; + type = type.BaseType; + } + + return depth; + } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs index 23db5a47..303eb485 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs @@ -75,26 +75,26 @@ static internal partial class ProjectableInterpreter { // Projectable method (_, MethodDeclarationSyntax methodDecl) => - TryApplyMethodBody(methodDecl, allowBlockBody, memberSymbol, - expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor), + TryApplyMethodBody(member, methodDecl, semanticModel, allowBlockBody, memberSymbol, + expressionSyntaxRewriter, declarationSyntaxRewriter, context, compilation, descriptor), // Projectable method whose body is an Expression property (MethodDeclarationSyntax originalMethodDecl, PropertyDeclarationSyntax exprPropDecl) => TryApplyExpressionPropertyBody(originalMethodDecl, exprPropDecl, semanticModel, member, memberSymbol, - expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor), + expressionSyntaxRewriter, declarationSyntaxRewriter, context, compilation, descriptor), // Projectable property whose body is an Expression property (PropertyDeclarationSyntax originalPropertyDecl, PropertyDeclarationSyntax exprPropDecl) when IsExpressionDelegatePropertyDecl(exprPropDecl, semanticModel) => TryApplyExpressionPropertyBodyForProperty(originalPropertyDecl, exprPropDecl, semanticModel, member, memberSymbol, - expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor), + expressionSyntaxRewriter, declarationSyntaxRewriter, context, compilation, descriptor), // Projectable property (_, PropertyDeclarationSyntax propDecl) => - TryApplyPropertyBody(propDecl, allowBlockBody, memberSymbol, - expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor), + TryApplyPropertyBody(member, propDecl, semanticModel, allowBlockBody, memberSymbol, + expressionSyntaxRewriter, declarationSyntaxRewriter, context, compilation, descriptor), // Projectable constructor (_, ConstructorDeclarationSyntax ctorDecl) => diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs index 36d68bed..f72aeb72 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; diff --git a/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/HierarchyMembersConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/HierarchyMembersConverter.cs new file mode 100644 index 00000000..845d40c2 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/HierarchyMembersConverter.cs @@ -0,0 +1,142 @@ +using EntityFrameworkCore.Projectables.Generator.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace EntityFrameworkCore.Projectables.Generator.SyntaxRewriters +{ + /// + /// Converts methods/properties bodies of hierarchies of classes into typed expressions. + /// + internal class HierarchyMembersConverter + { + public ExpressionSyntax DuplicateMethodExpression(IList derivedTypes, ProjectableDescriptor descriptor) + { + var @this = SyntaxFactory.IdentifierName("@this"); + + var arguments = descriptor.ParametersList?.Parameters.Count > 1 ? ConvertParameters(descriptor.ParametersList) : null; + + // 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 + if (descriptor.ExpressionBody != null) + { + // @this is Type1 ? ((Type1)@this).Method(...) : ... + // ... ? ... : + // @this is TypeN ? ((TypeN)@this).Method(...) : ... + // virtualImplementation + return derivedTypes.Reverse().Aggregate(descriptor.ExpressionBody, AggregateTypes); + } + else + { + // DEV: handle generic types + var lastType = derivedTypes[derivedTypes.Count - 1]; + + // @this is Type1 ? ((Type1)@this).Method(...) : ... + // ... ? ... : + // ((TypeN)@this).Method(...) + return derivedTypes.Reverse().Skip(1) + .Aggregate((ExpressionSyntax)GetMethodInvocationExpression(lastType, descriptor.MemberName!, arguments), AggregateTypes); + } + + + ExpressionSyntax AggregateTypes(ExpressionSyntax expr, INamedTypeSymbol type) + { + return SyntaxFactory.ConditionalExpression( + SyntaxFactory.BinaryExpression(SyntaxKind.IsExpression, @this, GetTypeName(type)), + GetMethodInvocationExpression(type, descriptor.MemberName!, arguments), + expr); + } + } + + public ExpressionSyntax DuplicatePropertyExpression(IList derivedTypes, ProjectableDescriptor descriptor) + { + var @this = SyntaxFactory.IdentifierName("@this"); + + // Check if the property 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 + if (descriptor.ExpressionBody != null) + { + // @this is Type1 ? ((Type1)@this).Property : ... + // ... ? ... : + // @this is TypeN ? ((TypeN)@this).Property : ... + // virtualImplementation + return derivedTypes.Reverse().Aggregate(descriptor.ExpressionBody, AggregateTypes); + } + else + { + // DEV: handle generic types + var lastType = derivedTypes[derivedTypes.Count - 1]; + + // @this is Type1 ? ((Type1)@this).Property : ... + // ... ? ... : + // ((TypeN)@this).Property + return derivedTypes.Reverse().Skip(1) + .Aggregate((ExpressionSyntax)GetPropertyExpression(lastType, descriptor.MemberName!), AggregateTypes); + } + + + ExpressionSyntax AggregateTypes(ExpressionSyntax expr, INamedTypeSymbol type) + { + return SyntaxFactory.ConditionalExpression( + SyntaxFactory.BinaryExpression(SyntaxKind.IsExpression, @this, GetTypeName(type)), + GetPropertyExpression(type, descriptor.MemberName!), + expr); + } + } + + private static ArgumentListSyntax ConvertParameters(ParameterListSyntax parameters) + { + return SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(parameters.Parameters.Skip(1).Select(p => { + // Extract the name of the parameter (e.g., "myParam") + ExpressionSyntax identifier = SyntaxFactory.IdentifierName(p.Identifier); + + // Handle parameter modifiers (like 'ref', 'out', or 'in') + SyntaxToken? refKindKeyword = null; + if (p.Modifiers.Any(SyntaxKind.RefKeyword)) + refKindKeyword = SyntaxFactory.Token(SyntaxKind.RefKeyword); + else if (p.Modifiers.Any(SyntaxKind.OutKeyword)) + refKindKeyword = SyntaxFactory.Token(SyntaxKind.OutKeyword); + else if (p.Modifiers.Any(SyntaxKind.InKeyword)) + refKindKeyword = SyntaxFactory.Token(SyntaxKind.InKeyword); + + // Create the Argument node. If it has a ref/out modifier, pass it along. + if (refKindKeyword != null) + { + return SyntaxFactory.Argument(null, refKindKeyword.Value, identifier); + } + else + { + return SyntaxFactory.Argument(identifier); + } + }))); + } + + private static TypeSyntax GetTypeName(INamedTypeSymbol type) + { + return SyntaxFactory.ParseTypeName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + + private static InvocationExpressionSyntax GetMethodInvocationExpression(INamedTypeSymbol type, string methodName, ArgumentListSyntax? arguments) + { + var typeName = GetTypeName(type); + + var method = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParenthesizedExpression(SyntaxFactory.CastExpression(typeName, SyntaxFactory.IdentifierName("@this"))), + SyntaxFactory.IdentifierName(methodName)); + + // ((Type)@this).Method(...) + return arguments != null ? SyntaxFactory.InvocationExpression(method, arguments) : SyntaxFactory.InvocationExpression(method); + } + + private static MemberAccessExpressionSyntax GetPropertyExpression(INamedTypeSymbol type, string propertyName) + { + var typeName = GetTypeName(type); + + return SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ParenthesizedExpression(SyntaxFactory.CastExpression(typeName, SyntaxFactory.IdentifierName("@this"))), + SyntaxFactory.IdentifierName(propertyName)); + } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt new file mode 100644 index 00000000..93d07640 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt @@ -0,0 +1,19 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt new file mode 100644 index 00000000..3b615706 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt @@ -0,0 +1,19 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt new file mode 100644 index 00000000..17e10818 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt @@ -0,0 +1,19 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt new file mode 100644 index 00000000..78fa9b36 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt @@ -0,0 +1,41 @@ +[ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Bar_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Bar @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 2; + } + } +} + +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; + } + } +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs index 2ea9fe0b..0ad8a882 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs @@ -911,4 +911,155 @@ public static bool IsTerminal(this Entity entity) return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + + [Fact] + public Task BlockBodiedMethod_Hierarchy() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + [Projectable(AllowBlockBody = true)] + public virtual int Id(){ + return 1; + } + } + + public class Bar : Foo { + override public int Id(){ + return 2; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_HierarchyMultiple() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + [Projectable(AllowBlockBody = true)] + public virtual int Id(){ + return 1; + } + } + + public class Bar : Foo { + override public int Id(){ + return 2; + } + } + + public class Baz : Foo { + override public int Id(){ + return 3; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_HierarchyNotNestedWithAttribute() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + [Projectable(AllowBlockBody = true)] + public virtual int Id(){ + return 1; + } + } + + public class Bar : Foo { + [Projectable(AllowBlockBody = true)] + override public int Id(){ + return 2; + } + } + + public class Baz : Bar { + override public int Id(){ + return 3; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Equal(2, result.GeneratedTrees.Length); + + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); + } + + [Fact] + public Task BlockBodiedMethod_HierarchyNestedWithoutAttribute() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + [Projectable(AllowBlockBody = true)] + public virtual int Id(){ + return 1; + } + } + + public class Bar : Foo { + override public int Id(){ + return 2; + } + } + + public class Baz : Bar { + override public int Id(){ + return 3; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt new file mode 100644 index 00000000..93d07640 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt @@ -0,0 +1,19 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstract.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstract.verified.txt new file mode 100644 index 00000000..dfca5ebd --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstract.verified.txt @@ -0,0 +1,19 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => ((global::Foo.Bar)@this).Id(); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstractMultiple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstractMultiple.verified.txt new file mode 100644 index 00000000..651d6ffe --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstractMultiple.verified.txt @@ -0,0 +1,19 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : ((global::Foo.Bar)@this).Id(); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt new file mode 100644 index 00000000..3b615706 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt @@ -0,0 +1,19 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt new file mode 100644 index 00000000..17e10818 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt @@ -0,0 +1,19 @@ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt new file mode 100644 index 00000000..78fa9b36 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt @@ -0,0 +1,41 @@ +[ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Bar_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Bar @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 2; + } + } +} + +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; + } + } +} +] \ 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..c734df30 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs @@ -799,4 +799,245 @@ class Bar { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + + [Fact] + public Task Hierarchy() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + [Projectable] + public virtual int Id() => 1; + } + + public class Bar : Foo { + override public int Id() => 2; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task HierarchyMultiple() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + [Projectable] + public virtual int Id() => 1; + } + + public class Bar : Foo { + override public int Id() => 2; + } + + public class Baz : Foo { + override public int Id() => 3; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task HierarchyNotNestedWithAttribute() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + [Projectable] + public virtual int Id() => 1; + } + + public class Bar : Foo { + [Projectable] + override public int Id() => 2; + } + + public class Baz : Bar { + override public int Id() => 3; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Equal(2, result.GeneratedTrees.Length); + + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); + } + + [Fact] + public Task HierarchyNestedWithoutAttribute() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + [Projectable] + public virtual int Id() => 1; + } + + public class Bar : Foo { + override public int Id() => 2; + } + + public class Baz : Bar { + override public int Id() => 3; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task HierarchyAbstract() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public abstract class Foo { + [Projectable] + public abstract int Id(); + } + + public class Bar : Foo { + override public int Id() => 2; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task HierarchyAbstractMultiple() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public abstract class Foo { + [Projectable] + public abstract int Id(); + } + + public class Bar : Foo { + override public int Id() => 2; + } + + public class Baz : Bar { + override public int Id() => 3; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public void HierarchyAbstractWithNoDerived() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public abstract class Foo { + [Projectable] + public abstract int Id(); + } +} +"); + + var result = RunGenerator(compilation); + + var diag = Assert.Single(result.Diagnostics); + Assert.Equal("EFP0006", diag.Id); + Assert.Equal(DiagnosticSeverity.Error, diag.Severity); + } + + [Fact] + public void HierarchyAbstractWithNoDerivedOverwritten() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public abstract class Foo { + [Projectable] + public abstract int Id(); + } + + public abstract class Bar : Foo { } +} +"); + + var result = RunGenerator(compilation); + + var diag = Assert.Single(result.Diagnostics); + Assert.Equal("EFP0006", diag.Id); + Assert.Equal(DiagnosticSeverity.Error, diag.Severity); + } } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt new file mode 100644 index 00000000..58768d58 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt @@ -0,0 +1,18 @@ +// +#nullable disable +using System; +using System.Linq.Expressions; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 2; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt new file mode 100644 index 00000000..d1a3e4cb --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.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_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 2; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt new file mode 100644 index 00000000..4f7fc08e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt @@ -0,0 +1,18 @@ +// +#nullable disable +using System; +using System.Linq.Expressions; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id : 2; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt new file mode 100644 index 00000000..ad0dff85 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.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_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id : 2; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs index 1885e655..074b824e 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs @@ -42,6 +42,33 @@ class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task Method_UsesMethodBody_Hierarchy() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + public class Foo { + [Projectable(UseMemberBody = nameof(IdImpl))] + public virtual int Id() => 1; + + private int IdImpl() => 2; + } + + public class Bar : Foo { + override public int Id() => 3; + } +} +"); + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + [Fact] public Task Method_UsesExpressionPropertyBody_StaticExtension() { @@ -211,6 +238,34 @@ private static Expression> IsPositiveExpr { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task Method_UsesExpressionPropertyBody_Hierarchy() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq.Expressions; +using EntityFrameworkCore.Projectables; +namespace Foo { + public class Foo { + [Projectable(UseMemberBody = nameof(IdImpl))] + public virtual int Id() => 1; + + private static Expression> IdImpl => @this => 2; + } + + public class Bar : Foo { + override public int Id() => 3; + } +} +"); + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + [Fact] public Task Property_UsesPropertyBody_SameType() { @@ -236,6 +291,33 @@ class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task Property_UsesPropertyBody_Hierarchy() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + public class Foo { + [Projectable(UseMemberBody = nameof(IdImpl))] + public virtual int Id => 1; + + private int IdImpl => 2; + } + + public class Bar : Foo { + override public int Id => 3; + } +} +"); + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + [Fact] public Task StaticMethod_UsesStaticMethodBody() { @@ -285,6 +367,34 @@ class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task Property_UsesExpressionPropertyBody_Hierarchy() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq.Expressions; +using EntityFrameworkCore.Projectables; +namespace Foo { + public class Foo { + [Projectable(UseMemberBody = nameof(IdImpl))] + public virtual int Id => 1; + + private static Expression> IdImpl => @this => 2; + } + + public class Bar : Foo { + override public int Id => 3; + } +} +"); + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + [Fact] public void Property_UsesExpressionPropertyBody_IncompatibleReturnType_EmitsEFP0011() { From 3b5a47a9d9ebc3b11e135bfb7586f1940e29a6de Mon Sep 17 00:00:00 2001 From: R Hudylko Date: Wed, 17 Jun 2026 14:18:30 +0200 Subject: [PATCH 2/5] Rewritten "base" translation + fixed @this translation in switch expression --- .../ProjectableInterpreter.BodyProcessors.cs | 12 +- .../ProjectableInterpreter.Helpers.cs | 15 +- .../Models/ProjectableDescriptor.cs | 2 + .../ProjectionExpressionGenerator.cs | 146 ++++++++++-------- .../ExpressionSyntaxRewriter.cs | 14 +- .../Extensions/ExpressionExtensions.cs | 5 +- .../Internal/CustomQueryCompiler.cs | 3 +- .../IProjectionExpressionBaseResolver.cs | 10 ++ .../Services/ProjectableExpressionReplacer.cs | 66 +++++++- .../Services/ProjectionExpressionResolver.cs | 47 ++++-- ...s.BlockBodiedMethod_Hierarchy.verified.txt | 26 +++- ...odiedMethod_HierarchyMultiple.verified.txt | 26 +++- ...erarchyNestedWithoutAttribute.verified.txt | 26 +++- ...erarchyNotNestedWithAttribute.verified.txt | 40 +++++ .../BlockBodyTests.cs | 14 +- ...s.BaseMemberExplicitReference.verified.txt | 2 +- ...s.BaseMethodExplicitReference.verified.txt | 2 +- .../MethodTests.Hierarchy.verified.txt | 26 +++- .../MethodTests.HierarchyBase.verified.txt | 61 ++++++++ ...MethodTests.HierarchyMultiple.verified.txt | 26 +++- ...erarchyNestedWithoutAttribute.verified.txt | 26 +++- ...erarchyNotNestedWithAttribute.verified.txt | 40 +++++ .../MethodTests.cs | 44 +++++- ...ressionPropertyBody_Hierarchy.verified.txt | 25 ++- ...thod_UsesMethodBody_Hierarchy.verified.txt | 24 ++- ...ressionPropertyBody_Hierarchy.verified.txt | 25 ++- ...ty_UsesPropertyBody_Hierarchy.verified.txt | 24 ++- .../UseMemberBodyTests.cs | 16 +- .../ProjectableExpressionReplacerTests.cs | 74 +++++++++ 29 files changed, 728 insertions(+), 139 deletions(-) create mode 100644 src/EntityFrameworkCore.Projectables/Services/IProjectionExpressionBaseResolver.cs create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyBase.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs index a3ca1ddd..6648f208 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs @@ -28,7 +28,7 @@ private static bool TryApplyMethodBody( ExpressionSyntax? bodyExpression = null; var isExpressionBodied = false; - var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMemberDeclarationSyntax), compilation, semanticModel); + var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMemberDeclarationSyntax), compilation); var isHierarchy = derivedTypes?.Count > 0; if (methodDeclarationSyntax.ExpressionBody is not null) @@ -82,6 +82,7 @@ private static bool TryApplyMethodBody( // If we are rewriting a hierarchy method we need to invoke the derived types' overrides if(isHierarchy) { + descriptor.HierarchyOriginalExpressionBody = descriptor.ExpressionBody; descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicateMethodExpression(derivedTypes!, descriptor); } @@ -107,7 +108,7 @@ private static bool TryApplyExpressionPropertyBody( Compilation? compilation, ProjectableDescriptor descriptor) { - var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMethodDecl), compilation, semanticModel); + var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMethodDecl), compilation); var isHierarchy = derivedTypes?.Count > 0; var rawExpr = TryGetPropertyGetterExpression(exprPropDecl); @@ -213,6 +214,7 @@ private static bool TryApplyExpressionPropertyBody( // If we are rewriting a hierarchy method we need to invoke the derived types' overrides if (isHierarchy) { + descriptor.HierarchyOriginalExpressionBody = descriptor.ExpressionBody; descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicateMethodExpression(derivedTypes!, descriptor); } @@ -239,7 +241,7 @@ private static bool TryApplyExpressionPropertyBodyForProperty( Compilation? compilation, ProjectableDescriptor descriptor) { - var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalPropertyDecl), compilation, semanticModel); + var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalPropertyDecl), compilation); var isHierarchy = derivedTypes?.Count > 0; var rawExpr = TryGetPropertyGetterExpression(exprPropDecl); @@ -275,6 +277,7 @@ private static bool TryApplyExpressionPropertyBodyForProperty( // If we are rewriting a hierarchy method we need to invoke the derived types' overrides if (isHierarchy) { + descriptor.HierarchyOriginalExpressionBody = descriptor.ExpressionBody; descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicatePropertyExpression(derivedTypes!, descriptor); } @@ -297,7 +300,7 @@ private static bool TryApplyPropertyBody( Compilation? compilation, ProjectableDescriptor descriptor) { - var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMemberDeclarationSyntax), compilation, semanticModel); + var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMemberDeclarationSyntax), compilation); var isHierarchy = derivedTypes?.Count > 0; ExpressionSyntax? bodyExpression = null; @@ -356,6 +359,7 @@ private static bool TryApplyPropertyBody( // If we are rewriting a hierarchy method we need to invoke the derived types' overrides if (isHierarchy) { + descriptor.HierarchyOriginalExpressionBody = descriptor.ExpressionBody; descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicatePropertyExpression(derivedTypes!, descriptor); } diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs index 1d184a90..5ee9cd08 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs @@ -200,16 +200,16 @@ private static IEnumerable GetAllTypes(INamespaceSymbol namesp } } - private static IList GetDerivedTypes(ISymbol? symbol, Compilation? compilation, SemanticModel semanticModel) + private static IList GetDerivedTypes(ISymbol? symbol, Compilation? compilation) { if (symbol != null && compilation != null && (symbol.IsAbstract || symbol.IsVirtual || symbol.IsOverride)) { var types = GetAllTypes(compilation.GlobalNamespace) .Where(t => IsDerivedFrom(t, symbol.ContainingType) && t.DeclaringSyntaxReferences.Any(s => ((ClassDeclarationSyntax)s.GetSyntax()).Members.Any(m => { - var ss = semanticModel.GetDeclaredSymbol(m); - return (ss != null && ss.IsOverride && ss.Kind == symbol.Kind && ss.Name == symbol.Name); - }))) + var ss = compilation.GetSemanticModel(m.SyntaxTree).GetDeclaredSymbol(m); + return (ss != null && ss.IsOverride && ss.Kind == symbol.Kind && ss.Name == symbol.Name); + }))) .OrderByDescending(GetDepth) // More specific types first .ThenBy(t => t.Name) .ToList(); @@ -218,10 +218,10 @@ private static IList GetDerivedTypes(ISymbol? symbol, Compilat // with the Projectable attribute (generation will be delegated to them) var typesToRemove = types.Where(t => types.Any(tt => IsDerivedFrom(t, tt) && tt.DeclaringSyntaxReferences.Any(s => ((ClassDeclarationSyntax)s.GetSyntax()).Members.First(m => { - var ss = semanticModel.GetDeclaredSymbol(m); + var ss = compilation.GetSemanticModel(m.SyntaxTree).GetDeclaredSymbol(m); return (ss != null && ss.IsOverride && ss.Kind == symbol.Kind && ss.Name == symbol.Name); }).AttributeLists.Any(a => a.Attributes.Any(aa => { - var attributeSymbol = semanticModel.GetSymbolInfo(aa).Symbol; + var attributeSymbol = compilation.GetSemanticModel(aa.SyntaxTree).GetSymbolInfo(aa).Symbol; INamedTypeSymbol attributeTypeSymbol; if (attributeSymbol is IMethodSymbol methodSymbol) @@ -233,7 +233,8 @@ private static IList GetDerivedTypes(ISymbol? symbol, Compilat attributeTypeSymbol = ((INamedTypeSymbol)attributeSymbol!); } - return attributeTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::EntityFrameworkCore.Projectables.ProjectableAttribute"; + return attributeTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == + "global::EntityFrameworkCore.Projectables.ProjectableAttribute"; }))))).ToList(); foreach(var type in typesToRemove) diff --git a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableDescriptor.cs b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableDescriptor.cs index e653a018..55dfdbbe 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableDescriptor.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableDescriptor.cs @@ -34,4 +34,6 @@ internal class ProjectableDescriptor public SyntaxList? ConstraintClauses { get; set; } public ExpressionSyntax? ExpressionBody { get; set; } + + public ExpressionSyntax? HierarchyOriginalExpressionBody { get; set; } } \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs index f72aeb72..31ea2bb4 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs @@ -89,7 +89,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return; } - Execute(member, semanticModel, memberSymbol, attribute, globalOptions, compilation, spc); + /*try + {*/ + Execute(member, semanticModel, memberSymbol, attribute, globalOptions, compilation, spc); + /*} + catch(Exception e) + { + throw new Exception(e.StackTrace.Replace("\r\n", "
").Replace("\n", "
")); + }*/ }); // Build the projection registry: collect all entries and emit a single registry file @@ -228,80 +235,91 @@ private static void Execute( } var generatedClassName = ProjectionExpressionClassNameGenerator.GenerateName(projectable.ClassNamespace, projectable.NestedInClassNames, projectable.MemberName, projectable.ParameterTypeNames); - var generatedFileName = projectable.ClassTypeParameterList is not null ? $"{generatedClassName}-{projectable.ClassTypeParameterList.ChildNodes().Count()}.g.cs" : $"{generatedClassName}.g.cs"; - - var classSyntax = ClassDeclaration(generatedClassName) - .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) - .WithTypeParameterList(projectable.ClassTypeParameterList) - .WithConstraintClauses(projectable.ClassConstraintClauses ?? List()) - .AddAttributeLists( - AttributeList() - .AddAttributes(_editorBrowsableAttribute) - ) - .WithLeadingTrivia(member is ConstructorDeclarationSyntax ctor && compilation is not null ? BuildSourceDocComment(ctor, compilation) : TriviaList()) - .AddMembers( - MethodDeclaration( - GenericName( - Identifier("global::System.Linq.Expressions.Expression"), - TypeArgumentList( - SingletonSeparatedList( - (TypeSyntax)GenericName( - Identifier("global::System.Func"), - GetLambdaTypeArgumentListSyntax(projectable) + + AddSource(generatedClassName, projectable.ExpressionBody); + if(projectable.HierarchyOriginalExpressionBody != null) + { + AddSource(generatedClassName + "_Base", projectable.HierarchyOriginalExpressionBody); + } + + + void AddSource(string generatedClassName, ExpressionSyntax? body) + { + var generatedFileName = projectable.ClassTypeParameterList is not null ? $"{generatedClassName}-{projectable.ClassTypeParameterList.ChildNodes().Count()}.g.cs" : $"{generatedClassName}.g.cs"; + + var classSyntax = ClassDeclaration(generatedClassName) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithTypeParameterList(projectable.ClassTypeParameterList) + .WithConstraintClauses(projectable.ClassConstraintClauses ?? List()) + .AddAttributeLists( + AttributeList() + .AddAttributes(_editorBrowsableAttribute) + ) + .WithLeadingTrivia(member is ConstructorDeclarationSyntax ctor && compilation is not null ? BuildSourceDocComment(ctor, compilation) : TriviaList()) + .AddMembers( + MethodDeclaration( + GenericName( + Identifier("global::System.Linq.Expressions.Expression"), + TypeArgumentList( + SingletonSeparatedList( + (TypeSyntax)GenericName( + Identifier("global::System.Func"), + GetLambdaTypeArgumentListSyntax(projectable) + ) ) ) - ) - ), - "Expression" - ) - .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) - .WithTypeParameterList(projectable.TypeParameterList) - .WithConstraintClauses(projectable.ConstraintClauses ?? List()) - .WithBody( - Block( - ReturnStatement( - ParenthesizedLambdaExpression( - projectable.ParametersList ?? ParameterList(), - null, - projectable.ExpressionBody + ), + "Expression" + ) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithTypeParameterList(projectable.TypeParameterList) + .WithConstraintClauses(projectable.ConstraintClauses ?? List()) + .WithBody( + Block( + ReturnStatement( + ParenthesizedLambdaExpression( + projectable.ParametersList ?? ParameterList(), + null, + body + ) ) ) ) - ) - ); + ); - var compilationUnit = CompilationUnit(); + var compilationUnit = CompilationUnit(); - foreach (var usingDirective in projectable.UsingDirectives!) - { - compilationUnit = compilationUnit.AddUsings(usingDirective); - } + foreach (var usingDirective in projectable.UsingDirectives!) + { + compilationUnit = compilationUnit.AddUsings(usingDirective); + } - if (projectable.ClassNamespace is not null) - { - compilationUnit = compilationUnit.AddUsings( - UsingDirective( - ParseName(projectable.ClassNamespace) - ) - ); - } + if (projectable.ClassNamespace is not null) + { + compilationUnit = compilationUnit.AddUsings( + UsingDirective( + ParseName(projectable.ClassNamespace) + ) + ); + } - compilationUnit = compilationUnit - .AddMembers( - NamespaceDeclaration( - ParseName("EntityFrameworkCore.Projectables.Generated") - ).AddMembers(classSyntax) - ) - .WithLeadingTrivia( - TriviaList( - Comment("// "), - // Uncomment line below, for debugging purposes, to see when the generator is run on source generated files - // CarriageReturnLineFeed, Comment($"// Generated at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff} UTC for '{memberSymbol.Name}' in '{memberSymbol.ContainingType?.Name}'"), - Trivia(NullableDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)) + compilationUnit = compilationUnit + .AddMembers( + NamespaceDeclaration( + ParseName("EntityFrameworkCore.Projectables.Generated") + ).AddMembers(classSyntax) ) - ); + .WithLeadingTrivia( + TriviaList( + Comment("// "), + // Uncomment line below, for debugging purposes, to see when the generator is run on source generated files + // CarriageReturnLineFeed, Comment($"// Generated at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff} UTC for '{memberSymbol.Name}' in '{memberSymbol.ContainingType?.Name}'"), + Trivia(NullableDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)) + ) + ); - context.AddSource(generatedFileName, SourceText.From(compilationUnit.NormalizeWhitespace().ToFullString(), Encoding.UTF8)); + context.AddSource(generatedFileName, SourceText.From(compilationUnit.NormalizeWhitespace().ToFullString(), Encoding.UTF8)); + } static TypeArgumentListSyntax GetLambdaTypeArgumentListSyntax(ProjectableDescriptor projectable) { diff --git a/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs index 619a90f9..183548db 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs @@ -116,12 +116,22 @@ public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullCondition 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 + 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/Extensions/ExpressionExtensions.cs b/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs index 3de24d8d..b411b8dc 100644 --- a/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs +++ b/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs @@ -9,5 +9,8 @@ public static class ExpressionExtensions /// Replaces all calls to properties and methods that are marked with the Projectable attribute with their respective expression tree /// public static Expression ExpandProjectables(this Expression expression) - => new ProjectableExpressionReplacer(new ProjectionExpressionResolver(), false).Replace(expression); + { + var resolver = new ProjectionExpressionResolver(); + return new ProjectableExpressionReplacer(resolver, resolver, false).Replace(expression); + } } \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs index f8a206e9..e0958e5e 100644 --- a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs @@ -61,7 +61,8 @@ public CustomQueryCompiler(IQueryCompiler decoratedQueryCompiler, var trackingByDefault = (contextOptions.FindExtension()?.QueryTrackingBehavior ?? QueryTrackingBehavior.TrackAll) == QueryTrackingBehavior.TrackAll; - _projectableExpressionReplacer = new ProjectableExpressionReplacer(new ProjectionExpressionResolver(), trackingByDefault); + var resolver = new ProjectionExpressionResolver(); + _projectableExpressionReplacer = new ProjectableExpressionReplacer(resolver, resolver, trackingByDefault); } public override Func CreateCompiledAsyncQuery(Expression query) diff --git a/src/EntityFrameworkCore.Projectables/Services/IProjectionExpressionBaseResolver.cs b/src/EntityFrameworkCore.Projectables/Services/IProjectionExpressionBaseResolver.cs new file mode 100644 index 00000000..f3455279 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables/Services/IProjectionExpressionBaseResolver.cs @@ -0,0 +1,10 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace EntityFrameworkCore.Projectables.Services; + +public interface IProjectionExpressionBaseResolver +{ + LambdaExpression FindGeneratedBaseExpression(MemberInfo projectableMemberInfo, + ProjectableAttribute? projectableAttribute = null); +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs index 72bc0f8a..244abbc8 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs @@ -13,8 +13,10 @@ namespace EntityFrameworkCore.Projectables.Services public sealed class ProjectableExpressionReplacer : ExpressionVisitor { private readonly IProjectionExpressionResolver _resolver; + private readonly IProjectionExpressionBaseResolver _resolverBase; private readonly ExpressionArgumentReplacer _expressionArgumentReplacer = new(); private readonly Dictionary _projectableMemberCache = new(); + private readonly Dictionary _projectableBaseMemberCache = new(); private readonly HashSet _expandingConstructors = new(); private IQueryProvider? _currentQueryProvider; private bool _disableRootRewrite = false; @@ -38,15 +40,38 @@ public sealed class ProjectableExpressionReplacer : ExpressionVisitor private readonly static ConditionalWeakTable _closedSelectCache = new(); private readonly static ConditionalWeakTable _closedWhereCache = new(); - public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver, bool trackByDefault = false) + public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver, bool trackByDefault = false): + this(projectionExpressionResolver, null!, trackByDefault) { } + public ProjectableExpressionReplacer( + IProjectionExpressionResolver projectionExpressionResolver, + IProjectionExpressionBaseResolver projectionExpressionBaseResolver, + bool trackByDefault = false) { _trackingByDefault = trackByDefault; _resolver = projectionExpressionResolver; + _resolverBase = projectionExpressionBaseResolver; } bool TryGetReflectedExpression(MemberInfo memberInfo, [NotNullWhen(true)] out LambdaExpression? reflectedExpression) { - if (!_projectableMemberCache.TryGetValue(memberInfo, out reflectedExpression)) + return TryGetReflectedExpression(memberInfo, false, out reflectedExpression); + } + bool TryGetReflectedExpression(MemberInfo memberInfo, bool isBase, [NotNullWhen(true)] out LambdaExpression? reflectedExpression) + { + if (isBase) + { + if (!_projectableBaseMemberCache.TryGetValue(memberInfo, out reflectedExpression)) + { + var projectableAttribute = memberInfo.GetCustomAttribute(false); + + reflectedExpression = projectableAttribute is not null + ? _resolverBase?.FindGeneratedBaseExpression(memberInfo, projectableAttribute) + : null; + + _projectableBaseMemberCache.Add(memberInfo, reflectedExpression); + } + } + else if (!_projectableMemberCache.TryGetValue(memberInfo, out reflectedExpression)) { var projectableAttribute = memberInfo.GetCustomAttribute(false); @@ -185,7 +210,12 @@ 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); + + if (TryGetReflectedExpression(methodInfo, isBase, out var reflectedExpression)) { for (var parameterIndex = 0; parameterIndex < reflectedExpression.Parameters.Count; parameterIndex++) { @@ -198,6 +228,19 @@ protected override Expression VisitMethodCall(MethodCallExpression node) 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); } } @@ -213,6 +256,17 @@ protected override Expression VisitMethodCall(MethodCallExpression node) return base.VisitMethodCall(node); } + private 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; + } + protected override Expression VisitNew(NewExpression node) { var constructor = node.Constructor; @@ -300,7 +354,11 @@ 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 + var isBase = (node.Expression is UnaryExpression u && u.NodeType == ExpressionType.Convert && + u.Type == u.Operand.Type.BaseType && u.Operand is ParameterExpression p && p.Name == "@this"); + + if (TryGetReflectedExpression(nodeMember, isBase, out var reflectedExpression)) { if (nodeExpression is not null) { diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs index 21da9a46..e33fe99b 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs @@ -9,7 +9,7 @@ namespace EntityFrameworkCore.Projectables.Services { - public sealed class ProjectionExpressionResolver : IProjectionExpressionResolver + public sealed class ProjectionExpressionResolver : IProjectionExpressionResolver, IProjectionExpressionBaseResolver { // We never store null in the dictionary; assemblies without a registry use a sentinel delegate. private readonly static Func _nullRegistry = static _ => null!; @@ -20,6 +20,7 @@ public sealed class ProjectionExpressionResolver : IProjectionExpressionResolver /// EF Core never repeats reflection work for the same member across queries. /// private readonly static ConcurrentDictionary _expressionCache = new(); + private readonly static ConcurrentDictionary _expressionBaseCache = new(); /// /// Caches → C#-formatted name strings, since the same parameter types @@ -78,16 +79,22 @@ public sealed class ProjectionExpressionResolver : IProjectionExpressionResolver public LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo, ProjectableAttribute? projectableAttribute = null) - => _expressionCache.GetOrAdd(projectableMemberInfo, static (mi, a) => ResolveExpressionCore(mi, a), + => _expressionCache.GetOrAdd(projectableMemberInfo, static (mi, a) => ResolveExpressionCore(mi, false, a), projectableAttribute); - private static LambdaExpression ResolveExpressionCore(MemberInfo projectableMemberInfo, + public LambdaExpression FindGeneratedBaseExpression(MemberInfo projectableMemberInfo, ProjectableAttribute? projectableAttribute = null) + => _expressionBaseCache.GetOrAdd(projectableMemberInfo, static (mi, a) => ResolveExpressionCore(mi, true, a), + projectableAttribute); + + private static LambdaExpression ResolveExpressionCore(MemberInfo projectableMemberInfo, + bool isBase, + ProjectableAttribute? projectableAttribute) { projectableAttribute ??= projectableMemberInfo.GetCustomAttribute() ?? throw new InvalidOperationException("Expected member to have a Projectable attribute. None found"); - var expression = GetExpressionFromGeneratedType(projectableMemberInfo); + var expression = GetExpressionFromGeneratedType(projectableMemberInfo, isBase); if (expression is null && projectableAttribute.UseMemberBody is not null) { @@ -108,20 +115,28 @@ private static LambdaExpression ResolveExpressionCore(MemberInfo projectableMemb throw new InvalidOperationException($"Unable to resolve generated expression for {fullName}."); } - private static LambdaExpression? GetExpressionFromGeneratedType(MemberInfo projectableMemberInfo) + private static LambdaExpression? GetExpressionFromGeneratedType(MemberInfo projectableMemberInfo, bool isBase) { var declaringType = projectableMemberInfo.DeclaringType ?? throw new InvalidOperationException("Expected a valid type here"); - // Fast path: check the per-assembly static registry (generated by source generator). + // Fast path (isBase=false): check the per-assembly static registry (generated by source generator). // The first call per assembly does a reflection lookup to find the registry class and // 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); + LambdaExpression? registeredExpr; + if (!isBase) + { + var registry = GetAssemblyRegistry(declaringType.Assembly); + registeredExpr = registry?.Invoke(projectableMemberInfo); + } + else + { + registeredExpr = null; // We don't have a registry for base expressions for now + } return registeredExpr ?? // Slow path: reflection fallback for open-generic class members and generic methods // that are not yet in the registry. - FindGeneratedExpressionViaReflection(projectableMemberInfo); + FindGeneratedExpressionViaReflection(projectableMemberInfo, isBase); } private static LambdaExpression? GetExpressionFromMemberBody(MemberInfo projectableMemberInfo, string memberName) @@ -217,6 +232,7 @@ private static bool ParameterTypesMatch( /// significantly more expensive to build than simple method-body trees. /// private readonly static ConcurrentDictionary _reflectionCache = new(); + private readonly static ConcurrentDictionary _reflectionBaseCache = new(); /// /// Resolves the for a [Projectable] member using the @@ -227,8 +243,12 @@ private static bool ParameterTypesMatch( /// public static LambdaExpression? FindGeneratedExpressionViaReflection(MemberInfo projectableMemberInfo) { - var result = _reflectionCache.GetOrAdd(projectableMemberInfo, - static mi => BuildReflectionExpression(mi) ?? _reflectionNullSentinel); + return FindGeneratedExpressionViaReflection(projectableMemberInfo, false); + } + private static LambdaExpression? FindGeneratedExpressionViaReflection(MemberInfo projectableMemberInfo, bool isBase) + { + var result = (isBase ? _reflectionBaseCache : _reflectionCache).GetOrAdd(projectableMemberInfo, + mi => BuildReflectionExpression(mi, isBase) ?? _reflectionNullSentinel); return ReferenceEquals(result, _reflectionNullSentinel) ? null : result; } @@ -244,7 +264,7 @@ private static bool ParameterTypesMatch( /// instance is ultimately stored per member. /// /// - private static LambdaExpression? BuildReflectionExpression(MemberInfo projectableMemberInfo) + private static LambdaExpression? BuildReflectionExpression(MemberInfo projectableMemberInfo, bool isBase) { var declaringType = projectableMemberInfo.DeclaringType ?? throw new InvalidOperationException("Expected a valid type here"); @@ -326,6 +346,9 @@ private static bool ParameterTypesMatch( memberLookupName, parameterTypeNames); + if (isBase) + generatedContainingTypeName = generatedContainingTypeName + "_Base"; + var expressionFactoryType = declaringType.Assembly.GetType(generatedContainingTypeName); if (expressionFactoryType is null) diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt index 93d07640..019208a1 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt @@ -1,4 +1,25 @@ -// +[ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 1; + } + } +} + +// #nullable disable using System; using System.Linq; @@ -16,4 +37,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt index 3b615706..64e49e09 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt @@ -1,4 +1,25 @@ -// +[ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 1; + } + } +} + +// #nullable disable using System; using System.Linq; @@ -16,4 +37,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 1; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt index 17e10818..7958b53a 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt @@ -1,4 +1,25 @@ -// +[ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 1; + } + } +} + +// #nullable disable using System; using System.Linq; @@ -16,4 +37,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt index 78fa9b36..44aa798b 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt @@ -7,6 +7,26 @@ using System.Collections.Generic; using EntityFrameworkCore.Projectables; using Foo; +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Bar_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Bar @this) => 2; + } + } +} + +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + namespace EntityFrameworkCore.Projectables.Generated { [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -27,6 +47,26 @@ using System.Collections.Generic; using EntityFrameworkCore.Projectables; using Foo; +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 1; + } + } +} + +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + namespace EntityFrameworkCore.Projectables.Generated { [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs index 0ad8a882..ebf0fd3c 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs @@ -940,9 +940,9 @@ override public int Id(){ var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } [Fact] @@ -979,9 +979,9 @@ override public int Id(){ var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } [Fact] @@ -1019,7 +1019,7 @@ override public int Id(){ var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); + Assert.Equal(4, result.GeneratedTrees.Length); return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } @@ -1058,8 +1058,8 @@ override public int Id(){ var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } } \ No newline at end of file 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.Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt index 93d07640..019208a1 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt @@ -1,4 +1,25 @@ -// +[ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 1; + } + } +} + +// #nullable disable using System; using System.Linq; @@ -16,4 +37,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyBase.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyBase.verified.txt new file mode 100644 index 00000000..6ac79c7c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyBase.verified.txt @@ -0,0 +1,61 @@ +[ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Bar_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Bar @this) => true ? 2 : ((global::Foo.Foo)@this).Id(); + } + } +} + +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 1; + } + } +} + +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; + } + } +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt index 3b615706..64e49e09 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt @@ -1,4 +1,25 @@ -// +[ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 1; + } + } +} + +// #nullable disable using System; using System.Linq; @@ -16,4 +37,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 1; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt index 17e10818..7958b53a 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt @@ -1,4 +1,25 @@ -// +[ +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 1; + } + } +} + +// #nullable disable using System; using System.Linq; @@ -16,4 +37,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt index 78fa9b36..44aa798b 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt @@ -7,6 +7,26 @@ using System.Collections.Generic; using EntityFrameworkCore.Projectables; using Foo; +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Bar_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Bar @this) => 2; + } + } +} + +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + namespace EntityFrameworkCore.Projectables.Generated { [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -27,6 +47,26 @@ using System.Collections.Generic; using EntityFrameworkCore.Projectables; using Foo; +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 1; + } + } +} + +// +#nullable disable +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + namespace EntityFrameworkCore.Projectables.Generated { [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs index c734df30..36353157 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.cs @@ -824,9 +824,9 @@ public class Bar : Foo { var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } [Fact] @@ -857,9 +857,9 @@ public class Baz : Foo { var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } [Fact] @@ -891,7 +891,7 @@ public class Baz : Bar { var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); + Assert.Equal(4, result.GeneratedTrees.Length); return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } @@ -924,9 +924,9 @@ public class Baz : Bar { var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } [Fact] @@ -1040,4 +1040,34 @@ public abstract class Bar : Foo { } Assert.Equal("EFP0006", diag.Id); Assert.Equal(DiagnosticSeverity.Error, diag.Severity); } + + [Fact] + public Task HierarchyBase() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + [Projectable] + public virtual int Id() => 1; + } + + public class Bar : Foo { + [Projectable] + override public int Id() => true ? 2 : base.Id(); + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Equal(3, result.GeneratedTrees.Length); + + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); + } } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt index 58768d58..9725ed0c 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt @@ -1,4 +1,24 @@ -// +[ +// +#nullable disable +using System; +using System.Linq.Expressions; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 2; + } + } +} + +// #nullable disable using System; using System.Linq.Expressions; @@ -15,4 +35,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 2; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt index d1a3e4cb..ca113d15 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt @@ -1,4 +1,23 @@ -// +[ +// +#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_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 2; + } + } +} + +// #nullable disable using System; using EntityFrameworkCore.Projectables; @@ -14,4 +33,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 2; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt index 4f7fc08e..0a41464f 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt @@ -1,4 +1,24 @@ -// +[ +// +#nullable disable +using System; +using System.Linq.Expressions; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 2; + } + } +} + +// #nullable disable using System; using System.Linq.Expressions; @@ -15,4 +35,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id : 2; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt index ad0dff85..e77af574 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt @@ -1,4 +1,23 @@ -// +[ +// +#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_Foo_Id_Base + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => 2; + } + } +} + +// #nullable disable using System; using EntityFrameworkCore.Projectables; @@ -14,4 +33,5 @@ namespace EntityFrameworkCore.Projectables.Generated return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id : 2; } } -} \ No newline at end of file +} +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs index 074b824e..01e7ce2d 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs @@ -64,9 +64,9 @@ public class Bar : Foo { var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } [Fact] @@ -261,9 +261,9 @@ public class Bar : Foo { var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } [Fact] @@ -313,9 +313,9 @@ public class Bar : Foo { var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } [Fact] @@ -390,9 +390,9 @@ public class Bar : Foo { var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); + Assert.Equal(2, result.GeneratedTrees.Length); - return Verifier.Verify(result.GeneratedTrees[0].ToString()); + return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); } [Fact] diff --git a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs index 19d58391..1eda5954 100644 --- a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs @@ -209,5 +209,79 @@ private sealed class FakeClosureWithIQueryableProperty { public IQueryable? Items { get; set; } } + + public class ProjectableExpressionResolverStubBase : IProjectionExpressionResolver, IProjectionExpressionBaseResolver + { + readonly Func _implementation; + readonly Func _implementationBase; + + public ProjectableExpressionResolverStubBase(Func implementation, + Func implementationBase) + { + _implementation = implementation; + _implementationBase = implementationBase; + } + + public LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo, + ProjectableAttribute? projectableAttribute = null) => _implementation(projectableMemberInfo, projectableAttribute); + public LambdaExpression FindGeneratedBaseExpression(MemberInfo projectableMemberInfo, + ProjectableAttribute? projectableAttribute = null) => _implementationBase(projectableMemberInfo, projectableAttribute); + } + + class Foo + { + [Projectable] + public virtual int VirtualProperty => 1; + + [Projectable] + public virtual int VirtualMethod() => 1; + } + + class Bar : Foo + { + [Projectable] + override public int VirtualProperty => true ? 2 : base.VirtualProperty; + + [Projectable] + override public int VirtualMethod() => true ? 2 : base.VirtualProperty; + } + + [Fact] + public void VisitMember_HierarchyBaseProperty() + { + Expression> input = x => x.VirtualProperty; + Expression> expectedFooBase = x => 1; + Expression> expectedBar = x => true ? 2 : ((Foo)x).VirtualProperty; + Expression> expectedFoo = x => x is Bar ? true ? 2 : 1 : 1; + + var resolver = new ProjectableExpressionResolverStubBase( + (x, a) => x.DeclaringType == typeof(Foo) ? expectedFoo : expectedBar, + (x, a) => expectedFooBase + ); + var subject = new ProjectableExpressionReplacer(resolver); + + var actual = subject.Replace(input); + + Assert.Equal(expectedFoo.ToString(), actual.ToString()); + } + + [Fact] + public void VisitMember_HierarchyBaseMethod() + { + Expression> input = x => x.VirtualMethod(); + Expression> expectedFooBase = x => 1; + Expression> expectedBar = x => true ? 2 : ((Foo)x).VirtualMethod(); + Expression> expectedFoo = x => x is Bar ? true ? 2 : 1 : 1; + + var resolver = new ProjectableExpressionResolverStubBase( + (x, a) => x.DeclaringType == typeof(Foo) ? expectedFoo : expectedBar, + (x, a) => expectedFooBase + ); + var subject = new ProjectableExpressionReplacer(resolver); + + var actual = subject.Replace(input); + + Assert.Equal(expectedFoo.ToString(), actual.ToString()); + } } } From 08e4ce62c002c2b1e9b92dc2895ea0a0dc148816 Mon Sep 17 00:00:00 2001 From: R Hudylko Date: Wed, 17 Jun 2026 16:02:04 +0200 Subject: [PATCH 3/5] Cleanup --- .../ProjectionExpressionGenerator.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs index 31ea2bb4..6347c30c 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs @@ -89,14 +89,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return; } - /*try - {*/ - Execute(member, semanticModel, memberSymbol, attribute, globalOptions, compilation, spc); - /*} - catch(Exception e) - { - throw new Exception(e.StackTrace.Replace("\r\n", "
").Replace("\n", "
")); - }*/ + Execute(member, semanticModel, memberSymbol, attribute, globalOptions, compilation, spc); }); // Build the projection registry: collect all entries and emit a single registry file From 017a6153b82072295f026c7c2bfb9d9b8d26fdff Mon Sep 17 00:00:00 2001 From: R Hudylko Date: Mon, 22 Jun 2026 17:55:40 +0200 Subject: [PATCH 4/5] Hierarchy runtime implementation + docs --- README.md | 1 + docs/advanced/polymorphic-dispatch.md | 87 ++++++ docs/guide/projectable-methods.md | 18 ++ docs/guide/projectable-properties.md | 18 ++ docs/reference/projectable-attribute.md | 36 +++ .../ProjectableAttribute.cs | 5 + ...meworkCore.Projectables.Abstractions.props | 3 + .../Infrastructure/Diagnostics.cs | 4 +- .../ProjectableInterpreter.BodyProcessors.cs | 60 +--- .../ProjectableInterpreter.Helpers.cs | 95 ------ .../Interpretation/ProjectableInterpreter.cs | 18 +- .../Models/ProjectableAttributeData.cs | 9 + .../Models/ProjectableDescriptor.cs | 2 - .../Models/ProjectableGlobalOptions.cs | 7 + .../ProjectionExpressionGenerator.cs | 147 +++++---- .../ExpressionSyntaxRewriter.cs | 16 +- .../HierarchyMembersConverter.cs | 142 --------- .../Extensions/ExpressionExtensions.cs | 5 +- .../Internal/CustomQueryCompiler.cs | 3 +- .../IProjectionExpressionBaseResolver.cs | 10 - .../Services/ProjectableExpressionReplacer.cs | 282 ++++++++++++++---- .../Services/ProjectionExpressionResolver.cs | 47 +-- ...s.BlockBodiedMethod_Hierarchy.verified.txt | 41 --- ...odiedMethod_HierarchyMultiple.verified.txt | 41 --- ...erarchyNestedWithoutAttribute.verified.txt | 41 --- ...erarchyNotNestedWithAttribute.verified.txt | 81 ----- .../BlockBodyTests.cs | 157 +--------- .../MethodTests.Hierarchy.verified.txt | 41 --- ...MethodTests.HierarchyAbstract.verified.txt | 19 -- ...sts.HierarchyAbstractMultiple.verified.txt | 19 -- .../MethodTests.HierarchyBase.verified.txt | 61 ---- ...MethodTests.HierarchyMultiple.verified.txt | 41 --- ...erarchyNestedWithoutAttribute.verified.txt | 41 --- ...erarchyNotNestedWithAttribute.verified.txt | 81 ----- .../MethodTests.cs | 260 +--------------- .../PropertyTests.cs | 23 ++ ...ressionPropertyBody_Hierarchy.verified.txt | 39 --- ...thod_UsesMethodBody_Hierarchy.verified.txt | 37 --- ...ressionPropertyBody_Hierarchy.verified.txt | 39 --- ...ty_UsesPropertyBody_Hierarchy.verified.txt | 37 --- .../UseMemberBodyTests.cs | 110 ------- .../ProjectableExpressionReplacerTests.cs | 277 +++++++++++++++-- 42 files changed, 792 insertions(+), 1709 deletions(-) create mode 100644 docs/advanced/polymorphic-dispatch.md delete mode 100644 src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/HierarchyMembersConverter.cs delete mode 100644 src/EntityFrameworkCore.Projectables/Services/IProjectionExpressionBaseResolver.cs delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstract.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstractMultiple.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyBase.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt 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..5bd5a056 --- /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"; +} + + +var 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 \ No newline at end of file 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 bd44b763..21ee2136 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs @@ -46,8 +46,8 @@ static internal class Diagnostics public readonly static DiagnosticDescriptor RequiresBodyDefinition = new DiagnosticDescriptor( id: "EFP0006", - title: "Method or property should expose a body definition if not overwritten in classes derived from the declaring class", - 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 not overwritten in at least one class derived from the class where the method or property is declared.", + 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. 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 6648f208..9ffb3eb2 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs @@ -14,23 +14,18 @@ static internal partial class ProjectableInterpreter /// Returns false and reports diagnostics on failure. /// private static bool TryApplyMethodBody( - MemberDeclarationSyntax originalMemberDeclarationSyntax, MethodDeclarationSyntax methodDeclarationSyntax, - SemanticModel semanticModel, bool allowBlockBody, + bool polymorphicDispatch, ISymbol memberSymbol, ExpressionSyntaxRewriter expressionSyntaxRewriter, DeclarationSyntaxRewriter declarationSyntaxRewriter, SourceProductionContext context, - Compilation? compilation, ProjectableDescriptor descriptor) { ExpressionSyntax? bodyExpression = null; var isExpressionBodied = false; - var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMemberDeclarationSyntax), compilation); - var isHierarchy = derivedTypes?.Count > 0; - if (methodDeclarationSyntax.ExpressionBody is not null) { bodyExpression = methodDeclarationSyntax.ExpressionBody.Expression; @@ -54,7 +49,7 @@ private static bool TryApplyMethodBody( return false; // diagnostics already reported by BlockStatementConverter } } - else if (!isHierarchy) + else if (!polymorphicDispatch) { return ReportRequiresBodyAndFail(context, methodDeclarationSyntax, memberSymbol.Name); } @@ -79,13 +74,6 @@ private static bool TryApplyMethodBody( ApplyExtensionBlockTypeParameters(memberSymbol, descriptor); } - // If we are rewriting a hierarchy method we need to invoke the derived types' overrides - if(isHierarchy) - { - descriptor.HierarchyOriginalExpressionBody = descriptor.ExpressionBody; - descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicateMethodExpression(derivedTypes!, descriptor); - } - return true; } @@ -100,23 +88,20 @@ private static bool TryApplyExpressionPropertyBody( MethodDeclarationSyntax originalMethodDecl, PropertyDeclarationSyntax exprPropDecl, SemanticModel semanticModel, + bool polymorphicDispatch, MemberDeclarationSyntax member, ISymbol memberSymbol, ExpressionSyntaxRewriter expressionSyntaxRewriter, DeclarationSyntaxRewriter declarationSyntaxRewriter, SourceProductionContext context, - Compilation? compilation, ProjectableDescriptor descriptor) { - var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMethodDecl), compilation); - var isHierarchy = derivedTypes?.Count > 0; - var rawExpr = TryGetPropertyGetterExpression(exprPropDecl); var (innerBody, lambdaParamNames) = rawExpr is not null ? TryExtractLambdaBodyAndParams(rawExpr, semanticModel, member.SyntaxTree) : (null, []); - if (innerBody is null && !isHierarchy) + if (innerBody is null && !polymorphicDispatch) { return ReportRequiresBodyAndFail(context, exprPropDecl, memberSymbol.Name); } @@ -211,13 +196,6 @@ private static bool TryApplyExpressionPropertyBody( ApplyParameterList(originalMethodDecl.ParameterList, declarationSyntaxRewriter, descriptor); ApplyTypeParameters(originalMethodDecl, declarationSyntaxRewriter, descriptor); - // If we are rewriting a hierarchy method we need to invoke the derived types' overrides - if (isHierarchy) - { - descriptor.HierarchyOriginalExpressionBody = descriptor.ExpressionBody; - descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicateMethodExpression(derivedTypes!, descriptor); - } - return true; } @@ -233,23 +211,20 @@ private static bool TryApplyExpressionPropertyBodyForProperty( PropertyDeclarationSyntax originalPropertyDecl, PropertyDeclarationSyntax exprPropDecl, SemanticModel semanticModel, + bool polymorphicDispatch, MemberDeclarationSyntax member, ISymbol memberSymbol, ExpressionSyntaxRewriter expressionSyntaxRewriter, DeclarationSyntaxRewriter declarationSyntaxRewriter, SourceProductionContext context, - Compilation? compilation, ProjectableDescriptor descriptor) { - var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalPropertyDecl), compilation); - var isHierarchy = derivedTypes?.Count > 0; - var rawExpr = TryGetPropertyGetterExpression(exprPropDecl); var (innerBody, firstParamName) = rawExpr is not null ? TryExtractLambdaBodyAndFirstParam(rawExpr, semanticModel, member.SyntaxTree) : (null, null); - if (innerBody is null && !isHierarchy) + if (innerBody is null && !polymorphicDispatch) { return ReportRequiresBodyAndFail(context, exprPropDecl, memberSymbol.Name); } @@ -274,13 +249,6 @@ private static bool TryApplyExpressionPropertyBodyForProperty( descriptor.ReturnTypeName = returnType.ToString(); descriptor.ExpressionBody = visitedBody; - // If we are rewriting a hierarchy method we need to invoke the derived types' overrides - if (isHierarchy) - { - descriptor.HierarchyOriginalExpressionBody = descriptor.ExpressionBody; - descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicatePropertyExpression(derivedTypes!, descriptor); - } - return true; } @@ -289,20 +257,15 @@ private static bool TryApplyExpressionPropertyBodyForProperty( /// Returns false and reports diagnostics on failure. /// private static bool TryApplyPropertyBody( - MemberDeclarationSyntax originalMemberDeclarationSyntax, PropertyDeclarationSyntax propertyDeclarationSyntax, - SemanticModel semanticModel, bool allowBlockBody, + bool polymorphicDispatch, ISymbol memberSymbol, ExpressionSyntaxRewriter expressionSyntaxRewriter, DeclarationSyntaxRewriter declarationSyntaxRewriter, SourceProductionContext context, - Compilation? compilation, ProjectableDescriptor descriptor) { - var derivedTypes = GetDerivedTypes(semanticModel.GetDeclaredSymbol(originalMemberDeclarationSyntax), compilation); - var isHierarchy = derivedTypes?.Count > 0; - ExpressionSyntax? bodyExpression = null; var isBlockBodiedGetter = false; @@ -343,7 +306,7 @@ private static bool TryApplyPropertyBody( } } - if (bodyExpression is null && !isHierarchy) + if (bodyExpression is null && !polymorphicDispatch) { return ReportRequiresBodyAndFail(context, propertyDeclarationSyntax, memberSymbol.Name); } @@ -356,13 +319,6 @@ private static bool TryApplyPropertyBody( ? bodyExpression : (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression); - // If we are rewriting a hierarchy method we need to invoke the derived types' overrides - if (isHierarchy) - { - descriptor.HierarchyOriginalExpressionBody = descriptor.ExpressionBody; - descriptor.ExpressionBody = new HierarchyMembersConverter().DuplicatePropertyExpression(derivedTypes!, descriptor); - } - return true; } diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs index 5ee9cd08..ec6c25f8 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.Helpers.cs @@ -183,100 +183,5 @@ private static bool ReportRequiresBodyAndFail( memberName)); return false; } - - private static IEnumerable GetAllTypes(INamespaceSymbol namespaceSymbol) - { - foreach (var type in namespaceSymbol.GetTypeMembers()) - { - yield return type; - } - - foreach (var nestedNamespace in namespaceSymbol.GetNamespaceMembers()) - { - foreach (var type in GetAllTypes(nestedNamespace)) - { - yield return type; - } - } - } - - private static IList GetDerivedTypes(ISymbol? symbol, Compilation? compilation) - { - if (symbol != null && compilation != null && (symbol.IsAbstract || symbol.IsVirtual || symbol.IsOverride)) - { - var types = GetAllTypes(compilation.GlobalNamespace) - .Where(t => IsDerivedFrom(t, symbol.ContainingType) && - t.DeclaringSyntaxReferences.Any(s => ((ClassDeclarationSyntax)s.GetSyntax()).Members.Any(m => { - var ss = compilation.GetSemanticModel(m.SyntaxTree).GetDeclaredSymbol(m); - return (ss != null && ss.IsOverride && ss.Kind == symbol.Kind && ss.Name == symbol.Name); - }))) - .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 => IsDerivedFrom(t, tt) && - tt.DeclaringSyntaxReferences.Any(s => ((ClassDeclarationSyntax)s.GetSyntax()).Members.First(m => { - var ss = compilation.GetSemanticModel(m.SyntaxTree).GetDeclaredSymbol(m); - return (ss != null && ss.IsOverride && ss.Kind == symbol.Kind && ss.Name == symbol.Name); - }).AttributeLists.Any(a => a.Attributes.Any(aa => { - var attributeSymbol = compilation.GetSemanticModel(aa.SyntaxTree).GetSymbolInfo(aa).Symbol; - - INamedTypeSymbol attributeTypeSymbol; - if (attributeSymbol is IMethodSymbol methodSymbol) - { - attributeTypeSymbol = methodSymbol.ContainingType; - } - else - { - attributeTypeSymbol = ((INamedTypeSymbol)attributeSymbol!); - } - - return attributeTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == - "global::EntityFrameworkCore.Projectables.ProjectableAttribute"; - }))))).ToList(); - - foreach(var type in typesToRemove) - { - types.Remove(type); - } - - return types; - } - else - { - return Array.Empty(); - } - } - - private static bool IsDerivedFrom(INamedTypeSymbol candidate, INamedTypeSymbol baseClass) - { - var current = candidate.BaseType; - - while (current != null) - { - // SymbolEqualityComparer ensures we compare symbols accurately across compilation boundaries - if (SymbolEqualityComparer.Default.Equals(current, baseClass)) - { - return true; - } - current = current.BaseType; - } - - return false; - } - - private static int GetDepth(INamedTypeSymbol type) - { - var depth = 0; - while(type.BaseType != null) - { - depth++; - type = type.BaseType; - } - - return depth; - } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs index 303eb485..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,26 +77,26 @@ static internal partial class ProjectableInterpreter { // Projectable method (_, MethodDeclarationSyntax methodDecl) => - TryApplyMethodBody(member, methodDecl, semanticModel, allowBlockBody, memberSymbol, - expressionSyntaxRewriter, declarationSyntaxRewriter, context, compilation, descriptor), + 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, - expressionSyntaxRewriter, declarationSyntaxRewriter, context, compilation, descriptor), + 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, - expressionSyntaxRewriter, declarationSyntaxRewriter, context, compilation, descriptor), + semanticModel, polymorphicDispatch, member, memberSymbol, + expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor), // Projectable property (_, PropertyDeclarationSyntax propDecl) => - TryApplyPropertyBody(member, propDecl, semanticModel, allowBlockBody, memberSymbol, - expressionSyntaxRewriter, declarationSyntaxRewriter, context, compilation, descriptor), + TryApplyPropertyBody(propDecl, allowBlockBody, polymorphicDispatch, memberSymbol, + expressionSyntaxRewriter, declarationSyntaxRewriter, context, descriptor), // Projectable constructor (_, ConstructorDeclarationSyntax ctorDecl) => 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/ProjectableDescriptor.cs b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableDescriptor.cs index 55dfdbbe..e653a018 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableDescriptor.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableDescriptor.cs @@ -34,6 +34,4 @@ internal class ProjectableDescriptor public SyntaxList? ConstraintClauses { get; set; } public ExpressionSyntax? ExpressionBody { get; set; } - - public ExpressionSyntax? HierarchyOriginalExpressionBody { get; set; } } \ No newline at end of file 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 6347c30c..55c0ee57 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs @@ -93,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 @@ -121,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) @@ -208,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; } @@ -228,91 +231,81 @@ private static void Execute( } var generatedClassName = ProjectionExpressionClassNameGenerator.GenerateName(projectable.ClassNamespace, projectable.NestedInClassNames, projectable.MemberName, projectable.ParameterTypeNames); - - AddSource(generatedClassName, projectable.ExpressionBody); - if(projectable.HierarchyOriginalExpressionBody != null) - { - AddSource(generatedClassName + "_Base", projectable.HierarchyOriginalExpressionBody); - } - - - void AddSource(string generatedClassName, ExpressionSyntax? body) - { - var generatedFileName = projectable.ClassTypeParameterList is not null ? $"{generatedClassName}-{projectable.ClassTypeParameterList.ChildNodes().Count()}.g.cs" : $"{generatedClassName}.g.cs"; - - var classSyntax = ClassDeclaration(generatedClassName) - .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) - .WithTypeParameterList(projectable.ClassTypeParameterList) - .WithConstraintClauses(projectable.ClassConstraintClauses ?? List()) - .AddAttributeLists( - AttributeList() - .AddAttributes(_editorBrowsableAttribute) - ) - .WithLeadingTrivia(member is ConstructorDeclarationSyntax ctor && compilation is not null ? BuildSourceDocComment(ctor, compilation) : TriviaList()) - .AddMembers( - MethodDeclaration( - GenericName( - Identifier("global::System.Linq.Expressions.Expression"), - TypeArgumentList( - SingletonSeparatedList( - (TypeSyntax)GenericName( - Identifier("global::System.Func"), - GetLambdaTypeArgumentListSyntax(projectable) - ) + var generatedFileName = projectable.ClassTypeParameterList is not null ? $"{generatedClassName}-{projectable.ClassTypeParameterList.ChildNodes().Count()}.g.cs" : $"{generatedClassName}.g.cs"; + + var classSyntax = ClassDeclaration(generatedClassName) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithTypeParameterList(projectable.ClassTypeParameterList) + .WithConstraintClauses(projectable.ClassConstraintClauses ?? List()) + .AddAttributeLists( + AttributeList() + .AddAttributes(_editorBrowsableAttribute) + ) + .WithLeadingTrivia(member is ConstructorDeclarationSyntax ctor && compilation is not null ? BuildSourceDocComment(ctor, compilation) : TriviaList()) + .AddMembers( + MethodDeclaration( + GenericName( + Identifier("global::System.Linq.Expressions.Expression"), + TypeArgumentList( + SingletonSeparatedList( + (TypeSyntax)GenericName( + Identifier("global::System.Func"), + GetLambdaTypeArgumentListSyntax(projectable) ) ) - ), - "Expression" - ) - .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) - .WithTypeParameterList(projectable.TypeParameterList) - .WithConstraintClauses(projectable.ConstraintClauses ?? List()) - .WithBody( - Block( - ReturnStatement( - ParenthesizedLambdaExpression( - projectable.ParametersList ?? ParameterList(), - null, - body - ) + ) + ), + "Expression" + ) + .WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword))) + .WithTypeParameterList(projectable.TypeParameterList) + .WithConstraintClauses(projectable.ConstraintClauses ?? List()) + .WithBody( + Block( + ReturnStatement( + ParenthesizedLambdaExpression( + projectable.ParametersList ?? ParameterList(), + null, + projectable.ExpressionBody ) ) ) - ); + ) + ); - var compilationUnit = CompilationUnit(); + var compilationUnit = CompilationUnit(); - foreach (var usingDirective in projectable.UsingDirectives!) - { - compilationUnit = compilationUnit.AddUsings(usingDirective); - } + foreach (var usingDirective in projectable.UsingDirectives!) + { + compilationUnit = compilationUnit.AddUsings(usingDirective); + } - if (projectable.ClassNamespace is not null) - { - compilationUnit = compilationUnit.AddUsings( - UsingDirective( - ParseName(projectable.ClassNamespace) - ) - ); - } + if (projectable.ClassNamespace is not null) + { + compilationUnit = compilationUnit.AddUsings( + UsingDirective( + ParseName(projectable.ClassNamespace) + ) + ); + } - compilationUnit = compilationUnit - .AddMembers( - NamespaceDeclaration( - ParseName("EntityFrameworkCore.Projectables.Generated") - ).AddMembers(classSyntax) + compilationUnit = compilationUnit + .AddMembers( + NamespaceDeclaration( + ParseName("EntityFrameworkCore.Projectables.Generated") + ).AddMembers(classSyntax) + ) + .WithLeadingTrivia( + TriviaList( + Comment("// "), + // Uncomment line below, for debugging purposes, to see when the generator is run on source generated files + // CarriageReturnLineFeed, Comment($"// Generated at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff} UTC for '{memberSymbol.Name}' in '{memberSymbol.ContainingType?.Name}'"), + Trivia(NullableDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)) ) - .WithLeadingTrivia( - TriviaList( - Comment("// "), - // Uncomment line below, for debugging purposes, to see when the generator is run on source generated files - // CarriageReturnLineFeed, Comment($"// Generated at {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.fff} UTC for '{memberSymbol.Name}' in '{memberSymbol.ContainingType?.Name}'"), - Trivia(NullableDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)) - ) - ); + ); + + context.AddSource(generatedFileName, SourceText.From(compilationUnit.NormalizeWhitespace().ToFullString(), Encoding.UTF8)); - context.AddSource(generatedFileName, SourceText.From(compilationUnit.NormalizeWhitespace().ToFullString(), Encoding.UTF8)); - } static TypeArgumentListSyntax GetLambdaTypeArgumentListSyntax(ProjectableDescriptor projectable) { diff --git a/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/ExpressionSyntaxRewriter.cs index 183548db..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,13 +102,15 @@ 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 and cast it to the base type + // 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)), diff --git a/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/HierarchyMembersConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/HierarchyMembersConverter.cs deleted file mode 100644 index 845d40c2..00000000 --- a/src/EntityFrameworkCore.Projectables.Generator/SyntaxRewriters/HierarchyMembersConverter.cs +++ /dev/null @@ -1,142 +0,0 @@ -using EntityFrameworkCore.Projectables.Generator.Models; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace EntityFrameworkCore.Projectables.Generator.SyntaxRewriters -{ - /// - /// Converts methods/properties bodies of hierarchies of classes into typed expressions. - /// - internal class HierarchyMembersConverter - { - public ExpressionSyntax DuplicateMethodExpression(IList derivedTypes, ProjectableDescriptor descriptor) - { - var @this = SyntaxFactory.IdentifierName("@this"); - - var arguments = descriptor.ParametersList?.Parameters.Count > 1 ? ConvertParameters(descriptor.ParametersList) : null; - - // 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 - if (descriptor.ExpressionBody != null) - { - // @this is Type1 ? ((Type1)@this).Method(...) : ... - // ... ? ... : - // @this is TypeN ? ((TypeN)@this).Method(...) : ... - // virtualImplementation - return derivedTypes.Reverse().Aggregate(descriptor.ExpressionBody, AggregateTypes); - } - else - { - // DEV: handle generic types - var lastType = derivedTypes[derivedTypes.Count - 1]; - - // @this is Type1 ? ((Type1)@this).Method(...) : ... - // ... ? ... : - // ((TypeN)@this).Method(...) - return derivedTypes.Reverse().Skip(1) - .Aggregate((ExpressionSyntax)GetMethodInvocationExpression(lastType, descriptor.MemberName!, arguments), AggregateTypes); - } - - - ExpressionSyntax AggregateTypes(ExpressionSyntax expr, INamedTypeSymbol type) - { - return SyntaxFactory.ConditionalExpression( - SyntaxFactory.BinaryExpression(SyntaxKind.IsExpression, @this, GetTypeName(type)), - GetMethodInvocationExpression(type, descriptor.MemberName!, arguments), - expr); - } - } - - public ExpressionSyntax DuplicatePropertyExpression(IList derivedTypes, ProjectableDescriptor descriptor) - { - var @this = SyntaxFactory.IdentifierName("@this"); - - // Check if the property 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 - if (descriptor.ExpressionBody != null) - { - // @this is Type1 ? ((Type1)@this).Property : ... - // ... ? ... : - // @this is TypeN ? ((TypeN)@this).Property : ... - // virtualImplementation - return derivedTypes.Reverse().Aggregate(descriptor.ExpressionBody, AggregateTypes); - } - else - { - // DEV: handle generic types - var lastType = derivedTypes[derivedTypes.Count - 1]; - - // @this is Type1 ? ((Type1)@this).Property : ... - // ... ? ... : - // ((TypeN)@this).Property - return derivedTypes.Reverse().Skip(1) - .Aggregate((ExpressionSyntax)GetPropertyExpression(lastType, descriptor.MemberName!), AggregateTypes); - } - - - ExpressionSyntax AggregateTypes(ExpressionSyntax expr, INamedTypeSymbol type) - { - return SyntaxFactory.ConditionalExpression( - SyntaxFactory.BinaryExpression(SyntaxKind.IsExpression, @this, GetTypeName(type)), - GetPropertyExpression(type, descriptor.MemberName!), - expr); - } - } - - private static ArgumentListSyntax ConvertParameters(ParameterListSyntax parameters) - { - return SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(parameters.Parameters.Skip(1).Select(p => { - // Extract the name of the parameter (e.g., "myParam") - ExpressionSyntax identifier = SyntaxFactory.IdentifierName(p.Identifier); - - // Handle parameter modifiers (like 'ref', 'out', or 'in') - SyntaxToken? refKindKeyword = null; - if (p.Modifiers.Any(SyntaxKind.RefKeyword)) - refKindKeyword = SyntaxFactory.Token(SyntaxKind.RefKeyword); - else if (p.Modifiers.Any(SyntaxKind.OutKeyword)) - refKindKeyword = SyntaxFactory.Token(SyntaxKind.OutKeyword); - else if (p.Modifiers.Any(SyntaxKind.InKeyword)) - refKindKeyword = SyntaxFactory.Token(SyntaxKind.InKeyword); - - // Create the Argument node. If it has a ref/out modifier, pass it along. - if (refKindKeyword != null) - { - return SyntaxFactory.Argument(null, refKindKeyword.Value, identifier); - } - else - { - return SyntaxFactory.Argument(identifier); - } - }))); - } - - private static TypeSyntax GetTypeName(INamedTypeSymbol type) - { - return SyntaxFactory.ParseTypeName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - } - - private static InvocationExpressionSyntax GetMethodInvocationExpression(INamedTypeSymbol type, string methodName, ArgumentListSyntax? arguments) - { - var typeName = GetTypeName(type); - - var method = SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.ParenthesizedExpression(SyntaxFactory.CastExpression(typeName, SyntaxFactory.IdentifierName("@this"))), - SyntaxFactory.IdentifierName(methodName)); - - // ((Type)@this).Method(...) - return arguments != null ? SyntaxFactory.InvocationExpression(method, arguments) : SyntaxFactory.InvocationExpression(method); - } - - private static MemberAccessExpressionSyntax GetPropertyExpression(INamedTypeSymbol type, string propertyName) - { - var typeName = GetTypeName(type); - - return SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.ParenthesizedExpression(SyntaxFactory.CastExpression(typeName, SyntaxFactory.IdentifierName("@this"))), - SyntaxFactory.IdentifierName(propertyName)); - } - } -} diff --git a/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs b/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs index b411b8dc..3de24d8d 100644 --- a/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs +++ b/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs @@ -9,8 +9,5 @@ public static class ExpressionExtensions /// Replaces all calls to properties and methods that are marked with the Projectable attribute with their respective expression tree /// public static Expression ExpandProjectables(this Expression expression) - { - var resolver = new ProjectionExpressionResolver(); - return new ProjectableExpressionReplacer(resolver, resolver, false).Replace(expression); - } + => new ProjectableExpressionReplacer(new ProjectionExpressionResolver(), false).Replace(expression); } \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs index e0958e5e..f8a206e9 100644 --- a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs @@ -61,8 +61,7 @@ public CustomQueryCompiler(IQueryCompiler decoratedQueryCompiler, var trackingByDefault = (contextOptions.FindExtension()?.QueryTrackingBehavior ?? QueryTrackingBehavior.TrackAll) == QueryTrackingBehavior.TrackAll; - var resolver = new ProjectionExpressionResolver(); - _projectableExpressionReplacer = new ProjectableExpressionReplacer(resolver, resolver, trackingByDefault); + _projectableExpressionReplacer = new ProjectableExpressionReplacer(new ProjectionExpressionResolver(), trackingByDefault); } public override Func CreateCompiledAsyncQuery(Expression query) diff --git a/src/EntityFrameworkCore.Projectables/Services/IProjectionExpressionBaseResolver.cs b/src/EntityFrameworkCore.Projectables/Services/IProjectionExpressionBaseResolver.cs deleted file mode 100644 index f3455279..00000000 --- a/src/EntityFrameworkCore.Projectables/Services/IProjectionExpressionBaseResolver.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Linq.Expressions; -using System.Reflection; - -namespace EntityFrameworkCore.Projectables.Services; - -public interface IProjectionExpressionBaseResolver -{ - LambdaExpression FindGeneratedBaseExpression(MemberInfo projectableMemberInfo, - ProjectableAttribute? projectableAttribute = null); -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs index 244abbc8..9d1e946b 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs @@ -13,14 +13,13 @@ namespace EntityFrameworkCore.Projectables.Services public sealed class ProjectableExpressionReplacer : ExpressionVisitor { private readonly IProjectionExpressionResolver _resolver; - private readonly IProjectionExpressionBaseResolver _resolverBase; private readonly ExpressionArgumentReplacer _expressionArgumentReplacer = new(); private readonly Dictionary _projectableMemberCache = new(); - private readonly Dictionary _projectableBaseMemberCache = new(); private readonly HashSet _expandingConstructors = new(); 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) @@ -40,38 +39,16 @@ public sealed class ProjectableExpressionReplacer : ExpressionVisitor private readonly static ConditionalWeakTable _closedSelectCache = new(); private readonly static ConditionalWeakTable _closedWhereCache = new(); - public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver, bool trackByDefault = false): - this(projectionExpressionResolver, null!, trackByDefault) { } - public ProjectableExpressionReplacer( - IProjectionExpressionResolver projectionExpressionResolver, - IProjectionExpressionBaseResolver projectionExpressionBaseResolver, - bool trackByDefault = false) + public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver, bool trackByDefault = false) { _trackingByDefault = trackByDefault; _resolver = projectionExpressionResolver; - _resolverBase = projectionExpressionBaseResolver; + _polymorphicDispatchGlobal = false; // DEV: retrieve from global config } bool TryGetReflectedExpression(MemberInfo memberInfo, [NotNullWhen(true)] out LambdaExpression? reflectedExpression) { - return TryGetReflectedExpression(memberInfo, false, out reflectedExpression); - } - bool TryGetReflectedExpression(MemberInfo memberInfo, bool isBase, [NotNullWhen(true)] out LambdaExpression? reflectedExpression) - { - if (isBase) - { - if (!_projectableBaseMemberCache.TryGetValue(memberInfo, out reflectedExpression)) - { - var projectableAttribute = memberInfo.GetCustomAttribute(false); - - reflectedExpression = projectableAttribute is not null - ? _resolverBase?.FindGeneratedBaseExpression(memberInfo, projectableAttribute) - : null; - - _projectableBaseMemberCache.Add(memberInfo, reflectedExpression); - } - } - else if (!_projectableMemberCache.TryGetValue(memberInfo, out reflectedExpression)) + if (!_projectableMemberCache.TryGetValue(memberInfo, out reflectedExpression)) { var projectableAttribute = memberInfo.GetCustomAttribute(false); @@ -215,56 +192,168 @@ protected override Expression VisitMethodCall(MethodCallExpression node) // unwrapping nested casts, because the original parameter might have been replaced var isBase = (node.Object is UnaryExpression u && UnwrapUnaryConvert(u) != u); - if (TryGetReflectedExpression(methodInfo, isBase, out var reflectedExpression)) + 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) { - // 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 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) { - var unwrapped = UnwrapUnaryConvert(u2); - if (unwrapped != u2) - { - mappedArgumentExpression = Expression.Convert(unwrapped, parameterExpression.Type); - } + // @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); + - _expressionArgumentReplacer.ParameterArgumentMapping.Add(parameterExpression, mappedArgumentExpression); + 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); + } + } + + var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body); + _expressionArgumentReplacer.ParameterArgumentMapping.Clear(); - return base.Visit( - updatedBody - ); + return Visit(updatedBody); + } } return base.VisitMethodCall(node); } - private Expression UnwrapUnaryConvert(UnaryExpression 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) @@ -354,22 +443,89 @@ PropertyInfo property when nodeExpression is not null _ => node.Member }; - // Check if we are rewriting a base property ((BaseType)@this).MyProp - var isBase = (node.Expression is UnaryExpression u && u.NodeType == ExpressionType.Convert && - u.Type == u.Operand.Type.BaseType && u.Operand is ParameterExpression p && p.Name == "@this"); + // 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, isBase, out var reflectedExpression)) + 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 e33fe99b..5b78eaee 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs @@ -9,7 +9,7 @@ namespace EntityFrameworkCore.Projectables.Services { - public sealed class ProjectionExpressionResolver : IProjectionExpressionResolver, IProjectionExpressionBaseResolver + public sealed class ProjectionExpressionResolver : IProjectionExpressionResolver { // We never store null in the dictionary; assemblies without a registry use a sentinel delegate. private readonly static Func _nullRegistry = static _ => null!; @@ -20,7 +20,6 @@ public sealed class ProjectionExpressionResolver : IProjectionExpressionResolver /// EF Core never repeats reflection work for the same member across queries. /// private readonly static ConcurrentDictionary _expressionCache = new(); - private readonly static ConcurrentDictionary _expressionBaseCache = new(); /// /// Caches → C#-formatted name strings, since the same parameter types @@ -79,22 +78,16 @@ public sealed class ProjectionExpressionResolver : IProjectionExpressionResolver public LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo, ProjectableAttribute? projectableAttribute = null) - => _expressionCache.GetOrAdd(projectableMemberInfo, static (mi, a) => ResolveExpressionCore(mi, false, a), - projectableAttribute); - - public LambdaExpression FindGeneratedBaseExpression(MemberInfo projectableMemberInfo, - ProjectableAttribute? projectableAttribute = null) - => _expressionBaseCache.GetOrAdd(projectableMemberInfo, static (mi, a) => ResolveExpressionCore(mi, true, a), + => _expressionCache.GetOrAdd(projectableMemberInfo, static (mi, a) => ResolveExpressionCore(mi, a), projectableAttribute); private static LambdaExpression ResolveExpressionCore(MemberInfo projectableMemberInfo, - bool isBase, ProjectableAttribute? projectableAttribute) { projectableAttribute ??= projectableMemberInfo.GetCustomAttribute() ?? throw new InvalidOperationException("Expected member to have a Projectable attribute. None found"); - var expression = GetExpressionFromGeneratedType(projectableMemberInfo, isBase); + var expression = GetExpressionFromGeneratedType(projectableMemberInfo); if (expression is null && projectableAttribute.UseMemberBody is not null) { @@ -115,28 +108,20 @@ private static LambdaExpression ResolveExpressionCore(MemberInfo projectableMemb throw new InvalidOperationException($"Unable to resolve generated expression for {fullName}."); } - private static LambdaExpression? GetExpressionFromGeneratedType(MemberInfo projectableMemberInfo, bool isBase) + private static LambdaExpression? GetExpressionFromGeneratedType(MemberInfo projectableMemberInfo) { var declaringType = projectableMemberInfo.DeclaringType ?? throw new InvalidOperationException("Expected a valid type here"); - // Fast path (isBase=false): check the per-assembly static registry (generated by source generator). + // Fast path: check the per-assembly static registry (generated by source generator). // The first call per assembly does a reflection lookup to find the registry class and // caches it as a delegate; subsequent calls use the cached delegate for an O(1) dictionary lookup. - LambdaExpression? registeredExpr; - if (!isBase) - { - var registry = GetAssemblyRegistry(declaringType.Assembly); - registeredExpr = registry?.Invoke(projectableMemberInfo); - } - else - { - registeredExpr = null; // We don't have a registry for base expressions for now - } - + 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. - FindGeneratedExpressionViaReflection(projectableMemberInfo, isBase); + FindGeneratedExpressionViaReflection(projectableMemberInfo); } private static LambdaExpression? GetExpressionFromMemberBody(MemberInfo projectableMemberInfo, string memberName) @@ -232,7 +217,6 @@ private static bool ParameterTypesMatch( /// significantly more expensive to build than simple method-body trees. /// private readonly static ConcurrentDictionary _reflectionCache = new(); - private readonly static ConcurrentDictionary _reflectionBaseCache = new(); /// /// Resolves the for a [Projectable] member using the @@ -243,12 +227,8 @@ private static bool ParameterTypesMatch( /// public static LambdaExpression? FindGeneratedExpressionViaReflection(MemberInfo projectableMemberInfo) { - return FindGeneratedExpressionViaReflection(projectableMemberInfo, false); - } - private static LambdaExpression? FindGeneratedExpressionViaReflection(MemberInfo projectableMemberInfo, bool isBase) - { - var result = (isBase ? _reflectionBaseCache : _reflectionCache).GetOrAdd(projectableMemberInfo, - mi => BuildReflectionExpression(mi, isBase) ?? _reflectionNullSentinel); + var result = _reflectionCache.GetOrAdd(projectableMemberInfo, + static mi => BuildReflectionExpression(mi) ?? _reflectionNullSentinel); return ReferenceEquals(result, _reflectionNullSentinel) ? null : result; } @@ -264,7 +244,7 @@ private static bool ParameterTypesMatch( /// instance is ultimately stored per member. /// /// - private static LambdaExpression? BuildReflectionExpression(MemberInfo projectableMemberInfo, bool isBase) + private static LambdaExpression? BuildReflectionExpression(MemberInfo projectableMemberInfo) { var declaringType = projectableMemberInfo.DeclaringType ?? throw new InvalidOperationException("Expected a valid type here"); @@ -346,9 +326,6 @@ private static bool ParameterTypesMatch( memberLookupName, parameterTypeNames); - if (isBase) - generatedContainingTypeName = generatedContainingTypeName + "_Base"; - var expressionFactoryType = declaringType.Assembly.GetType(generatedContainingTypeName); if (expressionFactoryType is null) diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt deleted file mode 100644 index 019208a1..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_Hierarchy.verified.txt +++ /dev/null @@ -1,41 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 1; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt deleted file mode 100644 index 64e49e09..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyMultiple.verified.txt +++ /dev/null @@ -1,41 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 1; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 1; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt deleted file mode 100644 index 7958b53a..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNestedWithoutAttribute.verified.txt +++ /dev/null @@ -1,41 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 1; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt deleted file mode 100644 index 44aa798b..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.BlockBodiedMethod_HierarchyNotNestedWithAttribute.verified.txt +++ /dev/null @@ -1,81 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Bar_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Bar @this) => 2; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Bar_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Bar @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 2; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 1; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/BlockBodyTests.cs index ebf0fd3c..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; @@ -911,155 +907,4 @@ public static bool IsTerminal(this Entity entity) return Verifier.Verify(result.GeneratedTrees[0].ToString()); } - - [Fact] - public Task BlockBodiedMethod_Hierarchy() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public class Foo { - [Projectable(AllowBlockBody = true)] - public virtual int Id(){ - return 1; - } - } - - public class Bar : Foo { - override public int Id(){ - return 2; - } - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - - [Fact] - public Task BlockBodiedMethod_HierarchyMultiple() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public class Foo { - [Projectable(AllowBlockBody = true)] - public virtual int Id(){ - return 1; - } - } - - public class Bar : Foo { - override public int Id(){ - return 2; - } - } - - public class Baz : Foo { - override public int Id(){ - return 3; - } - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - - [Fact] - public Task BlockBodiedMethod_HierarchyNotNestedWithAttribute() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public class Foo { - [Projectable(AllowBlockBody = true)] - public virtual int Id(){ - return 1; - } - } - - public class Bar : Foo { - [Projectable(AllowBlockBody = true)] - override public int Id(){ - return 2; - } - } - - public class Baz : Bar { - override public int Id(){ - return 3; - } - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(4, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - - [Fact] - public Task BlockBodiedMethod_HierarchyNestedWithoutAttribute() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public class Foo { - [Projectable(AllowBlockBody = true)] - public virtual int Id(){ - return 1; - } - } - - public class Bar : Foo { - override public int Id(){ - return 2; - } - } - - public class Baz : Bar { - override public int Id(){ - return 3; - } - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt deleted file mode 100644 index 019208a1..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.Hierarchy.verified.txt +++ /dev/null @@ -1,41 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 1; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstract.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstract.verified.txt deleted file mode 100644 index dfca5ebd..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstract.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => ((global::Foo.Bar)@this).Id(); - } - } -} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstractMultiple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstractMultiple.verified.txt deleted file mode 100644 index 651d6ffe..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyAbstractMultiple.verified.txt +++ /dev/null @@ -1,19 +0,0 @@ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : ((global::Foo.Bar)@this).Id(); - } - } -} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyBase.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyBase.verified.txt deleted file mode 100644 index 6ac79c7c..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyBase.verified.txt +++ /dev/null @@ -1,61 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Bar_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Bar @this) => true ? 2 : ((global::Foo.Foo)@this).Id(); - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 1; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt deleted file mode 100644 index 64e49e09..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyMultiple.verified.txt +++ /dev/null @@ -1,41 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 1; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 1; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt deleted file mode 100644 index 7958b53a..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNestedWithoutAttribute.verified.txt +++ /dev/null @@ -1,41 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 1; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt deleted file mode 100644 index 44aa798b..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/MethodTests.HierarchyNotNestedWithAttribute.verified.txt +++ /dev/null @@ -1,81 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Bar_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Bar @this) => 2; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Bar_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Bar @this) => @this is global::Foo.Baz ? ((global::Foo.Baz)@this).Id() : 2; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 1; - } - } -} - -// -#nullable disable -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 1; - } - } -} -] \ 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 36353157..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; @@ -801,222 +797,7 @@ class Bar { } [Fact] - public Task Hierarchy() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public class Foo { - [Projectable] - public virtual int Id() => 1; - } - - public class Bar : Foo { - override public int Id() => 2; - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - - [Fact] - public Task HierarchyMultiple() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public class Foo { - [Projectable] - public virtual int Id() => 1; - } - - public class Bar : Foo { - override public int Id() => 2; - } - - public class Baz : Foo { - override public int Id() => 3; - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - - [Fact] - public Task HierarchyNotNestedWithAttribute() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public class Foo { - [Projectable] - public virtual int Id() => 1; - } - - public class Bar : Foo { - [Projectable] - override public int Id() => 2; - } - - public class Baz : Bar { - override public int Id() => 3; - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(4, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - - [Fact] - public Task HierarchyNestedWithoutAttribute() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public class Foo { - [Projectable] - public virtual int Id() => 1; - } - - public class Bar : Foo { - override public int Id() => 2; - } - - public class Baz : Bar { - override public int Id() => 3; - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - - [Fact] - public Task HierarchyAbstract() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public abstract class Foo { - [Projectable] - public abstract int Id(); - } - - public class Bar : Foo { - override public int Id() => 2; - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); - - return Verifier.Verify(result.GeneratedTrees[0].ToString()); - } - - [Fact] - public Task HierarchyAbstractMultiple() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public abstract class Foo { - [Projectable] - public abstract int Id(); - } - - public class Bar : Foo { - override public int Id() => 2; - } - - public class Baz : Bar { - override public int Id() => 3; - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); - - return Verifier.Verify(result.GeneratedTrees[0].ToString()); - } - - [Fact] - public void HierarchyAbstractWithNoDerived() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public abstract class Foo { - [Projectable] - public abstract int Id(); - } -} -"); - - var result = RunGenerator(compilation); - - var diag = Assert.Single(result.Diagnostics); - Assert.Equal("EFP0006", diag.Id); - Assert.Equal(DiagnosticSeverity.Error, diag.Severity); - } - - [Fact] - public void HierarchyAbstractWithNoDerivedOverwritten() + public void AbstractWithPolymorphicDispatch() { var compilation = CreateCompilation(@" using System; @@ -1026,48 +807,15 @@ public void HierarchyAbstractWithNoDerivedOverwritten() namespace Foo { public abstract class Foo { - [Projectable] + [Projectable(PolymorphicDispatch = true)] public abstract int Id(); } - - public abstract class Bar : Foo { } -} -"); - - var result = RunGenerator(compilation); - - var diag = Assert.Single(result.Diagnostics); - Assert.Equal("EFP0006", diag.Id); - Assert.Equal(DiagnosticSeverity.Error, diag.Severity); - } - - [Fact] - public Task HierarchyBase() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq; -using System.Collections.Generic; -using EntityFrameworkCore.Projectables; - -namespace Foo { - public class Foo { - [Projectable] - public virtual int Id() => 1; - } - - public class Bar : Foo { - [Projectable] - override public int Id() => true ? 2 : base.Id(); - } } "); var result = RunGenerator(compilation); Assert.Empty(result.Diagnostics); - Assert.Equal(3, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); + 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.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt deleted file mode 100644 index 9725ed0c..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_Hierarchy.verified.txt +++ /dev/null @@ -1,39 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq.Expressions; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 2; - } - } -} - -// -#nullable disable -using System; -using System.Linq.Expressions; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 2; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt deleted file mode 100644 index ca113d15..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesMethodBody_Hierarchy.verified.txt +++ /dev/null @@ -1,37 +0,0 @@ -[ -// -#nullable disable -using System; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 2; - } - } -} - -// -#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_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id() : 2; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt deleted file mode 100644 index 0a41464f..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_Hierarchy.verified.txt +++ /dev/null @@ -1,39 +0,0 @@ -[ -// -#nullable disable -using System; -using System.Linq.Expressions; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 2; - } - } -} - -// -#nullable disable -using System; -using System.Linq.Expressions; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id : 2; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt deleted file mode 100644 index e77af574..00000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesPropertyBody_Hierarchy.verified.txt +++ /dev/null @@ -1,37 +0,0 @@ -[ -// -#nullable disable -using System; -using EntityFrameworkCore.Projectables; -using Foo; - -namespace EntityFrameworkCore.Projectables.Generated -{ - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_Foo_Id_Base - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => 2; - } - } -} - -// -#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_Foo_Id - { - static global::System.Linq.Expressions.Expression> Expression() - { - return (global::Foo.Foo @this) => @this is global::Foo.Bar ? ((global::Foo.Bar)@this).Id : 2; - } - } -} -] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs index 01e7ce2d..1885e655 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.cs @@ -42,33 +42,6 @@ class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } - [Fact] - public Task Method_UsesMethodBody_Hierarchy() - { - var compilation = CreateCompilation(@" -using System; -using EntityFrameworkCore.Projectables; -namespace Foo { - public class Foo { - [Projectable(UseMemberBody = nameof(IdImpl))] - public virtual int Id() => 1; - - private int IdImpl() => 2; - } - - public class Bar : Foo { - override public int Id() => 3; - } -} -"); - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - [Fact] public Task Method_UsesExpressionPropertyBody_StaticExtension() { @@ -238,34 +211,6 @@ private static Expression> IsPositiveExpr { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } - [Fact] - public Task Method_UsesExpressionPropertyBody_Hierarchy() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq.Expressions; -using EntityFrameworkCore.Projectables; -namespace Foo { - public class Foo { - [Projectable(UseMemberBody = nameof(IdImpl))] - public virtual int Id() => 1; - - private static Expression> IdImpl => @this => 2; - } - - public class Bar : Foo { - override public int Id() => 3; - } -} -"); - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - [Fact] public Task Property_UsesPropertyBody_SameType() { @@ -291,33 +236,6 @@ class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } - [Fact] - public Task Property_UsesPropertyBody_Hierarchy() - { - var compilation = CreateCompilation(@" -using System; -using EntityFrameworkCore.Projectables; -namespace Foo { - public class Foo { - [Projectable(UseMemberBody = nameof(IdImpl))] - public virtual int Id => 1; - - private int IdImpl => 2; - } - - public class Bar : Foo { - override public int Id => 3; - } -} -"); - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - [Fact] public Task StaticMethod_UsesStaticMethodBody() { @@ -367,34 +285,6 @@ class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } - [Fact] - public Task Property_UsesExpressionPropertyBody_Hierarchy() - { - var compilation = CreateCompilation(@" -using System; -using System.Linq.Expressions; -using EntityFrameworkCore.Projectables; -namespace Foo { - public class Foo { - [Projectable(UseMemberBody = nameof(IdImpl))] - public virtual int Id => 1; - - private static Expression> IdImpl => @this => 2; - } - - public class Bar : Foo { - override public int Id => 3; - } -} -"); - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Equal(2, result.GeneratedTrees.Length); - - return Verifier.Verify(result.GeneratedTrees.OrderBy(t => t.FilePath).Select(t => t.ToString())); - } - [Fact] public void Property_UsesExpressionPropertyBody_IncompatibleReturnType_EmitsEFP0011() { diff --git a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs index 1eda5954..b2081ab3 100644 --- a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs @@ -210,78 +210,305 @@ private sealed class FakeClosureWithIQueryableProperty public IQueryable? Items { get; set; } } - public class ProjectableExpressionResolverStubBase : IProjectionExpressionResolver, IProjectionExpressionBaseResolver + public class ProjectableExpressionResolverStubBase : IProjectionExpressionResolver { readonly Func _implementation; - readonly Func _implementationBase; - public ProjectableExpressionResolverStubBase(Func implementation, - Func implementationBase) + public ProjectableExpressionResolverStubBase(Func implementation) { _implementation = implementation; - _implementationBase = implementationBase; } public LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo, ProjectableAttribute? projectableAttribute = null) => _implementation(projectableMemberInfo, projectableAttribute); - public LambdaExpression FindGeneratedBaseExpression(MemberInfo projectableMemberInfo, - ProjectableAttribute? projectableAttribute = null) => _implementationBase(projectableMemberInfo, projectableAttribute); } class Foo { - [Projectable] + [Projectable(PolymorphicDispatch = true)] public virtual int VirtualProperty => 1; - [Projectable] + [Projectable(PolymorphicDispatch = true)] public virtual int VirtualMethod() => 1; } class Bar : Foo { [Projectable] - override public int VirtualProperty => true ? 2 : base.VirtualProperty; + override public int VirtualProperty => 2; [Projectable] - override public int VirtualMethod() => true ? 2 : base.VirtualProperty; + override public int VirtualMethod() => 2; } [Fact] - public void VisitMember_HierarchyBaseProperty() + public void VisitMember_PolymorphicProperty() { Expression> input = x => x.VirtualProperty; - Expression> expectedFooBase = x => 1; - Expression> expectedBar = x => true ? 2 : ((Foo)x).VirtualProperty; - Expression> expectedFoo = x => x is Bar ? true ? 2 : 1 : 1; + 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, - (x, a) => expectedFooBase + (x, a) => x.DeclaringType == typeof(Foo) ? expectedFoo : expectedBar ); var subject = new ProjectableExpressionReplacer(resolver); var actual = subject.Replace(input); - Assert.Equal(expectedFoo.ToString(), actual.ToString()); + Assert.Equal(expected.ToString(), actual.ToString()); } [Fact] - public void VisitMember_HierarchyBaseMethod() + public void VisitMember_PolymorphicMethod() { Expression> input = x => x.VirtualMethod(); - Expression> expectedFooBase = x => 1; - Expression> expectedBar = x => true ? 2 : ((Foo)x).VirtualMethod(); - Expression> expectedFoo = x => x is Bar ? true ? 2 : 1 : 1; + 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(Foo) ? expectedFoo : expectedBar, - (x, a) => expectedFooBase + (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(expectedFoo.ToString(), actual.ToString()); + 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()); } } } From c99937292fa46866f28e2db6a06738707d3b96f9 Mon Sep 17 00:00:00 2001 From: Xriuk Date: Tue, 23 Jun 2026 09:28:41 +0200 Subject: [PATCH 5/5] Update polymorphic-dispatch.md --- docs/advanced/polymorphic-dispatch.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/polymorphic-dispatch.md b/docs/advanced/polymorphic-dispatch.md index 5bd5a056..9a4a9f93 100644 --- a/docs/advanced/polymorphic-dispatch.md +++ b/docs/advanced/polymorphic-dispatch.md @@ -16,7 +16,7 @@ public class Bar : Foo{ } -var bar = new Bar(); +Foo bar = new Bar(); bar.Name(); // "Bar" ``` @@ -84,4 +84,4 @@ public class Bar : Foo{ ## Enabling Polymorphic Dispatch -Add `PolymorphicDispatch = true` to the Projectables \ No newline at end of file +Add `PolymorphicDispatch = true` to the Projectables