4
\$\begingroup\$

In one of my frameworks that I use with many tools I have an ExpressionVisitor whose job is to resolve the exact property, it's declaring type and instance. I later need this information to create keys/names for my settings (to query a database or read the app.config)

Namespace+Type.Member

also other (shorter) combinations of that three parts.

The code might look as if there were a couple of workarounds but it has to cope with some special cases to get to the right member and type; this means it needs to be able to resolve it from

  • inside an instance or static class () => Property
  • outside of a class () => instance.Property
  • the right instance if there are multiple expression within the same scope.
  • members of derived classes - this one is tricky because expressions resolve to the base class member (which apparently is not a bug) so just before returning the result I have to check if the resolved type and the declaring-type are different and if this is true I need to get another member - this time the right one. This is important because otherwise the name would be invalid as the name of the base class would be wrong.

Here's an example of it pure usage:

void Main()
{
    var user1 = new User();
    var user2 = new SuperUser();

    // multiple expressions
    var expr1 = (Expression<Func<object>>)(() => user1.Name);
    var expr2 = (Expression<Func<object>>)(() => user2.Name);
    var expr3 = (Expression<Func<object>>)(() => User.Default);

    // needs to be able to identify the right one
    SettingVisitor.GetSettingInfo(expr2).Dump(); // SuperUser, instance, Name
    SettingVisitor.GetSettingInfo(expr1).Dump(); // User, instance, Name
    SettingVisitor.GetSettingInfo(expr3).Dump(); // User, null, Default
}

class User
{
    public string Name { get; set; }

    public static User Default { get; } = new User();
}

class SuperUser : User
{
    public string SuperName { get; set; }
}

I have more helpers build on top of it so the actual usage is much more convenient and moste of the time looks like this:

class User
{
  ...

  public string Name 
  { 
      get => _configuration.GetValue(() => Name);
      set => _configuration.SetValue(() => Name, value);
  }
}

An this is the API (1:1 copy from my project):

internal class SettingVisitor : ExpressionVisitor
{
    private readonly bool _nonPublic;
    private Type _type;
    private object _instance;
    private MemberInfo _member;
    private string _closureMemberName;

    private SettingVisitor(bool nonPublic) => _nonPublic = nonPublic;

    public static (Type Type, object Instance, MemberInfo Member) GetSettingInfo(LambdaExpression expression, bool nonPublic = false)
    {
        var visitor = new SettingVisitor(nonPublic);
        visitor.Visit(expression);

        if (visitor._type is null)
        {
            throw ("UnsupportedSettingExpression", "Member's declaring type could not be determined.").ToDynamicException();
        }

        // This fixes the visitor not resolving the overriden member correctly.
        if (visitor._member.DeclaringType != visitor._type)
        {
            visitor._member = visitor._type.GetMember(visitor._member.Name).Single();
        }

        return (visitor._type, visitor._instance, visitor._member);
    }   

    protected override Expression VisitMember(MemberExpression node)
    {
        // Supports:
        // - static fields and properties
        // - instance fields and properties

        // The first member is the setting.
        _member = _member ?? node.Member;

        switch (node.Member)
        {
            // (() => Type.Member) - static usage 
            case FieldInfo field when field.IsStatic:
            case PropertyInfo property when property.GetGetMethod(_nonPublic).IsStatic:
                _type = node.Member.DeclaringType;
                break;

            // (() => instance.Member) - via instance (also this)
            case FieldInfo field:
            case PropertyInfo property:
                // This is necessary to correctly resolve the member when there are multiple instances.
                _closureMemberName = node.Member.Name;
                break;
        }

        return base.VisitMember(node);
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        // Supports:
        // - Member (closures)
        // - instance.Member

        if (node.Type.Name.StartsWith("<>c__DisplayClass"))
        {
            var closureType = node.Type.GetField(_closureMemberName);
            _type = closureType.FieldType;
            _instance = closureType.GetValue(node.Value);
        }
        else
        {
            _type = node.Value.GetType();
            _instance = node.Value;
        }

        return base.VisitConstant(node);
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        // Supports:
        // - types passed via generics like .From<T>().Select(x => x.Y);
        _type = node.Type;
        return base.VisitParameter(node);
    }
}

