Skip to content

Fix PropertyPolicies to consider both getter and setter accessibility #116080

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 5, 2025

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented May 28, 2025

Fixes an issue where PropertyPolicies.GetMemberAttributes in MetadataLoadContext only considered getter accessibility when determining property visibility, causing properties with public setters but private getters to be incorrectly excluded from GetProperties(BindingFlags.Public).

Problem

TaskHostFactory does not work with public properties that have private get accessors as task parameters. For example:

public sealed class SampleTask : Task
{
    public string? S1 { private get; set; }  // This property was not discoverable
    public string? S2 { get; set; }          // This property worked fine
}

When MSBuild's TaskHostFactory uses MetadataLoadContext to discover task properties, the S1 property above would not be found because PropertyPolicies.GetMemberAttributes only looked at the getter accessibility (private) rather than considering that the setter is public.

Root Cause

The issue was in PropertyPolicies.GetAccessorMethod() which returned property.GetMethod ?? property.SetMethod, prioritizing the getter. When the getter was private but setter was public, the property was marked as private and excluded from public property enumeration.

Solution

  1. Modified PropertyPolicies.GetMemberAttributes to use GetMostAccessibleAccessor() instead of GetAccessorMethod()

  2. Added GetMostAccessibleAccessor() method that:

    • Examines both getter and setter accessibility
    • Returns the most accessible accessor using proper accessibility ranking:
      • Public (4) > Family/Assembly/FamORAssem (3) > FamANDAssem (2) > Private (1)
    • Falls back to getter if accessibilities are equal (preserving original behavior)
  3. Applied fix to both implementations:

    • src/libraries/System.Reflection.MetadataLoadContext/src/System/Reflection/Runtime/BindingFlagSupport/PropertyPolicies.cs
    • src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/BindingFlagSupport/PropertyPolicies.cs

Testing

Added comprehensive tests that verify:

  • Properties with public setters but private getters are included in public property enumeration
  • Properties with mixed accessor visibility behave consistently with regular reflection
  • All existing functionality continues to work (686 tests pass, including 2 new tests)

Example

Before this fix:

// MetadataLoadContext would only find S2
properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
// Result: [S2] - S1 missing because getter is private

After this fix:

// MetadataLoadContext now finds both S1 and S2, matching regular reflection
properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); 
// Result: [S1, S2] - S1 included because setter is public

This ensures MetadataLoadContext follows the same property visibility semantics as regular .NET reflection, where a property is considered public if any accessor is public.

Fixes #116012.

Warning

Firewall rules blocked me from connecting to one or more addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

  • badhost
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile System.Net.Http.Functional.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.2-beta.25260.104/build/../tools/net/xunit.console.dll System.Net.Http.Functional.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing (dns block)
  • does.not.exist.sorry
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest <SendAsync_ConnectionFailure_RecordsActivitiesWithCorrectErrorInfo>g__RunTest|18_0 /tmp/yinewg3b.bbx 1.1 False dns (dns block)
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest <SendAsync_ConnectionFailure_RecordsActivitiesWithCorrectErrorInfo>g__RunTest|18_0 /tmp/ycho5lgn.idp 1.1 True dns (dns block)
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest <SendAsync_ConnectionFailure_RecordsActivitiesWithCorrectErrorInfo>g__RunTest|18_0 /tmp/y0we1k00.s3k 2.0 True dns (dns block)
  • nosuchhost.invalid
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile System.Net.Http.Functional.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.2-beta.25260.104/build/../tools/net/xunit.console.dll System.Net.Http.Functional.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing (dns block)
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest+<>c <SendAsync_ExpectedDiagnosticExceptionLogging>b__9_0 /tmp/2ckzwn2w.1tl 1.1 True (dns block)
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.DiagnosticsTest+<>c <SendAsync_ExpectedDiagnosticExceptionActivityLogging>b__23_0 /tmp/nx20jrpq.dco 1.1 False (dns block)
  • server
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig System.DirectoryServices.Protocols.Tests.runtimeconfig.json --depsfile System.DirectoryServices.Protocols.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.2-beta.25260.104/build/../tools/net/xunit.console.dll System.DirectoryServices.Protocols.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing (dns block)
  • www.microsoft.com
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/System.Net.Http.Functional.Tests.deps.json /home/REDACTED/work/runtime/runtime/artifacts/bin/System.Net.Http.Functional.Tests/Debug/net10.0-linux/Microsoft.DotNet.RemoteExecutor.dll System.Net.Http.Functional.Tests, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Net.Http.Functional.Tests.HttpClientHandler_ServerCertificates_Test+<>c <HttpClientUsesSslCertEnvironmentVariables>b__26_0 /tmp/tf3iqxu1.dyy 1.1 True (dns block)
  • www.some.example
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net10.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Net.Http.Functional.Tests.runtimeconfig.json --depsfile System.Net.Http.Functional.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.2-beta.25260.104/build/../tools/net/xunit.console.dll System.Net.Http.Functional.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing (dns block)

