diff --git a/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs b/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs
index 7926bc4..ebe65ba 100644
--- a/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs
+++ b/src/EntityFrameworkCore.Projectables.CodeFixes/MissingParameterlessConstructorCodeFixProvider.cs
@@ -14,6 +14,9 @@ namespace EntityFrameworkCore.Projectables.CodeFixes;
/// Inserts a public ClassName() { } constructor into the class that carries the
/// [Projectable] constructor, satisfying the object-initializer requirement of the
/// generated expression tree.
+/// When all containing type declarations are partial (inline generation mode) a second
+/// action offering a private ClassName() { } constructor is also registered, since
+/// the inline accessor is generated inside the class and can access private constructors.
///
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingParameterlessConstructorCodeFixProvider))]
[Shared]
@@ -44,17 +47,56 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
var typeName = typeDecl.Identifier.Text;
+ // Always offer the public constructor fix.
context.RegisterCodeFix(
CodeAction.Create(
title: $"Add parameterless constructor to '{typeName}'",
- createChangedDocument: ct => AddParameterlessConstructorAsync(context.Document, typeDecl, ct),
+ createChangedDocument: ct => AddParameterlessConstructorAsync(
+ context.Document, typeDecl, SyntaxKind.PublicKeyword, ct),
equivalenceKey: "EFP0008_AddParameterlessConstructor"),
diagnostic);
+
+ // When the full containing-type hierarchy is partial the accessor is generated inline
+ // inside the class; private constructors are then accessible from the accessor.
+ // Offer an additional private constructor fix in that case.
+ var isFullHierarchyPartial = IsFullHierarchyPartial(typeDecl);
+ if (isFullHierarchyPartial)
+ {
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: $"Add private parameterless constructor to '{typeName}'",
+ createChangedDocument: ct => AddParameterlessConstructorAsync(
+ context.Document, typeDecl, SyntaxKind.PrivateKeyword, ct),
+ equivalenceKey: "EFP0008_AddPrivateParameterlessConstructor"),
+ diagnostic);
+ }
+ }
+
+ ///
+ /// Returns true when and every ancestor
+ /// all carry the partial modifier,
+ /// meaning the Roslyn generator will use inline generation for this type.
+ ///
+ private static bool IsFullHierarchyPartial(TypeDeclarationSyntax typeDecl)
+ {
+ SyntaxNode? current = typeDecl;
+ while (current is TypeDeclarationSyntax tds)
+ {
+ if (!tds.Modifiers.Any(SyntaxKind.PartialKeyword))
+ {
+ return false;
+ }
+
+ current = tds.Parent;
+ }
+
+ return true;
}
private async static Task AddParameterlessConstructorAsync(
Document document,
TypeDeclarationSyntax typeDecl,
+ SyntaxKind accessibilityKeyword,
CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
@@ -66,7 +108,7 @@ private async static Task AddParameterlessConstructorAsync(
var parameterlessCtor = SyntaxFactory
.ConstructorDeclaration(typeDecl.Identifier.WithoutTrivia())
.WithModifiers(SyntaxFactory.TokenList(
- SyntaxFactory.Token(SyntaxKind.PublicKeyword)
+ SyntaxFactory.Token(accessibilityKeyword)
.WithTrailingTrivia(SyntaxFactory.Space)))
.WithParameterList(SyntaxFactory.ParameterList())
.WithBody(SyntaxFactory.Block())
@@ -81,4 +123,3 @@ private async static Task AddParameterlessConstructorAsync(
return document.WithSyntaxRoot(newRoot);
}
}
-
diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
index 5f28270..c0d587b 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
+++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md
@@ -1 +1,6 @@
-
\ No newline at end of file
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|---------------------------------------------------------------------------------------------------------------
+EFP0013 | Design | Info | Containing class should be partial to allow [Projectable] inline accessor with private/protected member access
+
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs
index 9fab2f2..9c1f8b8 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Infrastructure/Diagnostics.cs
@@ -99,4 +99,12 @@ static internal class Diagnostics
category: "Design",
DiagnosticSeverity.Info,
isEnabledByDefault: true);
-}
\ No newline at end of file
+
+ public readonly static DiagnosticDescriptor ContainingClassShouldBePartial = new DiagnosticDescriptor(
+ id: "EFP0013",
+ title: "Containing class should be partial",
+ messageFormat: "Class '{0}' should be declared as 'partial' to allow [Projectable] to generate the expression accessor inside the class, enabling access to private and protected members",
+ category: "Design",
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true);
+}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs
index c0367fc..43319e2 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.BodyProcessors.cs
@@ -388,12 +388,24 @@ private static bool TryApplyConstructorBody(
// Verify the containing type has an accessible parameterless (instance) constructor.
// The generated projection is: new T() { Prop = ... }, which requires one.
+ //
+ // When every containing type declaration is partial the accessor is generated inline
+ // inside the class itself, where private and protected constructors are accessible.
+ var containingTypeDecls = constructorDeclarationSyntax.Ancestors()
+ .OfType()
+ .ToList();
+ var isInlineContext = containingTypeDecls.Count > 0
+ && containingTypeDecls.All(t => t.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));
+
var hasAccessibleParameterlessConstructor = containingType.Constructors
.Any(c => !c.IsStatic
&& c.Parameters.IsEmpty
- && c.DeclaredAccessibility is Accessibility.Public
- or Accessibility.Internal
- or Accessibility.ProtectedOrInternal);
+ && (c.DeclaredAccessibility is Accessibility.Public
+ or Accessibility.Internal
+ or Accessibility.ProtectedOrInternal
+ || (isInlineContext
+ && c.DeclaredAccessibility is Accessibility.Private
+ or Accessibility.Protected)));
if (!hasAccessibleParameterlessConstructor)
{
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
index 38821b1..5a9a318 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
@@ -36,6 +36,16 @@ public class ProjectionExpressionGenerator : IIncrementalGenerator
)
);
+ private readonly static AttributeSyntax _obsoleteAttribute =
+ Attribute(
+ ParseName("global::System.Obsolete"),
+ AttributeArgumentList(
+ SingletonSeparatedList(
+ AttributeArgument(
+ LiteralExpression(
+ SyntaxKind.StringLiteralExpression,
+ Literal("Generated member. Do not use."))))));
+
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Extract only pure stable data from the attribute in the transform.
@@ -185,7 +195,7 @@ private static void Execute(
{
throw new InvalidOperationException("Expected a memberName here");
}
-
+
// Report EFP0012 when a [Projectable] method is a factory that could be a constructor.
if (member is MethodDeclarationSyntax factoryCandidate && SyntaxHelpers.TryGetFactoryMethodPattern(factoryCandidate, out _))
{
@@ -198,6 +208,135 @@ 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";
+ // Determine whether inline generation is possible:
+ // all containing type declarations in the syntax hierarchy must carry the `partial` modifier,
+ // and the member must not be a C# 14 extension member (those live in a synthetic type).
+ var isExtensionMember = memberSymbol.ContainingType is { IsExtension: true };
+ var containingTypeDecls = member.Ancestors().OfType().ToList();
+ var generateInline = !isExtensionMember
+ && containingTypeDecls.Count > 0
+ && containingTypeDecls.All(t => t.Modifiers.Any(SyntaxKind.PartialKeyword));
+
+ // EFP0013: suggest making the class partial to enable inline generation.
+ if (!isExtensionMember && containingTypeDecls.Count > 0 && !generateInline)
+ {
+ var firstNonPartial = containingTypeDecls.First(t => !t.Modifiers.Any(SyntaxKind.PartialKeyword));
+ context.ReportDiagnostic(Diagnostic.Create(
+ Infrastructure.Diagnostics.ContainingClassShouldBePartial,
+ firstNonPartial.Identifier.GetLocation(),
+ firstNonPartial.Identifier.Text));
+ }
+
+ if (generateInline)
+ {
+ EmitInlinePartialClass(member, projectable, generatedFileName, containingTypeDecls, compilation, context);
+ }
+ else
+ {
+ EmitExternalClass(member, projectable, generatedClassName, generatedFileName, compilation, context);
+ }
+ }
+
+ ///
+ /// Generates the expression accessor as a private static method inside the declaring
+ /// partial class. The method is hidden from the IDE via [EditorBrowsable(Never)] and
+ /// [Obsolete], and its name starts with __Projectable__ to signal it is generated.
+ /// Generating inside the class allows the lambda to capture private / protected
+ /// members that would be inaccessible from an external generated class.
+ ///
+ private static void EmitInlinePartialClass(
+ MemberDeclarationSyntax member,
+ ProjectableDescriptor projectable,
+ string generatedFileName,
+ List containingTypeDecls,
+ Compilation? compilation,
+ SourceProductionContext context)
+ {
+ var inlineMethodName = ProjectionExpressionClassNameGenerator.GenerateInlineMethodName(
+ projectable.MemberName!, projectable.ParameterTypeNames);
+
+ var methodDecl = MethodDeclaration(
+ GenericName(
+ Identifier("global::System.Linq.Expressions.Expression"),
+ TypeArgumentList(
+ SingletonSeparatedList(
+ (TypeSyntax)GenericName(
+ Identifier("global::System.Func"),
+ GetLambdaTypeArgumentListSyntax(projectable)
+ )
+ )
+ )
+ ),
+ inlineMethodName
+ )
+ .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.StaticKeyword)))
+ .WithTypeParameterList(projectable.TypeParameterList)
+ .WithConstraintClauses(projectable.ConstraintClauses ?? List())
+ .AddAttributeLists(
+ AttributeList().AddAttributes(_editorBrowsableAttribute),
+ AttributeList().AddAttributes(_obsoleteAttribute)
+ )
+ .WithBody(
+ Block(
+ ReturnStatement(
+ ParenthesizedLambdaExpression(
+ projectable.ParametersList ?? ParameterList(),
+ null,
+ projectable.ExpressionBody
+ )
+ )
+ )
+ );
+
+ // Wrap the method in the partial class hierarchy (innermost containing type first).
+ MemberDeclarationSyntax current = methodDecl;
+ foreach (var typeDecl in containingTypeDecls)
+ {
+ current = CreatePartialTypeStub(typeDecl).AddMembers(current);
+ }
+
+ var compilationUnit = CompilationUnit();
+
+ foreach (var usingDirective in projectable.UsingDirectives!)
+ {
+ compilationUnit = compilationUnit.AddUsings(usingDirective);
+ }
+
+ if (projectable.ClassNamespace is not null)
+ {
+ compilationUnit = compilationUnit.AddMembers(
+ NamespaceDeclaration(ParseName(projectable.ClassNamespace))
+ .AddMembers((TypeDeclarationSyntax)current));
+ }
+ else
+ {
+ compilationUnit = compilationUnit.AddMembers((TypeDeclarationSyntax)current);
+ }
+
+ compilationUnit = compilationUnit
+ .WithLeadingTrivia(
+ TriviaList(
+ Comment("// "),
+ Trivia(NullableDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))
+ )
+ );
+
+ context.AddSource(generatedFileName, SourceText.From(compilationUnit.NormalizeWhitespace().ToFullString(), Encoding.UTF8));
+ }
+
+ ///
+ /// Generates the expression accessor as an external static class in the
+ /// EntityFrameworkCore.Projectables.Generated namespace. This is the classic
+ /// (non-inline) code path used when the containing class is not fully partial.
+ ///
+ private static void EmitExternalClass(
+ MemberDeclarationSyntax member,
+ ProjectableDescriptor projectable,
+ string generatedClassName,
+ string generatedFileName,
+ Compilation? compilation,
+ SourceProductionContext context)
+ {
var classSyntax = ClassDeclaration(generatedClassName)
.WithModifiers(TokenList(Token(SyntaxKind.StaticKeyword)))
.WithTypeParameterList(projectable.ClassTypeParameterList)
@@ -263,35 +402,35 @@ private static void Execute(
.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));
+ }
- static TypeArgumentListSyntax GetLambdaTypeArgumentListSyntax(ProjectableDescriptor projectable)
+ ///
+ /// Creates a minimal partial stub of containing only
+ /// the partial modifier, the type name, and the type-parameter list. All attribute
+ /// lists, base types, constraints, and members are stripped so the stub can be used as a
+ /// container wrapper in generated partial-class source files.
+ ///
+ private static TypeDeclarationSyntax CreatePartialTypeStub(TypeDeclarationSyntax originalDecl)
+ {
+ var stub = originalDecl
+ .WithAttributeLists(List())
+ .WithModifiers(TokenList(Token(SyntaxKind.PartialKeyword)))
+ .WithBaseList(null)
+ .WithConstraintClauses(List())
+ .WithMembers(List());
+
+ // Remove the primary constructor parameter list for record declarations.
+ if (stub is RecordDeclarationSyntax record)
{
- var lambdaTypeArguments = TypeArgumentList(
- SeparatedList(
- // In Roslyn's syntax model, ParameterSyntax.Type is nullable: it is null for
- // implicitly-typed lambda parameters (e.g. `(x, y) => x + y`).
- // We filter those out to avoid passing null nodes into TypeArgumentList,
- // which would cause a NullReferenceException at generation time.
- // In practice all [Projectable] members have explicitly-typed parameters,
- // so this filter acts as a defensive guard rather than a functional branch.
- projectable.ParametersList?.Parameters.Where(p => p.Type is not null).Select(p => p.Type!)
- )
- );
-
- if (projectable.ReturnTypeName is not null)
- {
- lambdaTypeArguments = lambdaTypeArguments.AddArguments(ParseTypeName(projectable.ReturnTypeName));
- }
-
- return lambdaTypeArguments;
+ return record.WithParameterList(null);
}
+
+ return stub;
}
///
@@ -349,7 +488,27 @@ static TypeArgumentListSyntax GetLambdaTypeArgumentListSyntax(ProjectableDescrip
memberLookupName = memberSymbol.Name;
}
- // Build the generated class name using the same logic as Execute
+ var declaringTypeFullName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ // Determine whether inline generation was used (all containing types are partial).
+ var isInline = IsContainingTypeHierarchyPartial(containingType);
+
+ if (isInline)
+ {
+ var inlineMethodName = ProjectionExpressionClassNameGenerator.GenerateInlineMethodName(
+ memberLookupName,
+ parameterTypeNames.IsEmpty ? null : (IEnumerable)parameterTypeNames);
+
+ return new ProjectionRegistryEntry(
+ DeclaringTypeFullName: declaringTypeFullName,
+ MemberKind: memberKind,
+ MemberLookupName: memberLookupName,
+ GeneratedClassFullName: string.Empty,
+ ParameterTypeNames: parameterTypeNames,
+ InlineMethodName: inlineMethodName);
+ }
+
+ // Build the generated class name using the same logic as EmitExternalClass.
var classNamespace = containingType.ContainingNamespace.IsGlobalNamespace
? null
: containingType.ContainingNamespace.ToDisplayString();
@@ -364,8 +523,6 @@ static TypeArgumentListSyntax GetLambdaTypeArgumentListSyntax(ProjectableDescrip
var generatedClassFullName = "EntityFrameworkCore.Projectables.Generated." + generatedClassName;
- var declaringTypeFullName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
-
return new ProjectionRegistryEntry(
DeclaringTypeFullName: declaringTypeFullName,
MemberKind: memberKind,
@@ -374,6 +531,30 @@ static TypeArgumentListSyntax GetLambdaTypeArgumentListSyntax(ProjectableDescrip
ParameterTypeNames: parameterTypeNames);
}
+ ///
+ /// Returns true when every type in the containing-type hierarchy (from
+ /// up to the outermost type) has at least one partial
+ /// declaration. Used to decide whether to generate an inline accessor or an external class.
+ ///
+ private static bool IsContainingTypeHierarchyPartial(INamedTypeSymbol typeSymbol)
+ {
+ var isPartial = typeSymbol.DeclaringSyntaxReferences
+ .Any(r => r.GetSyntax() is TypeDeclarationSyntax tds
+ && tds.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));
+
+ if (!isPartial)
+ {
+ return false;
+ }
+
+ if (typeSymbol.ContainingType is { } outer)
+ {
+ return IsContainingTypeHierarchyPartial(outer);
+ }
+
+ return true;
+ }
+
private static IEnumerable GetRegistryNestedTypePath(INamedTypeSymbol typeSymbol)
{
if (typeSymbol.ContainingType is not null)
@@ -385,4 +566,26 @@ private static IEnumerable GetRegistryNestedTypePath(INamedTypeSymbol ty
}
yield return typeSymbol.Name;
}
+
+ private static TypeArgumentListSyntax GetLambdaTypeArgumentListSyntax(ProjectableDescriptor projectable)
+ {
+ var lambdaTypeArguments = TypeArgumentList(
+ SeparatedList(
+ // In Roslyn's syntax model, ParameterSyntax.Type is nullable: it is null for
+ // implicitly-typed lambda parameters (e.g. `(x, y) => x + y`).
+ // We filter those out to avoid passing null nodes into TypeArgumentList,
+ // which would cause a NullReferenceException at generation time.
+ // In practice all [Projectable] members have explicitly-typed parameters,
+ // so this filter acts as a defensive guard rather than a functional branch.
+ projectable.ParametersList?.Parameters.Where(p => p.Type is not null).Select(p => p.Type!)
+ )
+ );
+
+ if (projectable.ReturnTypeName is not null)
+ {
+ lambdaTypeArguments = lambdaTypeArguments.AddArguments(ParseTypeName(projectable.ReturnTypeName));
+ }
+
+ return lambdaTypeArguments;
+ }
}
\ No newline at end of file
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Registry/ProjectionRegistryEmitter.cs b/src/EntityFrameworkCore.Projectables.Generator/Registry/ProjectionRegistryEmitter.cs
index e64bffd..cdf3be6 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Registry/ProjectionRegistryEmitter.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Registry/ProjectionRegistryEmitter.cs
@@ -64,6 +64,8 @@ public static void Emit(ImmutableArray entries, Source
EmitTryGetMethod(writer);
writer.WriteLine();
EmitRegisterHelper(writer);
+ writer.WriteLine();
+ EmitRegisterInlineHelper(writer);
writer.Indent--;
writer.WriteLine("}");
@@ -103,6 +105,8 @@ private static void EmitBuildMethod(IndentedTextWriter writer, List
/// Emits a single Register(map, typeof(T).GetXxx(...), "ClassName") call
/// for one projectable entry inside Build().
+ /// When is set the accessor lives
+ /// directly on the declaring type and is looked up via RegisterInline instead.
///
private static void WriteRegistryEntryStatement(IndentedTextWriter writer, ProjectionRegistryEntry entry)
{
@@ -124,7 +128,17 @@ private static void WriteRegistryEntryStatement(IndentedTextWriter writer, Proje
_ => null
};
- if (memberCallExpr is not null)
+ if (memberCallExpr is null)
+ {
+ return;
+ }
+
+ if (entry.InlineMethodName is not null)
+ {
+ // Inline accessor: the Expression method lives on the declaring type itself.
+ writer.WriteLine($"RegisterInline(map, {memberCallExpr}, \"{entry.InlineMethodName}\");");
+ }
+ else
{
writer.WriteLine($"Register(map, {memberCallExpr}, \"{entry.GeneratedClassFullName}\");");
}
@@ -188,6 +202,26 @@ private static void EmitRegisterHelper(IndentedTextWriter writer)
writer.WriteLine("}");
}
+ ///
+ /// Emits the private RegisterInline static helper used when the expression accessor
+ /// is generated inside the declaring partial class (inline mode). Instead of looking up a
+ /// separate generated type, it resolves the private method directly on the declaring type.
+ ///
+ private static void EmitRegisterInlineHelper(IndentedTextWriter writer)
+ {
+ writer.WriteLine("private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)");
+ writer.WriteLine("{");
+ writer.Indent++;
+ writer.WriteLine("if (m is null) return;");
+ writer.WriteLine("var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);");
+ writer.WriteLine("if (exprMethod is not null)");
+ writer.Indent++;
+ writer.WriteLine("map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;");
+ writer.Indent--;
+ writer.Indent--;
+ writer.WriteLine("}");
+ }
+
///
/// Returns the C# expression for a Type[] used in reflection method/constructor lookups.
/// Returns global::System.Type.EmptyTypes when is empty.
diff --git a/src/EntityFrameworkCore.Projectables.Generator/Registry/ProjectionRegistryEntry.cs b/src/EntityFrameworkCore.Projectables.Generator/Registry/ProjectionRegistryEntry.cs
index 4e20b9d..a4694d5 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/Registry/ProjectionRegistryEntry.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/Registry/ProjectionRegistryEntry.cs
@@ -12,7 +12,8 @@ sealed internal record ProjectionRegistryEntry(
ProjectionRegistryMemberType MemberKind,
string MemberLookupName,
string GeneratedClassFullName,
- EquatableImmutableArray ParameterTypeNames
+ EquatableImmutableArray ParameterTypeNames,
+ string? InlineMethodName = null
);
///
diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs
index ee30ce6..2494754 100644
--- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs
+++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs
@@ -131,5 +131,42 @@ private static void AppendSanitizedTypeName(StringBuilder sb, string typeName)
private static bool IsInvalidIdentifierChar(char c) =>
c == '.' || c == '<' || c == '>' || c == ',' || c == ' ' ||
c == '[' || c == ']' || c == '`' || c == ':' || c == '?';
+
+ ///
+ /// Returns the name of the private hidden accessor method generated inside the declaring
+ /// partial class when inline generation is used. The name is unique within the class and
+ /// encodes the member name plus parameter types for overload disambiguation.
+ ///
+ /// Example: Score → __Projectable__Score,
+ /// Add(int, long) → __Projectable__Add_P0_int_P1_long.
+ ///
+ ///
+ public static string GenerateInlineMethodName(string memberName, IEnumerable? parameterTypeNames)
+ {
+ var sb = new StringBuilder("__Projectable__");
+
+ if (memberName.IndexOf('.') >= 0)
+ {
+ sb.Append(memberName.Replace(".", "__"));
+ }
+ else
+ {
+ sb.Append(memberName);
+ }
+
+ if (parameterTypeNames is not null)
+ {
+ var idx = 0;
+ foreach (var typeName in parameterTypeNames)
+ {
+ sb.Append("_P");
+ sb.Append(idx++);
+ sb.Append('_');
+ AppendSanitizedTypeName(sb, typeName);
+ }
+ }
+
+ return sb.ToString();
+ }
}
}
diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
index d00daad..839194e 100644
--- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
+++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
@@ -284,6 +284,30 @@ private static Func BuildReflectionFactory(MemberInfo projecta
}
}
+ // ── Inline path ─────────────────────────────────────────────────────────────
+ // When the containing class was declared partial the generator emits the accessor
+ // as a private static method directly on the declaring type instead of an external
+ // generated class. Try this path first — it is O(1) hash lookup on the closed type.
+ var inlineMethodName = ProjectionExpressionClassNameGenerator.GenerateInlineMethodName(
+ memberLookupName, parameterTypeNames);
+
+ var inlineExpressionMethod = originalDeclaringType.GetMethod(
+ inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+
+ if (inlineExpressionMethod is not null)
+ {
+ if (projectableMemberInfo is MethodInfo mi2
+ && mi2.GetGenericArguments() is { Length: > 0 } methodGenericArgs2)
+ {
+ inlineExpressionMethod = inlineExpressionMethod.MakeGenericMethod(methodGenericArgs2);
+ }
+
+ var callInline = Expression.Call(inlineExpressionMethod);
+ var castInline = Expression.Convert(callInline, typeof(LambdaExpression));
+ return Expression.Lambda>(castInline).Compile();
+ }
+
+ // ── External-class path (original slow path) ────────────────────────────────
// GetNestedTypePath() returns a Type[] — project to string[] with a direct loop, no LINQ Select.
var generatedContainingTypeName = ProjectionExpressionClassNameGenerator.GenerateFullName(
declaringType.Namespace,
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddPrivateParamLessConstructor_ToPartialClass.verified.txt b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddPrivateParamLessConstructor_ToPartialClass.verified.txt
new file mode 100644
index 0000000..90437bb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.AddPrivateParamLessConstructor_ToPartialClass.verified.txt
@@ -0,0 +1,16 @@
+
+namespace Foo {
+ partial class PersonDto {
+ private PersonDto()
+ {
+ }
+
+ public string Name { get; set; }
+
+ [Projectable]
+ public PersonDto(string name)
+ {
+ Name = name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.cs b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.cs
index d90bcbe..b62da44 100644
--- a/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.CodeFixes.Tests/MissingParamLessConstructorCodeFixProviderTests.cs
@@ -102,6 +102,91 @@ public Empty(int value) { }
"EFP0008",
FirstConstructorIdentifierSpan,
_provider));
+
+ // ── Partial class — private constructor fix ───────────────────────────────
+
+ [Fact]
+ public async Task PartialClass_OffersBothPublicAndPrivateConstructorFix()
+ {
+ var actions = await GetCodeFixActionsAsync(
+ @"
+namespace Foo {
+ partial class MyClass {
+ [Projectable]
+ public MyClass(int value) { }
+ }
+}",
+ "EFP0008",
+ FirstConstructorIdentifierSpan,
+ _provider);
+
+ Assert.Equal(2, actions.Count);
+ Assert.Contains(actions, a => a.Title.Contains("private", StringComparison.OrdinalIgnoreCase));
+ Assert.Contains(actions, a => !a.Title.Contains("private", StringComparison.OrdinalIgnoreCase));
+ }
+
+ [Fact]
+ public async Task NonPartialClass_OffersOnlyPublicConstructorFix()
+ {
+ var actions = await GetCodeFixActionsAsync(
+ @"
+namespace Foo {
+ class MyClass {
+ [Projectable]
+ public MyClass(int value) { }
+ }
+}",
+ "EFP0008",
+ FirstConstructorIdentifierSpan,
+ _provider);
+
+ var action = Assert.Single(actions);
+ Assert.DoesNotContain("private", action.Title, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public Task AddPrivateParamLessConstructor_ToPartialClass() =>
+ Verifier.Verify(
+ ApplyCodeFixAsync(
+ @"
+namespace Foo {
+ partial class PersonDto {
+ public string Name { get; set; }
+
+ [Projectable]
+ public PersonDto(string name)
+ {
+ Name = name;
+ }
+ }
+}",
+ "EFP0008",
+ FirstConstructorIdentifierSpan,
+ _provider,
+ actionIndex: 1)); // index 1 = private fix
+
+ [Fact]
+ public async Task PartialClass_NestedInsideNonPartialOuter_OffersOnlyPublicFix()
+ {
+ // Inner is partial but outer is not → inline generation won't be used
+ // → private constructor would still trigger EFP0008 → only offer public fix.
+ var actions = await GetCodeFixActionsAsync(
+ @"
+namespace Foo {
+ class Outer {
+ partial class Inner {
+ [Projectable]
+ public Inner(int value) { }
+ }
+ }
+}",
+ "EFP0008",
+ FirstConstructorIdentifierSpan,
+ _provider);
+
+ var action = Assert.Single(actions);
+ Assert.DoesNotContain("private", action.Title, StringComparison.OrdinalIgnoreCase);
+ }
}
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivateMethodProjectable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivateMethodProjectable.DotNet10_0.verified.txt
new file mode 100644
index 0000000..e70aeac
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivateMethodProjectable.DotNet10_0.verified.txt
@@ -0,0 +1,3 @@
+SELECT [p].[Id]
+FROM [PrivateMethodEntity] AS [p]
+WHERE [p].[Id] * 2 + 3 > 5
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivateMethodProjectable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivateMethodProjectable.DotNet9_0.verified.txt
new file mode 100644
index 0000000..e70aeac
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivateMethodProjectable.DotNet9_0.verified.txt
@@ -0,0 +1,3 @@
+SELECT [p].[Id]
+FROM [PrivateMethodEntity] AS [p]
+WHERE [p].[Id] * 2 + 3 > 5
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivateMethodProjectable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivateMethodProjectable.verified.txt
new file mode 100644
index 0000000..e70aeac
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivateMethodProjectable.verified.txt
@@ -0,0 +1,3 @@
+SELECT [p].[Id]
+FROM [PrivateMethodEntity] AS [p]
+WHERE [p].[Id] * 2 + 3 > 5
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivatePropertyProjectable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivatePropertyProjectable.DotNet10_0.verified.txt
new file mode 100644
index 0000000..6cbac8e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivatePropertyProjectable.DotNet10_0.verified.txt
@@ -0,0 +1,3 @@
+SELECT [p].[Id]
+FROM [PrivatePropEntity] AS [p]
+WHERE [p].[Id] * 2 + 1 > 5
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivatePropertyProjectable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivatePropertyProjectable.DotNet9_0.verified.txt
new file mode 100644
index 0000000..6cbac8e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivatePropertyProjectable.DotNet9_0.verified.txt
@@ -0,0 +1,3 @@
+SELECT [p].[Id]
+FROM [PrivatePropEntity] AS [p]
+WHERE [p].[Id] * 2 + 1 > 5
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivatePropertyProjectable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivatePropertyProjectable.verified.txt
new file mode 100644
index 0000000..6cbac8e
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnPrivatePropertyProjectable.verified.txt
@@ -0,0 +1,3 @@
+SELECT [p].[Id]
+FROM [PrivatePropEntity] AS [p]
+WHERE [p].[Id] * 2 + 1 > 5
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnProtectedPropertyProjectable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnProtectedPropertyProjectable.DotNet10_0.verified.txt
new file mode 100644
index 0000000..5f21964
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnProtectedPropertyProjectable.DotNet10_0.verified.txt
@@ -0,0 +1,3 @@
+SELECT [p].[Id]
+FROM [ProtectedPropEntity] AS [p]
+WHERE [p].[Id] * 2 + 1 > 5
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnProtectedPropertyProjectable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnProtectedPropertyProjectable.DotNet9_0.verified.txt
new file mode 100644
index 0000000..5f21964
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnProtectedPropertyProjectable.DotNet9_0.verified.txt
@@ -0,0 +1,3 @@
+SELECT [p].[Id]
+FROM [ProtectedPropEntity] AS [p]
+WHERE [p].[Id] * 2 + 1 > 5
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnProtectedPropertyProjectable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnProtectedPropertyProjectable.verified.txt
new file mode 100644
index 0000000..5f21964
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.FilterOnProtectedPropertyProjectable.verified.txt
@@ -0,0 +1,3 @@
+SELECT [p].[Id]
+FROM [ProtectedPropEntity] AS [p]
+WHERE [p].[Id] * 2 + 1 > 5
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectConstructorWithPrivateHelper.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectConstructorWithPrivateHelper.DotNet10_0.verified.txt
new file mode 100644
index 0000000..2776db3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectConstructorWithPrivateHelper.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], [p].[FirstName] + N' ' + [p].[LastName] AS [FullName]
+FROM [PartialClassPersonSource] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectConstructorWithPrivateHelper.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectConstructorWithPrivateHelper.DotNet9_0.verified.txt
new file mode 100644
index 0000000..2776db3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectConstructorWithPrivateHelper.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], [p].[FirstName] + N' ' + [p].[LastName] AS [FullName]
+FROM [PartialClassPersonSource] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectConstructorWithPrivateHelper.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectConstructorWithPrivateHelper.verified.txt
new file mode 100644
index 0000000..2776db3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectConstructorWithPrivateHelper.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id], [p].[FirstName] + N' ' + [p].[LastName] AS [FullName]
+FROM [PartialClassPersonSource] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivateMethodProjectable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivateMethodProjectable.DotNet10_0.verified.txt
new file mode 100644
index 0000000..5c127ac
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivateMethodProjectable.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id] * 2 + 3
+FROM [PrivateMethodEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivateMethodProjectable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivateMethodProjectable.DotNet9_0.verified.txt
new file mode 100644
index 0000000..5c127ac
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivateMethodProjectable.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id] * 2 + 3
+FROM [PrivateMethodEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivateMethodProjectable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivateMethodProjectable.verified.txt
new file mode 100644
index 0000000..5c127ac
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivateMethodProjectable.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id] * 2 + 3
+FROM [PrivateMethodEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivatePropertyProjectable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivatePropertyProjectable.DotNet10_0.verified.txt
new file mode 100644
index 0000000..6c0c3c9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivatePropertyProjectable.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id] * 2 + 1
+FROM [PrivatePropEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivatePropertyProjectable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivatePropertyProjectable.DotNet9_0.verified.txt
new file mode 100644
index 0000000..6c0c3c9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivatePropertyProjectable.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id] * 2 + 1
+FROM [PrivatePropEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivatePropertyProjectable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivatePropertyProjectable.verified.txt
new file mode 100644
index 0000000..6c0c3c9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectPrivatePropertyProjectable.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id] * 2 + 1
+FROM [PrivatePropEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectProtectedPropertyProjectable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectProtectedPropertyProjectable.DotNet10_0.verified.txt
new file mode 100644
index 0000000..98bcf33
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectProtectedPropertyProjectable.DotNet10_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id] * 2 + 1
+FROM [ProtectedPropEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectProtectedPropertyProjectable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectProtectedPropertyProjectable.DotNet9_0.verified.txt
new file mode 100644
index 0000000..98bcf33
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectProtectedPropertyProjectable.DotNet9_0.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id] * 2 + 1
+FROM [ProtectedPropEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectProtectedPropertyProjectable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectProtectedPropertyProjectable.verified.txt
new file mode 100644
index 0000000..98bcf33
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.SelectProtectedPropertyProjectable.verified.txt
@@ -0,0 +1,2 @@
+SELECT [p].[Id] * 2 + 1
+FROM [ProtectedPropEntity] AS [p]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.cs
new file mode 100644
index 0000000..bd0e63d
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassProjectableTests.cs
@@ -0,0 +1,170 @@
+using System.Linq;
+using System.Threading.Tasks;
+using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
+using Microsoft.EntityFrameworkCore;
+using VerifyXunit;
+using Xunit;
+
+// ── Entity types for inline (partial class) projectable tests ─────────────────
+// Defined at namespace level so they can be partial without requiring the
+// test class to be partial. EF Core only maps the source entity; DTOs are
+// pure projection targets used in Select expressions.
+namespace EntityFrameworkCore.Projectables.FunctionalTests
+{
+ /// Entity whose public [Projectable] property depends on a private [Projectable].
+ public partial class PrivatePropEntity
+ {
+ public int Id { get; set; }
+
+ [Projectable]
+ private int Doubled => Id * 2;
+
+ [Projectable]
+ public int Score => Doubled + 1;
+ }
+
+ /// Entity whose public [Projectable] property depends on a protected [Projectable].
+ public partial class ProtectedPropEntity
+ {
+ public int Id { get; set; }
+
+ [Projectable]
+ protected int Doubled => Id * 2;
+
+ [Projectable]
+ public int Score => Doubled + 1;
+ }
+
+ /// Entity whose public [Projectable] method depends on a private [Projectable] method.
+ public partial class PrivateMethodEntity
+ {
+ public int Id { get; set; }
+
+ [Projectable]
+ private int Double(int x) => x * 2;
+
+ [Projectable]
+ public int ComputedScore(int delta) => Double(Id) + delta;
+ }
+
+ // ── Constructor projectable with private static helper ──────────────────
+
+ /// Simple source entity for constructor projection tests.
+ public class PartialClassPersonSource
+ {
+ public int Id { get; set; }
+ public string FirstName { get; set; } = string.Empty;
+ public string LastName { get; set; } = string.Empty;
+ }
+
+ /// DTO built via a partial [Projectable] constructor that calls a private static helper.
+ public partial class PartialPersonDto
+ {
+ public int Id { get; set; }
+ public string FullName { get; set; } = string.Empty;
+
+ public PartialPersonDto() { } // required: EF Core uses the parameterless ctor for materialisation
+
+ [Projectable]
+ public PartialPersonDto(int id, string firstName, string lastName)
+ {
+ Id = id;
+ FullName = FormatName(firstName, lastName);
+ }
+
+ [Projectable]
+ private static string FormatName(string first, string last) => first + " " + last;
+ }
+
+ // ── Tests ─────────────────────────────────────────────────────────────────
+
+ [UsesVerify]
+ public class PartialClassProjectableTests
+ {
+ // ── Private property ──────────────────────────────────────────────────
+
+ [Fact]
+ public Task FilterOnPrivatePropertyProjectable()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Where(x => x.Score > 5);
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task SelectPrivatePropertyProjectable()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.Score);
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ // ── Protected property ────────────────────────────────────────────────
+
+ [Fact]
+ public Task FilterOnProtectedPropertyProjectable()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Where(x => x.Score > 5);
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task SelectProtectedPropertyProjectable()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.Score);
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ // ── Private method ────────────────────────────────────────────────────
+
+ [Fact]
+ public Task FilterOnPrivateMethodProjectable()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Where(x => x.ComputedScore(3) > 5);
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ [Fact]
+ public Task SelectPrivateMethodProjectable()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(x => x.ComputedScore(3));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+
+ // ── Constructor with private static helper ────────────────────────────
+
+ [Fact]
+ public Task SelectConstructorWithPrivateHelper()
+ {
+ using var dbContext = new SampleDbContext();
+
+ var query = dbContext.Set()
+ .Select(p => new PartialPersonDto(p.Id, p.FirstName, p.LastName));
+
+ return Verifier.Verify(query.ToQueryString());
+ }
+ }
+}
+
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.ProjectableConstructor_PartialClass_PrivateParameterlessCtor_Succeeds.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.ProjectableConstructor_PartialClass_PrivateParameterlessCtor_Succeeds.verified.txt
new file mode 100644
index 0000000..f1841f3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.ProjectableConstructor_PartialClass_PrivateParameterlessCtor_Succeeds.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class PersonDto
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable___ctor_P0_string()
+ {
+ return (string name) => new global::Foo.PersonDto()
+ {
+ Name = name
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.ProjectableConstructor_PartialClass_ProtectedParameterlessCtor_Succeeds.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.ProjectableConstructor_PartialClass_ProtectedParameterlessCtor_Succeeds.verified.txt
new file mode 100644
index 0000000..f1841f3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.ProjectableConstructor_PartialClass_ProtectedParameterlessCtor_Succeeds.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class PersonDto
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable___ctor_P0_string()
+ {
+ return (string name) => new global::Foo.PersonDto()
+ {
+ Name = name
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.cs
index cec1dc4..69e48a2 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ConstructorTests.cs
@@ -1341,4 +1341,119 @@ public CustomerDto(Customer customer)
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+
+ // ── Private / protected parameterless constructor in partial classes ──────
+
+ [Fact]
+ public Task ProjectableConstructor_PartialClass_PrivateParameterlessCtor_Succeeds()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ public partial class PersonDto {
+ public string Name { get; set; }
+
+ private PersonDto() { }
+
+ [Projectable]
+ public PersonDto(string name) {
+ Name = name;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task ProjectableConstructor_PartialClass_ProtectedParameterlessCtor_Succeeds()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ public partial class PersonDto {
+ public string Name { get; set; }
+
+ protected PersonDto() { }
+
+ [Projectable]
+ public PersonDto(string name) {
+ Name = name;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public void ProjectableConstructor_NonPartialClass_PrivateParameterlessCtor_EmitsDiagnostic()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class PersonDto {
+ public string Name { get; set; }
+
+ private PersonDto() { }
+
+ [Projectable]
+ public PersonDto(string name) {
+ Name = name;
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ // EFP0013 is Info (not counted in Diagnostics), EFP0008 is Error
+ var diagnostic = Assert.Single(result.Diagnostics);
+ Assert.Equal("EFP0008", diagnostic.Id);
+ Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
+ Assert.Empty(result.GeneratedTrees);
+ }
+
+ [Fact]
+ public void ProjectableConstructor_PartialInnerClass_NonPartialOuter_PrivateCtor_EmitsDiagnostic()
+ {
+ // Only the inner class is partial — the outer is not, so inline generation
+ // is NOT used and a private parameterless constructor must still fail.
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+
+namespace Foo {
+ class Outer {
+ public partial class PersonDto {
+ public string Name { get; set; }
+
+ private PersonDto() { }
+
+ [Projectable]
+ public PersonDto(string name) {
+ Name = name;
+ }
+ }
+ }
+}
+");
+ var result = RunGenerator(compilation);
+
+ var diagnostic = Assert.Single(result.Diagnostics);
+ Assert.Equal("EFP0008", diagnostic.Id);
+ Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
+ Assert.Empty(result.GeneratedTrees);
+ }
}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/FactoryMethodDiagnosticTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/FactoryMethodDiagnosticTests.cs
index da2c2bf..f902a87 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/FactoryMethodDiagnosticTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/FactoryMethodDiagnosticTests.cs
@@ -37,8 +37,7 @@ class MyObj {
}");
var result = RunGenerator(compilation);
- var diag = Assert.Single(result.Diagnostics);
- Assert.Equal("EFP0012", diag.Id);
+ var diag = Assert.Single(result.AllDiagnostics.Where(d => d.Id == "EFP0012"));
Assert.Equal(DiagnosticSeverity.Info, diag.Severity);
Assert.Equal("Create", diag.Location.SourceTree!
.GetRoot().FindToken(diag.Location.SourceSpan.Start).ValueText);
@@ -60,7 +59,7 @@ class Dest {
}");
var result = RunGenerator(compilation);
- var diag = Assert.Single(result.Diagnostics);
+ var diag = Assert.Single(result.AllDiagnostics.Where(d => d.Id == "EFP0012"));
Assert.Equal("EFP0012", diag.Id);
}
@@ -80,7 +79,7 @@ class Output {
}");
var result = RunGenerator(compilation);
- Assert.Single(result.Diagnostics.Where(d => d.Id == "EFP0012"));
+ Assert.Single(result.AllDiagnostics.Where(d => d.Id == "EFP0012"));
Assert.Single(result.GeneratedTrees); // expression tree is still generated
}
@@ -98,7 +97,7 @@ class MyObj {
}");
var result = RunGenerator(compilation);
- var diag = Assert.Single(result.Diagnostics);
+ var diag = Assert.Single(result.AllDiagnostics.Where(d => d.Id == "EFP0012"));
Assert.Equal("EFP0012", diag.Id);
}
@@ -122,7 +121,7 @@ public MyObj(int x) { }
}");
var result = RunGenerator(compilation);
- Assert.DoesNotContain(result.Diagnostics, d => d.Id == "EFP0012");
+ Assert.DoesNotContain(result.AllDiagnostics, d => d.Id == "EFP0012");
}
[Fact]
@@ -139,7 +138,7 @@ public MyObj() { }
}");
var result = RunGenerator(compilation);
- Assert.DoesNotContain(result.Diagnostics, d => d.Id == "EFP0012");
+ Assert.DoesNotContain(result.AllDiagnostics, d => d.Id == "EFP0012");
}
[Fact]
@@ -156,7 +155,7 @@ class MyObj {
}");
var result = RunGenerator(compilation);
- Assert.DoesNotContain(result.Diagnostics, d => d.Id == "EFP0012");
+ Assert.DoesNotContain(result.AllDiagnostics, d => d.Id == "EFP0012");
}
[Fact]
@@ -195,8 +194,7 @@ public MyObj(OtherObj o) {
var result = RunGenerator(compilation);
// A [Projectable] constructor is not a factory method — EFP0012 must not be reported.
- Assert.DoesNotContain(result.Diagnostics, d => d.Id == "EFP0012");
+ Assert.DoesNotContain(result.AllDiagnostics, d => d.Id == "EFP0012");
}
}
-
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Constructor_GeneratesInlineAccessor.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Constructor_GeneratesInlineAccessor.verified.txt
new file mode 100644
index 0000000..78fcca5
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Constructor_GeneratesInlineAccessor.verified.txt
@@ -0,0 +1,20 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class PointDto
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable___ctor_P0_int_P1_int()
+ {
+ return (int x, int y) => new global::Foo.PointDto()
+ {
+ X = x,
+ Y = y
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Constructor_WithPrivateStaticHelper.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Constructor_WithPrivateStaticHelper.verified.txt
new file mode 100644
index 0000000..42927e3
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Constructor_WithPrivateStaticHelper.verified.txt
@@ -0,0 +1,39 @@
+[
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class PersonDto
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable___ctor_P0_int_P1_string_P2_string()
+ {
+ return (int id, string firstName, string lastName) => new global::Foo.PersonDto()
+ {
+ Id = id,
+ FullName = global::Foo.PersonDto.FormatName(firstName, lastName)
+ };
+ }
+ }
+}
+
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class PersonDto
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__FormatName_P0_string_P1_string()
+ {
+ return (string first, string last) => first + " " + last;
+ }
+ }
+}
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_GenericClass_GeneratesInlineAccessor.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_GenericClass_GeneratesInlineAccessor.verified.txt
new file mode 100644
index 0000000..97d6c16
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_GenericClass_GeneratesInlineAccessor.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression, T>> __Projectable__GetValue()
+ {
+ return (global::Foo.MyClass @this) => @this.Value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_InlineAccessor_HasHidingAttributes.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_InlineAccessor_HasHidingAttributes.verified.txt
new file mode 100644
index 0000000..2f11393
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_InlineAccessor_HasHidingAttributes.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Score()
+ {
+ return (global::Foo.MyClass @this) => @this.Id * 2;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_InlineAccessor_RegistryUsesInlinePath.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_InlineAccessor_RegistryUsesInlinePath.verified.txt
new file mode 100644
index 0000000..73aa960
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_InlineAccessor_RegistryUsesInlinePath.verified.txt
@@ -0,0 +1,56 @@
+//
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ internal static class ProjectionRegistry
+ {
+ private static Dictionary Build()
+ {
+ const BindingFlags allFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
+ var map = new Dictionary();
+
+ RegisterInline(map, typeof(global::Foo.MyClass).GetProperty("IdPlus1", allFlags)?.GetMethod, "__Projectable__IdPlus1");
+
+ return map;
+ }
+
+ private static readonly Dictionary _map = Build();
+
+ public static LambdaExpression TryGet(MemberInfo member)
+ {
+ var handle = member switch
+ {
+ MethodInfo m => (nint?)m.MethodHandle.Value,
+ PropertyInfo p => p.GetMethod?.MethodHandle.Value,
+ ConstructorInfo c => (nint?)c.MethodHandle.Value,
+ _ => null
+ };
+
+ return handle.HasValue && _map.TryGetValue(handle.Value, out var expr) ? expr : null;
+ }
+
+ private static void Register(Dictionary map, MethodBase m, string exprClass)
+ {
+ if (m is null) return;
+ var exprType = m.DeclaringType?.Assembly.GetType(exprClass);
+ var exprMethod = exprType?.GetMethod("Expression", BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+ }
+}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_InlineMethodName_EncodesParameters.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_InlineMethodName_EncodesParameters.verified.txt
new file mode 100644
index 0000000..a6659eb
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_InlineMethodName_EncodesParameters.verified.txt
@@ -0,0 +1,35 @@
+[
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Add_P0_int()
+ {
+ return (global::Foo.MyClass @this, int x) => @this.Id + x;
+ }
+ }
+}
+
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Add_P0_long()
+ {
+ return (global::Foo.MyClass @this, long x) => @this.Id + x;
+ }
+ }
+}
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Method_GeneratesInlineAccessor.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Method_GeneratesInlineAccessor.verified.txt
new file mode 100644
index 0000000..b4146c7
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Method_GeneratesInlineAccessor.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__AddDelta_P0_int()
+ {
+ return (global::Foo.MyClass @this, int delta) => @this.Id + delta;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Nested_AllPartial_GeneratesInlineAccessor.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Nested_AllPartial_GeneratesInlineAccessor.verified.txt
new file mode 100644
index 0000000..add89d9
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Nested_AllPartial_GeneratesInlineAccessor.verified.txt
@@ -0,0 +1,19 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class Outer
+ {
+ partial class Inner
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__IdPlus1()
+ {
+ return (global::Foo.Outer.Inner @this) => @this.Id + 1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_PrivateMethod_UsedInPublicProjectable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_PrivateMethod_UsedInPublicProjectable.verified.txt
new file mode 100644
index 0000000..89b88b4
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_PrivateMethod_UsedInPublicProjectable.verified.txt
@@ -0,0 +1,35 @@
+[
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Double_P0_int()
+ {
+ return (global::Foo.MyClass @this, int x) => x * 2;
+ }
+ }
+}
+
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Score_P0_int()
+ {
+ return (global::Foo.MyClass @this, int delta) => @this.Double(@this.Id) + delta;
+ }
+ }
+}
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_PrivateProperty_UsedInPublicProjectable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_PrivateProperty_UsedInPublicProjectable.verified.txt
new file mode 100644
index 0000000..f1cac30
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_PrivateProperty_UsedInPublicProjectable.verified.txt
@@ -0,0 +1,35 @@
+[
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Doubled()
+ {
+ return (global::Foo.MyClass @this) => @this.Id * 2;
+ }
+ }
+}
+
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Score()
+ {
+ return (global::Foo.MyClass @this) => @this.Doubled + 1;
+ }
+ }
+}
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Property_GeneratesInlineAccessor.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Property_GeneratesInlineAccessor.verified.txt
new file mode 100644
index 0000000..13c36df
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_Property_GeneratesInlineAccessor.verified.txt
@@ -0,0 +1,16 @@
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__IdPlus1()
+ {
+ return (global::Foo.MyClass @this) => @this.Id + 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_ProtectedProperty_UsedInPublicProjectable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_ProtectedProperty_UsedInPublicProjectable.verified.txt
new file mode 100644
index 0000000..f1cac30
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.PartialClass_ProtectedProperty_UsedInPublicProjectable.verified.txt
@@ -0,0 +1,35 @@
+[
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Doubled()
+ {
+ return (global::Foo.MyClass @this) => @this.Id * 2;
+ }
+ }
+}
+
+//
+#nullable disable
+using EntityFrameworkCore.Projectables;
+
+namespace Foo
+{
+ partial class MyClass
+ {
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Score()
+ {
+ return (global::Foo.MyClass @this) => @this.Doubled + 1;
+ }
+ }
+}
+]
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.cs
new file mode 100644
index 0000000..e2d3f22
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/InlineTests.cs
@@ -0,0 +1,395 @@
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace EntityFrameworkCore.Projectables.Generator.Tests;
+
+///
+/// Tests for the inline expression accessor feature: when the containing class is declared
+/// partial, the generator emits the accessor as a private static hidden method
+/// inside the class itself (instead of an external generated class), allowing the lambda to
+/// capture private and protected members.
+///
+[UsesVerify]
+public class InlineTests : ProjectionExpressionGeneratorTestsBase
+{
+ public InlineTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { }
+
+ // ────────────────────────────────────────────────────────────────────────
+ // Inline generation: partial classes
+ // ────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public Task PartialClass_Property_GeneratesInlineAccessor()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class MyClass {
+ public int Id { get; set; }
+ [Projectable]
+ public int IdPlus1 => Id + 1;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+ // Inline: no external generated class, the accessor is inside the partial class
+ Assert.DoesNotContain("EntityFrameworkCore.Projectables.Generated", result.GeneratedTrees[0].GetText().ToString());
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task PartialClass_Method_GeneratesInlineAccessor()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class MyClass {
+ public int Id { get; set; }
+ [Projectable]
+ public int AddDelta(int delta) => Id + delta;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task PartialClass_InlineMethodName_EncodesParameters()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class MyClass {
+ public int Id { get; set; }
+ [Projectable]
+ public int Add(int x) => Id + x;
+ [Projectable]
+ public long Add(long x) => Id + x;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Equal(2, result.GeneratedTrees.Length);
+
+ return Verifier.Verify(result.GeneratedTrees.Select(t => t.ToString()));
+ }
+
+ [Fact]
+ public Task PartialClass_GenericClass_GeneratesInlineAccessor()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class MyClass {
+ public T Value { get; set; }
+ [Projectable]
+ public T GetValue() => Value;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task PartialClass_Nested_AllPartial_GeneratesInlineAccessor()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class Outer {
+ public partial class Inner {
+ public int Id { get; set; }
+ [Projectable]
+ public int IdPlus1 => Id + 1;
+ }
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task PartialClass_InlineAccessor_HasHidingAttributes()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class MyClass {
+ public int Id { get; set; }
+ [Projectable]
+ public int Score => Id * 2;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ var generatedText = result.GeneratedTrees[0].GetText().ToString();
+ Assert.Contains("EditorBrowsable", generatedText);
+ Assert.Contains("Obsolete", generatedText);
+ Assert.Contains("private static", generatedText);
+ Assert.Contains("__Projectable__Score", generatedText);
+
+ return Verifier.Verify(generatedText);
+ }
+
+ [Fact]
+ public Task PartialClass_InlineAccessor_RegistryUsesInlinePath()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class MyClass {
+ public int Id { get; set; }
+ [Projectable]
+ public int IdPlus1 => Id + 1;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.NotNull(result.RegistryTree);
+ var registryText = result.RegistryTree!.GetText().ToString();
+ // Registry must use RegisterInline, not Register
+ Assert.Contains("RegisterInline", registryText);
+ Assert.Contains("__Projectable__IdPlus1", registryText);
+
+ return Verifier.Verify(registryText);
+ }
+
+ // ────────────────────────────────────────────────────────────────────────
+ // Non-partial classes: EFP0013 is reported, external class is generated
+ // ────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void NonPartialClass_ReportsEFP0013()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Id { get; set; }
+ [Projectable]
+ public int IdPlus1 => Id + 1;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ // EFP0013 is Info — no Warning+ diagnostics
+ Assert.Empty(result.Diagnostics);
+ var diag = Assert.Single(result.AllDiagnostics.Where(d => d.Id == "EFP0013"));
+ Assert.Equal(DiagnosticSeverity.Info, diag.Severity);
+ Assert.Equal("C", diag.Location.SourceTree!
+ .GetRoot().FindToken(diag.Location.SourceSpan.Start).ValueText);
+ }
+
+ [Fact]
+ public void NonPartialClass_StillGeneratesExternalClass()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class C {
+ public int Id { get; set; }
+ [Projectable]
+ public int IdPlus1 => Id + 1;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Single(result.GeneratedTrees);
+ // External path: class lives in the Generated namespace
+ Assert.Contains("EntityFrameworkCore.Projectables.Generated", result.GeneratedTrees[0].GetText().ToString());
+ }
+
+ [Fact]
+ public void NonPartialNestedClass_OuterNotPartial_ReportsEFP0013OnOuter()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public class Outer { // not partial — EFP0013 on Outer
+ public partial class Inner { // inner is partial but outer is not
+ public int Id { get; set; }
+ [Projectable]
+ public int IdPlus1 => Id + 1;
+ }
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ var diag = Assert.Single(result.AllDiagnostics.Where(d => d.Id == "EFP0013"));
+ Assert.Equal("Outer", diag.Location.SourceTree!
+ .GetRoot().FindToken(diag.Location.SourceSpan.Start).ValueText);
+ }
+
+ [Fact]
+ public void ExtensionMember_NoEFP0013()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public class MyClass {
+ public int Id { get; set; }
+ }
+ public static class MyExtensions {
+ extension(MyClass self) {
+ [Projectable]
+ public int IdPlus1 => self.Id + 1;
+ }
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ // Extension members never get EFP0013
+ Assert.DoesNotContain(result.AllDiagnostics, d => d.Id == "EFP0013");
+ }
+
+ // ────────────────────────────────────────────────────────────────────────
+ // Private / protected member access via inline generation
+ // ────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public Task PartialClass_PrivateProperty_UsedInPublicProjectable()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class MyClass {
+ public int Id { get; set; }
+ [Projectable]
+ private int Doubled => Id * 2;
+ [Projectable]
+ public int Score => Doubled + 1;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Equal(2, result.GeneratedTrees.Length);
+ // Both inline accessors must be generated inside the partial class
+ Assert.All(result.GeneratedTrees, t =>
+ Assert.DoesNotContain("EntityFrameworkCore.Projectables.Generated", t.GetText().ToString()));
+
+ return Verifier.Verify(result.GeneratedTrees.Select(t => t.ToString()));
+ }
+
+ [Fact]
+ public Task PartialClass_ProtectedProperty_UsedInPublicProjectable()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class MyClass {
+ public int Id { get; set; }
+ [Projectable]
+ protected int Doubled => Id * 2;
+ [Projectable]
+ public int Score => Doubled + 1;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Equal(2, result.GeneratedTrees.Length);
+
+ return Verifier.Verify(result.GeneratedTrees.Select(t => t.ToString()));
+ }
+
+ [Fact]
+ public Task PartialClass_PrivateMethod_UsedInPublicProjectable()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class MyClass {
+ public int Id { get; set; }
+ [Projectable]
+ private int Double(int x) => x * 2;
+ [Projectable]
+ public int Score(int delta) => Double(Id) + delta;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Equal(2, result.GeneratedTrees.Length);
+
+ return Verifier.Verify(result.GeneratedTrees.Select(t => t.ToString()));
+ }
+
+ [Fact]
+ public Task PartialClass_Constructor_GeneratesInlineAccessor()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class PointDto {
+ public int X { get; set; }
+ public int Y { get; set; }
+ public PointDto() { }
+ [Projectable]
+ public PointDto(int x, int y) {
+ X = x;
+ Y = y;
+ }
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+ // Inline: accessor inside partial class, not in Generated namespace
+ Assert.DoesNotContain("EntityFrameworkCore.Projectables.Generated", result.GeneratedTrees[0].GetText().ToString());
+ Assert.Contains("__Projectable___ctor", result.GeneratedTrees[0].GetText().ToString());
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
+ [Fact]
+ public Task PartialClass_Constructor_WithPrivateStaticHelper()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class PersonDto {
+ public int Id { get; set; }
+ public string FullName { get; set; }
+ public PersonDto() { }
+ [Projectable]
+ public PersonDto(int id, string firstName, string lastName) {
+ Id = id;
+ FullName = FormatName(firstName, lastName);
+ }
+ [Projectable]
+ private static string FormatName(string first, string last) => first + "" "" + last;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ // Constructor + static method = 2 inline generated trees
+ Assert.Equal(2, result.GeneratedTrees.Length);
+
+ return Verifier.Verify(result.GeneratedTrees.Select(t => t.ToString()));
+ }
+}
+
+
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs
index 431c726..2a802b4 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs
@@ -33,9 +33,23 @@ public TestGeneratorRunResult(GeneratorDriverRunResult inner)
}
///
- /// Diagnostics from the generator run.
+ /// Diagnostics from the generator run with severity Warning or higher.
+ /// Info and Hidden diagnostics (e.g. EFP0012, EFP0013) are excluded so that
+ /// existing Assert.Empty(result.Diagnostics) assertions are not broken
+ /// when the generator emits new informational suggestions.
+ /// Use to include Info/Hidden diagnostics.
///
- public ImmutableArray Diagnostics => _inner.Diagnostics;
+ public ImmutableArray Diagnostics =>
+ _inner.Diagnostics
+ .Where(d => d.Severity >= DiagnosticSeverity.Warning)
+ .ToImmutableArray();
+
+ ///
+ /// All diagnostics from the generator run, including Info and Hidden severity.
+ /// Existing tests use (Warning+); use this property
+ /// when you need to assert on informational diagnostics such as EFP0012 or EFP0013.
+ ///
+ public ImmutableArray AllDiagnostics => _inner.Diagnostics;
///
/// Generated trees excluding ProjectionRegistry.g.cs.
@@ -201,7 +215,7 @@ protected TestGeneratorRunResult RunGenerator(Compilation compilation)
private void LogGeneratorResult(TestGeneratorRunResult result, Compilation outputCompilation)
{
- if (result.Diagnostics.IsEmpty)
+ if (result.AllDiagnostics.IsEmpty)
{
_testOutputHelper.WriteLine("Run did not produce diagnostics");
}
@@ -209,7 +223,7 @@ private void LogGeneratorResult(TestGeneratorRunResult result, Compilation outpu
{
_testOutputHelper.WriteLine("Diagnostics produced:");
- foreach (var diagnostic in result.Diagnostics)
+ foreach (var diagnostic in result.AllDiagnostics)
{
_testOutputHelper.WriteLine(" > " + diagnostic);
}
@@ -222,7 +236,7 @@ private void LogGeneratorResult(TestGeneratorRunResult result, Compilation outpu
}
// Verify that the generated code compiles without errors
- var hasGeneratorErrors = result.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error);
+ var hasGeneratorErrors = result.AllDiagnostics.Any(d => d.Severity == DiagnosticSeverity.Error);
if (!hasGeneratorErrors && result.AllGeneratedTrees.Length > 0)
{
_testOutputHelper.WriteLine("Checking that generated code compiles...");
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MethodOverloads_BothRegistered.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MethodOverloads_BothRegistered.verified.txt
index 76399cd..5c05186 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MethodOverloads_BothRegistered.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MethodOverloads_BothRegistered.verified.txt
@@ -45,5 +45,13 @@ namespace EntityFrameworkCore.Projectables.Generated
if (exprMethod is not null)
map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
}
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MixedPartialAndNonPartial_BothInRegistryWithCorrectHelpers.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MixedPartialAndNonPartial_BothInRegistryWithCorrectHelpers.verified.txt
new file mode 100644
index 0000000..7549c44
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MixedPartialAndNonPartial_BothInRegistryWithCorrectHelpers.verified.txt
@@ -0,0 +1,57 @@
+//
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ internal static class ProjectionRegistry
+ {
+ private static Dictionary Build()
+ {
+ const BindingFlags allFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
+ var map = new Dictionary();
+
+ RegisterInline(map, typeof(global::Foo.PartialEntity).GetProperty("InlineScore", allFlags)?.GetMethod, "__Projectable__InlineScore");
+ Register(map, typeof(global::Foo.NonPartialEntity).GetProperty("ExternalScore", allFlags)?.GetMethod, "EntityFrameworkCore.Projectables.Generated.Foo_NonPartialEntity_ExternalScore");
+
+ return map;
+ }
+
+ private static readonly Dictionary _map = Build();
+
+ public static LambdaExpression TryGet(MemberInfo member)
+ {
+ var handle = member switch
+ {
+ MethodInfo m => (nint?)m.MethodHandle.Value,
+ PropertyInfo p => p.GetMethod?.MethodHandle.Value,
+ ConstructorInfo c => (nint?)c.MethodHandle.Value,
+ _ => null
+ };
+
+ return handle.HasValue && _map.TryGetValue(handle.Value, out var expr) ? expr : null;
+ }
+
+ private static void Register(Dictionary map, MethodBase m, string exprClass)
+ {
+ if (m is null) return;
+ var exprType = m.DeclaringType?.Assembly.GetType(exprClass);
+ var exprMethod = exprType?.GetMethod("Expression", BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+ }
+}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MultipleProjectables_AllRegistered.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MultipleProjectables_AllRegistered.verified.txt
index 8e92fdf..107767f 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MultipleProjectables_AllRegistered.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.MultipleProjectables_AllRegistered.verified.txt
@@ -45,5 +45,13 @@ namespace EntityFrameworkCore.Projectables.Generated
if (exprMethod is not null)
map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
}
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.PartialClass_Constructor_RegistryUsesRegisterInline.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.PartialClass_Constructor_RegistryUsesRegisterInline.verified.txt
new file mode 100644
index 0000000..8fa8bb1
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.PartialClass_Constructor_RegistryUsesRegisterInline.verified.txt
@@ -0,0 +1,56 @@
+//
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ internal static class ProjectionRegistry
+ {
+ private static Dictionary Build()
+ {
+ const BindingFlags allFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
+ var map = new Dictionary();
+
+ RegisterInline(map, typeof(global::Foo.PointDto).GetConstructor(allFlags, null, new global::System.Type[] { typeof(int), typeof(int) }, null), "__Projectable___ctor_P0_int_P1_int");
+
+ return map;
+ }
+
+ private static readonly Dictionary _map = Build();
+
+ public static LambdaExpression TryGet(MemberInfo member)
+ {
+ var handle = member switch
+ {
+ MethodInfo m => (nint?)m.MethodHandle.Value,
+ PropertyInfo p => p.GetMethod?.MethodHandle.Value,
+ ConstructorInfo c => (nint?)c.MethodHandle.Value,
+ _ => null
+ };
+
+ return handle.HasValue && _map.TryGetValue(handle.Value, out var expr) ? expr : null;
+ }
+
+ private static void Register(Dictionary map, MethodBase m, string exprClass)
+ {
+ if (m is null) return;
+ var exprType = m.DeclaringType?.Assembly.GetType(exprClass);
+ var exprMethod = exprType?.GetMethod("Expression", BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+ }
+}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.PartialClass_Method_RegistryUsesRegisterInline.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.PartialClass_Method_RegistryUsesRegisterInline.verified.txt
new file mode 100644
index 0000000..8c62eb5
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.PartialClass_Method_RegistryUsesRegisterInline.verified.txt
@@ -0,0 +1,56 @@
+//
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ internal static class ProjectionRegistry
+ {
+ private static Dictionary Build()
+ {
+ const BindingFlags allFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
+ var map = new Dictionary();
+
+ RegisterInline(map, typeof(global::Foo.C).GetMethod("AddDelta", allFlags, null, new global::System.Type[] { typeof(int) }, null), "__Projectable__AddDelta_P0_int");
+
+ return map;
+ }
+
+ private static readonly Dictionary _map = Build();
+
+ public static LambdaExpression TryGet(MemberInfo member)
+ {
+ var handle = member switch
+ {
+ MethodInfo m => (nint?)m.MethodHandle.Value,
+ PropertyInfo p => p.GetMethod?.MethodHandle.Value,
+ ConstructorInfo c => (nint?)c.MethodHandle.Value,
+ _ => null
+ };
+
+ return handle.HasValue && _map.TryGetValue(handle.Value, out var expr) ? expr : null;
+ }
+
+ private static void Register(Dictionary map, MethodBase m, string exprClass)
+ {
+ if (m is null) return;
+ var exprType = m.DeclaringType?.Assembly.GetType(exprClass);
+ var exprMethod = exprType?.GetMethod("Expression", BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+ }
+}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.PartialClass_Property_RegistryUsesRegisterInline.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.PartialClass_Property_RegistryUsesRegisterInline.verified.txt
new file mode 100644
index 0000000..ced7f72
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.PartialClass_Property_RegistryUsesRegisterInline.verified.txt
@@ -0,0 +1,56 @@
+//
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ internal static class ProjectionRegistry
+ {
+ private static Dictionary Build()
+ {
+ const BindingFlags allFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
+ var map = new Dictionary();
+
+ RegisterInline(map, typeof(global::Foo.C).GetProperty("IdPlus1", allFlags)?.GetMethod, "__Projectable__IdPlus1");
+
+ return map;
+ }
+
+ private static readonly Dictionary _map = Build();
+
+ public static LambdaExpression TryGet(MemberInfo member)
+ {
+ var handle = member switch
+ {
+ MethodInfo m => (nint?)m.MethodHandle.Value,
+ PropertyInfo p => p.GetMethod?.MethodHandle.Value,
+ ConstructorInfo c => (nint?)c.MethodHandle.Value,
+ _ => null
+ };
+
+ return handle.HasValue && _map.TryGetValue(handle.Value, out var expr) ? expr : null;
+ }
+
+ private static void Register(Dictionary map, MethodBase m, string exprClass)
+ {
+ if (m is null) return;
+ var exprType = m.DeclaringType?.Assembly.GetType(exprClass);
+ var exprMethod = exprType?.GetMethod("Expression", BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
+ }
+}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.Registry_ConstBindingFlagsUsedInBuild.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.Registry_ConstBindingFlagsUsedInBuild.verified.txt
index f21aa56..f56b102 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.Registry_ConstBindingFlagsUsedInBuild.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.Registry_ConstBindingFlagsUsedInBuild.verified.txt
@@ -44,5 +44,13 @@ namespace EntityFrameworkCore.Projectables.Generated
if (exprMethod is not null)
map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
}
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.Registry_RegisterHelperUsesDeclaringTypeAssembly.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.Registry_RegisterHelperUsesDeclaringTypeAssembly.verified.txt
index f21aa56..f56b102 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.Registry_RegisterHelperUsesDeclaringTypeAssembly.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.Registry_RegisterHelperUsesDeclaringTypeAssembly.verified.txt
@@ -44,5 +44,13 @@ namespace EntityFrameworkCore.Projectables.Generated
if (exprMethod is not null)
map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
}
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt
index 5af832d..b5df448 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.SingleMethod_RegistryContainsEntry.verified.txt
@@ -44,5 +44,13 @@ namespace EntityFrameworkCore.Projectables.Generated
if (exprMethod is not null)
map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
}
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt
index f21aa56..f56b102 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.SingleProperty_RegistryContainsEntry.verified.txt
@@ -44,5 +44,13 @@ namespace EntityFrameworkCore.Projectables.Generated
if (exprMethod is not null)
map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
}
+
+ private static void RegisterInline(Dictionary map, MethodBase m, string inlineMethodName)
+ {
+ if (m is null) return;
+ var exprMethod = m.DeclaringType?.GetMethod(inlineMethodName, BindingFlags.Static | BindingFlags.NonPublic);
+ if (exprMethod is not null)
+ map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
+ }
}
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.cs
index 3a082b2..aa2a056 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.cs
@@ -142,4 +142,109 @@ class C {
return Verifier.Verify(result.RegistryTree!.GetText().ToString());
}
+
+ // ────────────────────────────────────────────────────────────────────────
+ // Inline (partial class) registry entries — all three member kinds
+ // ────────────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public Task PartialClass_Property_RegistryUsesRegisterInline()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class C {
+ public int Id { get; set; }
+ [Projectable]
+ public int IdPlus1 => Id + 1;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.NotNull(result.RegistryTree);
+ var registryText = result.RegistryTree!.GetText().ToString();
+ Assert.Contains("RegisterInline", registryText);
+ Assert.DoesNotContain("Register(map,", registryText); // only RegisterInline, no external Register
+
+ return Verifier.Verify(registryText);
+ }
+
+ [Fact]
+ public Task PartialClass_Method_RegistryUsesRegisterInline()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class C {
+ public int Id { get; set; }
+ [Projectable]
+ public int AddDelta(int delta) => Id + delta;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.NotNull(result.RegistryTree);
+ var registryText = result.RegistryTree!.GetText().ToString();
+ Assert.Contains("RegisterInline", registryText);
+ Assert.Contains("GetMethod", registryText);
+ Assert.Contains("__Projectable__AddDelta", registryText);
+
+ return Verifier.Verify(registryText);
+ }
+
+ [Fact]
+ public Task PartialClass_Constructor_RegistryUsesRegisterInline()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class PointDto {
+ public int X { get; set; }
+ public int Y { get; set; }
+ public PointDto() { }
+ [Projectable]
+ public PointDto(int x, int y) {
+ X = x;
+ Y = y;
+ }
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.NotNull(result.RegistryTree);
+ var registryText = result.RegistryTree!.GetText().ToString();
+ Assert.Contains("RegisterInline", registryText);
+ Assert.Contains("GetConstructor", registryText);
+ Assert.Contains("__Projectable___ctor", registryText);
+
+ return Verifier.Verify(registryText);
+ }
+
+ [Fact]
+ public Task MixedPartialAndNonPartial_BothInRegistryWithCorrectHelpers()
+ {
+ var compilation = CreateCompilation(@"
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ public partial class PartialEntity {
+ public int Id { get; set; }
+ [Projectable]
+ public int InlineScore => Id + 1;
+ }
+ public class NonPartialEntity {
+ public int Id { get; set; }
+ [Projectable]
+ public int ExternalScore => Id + 2;
+ }
+}");
+ var result = RunGenerator(compilation);
+
+ Assert.NotNull(result.RegistryTree);
+ var registryText = result.RegistryTree!.GetText().ToString();
+ // Partial class uses RegisterInline; non-partial uses Register
+ Assert.Contains("RegisterInline", registryText);
+ Assert.Contains("Register(map,", registryText);
+
+ return Verifier.Verify(registryText);
+ }
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt
index e7f84a8..0c16a55 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt
@@ -3,14 +3,14 @@
using System;
using System.Linq.Expressions;
using EntityFrameworkCore.Projectables;
-using Foo;
-namespace EntityFrameworkCore.Projectables.Generated
+namespace Foo
{
- [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- static class Foo_C_IsPositive
+ partial class C
{
- static global::System.Linq.Expressions.Expression> Expression()
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__IsPositive()
{
return (global::Foo.C @this) => @this.Value > 0;
}
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt
index 34f67c5..f211375 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt
@@ -3,14 +3,14 @@
using System;
using System.Linq.Expressions;
using EntityFrameworkCore.Projectables;
-using Foo;
-namespace EntityFrameworkCore.Projectables.Generated
+namespace Foo
{
- [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- static class Foo_C_Computed
+ partial class C
{
- static global::System.Linq.Expressions.Expression> Expression()
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ [global::System.Obsolete("Generated member. Do not use.")]
+ private static global::System.Linq.Expressions.Expression> __Projectable__Computed()
{
return (global::Foo.C @this) => @this.Id * 2;
}