5

When trying to get properties accessors from derived properties or use CanRead / CanWrite, for some reason base auto-properties are not taken into account.

CanRead and CanWrite return values based only on the derived type, also GetMethod and SetMethod don't contain methods from base type.

However when writing code accessors from base type can be used (so that we can read overridden auto-property with only setter defined in derived type).

Here is the code to reproduce it written as an unit test:

using System.Reflection;
using NUnit.Framework;

[TestFixture]
public class PropertiesReflectionTests
{
    public class WithAutoProperty
    {
        public virtual object Property { get; set; }
    }

    public class OverridesOnlySetter : WithAutoProperty
    {
        public override object Property
        {
            set => base.Property = value;
        }
    }

    private static readonly PropertyInfo Property = typeof(OverridesOnlySetter).GetProperty(nameof(OverridesOnlySetter.Property));

    // This one is passing
    [Test]
    public void Property_ShouldBeReadable()
    {
        var overridesOnlySetter = new OverridesOnlySetter {Property = "test"};

        Assert.AreEqual(overridesOnlySetter.Property, "test");
    }

    // This one is failing
    [Test]
    public void CanRead_ShouldBeTrue()
    {
        Assert.True(Property.CanRead);
    }

    // And this is failing too
    [Test]
    public void GetMethod_ShouldBeNotNull()
    {
        Assert.NotNull(Property.GetMethod);
    }
}

I expected last two tests to pass, what am I missing?

Ivan Shimko
  • 179
  • 1
  • 9
  • If you don't specify a getter, then obviously `Property.GetMethod` will return `null`. What did you expect? – haim770 Sep 02 '19 at 20:41
  • @haim770 The property is overridden and I can read a value from it (since it has a getter in base class). However I can't get this get method via reflection. That is what bothers me here. – Ivan Shimko Sep 03 '19 at 04:44
  • @haim I updated the example and included passing test that reads from this property via an object of derived type. – Ivan Shimko Sep 03 '19 at 05:03

2 Answers2

4

I expected last two tests to pass, what am I missing?

For a definitive answer, you'd have to ask the people who originally designed .NET and its type system. That said…

It seems to me that this is consistent with the goal of reflection providing information about how a type was written. Consider the alternative: what if the PropertyInfo object returned included both the setter from the derived class and the getter from the base class. It would be considerably more difficult to understand from the returned result what was actually declared where, and the PropertyInfo object itself would arguably be inconsistent. This is because there is the PropertyInfo.DeclaringType property which implies that all of the information for the member pertains just to that declaring type.

With members which are neither properties nor events (both of which encapsulate a pair of class members), you get the behavior you expected. Unless of course you pass BindingFlags.DeclaredOnly, which restricts the returned information to the declaring type. But in the case of those types of members, the DeclaringType property tells you unequivocally in which type the member was actually declared.

With a property, the DeclaringType tells you in which class the property was declared. And then the SetMethod and GetMethod properties tell you what that class declared.

IMHO, this makes the reflection API simpler, more consistent, and easier to understand. It does mean that you have to do a little more work to analyze virtual properties. But then, reflection is always going to involve "a little more work". :)

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • What you're saying makes really good sense! Another interesting part of it mentioned in comments of another answer here is that using `sealed override` makes tests pass. However, is there any system built-in way to reliably get all the accessors from virtual property? – Ivan Shimko Sep 03 '19 at 05:42
  • _"is there any system built-in way to reliably get all the accessors from virtual property?"_ -- in just one call? I don't think so. But it's not hard to walk the type hierarchy to identify the relevant accessors. I agree that it's interesting that using `sealed` changes how the accessors are represented; note that when you do this, the `DeclaringType` of the getter is the _derived_ type, not the base type where the getter is actually declared. If was more of an expert in the type system, I'd probably know exactly why this is; I assume it has to do with changes `sealed` makes to the type. – Peter Duniho Sep 03 '19 at 05:53
  • 1
    I agree with the answers provided here, even the "ask the designers", and I think this in any case would fall under "cannot change this now" type of response. I think the naming here is flawed, however, "CanRead" should probably have been named "HasGetter" or similar, since the PropertyInfo object says false even though you quite clearly **can read** the property. But even if they were to agree, they would almost certainly not change this now. – Lasse V. Karlsen Sep 03 '19 at 07:08
  • In my (new) answer, I try to do that "a little more work" you mention in the end. – Jeppe Stig Nielsen Sep 03 '19 at 07:41
  • @JeppeStigNielsen @Peter I've posted a [new question](https://stackoverflow.com/questions/57775951/why-does-sealed-override-of-a-property-in-c-sharp-copy-not-overriden-accessor-fr) regarding `sealed override`, maybe we'll get detailed explanation there. – Ivan Shimko Sep 03 '19 at 16:58
2

As Peter Duniho explains in his answer, this seems to require some work.

It would be easier if PropertyInfo had something like GetBaseDefinition(), but it does not (also this thread), so we have to go through the accessor method. It would also be easier if the method info for the accessor had a reference back to the property info, but it does not, so we run through all properties and assume there is exactly on match.

So here is a naive solution:

// does not necessarily work as expected if the property or one of its accessors
// (getter or setter) is not public
internal static bool CanReadExt(PropertyInfo pi)
{
  if (pi.CanRead)
    return true;

  // assume we have a setter since we do not have a getter
  var setter = pi.SetMethod
    ?? throw new Exception("Neither getter nor setter in property?");

  // try to acquire setter of base property
  var baseSetter = setter.GetBaseDefinition();

  // if the property was not overridden, we can return
  if (setter.DeclaringType == baseSetter.DeclaringType)
    return false;

  // try to find the base property
  var basePi = baseSetter.DeclaringType.GetProperties()
    .SingleOrDefault(x => x.SetMethod == baseSetter)
    ?? throw new Exception("Set accessor was overridden but we could not find property info for base property.");

  // recursively call ourselves
  return CanReadExt(basePi);
}

It returns true with your PropertiesReflectionTests.Property, so it works in that case. More care would be needed to handle every case, I guess.

This method can be made an extension method if you prefer.

A similar method CanWriteExt could be written.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181