diff --git a/src/Common/tests/TestUtilities/TestAccessor.cs b/src/Common/tests/TestUtilities/TestAccessor.cs index 8230ef9ee42..d5591b8eb72 100644 --- a/src/Common/tests/TestUtilities/TestAccessor.cs +++ b/src/Common/tests/TestUtilities/TestAccessor.cs @@ -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; default: throw new InvalidOperationException(); } @@ -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); + } } } diff --git a/src/test/unit/System.Windows.Forms/TestAccessorTests.cs b/src/test/unit/System.Windows.Forms/TestAccessorTests.cs index f19de0cc377..2c9bcf48f5a 100644 --- a/src/test/unit/System.Windows.Forms/TestAccessorTests.cs +++ b/src/test/unit/System.Windows.Forms/TestAccessorTests.cs @@ -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(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(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(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).