Repository navigation
System.Reflection.CustomAttributeFormatException thrown on a retrieval of derived attribute with overridden property getter - #117584
pedrobsaila wants to merge 22 commits into
Conversation
…l of derived attribute with overridden property getter
|
Hey @AaronRobinsonMSFT yes you can fold in your changes, no worries about that. |
@pedrobsaila I don't think I can push to your branch. I can create a PR from my branch or you can cherry-pick my changes, up to you. |
…nto 110412-bis
|
/ba-g Known issues. Issues fixed in #125051 |
|
@MichalStrehovsky Please take another look at this. I've narrowed the scenario considerably. |
| Type expectedExceptionType = PlatformDetection.IsNativeAot | ||
| ? typeof(AmbiguousMatchException) | ||
| : typeof(CustomAttributeFormatException); |
There was a problem hiding this comment.
Is the difference in exception type cause by the:
RuntimePropertyInfo? property = (RuntimePropertyInfo?)(type is null ?
baseAttributeType.GetProperty(name) :
baseAttributeType.GetProperty(name, type, []));
on the JIT side and
PropertyInfo? propertyInfo = walk.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
on the AOT side?
Could this be reconciled by adding type information to the GetProperty on the native AOT side too?
There was a problem hiding this comment.
Trying it, worst case we revert it and accept the difference. Best case we update the test because the behavior will now be same.
|
@MichalStrehovsky It looks like there is now a new failure for native AOT. Thoughts? I can also revert the changes and we can live with it for now. |
Do you have any thoughts on failure modes of these with CoreCLR: using System;
RunWithLog(() => typeof(C1).GetCustomAttributes(false));
RunWithLog(() => typeof(C2).GetCustomAttributes(false));
RunWithLog(() => typeof(C3).GetCustomAttributes(false));
static void RunWithLog(Action a)
{
try
{
a();
}
catch (Exception ex)
{
Console.WriteLine("*******************************************");
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
}
public class BaseAttributeWithTypeProp : Attribute
{
public virtual object Prop
{
get;
set;
}
}
public class DerivedAttributeWithObjectProp : BaseAttributeWithTypeProp
{
public override Type Prop
{
get => (Type)base.Prop;
}
}
[DerivedAttributeWithObjectProp(Prop = typeof(object))]
class C1;
[DerivedAttributeWithObjectProp(Prop = "Hello")]
class C2;
[DerivedAttributeWithObjectProp(Prop = (AttributeTargets)0)]
class C3;On .NET 10 this will print: I assume the first one might behave differently with this fix, but I don't believe the rest will be affected. This might be a game that is better not to play. We should ask Roslyn to block this at the compiler level. Then whether we have a sensible failure mode or behaviors is a different question. We certainly won't be writing C# tests for this anymore. |
|
Another thing that occurred to me to test: dynamic dd = new DerivedAttributeWithObjectProp();
dd.Prop = null;
public class BaseAttributeWithTypeProp : Attribute
{
public virtual object Prop
{
get;
set;
}
}
public class DerivedAttributeWithObjectProp : BaseAttributeWithTypeProp
{
public override Type Prop
{
get => (Type)base.Prop;
}
}This will throw Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'Property or indexer 'DerivedAttributeWithObjectProp.Prop' cannot be assigned to -- it is read only' Replacing the Then if you replace the I think we're running into a bunch of undefined corner cases on the C# language side. What the reflection side does is a separate question, but I don't think we even know what property the blob is supposed to refer to when C# makes it, and C# might not be sure either. |
|
@pedrobsaila I appreciate the contribution. My read from Aaron's and Michal's comments is that this may take a while. I'm going to move this to draft. Please feel free to move to ready when the time comes. |
|
I asked GPT-5.5 to explain what we're seeing. Roslyn has a very unique idea of what property the blob is setting, the algorithm is different from the default C# language overload resolution. Replicating it in reflection doesn't seem exactly trivial: Copilot Chat Conversation Export: Roslyn attribute argument handling explainedUser: @MichalStrehovsky @MichalStrehovskyLook at the two comments in this snippet. Why does one work and the other doesn't work. You have source code for the Roslyn C# compiler available here. CopilotThe attribute form works because Roslyn gives named attribute arguments special handling for overridden properties: it walks back to the “least overridden” property and uses that property’s type/accessors for validation. In your example, that means it effectively validates The normal assignment does not use that special attribute rule. For Key Roslyn code is in // SPEC: For each named-argument Arg in named-argument-list N:
// SPEC: Let Name be the identifier of the named-argument Arg.
// SPEC: Name must identify a non-static read-write public field or property on
// SPEC: attribute class T. If T has no such field or property, then a compile-time error occurs.
bool invalidNamedArgument = false;
TypeSymbol? namedArgumentType = null;
invalidNamedArgument |= (namedArgumentNameSymbol.DeclaredAccessibility != Accessibility.Public);
invalidNamedArgument |= namedArgumentNameSymbol.IsStatic;
if (!invalidNamedArgument)
{
switch (namedArgumentNameSymbol.Kind)
{
case SymbolKind.Field:
var fieldSymbol = (FieldSymbol)namedArgumentNameSymbol;
namedArgumentType = fieldSymbol.Type;
invalidNamedArgument |= fieldSymbol.IsReadOnly;
invalidNamedArgument |= fieldSymbol.IsConst;
break;
case SymbolKind.Property:
var propertySymbol = ((PropertySymbol)namedArgumentNameSymbol).GetLeastOverriddenProperty(this.ContainingType);
namedArgumentType = propertySymbol.Type;
invalidNamedArgument |= propertySymbol.IsReadOnly;
var getMethod = propertySymbol.GetMethod;
var setMethod = propertySymbol.SetMethod;
invalidNamedArgument = invalidNamedArgument || (object)getMethod == null || (object)setMethod == null;
if (!invalidNamedArgument)
{
invalidNamedArgument =
getMethod!.DeclaredAccessibility != Accessibility.Public ||
setMethod!.DeclaredAccessibility != Accessibility.Public;
}
break;The important line is: var propertySymbol = ((PropertySymbol)namedArgumentNameSymbol).GetLeastOverriddenProperty(this.ContainingType);
namedArgumentType = propertySymbol.Type;
internal PropertySymbol GetLeastOverriddenProperty(NamedTypeSymbol accessingTypeOpt)
{
accessingTypeOpt = accessingTypeOpt?.OriginalDefinition;
PropertySymbol p = this;
while (p.IsOverride && !p.HidesBasePropertiesByName)
{
// We might not be able to access the overridden method. For example,
//
// .assembly A
// {
// InternalsVisibleTo("B")
// public class A { internal virtual int P { get; } }
// }
//
// .assembly B
// {
// InternalsVisibleTo("C")
// public class B : A { internal override int P { get; } }
// }
//
// .assembly C
// {
// public class C : B { ... new B().P ... } // A.P is not accessible from here
// }
//
// See InternalsVisibleToAndStrongNameTests: IvtVirtualCall1, IvtVirtualCall2, IvtVirtual_ParamsAndDynamic.
PropertySymbol overridden = p.OverriddenProperty;
var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded;
if ((object)overridden == null ||
(accessingTypeOpt is { } && !AccessCheck.IsSymbolAccessible(overridden, accessingTypeOpt, ref discardedUseSiteInfo)))
{
break;
}
p = overridden;
}
return p;
}So for the attribute: [DerivedAttributeWithObjectProp(Prop = "Hello")]Roslyn:
For the normal assignment: dd.Prop = "Hello";there is no attribute-specific “least overridden property” rule. The member access is bound as the overriding property whose type is I used GitHub code search while looking this up; search results are limited and may be incomplete. You can view more matches in GitHub UI here: https://github.com/search?q=repo%3Adotnet%2Froslyn+GetLeastOverriddenProperty+Binder_Attributes.cs&type=code |
|
Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it. |
Fixes #110412
Fixes #119490
Sorry for the delay, I've been busy these last weeks