Skip to content

Implement partial class support for projectable members#193

Open
koenbeuk wants to merge 1 commit intomasterfrom
feat/partial-class-companion-nesting
Open

Implement partial class support for projectable members#193
koenbeuk wants to merge 1 commit intomasterfrom
feat/partial-class-companion-nesting

Conversation

@koenbeuk
Copy link
Collaborator

Enable access to private fields and methods in generated companions by implementing support for partial classes. This change allows projectable members to be accessed within their containing types, enhancing encapsulation and usability.

public partial class Order
{
    public int BasePrice { get; set; }

    private int Discount => 5;

    [Projectable]
    public int NetAmount => BasePrice - Discount;
}
namespace MyApp
{
    partial class Order
    {
        [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
        static class MyApp_Order_NetAmount
        {
            static global::System.Linq.Expressions.Expression<global::System.Func<global::MyApp.Order, int>> Expression()
            {
                return (global::MyApp.Order @this) => @this.BasePrice - @this.Discount;
            }
        }
    }
}

Marking a type as partial is optional, not doing so will still generate the current companion type which doesn't have access to internals (fine for most usecases)

…ess to private fields and methods in generated companions
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds generator support for emitting companion types nested inside user-declared partial types, enabling generated expressions to access private/protected members of the declaring type (and updates the projection registry naming accordingly).

Changes:

