46

I'm developing a API that uses lambda expressions to specify properties. I'm using this famous piece of code similar to this one (this is simplified and incomplete, just to make clear what I'm talking about):

public void Foo<T, P>(Expression<Func<T, P>> action)
{
    var expression = (MemberExpression)action.Body;
    string propertyName = expression.Member.Name;
    // ...
}

To be called like this:

Foo((String x) => x.Length);

Now I would like to specify a property path by chaining property names, like this:

Foo((MyClass x) => x.Name.Length);

Foo should be able to split the path into its property names ("Name" and "Length"). Is there a way to do this with reasonable effort?


There is a somehow similar looking question, but I think they are trying to combine lambda expressions there.

Another question also is dealing with nested property names, but I don't really understand what they are talking about.

Community
  • 1
  • 1
Stefan Steinegger
  • 63,782
  • 15
  • 129
  • 193

6 Answers6

38

Something like this?

public void Foo<T, P>(Expression<Func<T, P>> expr)
{
    MemberExpression me;
    switch (expr.Body.NodeType)
    {
        case ExpressionType.Convert:
        case ExpressionType.ConvertChecked:
            var ue = expr.Body as UnaryExpression;
            me = ((ue != null) ? ue.Operand : null) as MemberExpression;
            break;
        default:
            me = expr.Body as MemberExpression;
            break;
    }

    while (me != null)
    {
        string propertyName = me.Member.Name;
        Type propertyType = me.Type;

        Console.WriteLine(propertyName + ": " + propertyType);

        me = me.Expression as MemberExpression;
    }
}
LukeH
  • 263,068
  • 57
  • 365
  • 409
26

I played a little with ExpressionVisitor:

public static class PropertyPath<TSource>
{
    public static IReadOnlyList<MemberInfo> Get<TResult>(Expression<Func<TSource, TResult>> expression)
    {
        var visitor = new PropertyVisitor();
        visitor.Visit(expression.Body);
        visitor.Path.Reverse();
        return visitor.Path;
    }

    private class PropertyVisitor : ExpressionVisitor
    {
        internal readonly List<MemberInfo> Path = new List<MemberInfo>();

        protected override Expression VisitMember(MemberExpression node)
        {
            if (!(node.Member is PropertyInfo))
            {
                throw new ArgumentException("The path can only contain properties", nameof(node));
            }

            this.Path.Add(node.Member);
            return base.VisitMember(node);
        }
    }
}

Usage:

var path = string.Join(".", PropertyPath<string>.Get(x => x.Length).Select(p => p.Name));
Johan Larsson
  • 17,112
  • 9
  • 74
  • 88
  • Thanks for ExpressionVisitor. This was a really clean solution. I would personally create a PathVisitor instance per method call instead of using lock, but the docs do not say anything about recommendations either way or if it is a heavy object to create. It does not have a Dispose() so that indicates to me it does not hold a lot of resources. – angularsen Dec 17 '16 at 10:58
  • I added a revised version with no locking, moved generic params to class so you only need to specify TSource and a convenience method that returns a string with configurable string separator: https://gist.github.com/anjdreas/862c1cd9983d7525d2ddee0bb2706c3a – angularsen Dec 17 '16 at 11:38
  • Yeah, locking is dumb there, updating the answer. – Johan Larsson Dec 17 '16 at 12:44
  • @JohanLarsson Can you recall the reason for calling ```.Reverse``` on the path? – DGreen Sep 11 '18 at 15:27
  • I wanted the path in the reverse order of what the visitor produced :) Modify it to your needs, remember to write a couple of tests. – Johan Larsson Sep 11 '18 at 17:10
  • I really like this solution because it's clean, but i'd like to be able to invalidate method calls from being used. This seems to just ignore them, and ExpressionVisitor doesn't let you override the `VisitMethodCall()` method to allow me to throw, unfortunately. Anyone have an idea? – DLeh May 17 '21 at 22:04
  • Does this work with index properties, such as arrays and custom index? – XzaR Oct 07 '22 at 06:51
