4

In C# overriding auto-property and providing only one accessor makes reflection via PropertyInfo "lose" the other one, even though it is defined in base class.

It can look strange at a first glance, but seems to be reasonable after more detailed analysis.

However, changing override to sealed override also changes this behavior and allows to get all accessors:

using System.Reflection;
using NUnit.Framework;

[TestFixture]
public class PropertySealedOverrideReflectionTests
{
    public class Base
    {
        public virtual object Override { get; set; }
        public virtual object SealedOverride { get; set; }
    }

    public class Derived : Base
    {
        public override object Override { set => base.Override = value; }
        public sealed override object SealedOverride { set => base.Override = value; }
    }

    [Test]
    public void Override()
    {
        PropertyInfo overrideProperty = typeof(Derived).GetProperty(nameof(Derived.Override));
        // ---
        // getter from base class is "invisible" here
        // ---
        Assert.False(overrideProperty.CanRead);
        Assert.Null(overrideProperty.GetMethod);
    }

    [Test]
    public void SealedOverride()
    {
        PropertyInfo sealedOverrideProperty = typeof(Derived).GetProperty(nameof(Derived.SealedOverride));
        // ---
        // after changing to "sealed override" getter is in place
        // ---
        Assert.True(sealedOverrideProperty.CanRead);
        Assert.NotNull(sealedOverrideProperty.GetMethod);
    }
}

What does compiler change in type to do sealed override in provided scenario? What is the reason of such behavior?

Ivan Shimko
  • 179
  • 1
  • 9
  • Do you see this same behavior when the names are the same? Meaning `Override` in base class, `sealed override Override` in child class? All `sealed` should do in this instance is prevent you from overriding in another child class, like a child of Derived wouldn't be able to override SealedOverride – DetectivePikachu Sep 03 '19 at 17:05
  • Sorry didn't get your question. I named properties this way just for illustration purposes. – Ivan Shimko Sep 03 '19 at 17:08
  • 2
    It must be the case that the only way the C# compiler can ensure that the entire property override gets sealed, is by sealing both the `get` and the `set` accessor. Then to achieve that, it has to actually override the `get` accessor even though you do not write any getter in your override. Of course the C# compiler will just call the base implementation in the accessor it "invents" itself. With reflection, the presence of that get accessor shows up. – Jeppe Stig Nielsen Sep 03 '19 at 17:56
  • @detective Thanks, but it's clear from C# language perspective, what `sealed override` should do, the problem and behavior I refer to is accessibility of the property getter from base class via reflection, please see unit tests from the question. – Ivan Shimko Sep 03 '19 at 17:58

1 Answers1

3

What does compiler change in type to do sealed override in provided scenario? What is the reason of such behavior?

Because attributes such as "virtual" and "sealed" (or "final" in CLR parlance) apply to methods and not properties, the only way for the compiler to seal a property is to mark its methods as sealed. But, what if one or the other of the setter and getter is missing? Should the compiler mark the base type's method as sealed?

No, I think obviously not. :)

So, in order for there to be a method for the compiler to mark as sealed, it has to make one, even though you didn't declare one.

IMHO, it's instructive to look at both the information reflection gives you, as well as what the code actually compiles to. Here's a simple code example based on your scenario:

class Base
{
    public virtual object P1 { get; set; }
    public virtual object P2 { get; set; }
    public virtual object P3 { get; set; }
}

class Derived : Base
{
    public sealed override object P1 { set => base.P1 = value; }
    public override object P2 { set => base.P2 = value; }
}

I.e. the base class declares three virtual properties, all identical except for the name. Then the derived class overrides two of those virtual properties, sealing one of them.

If you take a look at the differences between the descriptor objects returned by reflection for the properties in Derived, you'll notice some things:

  • Even though we haven't declared a getter for P1, reflection returns one anyway, with the DeclaringType property returning the Derived type.
  • But for P2, reflection does not return a getter (this relates to your earlier question).
  • For P3, a getter is returned again, but for this one, the DeclaringType returns the Base type.
  • For the P1 getter, the MethodBase.Attributes includes MethodAttributes.Final, indicating that the method is sealed. This is the attribute that the compiler can't put on the base type (for obvious reasons), and so had to implement the method in the derived type, so that the attribute would have some place to live.

Finally, if we look at the generated code, we find that indeed, not only has the compiler created this method for us, it does in fact just delegate directly to the base class getter:

.method public hidebysig specialname virtual final 
        instance object  get_P1() cil managed
{
  // Code size       7 (0x7)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call       instance object TestSO57762322VirtualProperty.Base::get_P1()
  IL_0006:  ret
} // end of method Derived::get_P1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • 1
    Remarks: For `P3`, you can see already on the `PropertyInfo` that the `.DeclaringType` is `Base`, i.e. different from the type you asked about (which is kept in `.ReflectedType`).If you do not want anything not changed in the class you ask about, you can demand "declared only", passing e.g. `BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public` to the relevant overload of `GetProperty`. For `P1`, the `MethodBase` of the getter and setter also has a property `.IsFinal`, so you do not _have_ to check `.Attributes`. – Jeppe Stig Nielsen Sep 04 '19 at 06:53