Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/Common/tests/TestUtilities/TestAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,16 @@ public override bool TrySetMember(SetMemberBinder binder, object? value)
case FieldInfo fieldInfo:
fieldInfo.SetValue(_instance, value);
break;
case PropertyInfo propertyInfo:
case PropertyInfo { CanWrite: true } propertyInfo:
propertyInfo.SetValue(_instance, value);
break;
case PropertyInfo propertyInfo:
// Non-public auto property without a setter (e.g. get-only). Fall back to the
// compiler-generated backing field so tests can still set up state directly.
FieldInfo backingField = GetAutoPropertyBackingField(propertyInfo.DeclaringType!, propertyInfo.Name)
?? throw new InvalidOperationException($"'{propertyInfo.Name}' has no setter and no backing field could be found.");
backingField.SetValue(_instance, value);
break;
Comment on lines +170 to +176
default:
throw new InvalidOperationException();
}
Expand Down Expand Up @@ -235,5 +242,11 @@ public override bool TryGetMember(GetMemberBinder binder, out object? result)

return info;
}

private static FieldInfo? GetAutoPropertyBackingField(Type type, string propertyName)
{
string backingFieldName = $"<{propertyName}>k__BackingField";
return type.GetField(backingFieldName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic);
}
}
}
59 changes: 59 additions & 0 deletions src/test/unit/System.Windows.Forms/TestAccessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,65 @@ public void TestAccessor_DynamicAccess_BaseClassMethod()
Assert.Equal(42, (int)accessor.AMethod());
}

[Fact]
public void TestAccessor_CanReadAndWrite_NonPublicAutoProperty()
{
AutoPropertyClass obj = new();
var accessor = new TestAccessor<AutoPropertyClass>(obj);

accessor.Dynamic.Count = 25;
int value = accessor.Dynamic.Count;
Assert.Equal(25, value);
}

[Fact]
public void TestAccessor_CanWrite_NonPublicReadOnlyAutoProperty_ViaBackingField()
{
// Count has no setter, so writing it can only be accomplished by falling back to the
// compiler-generated backing field.
ReadOnlyClass obj = new();
var accessor = new TestAccessor<ReadOnlyClass>(obj);

int initial = accessor.Dynamic.Count;
Assert.Equal(5, initial);

accessor.Dynamic.Count = 42;
int updated = accessor.Dynamic.Count;
Assert.Equal(42, updated);
}

[Fact]
public void TestAccessor_FindsInherited_NonPublicReadOnlyAutoPropertyBackingField()
{
DerivedClass obj = new();
var accessor = new TestAccessor<DerivedClass>(obj);

accessor.Dynamic.Count = 15;
int value = accessor.Dynamic.Count;
Assert.Equal(15, value);
}

#pragma warning disable IDE0051 // unaccessed private
private class AutoPropertyClass
{
private int Count { get; set; }
}

private class ReadOnlyClass
{
private int Count { get; } = 5;
}

private class BaseClass
{
private int Count { get; }
}

private class DerivedClass : BaseClass
{
}
#pragma warning restore IDE0051

// As you can't use a ref struct as a generic parameter to Action/Func, you
// need to use a defined delegate to access an internal method that takes
// or returns a ref struct (such as Spans).
Expand Down