16

Old question, I know... but if it's only the names you need, an even simpler way to do it is:

expr.ToString().Split('.').Skip(1) 

EDIT:

public class A
{
    public B Property { get; set; }
}

public class B
{
    public C field;
}

[Fact]
public void FactMethodName()
{
    var exp = (Expression<Func<A, object>>) (x => x.Property.field);
    foreach (var part in exp.ToString().Split('.').Skip(1))
        Console.WriteLine(part);

    // Output:
    // Property
    // field
}
asgerhallas
  • 16,890
  • 6
  • 50
  • 68
  • Hmmm, that didn't work for me (`.ToString` only gave the last property name). Do you have a larger code sample with usage? – Pat Jul 31 '13 at 20:12
  • @Pat I edited in some working code. Hope that helps. Though a little late :) – asgerhallas Dec 11 '13 at 08:51
  • `ToString` wont work in case of value types being boxed, apart from being terribly slow. Just beware. – nawfal Dec 18 '13 at 20:49
  • 1
    I'm a little uncomfortable doing it this way. ToString is supposed to be for converting to a visual representation, it isn't a contract in any way and so (although unlikely) the format is permitted to change in the future. – Peter Morris Aug 24 '17 at 10:21
  • @PeterMorris I agree, but it is a terribly easy way to do it :) – asgerhallas Sep 09 '19 at 07:03
1

I have a shared .NET standard DTO between client and server and expressions are a great way to create query strings that can be rebuilt and executed Api side.

A perfect way to create Type safe queries over the wire.

I digress, I needed a property path also

x => x.Siblings.Age 

To produce a string like

"Siblings.Age"

I went with this

    public static string GetMemberPath(MemberExpression me)
    {
        var parts = new List<string>();

        while (me != null)
        {
            parts.Add(me.Member.Name);
            me = me.Expression as MemberExpression;
        }

        parts.Reverse();
        return string.Join(".", parts);
    }
  • Doesn't work for my scenario as me.Expression is not a MemberExpression, rather its a SimpleBinaryExpression. My expression is similar to: x => x.persons[0].Age – Teevus Apr 04 '20 at 01:28
  • 1
    @Teevus hadn't needed that, but you should be able to evaluate the binary expression first and then the member expression? – Billy Jake O'Connor Apr 04 '20 at 11:20
  • 1
    I ended up using ExpressionHelper.GetExpressionText(), works like a charm, but obviously requires a reference to ASP.NET MVC, so not suitable for everyone – Teevus Apr 05 '20 at 11:53
0
    public static string GetPath<T, TProperty>(this Expression<Func<T, TProperty>> exp)
    {
        return string.Join(".", GetItemsInPath(exp).Reverse());
    }

    private static IEnumerable<string> GetItemsInPath<T, TProperty>(Expression<Func<T, TProperty>> exp)
    {
        if (exp == null)
        {
            yield break;
        }
        var memberExp = FindMemberExpression(exp.Body);
        while (memberExp != null)
        {
            yield return memberExp.Member.Name;
            memberExp = FindMemberExpression(memberExp.Expression);
        }
    }
procma
  • 1,174
  • 3
  • 14
  • 24
0

I refactored these answers to use recursion, so no need for order reversal. I only need the property tree, so left out all but MemberExpressions. Should be simple to add functionality if need be.

public IEnumerable<string> PropertyPath<TModel, TValue>(
    Expression<Func<TModel, TValue>> expression)
{
    if (expression.Body is MemberExpression memberExpression)
        return PropertyPathRecurse(memberExpression);
        
    return Enumerable.Empty<string>();
}

private IEnumerable<string> PropertyPathRecurse(MemberExpression? expression)
{
    if (expression is null)
        return Enumerable.Empty<string>();
        
    return PropertyPathRecurse(expression.Expression as MemberExpression)
        .Append(expression.Member.Name);
}
Graeme Job
  • 1,089
  • 1
  • 8
  • 4