Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does GetProperties list a protected property (declared in a generic base class) twice?

When I declare the following simple classes:

class Class1<T>
{
    protected virtual T Prop1 { get; set; }
    protected virtual string Prop2 { get; set; }
}

class Class2 : Class1<string>
{
    protected override string Prop1 { get; set; }
    protected override string Prop2 { get; set; }
}

and now I use Reflection to get the properties of Class2 like this:

var hProperties = typeof(Class2).GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);

then Prop2 will be listed once while Prop1 will be listed twice! This behaviour seems strange to me. Shouldn't Prop1 and Prop2 be treated as identical??

What can I do to have Prop1 only once in hProperties? I don't want to use BindingFlags.DeclaredOnly as I also want to get other protected properties of Class1 that are not overridden.

like image 675
spetry Avatar asked Jul 10 '14 12:07

spetry


1 Answers

Let's look at the compiled assembly's metadata to make sure that the two properties have identical structure apart from the name:

enter image description here

I'm using ILDASM instead of the usual decompiler tools to make sure that nothing is hidden or displayed in a more friendly way. The two properties are identical apart from the name.

One of the two Prop1 properties returned is from Class1 and one of them is from Class2.

This appears to be a bug. The bug seems to be that base class members are not correctly added to the results. When DeclaredOnly is not specified all inherited properties should be returned as well.

I'm using DotPeek and the Reflector VS extension that allows to debug decompiled BCL code to debug the reflection code. The behavior seen in this question is triggered in this method:

    private void PopulateProperties(RuntimeType.RuntimeTypeCache.Filter filter, RuntimeType declaringType, Dictionary<string, List<RuntimePropertyInfo>> csPropertyInfos, bool[] usedSlots, ref RuntimeType.ListBuilder<RuntimePropertyInfo> list)
    {
      int token = RuntimeTypeHandle.GetToken(declaringType);
      if (MetadataToken.IsNullToken(token))
        return;
      MetadataEnumResult result;
      RuntimeTypeHandle.GetMetadataImport(declaringType).EnumProperties(token, out result);
      RuntimeModule module = RuntimeTypeHandle.GetModule(declaringType);
      int numVirtuals = RuntimeTypeHandle.GetNumVirtuals(declaringType);
      for (int index1 = 0; index1 < result.Length; ++index1)
      {
        int num = result[index1];
        if (filter.RequiresStringComparison())
        {
          if (ModuleHandle.ContainsPropertyMatchingHash(module, num, filter.GetHashToMatch()))
          {
            Utf8String name = declaringType.GetRuntimeModule().MetadataImport.GetName(num);
            if (!filter.Match(name))
              continue;
          }
          else
            continue;
        }
        bool isPrivate;
        RuntimePropertyInfo runtimePropertyInfo = new RuntimePropertyInfo(num, declaringType, this.m_runtimeTypeCache, out isPrivate);
        if (usedSlots != null)
        {
          if (!(declaringType != this.ReflectedType) || !isPrivate)
          {
            MethodInfo methodInfo = runtimePropertyInfo.GetGetMethod();
            if (methodInfo == (MethodInfo) null)
              methodInfo = runtimePropertyInfo.GetSetMethod();
            if (methodInfo != (MethodInfo) null)
            {
              int slot = RuntimeMethodHandle.GetSlot((IRuntimeMethodInfo) methodInfo);
              if (slot < numVirtuals)
              {
                if (!usedSlots[slot])
                  usedSlots[slot] = true;
                else
                  continue;
              }
            }
            if (csPropertyInfos != null)
            {
              string name = runtimePropertyInfo.Name;
              List<RuntimePropertyInfo> list1 = csPropertyInfos.GetValueOrDefault(name);
              if (list1 == null)
              {
                list1 = new List<RuntimePropertyInfo>(1);
                csPropertyInfos[name] = list1;
              }
              for (int index2 = 0; index2 < list1.Count; ++index2)
              {
                if (runtimePropertyInfo.EqualsSig(list1[index2]))
                {
                  list1 = (List<RuntimePropertyInfo>) null;
                  break;
                }
              }
              if (list1 != null)
                list1.Add(runtimePropertyInfo);
              else
                continue;
            }
            else
            {
              bool flag = false;
              for (int index2 = 0; index2 < list.Count; ++index2)
              {
                if (runtimePropertyInfo.EqualsSig(list[index2]))
                {
                  flag = true;
                  break;
                }
              }
              if (flag)
                continue;
            }
          }
          else
            continue;
        }
        list.Add(runtimePropertyInfo);
      }
    }

Why does the behavior disappear for public properties?

      if (!(declaringType != this.ReflectedType) || !isPrivate)

There's a check for that.

Class1<string>.Prop2 is filtered out here:

              bool flag = false;
              for (int index2 = 0; index2 < list.Count; ++index2)
              {
                if (runtimePropertyInfo.EqualsSig(list[index2]))
                {
                  flag = true;
                  break;
                }
              }
              if (flag)
                continue;

because EqualsSig returns true. It appears that properties are deduplicated by name and by sig if you are asking for private members... I don't know why. Seems deliberate, though.

It is tiring to follow this convoluted code. This is better and commented. I suspect they are removing private properties because you could elevate privileges by inheriting from some class to get all private members of it.

And here is the answer:

// For backward compatibility, even if the vtable slots don't match, we will still treat
// a property as duplicate if the names and signatures match.

So they added a hack for backwards compatibility.

You'll have to add your own processing to get the behavior you want. Maybe, Fastreflect can help.

like image 77
usr Avatar answered Sep 22 '22 00:09

usr