If you need me to access, download, or install something from one of these locations, you can either:


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@dotnet-policy-service dotnet-policy-service bot added the linkable-framework Issues associated with delivering a linker friendly framework label May 28, 2025
@Copilot Copilot AI changed the title [WIP] TaskHostFactory does not work with public properties with private get accessor as task parameters Fix PropertyPolicies to consider both getter and setter accessibility May 28, 2025
@Copilot Copilot AI requested a review from ericstj May 28, 2025 23:58
Copilot finished work on behalf of ericstj May 28, 2025 23:58
Copy link
Contributor Author

Copilot AI commented May 29, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of ericstj May 29, 2025 21:41
…tion into GetAccessibilityRank

Co-authored-by: ericstj <[email protected]>
Copy link
Contributor Author

Copilot AI commented May 30, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses

I tried to connect to the following addresses, but was blocked by firewall rules:

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of ericstj May 30, 2025 03:21
@ericstj ericstj marked this pull request as ready for review May 30, 2025 16:02
@ericstj ericstj requested a review from MichalStrehovsky as a code owner May 30, 2025 16:02
Copy link
Member

@ericstj ericstj left a comment

Choose a reason for hiding this comment

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

This looks good to me, lets move this out of draft. @jkotas @AaronRobinsonMSFT would you like to review?

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

See feedback above

Copilot finished work on behalf of ericstj June 3, 2025 01:56
@ericstj
Copy link
Member

ericstj commented Jun 3, 2025

I think this is ready for another pass when folks are ready.

Comment on lines 114 to 130
return GetAccessibilityRank(setter) > GetAccessibilityRank(getter) ? setter : getter;

// Define accessibility ranking
static int GetAccessibilityRank(MethodInfo method)
{
MethodAttributes access = method.Attributes & MethodAttributes.MemberAccessMask;
return access switch
{
MethodAttributes.Public => 4,
MethodAttributes.Family => 3, // protected
MethodAttributes.Assembly => 3, // internal
MethodAttributes.FamORAssem => 3, // protected internal
MethodAttributes.FamANDAssem => 2, // protected and internal
MethodAttributes.Private => 1,
_ => 0
};
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return GetAccessibilityRank(setter) > GetAccessibilityRank(getter) ? setter : getter;
// Define accessibility ranking
static int GetAccessibilityRank(MethodInfo method)
{
MethodAttributes access = method.Attributes & MethodAttributes.MemberAccessMask;
return access switch
{
MethodAttributes.Public => 4,
MethodAttributes.Family => 3, // protected
MethodAttributes.Assembly => 3, // internal
MethodAttributes.FamORAssem => 3, // protected internal
MethodAttributes.FamANDAssem => 2, // protected and internal
MethodAttributes.Private => 1,
_ => 0
};
}
return (setter.Attributes & MethodAttributes.MemberAccessMask) > (getter.Attributes & MethodAttributes.MemberAccessMask) ? setter : getter;

I do see the rationale behind the ranking. Why is FamANDAssem rank 2, and all Family, Assembly and FamORAssem share a rank 3?

I think it can be simplified down to just comparing the MemberAccess value.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, let me also add tests for that.

Copy link
Member

@ericstj ericstj Jun 5, 2025

Choose a reason for hiding this comment

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

It seems this only ever considers Private vs Public, so it's not possible test the difference between family and assembly -

if (inBaseClass && visibility == MethodAttributes.Private)
continue;
if (numCandidatesInDerivedTypes != 0 && policies.IsSuppressedByMoreDerivedMember(member, queriedMembers._members, startIndex: 0, endIndex: numCandidatesInDerivedTypes))
continue;
BindingFlags allFlagsThatMustMatch = default;
allFlagsThatMustMatch |= (isStatic ? BindingFlags.Static : BindingFlags.Instance);
if (isStatic && inBaseClass)
allFlagsThatMustMatch |= BindingFlags.FlattenHierarchy;
allFlagsThatMustMatch |= ((visibility == MethodAttributes.Public) ? BindingFlags.Public : BindingFlags.NonPublic);

I appreciate more what you're suggesting around unifying these implementations - this query abstraction ends up doing more work than really necessary.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks

@ericstj ericstj merged commit dcc9698 into main Jun 5, 2025
123 of 126 checks passed
@jkotas jkotas deleted the copilot/fix-116012 branch June 8, 2025 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
linkable-framework Issues associated with delivering a linker friendly framework
Projects
None yet
Development

Successfully merging this pull request may close these issues.

TaskHostFactory does not work with public properties with private get accessor as task parameters
5 participants