  • Generate companions nested under the user’s partial type chain (instead of the EntityFrameworkCore.Projectables.Generated namespace) when the declaring type is partial.
  • Update registry entries to use CLR nested-type naming (+) when companions are generated as nested types.
  • Add new generator snapshot tests and functional EF Core tests validating private-member access via partial types.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs Nests companions inside partial type shells; adjusts registry full type name for nested companions.
src/EntityFrameworkCore.Projectables.Generator/Models/ProjectableDescriptor.cs Adds IsDeclaringTypePartial flag to drive partial nesting behavior.
src/EntityFrameworkCore.Projectables.Generator/Interpretation/ProjectableInterpreter.cs Detects whether the declaring type is partial and stores it on the descriptor.
tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Property_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt Updates expected generated output to nested-companion form for partial type scenario.
tests/EntityFrameworkCore.Projectables.Generator.Tests/UseMemberBodyTests.Method_UsesExpressionPropertyBody_ExpressionPropertyInDifferentFile.verified.txt Updates expected generated output to nested-companion form for partial type scenario.
tests/EntityFrameworkCore.Projectables.Generator.Tests/PartialClassTests.cs Adds generator tests for partial-type nested companion generation and registry output.
tests/EntityFrameworkCore.Projectables.Generator.Tests/PartialClassTests.PartialClass_SimpleMethod_GeneratesNestedCompanion.verified.txt New snapshot for nested companion in partial type.
tests/EntityFrameworkCore.Projectables.Generator.Tests/PartialClassTests.PartialClass_PrivateMethodCall_CompanionCanCallPrivateMethod.verified.txt New snapshot validating nested companion can reference private method.
tests/EntityFrameworkCore.Projectables.Generator.Tests/PartialClassTests.PartialClass_PrivateFieldAccess_CompanionCanAccessPrivateField.verified.txt New snapshot validating nested companion can access private field.
tests/EntityFrameworkCore.Projectables.Generator.Tests/PartialClassTests.PartialClass_NestedPartialType_TwoLevelShellWrap.verified.txt New snapshot validating wrapping through nested partial type chain.
tests/EntityFrameworkCore.Projectables.Generator.Tests/PartialClassTests.PartialClass_Registry_UsesClrNestedTypeName.verified.txt New snapshot validating registry uses CLR nested type names for nested companions.
tests/EntityFrameworkCore.Projectables.Generator.Tests/PartialClassTests.PartialClass_MethodOverloads_EachGetsUniqueCompanionInsidePartialType.verified.txt New snapshot validating overload companions are unique and nested.
tests/EntityFrameworkCore.Projectables.Generator.Tests/PartialClassTests.PartialClass_MethodOverloads_Registry_BothEntriesUseClrNestedTypeName.verified.txt New snapshot validating overload registry entries use CLR nested naming.
tests/EntityFrameworkCore.Projectables.Generator.Tests/PartialClassTests.NonPartialClass_Unchanged.verified.txt New snapshot validating non-partial behavior remains unchanged.
tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassWithPrivateMembersTests.cs Adds EF Core functional tests exercising private projectable access via partial type nesting.
tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassWithPrivateMembersTests.FilterOnPrivateProjectableProperty.DotNet8_0.verified.txt New SQL snapshot for filter scenario.
tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassWithPrivateMembersTests.FilterOnPrivateProjectableProperty.DotNet9_0.verified.txt New SQL snapshot for filter scenario.
tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassWithPrivateMembersTests.FilterOnPrivateProjectableProperty.DotNet10_0.verified.txt New SQL snapshot for filter scenario.
tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassWithPrivateMembersTests.SelectPrivateProjectableProperty.DotNet8_0.verified.txt New SQL snapshot for select scenario.
tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassWithPrivateMembersTests.SelectPrivateProjectableProperty.DotNet9_0.verified.txt New SQL snapshot for select scenario.
tests/EntityFrameworkCore.Projectables.FunctionalTests/PartialClassWithPrivateMembersTests.SelectPrivateProjectableProperty.DotNet10_0.verified.txt New SQL snapshot for select scenario.

Comment on lines +257 to +267
if (projectable.IsDeclaringTypePartial)
{
// Nest the companion inside the user's partial type chain so it can access
// private/protected members of the enclosing type (C# nested-class access rules).
MemberDeclarationSyntax wrapped = classSyntax;
var currentType = memberSymbol.ContainingType;
while (currentType is not null)
{
wrapped = BuildPartialTypeShell(currentType).AddMembers(wrapped);
currentType = currentType.ContainingType;
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

When projectable.IsDeclaringTypePartial is true, the generator wraps the companion in all containing types and emits each wrapper as partial. This will break compilation if the projectable member is declared in a partial nested type whose outer containing type(s) are not partial (the generated partial class Outer { ... } will conflict with the user’s non-partial class Outer). Consider requiring the entire containing type chain to be partial before nesting (or falling back to the EntityFrameworkCore.Projectables.Generated namespace / emitting a diagnostic when an outer type is not partial).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +7
using System.Linq;
using System.Threading.Tasks;
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using Microsoft.EntityFrameworkCore;
using VerifyXunit;
using Xunit;

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

[Projectable] is used in this file but using EntityFrameworkCore.Projectables; is missing, which will fail compilation unless there is a global using (none appears to exist in this project). Add the missing using (or fully-qualify the attribute).

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +100
[Fact]
public Task PartialClass_NestedPartialType_TwoLevelShellWrap()
{
var compilation = CreateCompilation(@"
using EntityFrameworkCore.Projectables;
namespace Foo {
public partial class Outer {
public partial class Inner {
[Projectable]
public int Value() => 99;
}
}
}
");

var result = RunGenerator(compilation);

Assert.Empty(result.Diagnostics);
Assert.Single(result.GeneratedTrees);

return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The new partial-type behavior has good coverage for basic cases, but there’s no test for a projectable member inside a partial nested type where an outer containing type is not partial (e.g., class Outer { public partial class Inner { [Projectable] ... } }). Currently the generator will still emit a partial class Outer { ... } wrapper, which should fail compilation; please add a regression test for the expected behavior (fallback to Generated namespace or a diagnostic).

Copilot generated this review using guidance from repository custom instructions.
Copy link
Member

@PhenX PhenX left a comment

Choose a reason for hiding this comment

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

I see you create inner classes instead of "hidden" members, you didn't like it? Anyway, this inner class is fine too!
But I think we should run the benchmarks too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants