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.
MemberInfo
of the override? (I mean you have the_type
already). The only think that smells a bit isnode.Type.Name.StartsWith("<>c__DisplayClass")
, but the only alternative I can think of would be to detectCompilerGeneratedAttribute
. 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\$"<>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 usingSettingVisitor
to convert it to SQL or Application Settings query? A bit strange approach, but you get compiler checks for free, that is nice. \$\endgroup\$