Now you can tear it apart and tell me how good or bad it is and if there is anything that can be improved.

\$\endgroup\$
6
  • \$\begingroup\$ Why do you even need to get MemberInfo of the override? (I mean you have the _type already). The only think that smells a bit is node.Type.Name.StartsWith("<>c__DisplayClass"), but the only alternative I can think of would be to detect CompilerGeneratedAttribute. I would also like to see the full usage, because it reminds me of my own reflection-based systems for configuration parsing and storing (written for .NET 2.0), but that is entirely up to you. \$\endgroup\$
    – user52292
    Nov 9, 2018 at 19:28
  • \$\begingroup\$ @firda I need it to get the attribute it might be decorated with that customizes its name, data provider or name schema. I need it also to deserialize the json (or soemthing else) to a concrete type when I get it from a database or other storage. You can find some examples here in my integration tests. The thing that smells is how the compiler creates closures that are necessary to hold the property you pass via an expression. This string doesn't change so it's ok to use it this way. \$\endgroup\$
    – t3chb0t
    Nov 9, 2018 at 19:34
  • \$\begingroup\$ @firda if you'd like to see even more, this is the data structure that gets initializes with the three parameters determined by the visitor and initializes all the data that I need later to construct setting names and resolve data providers. Oh, of course, properties can also be decorated with validation attributes. \$\endgroup\$
    – t3chb0t
    Nov 9, 2018 at 19:48
  • \$\begingroup\$ Makes sense with the decorators. As for "<>c__DisplayClass", I was wondering if Mono and Core do the same, thus the smell. Did I get it right, that you never actually call the lamda (get => _configuration.GetValue(() => Name);) but use it only as some kind of descriptor, that you parse using SettingVisitor to convert it to SQL or Application Settings query? A bit strange approach, but you get compiler checks for free, that is nice. \$\endgroup\$
    – user52292
    Nov 9, 2018 at 19:58
  • \$\begingroup\$ @firda that's correct, I never call the lambda because I have no use for it. This is just a relatively cheap and very convenient reflection trick. The whole purpose of it is to avoid magic strings of any kind as keys to get or set settings. I derive them all from properties. here are some examples from my tests. I use it to create names of various depth/complexity/length, depending on how unique they should be. \$\endgroup\$
    – t3chb0t
    Nov 9, 2018 at 20:19

1 Answer 1

2
\$\begingroup\$

I have read the link and the conclusion about the bug was: "no bug, but as designed". This is something I cannot comprehend.

This clearly is bad design. When visiting SuperUser for MemberInfo Name, I expect correct behavior; that is:

  • DeclaringType should be User
  • ReflectedType should be SuperUser // <- this is also User, which is bad design!

Regarding your implementation, I would favor ReflectedType over DeclaringType.

 case PropertyInfo property when property.GetGetMethod(_nonPublic).IsStatic:
       _type = node.Member.ReflectedType;
       break;

And this one (but this is bad design from ExpressionVisitor as ReflectedType should have been correct in the first place!)

if (visitor._member.ReflectedType != visitor._type)
{
     visitor._member = visitor._type.GetMember(visitor._member.Name).Single();
}
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Yeah, that's a dirty hack. I've tried to convince them but as you've seen, they didn't want to listen ;-( I'll need to take a closer look to the ReflectedType. \$\endgroup\$
    – t3chb0t
    Jun 16, 2019 at 14:30
  • \$\begingroup\$ @t3chb0t You should, because you had some trouble with it a couple of days ago. If you would have mentioned this property to these bug-writers, perhaps they could have been convinced :-p \$\endgroup\$
    – dfhwze
    Jun 16, 2019 at 14:31
  • 1
    \$\begingroup\$ I now know how it works and I like it! And I want to turn back time an fix all the bugs that not using it had caused ;-] \$\endgroup\$
    – t3chb0t
    Jun 16, 2019 at 15:12
  • 1
    \$\begingroup\$ You could have been 'don t3chb0t'. Imagine the life you could have had.. \$\endgroup\$
    – dfhwze
    Jun 16, 2019 at 15:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.