0

I want to pass the name of a property of a model to a method. Instead of using the name as string, I am using lambda expression as it is easy to make a typo, and also property names may be changed. Now if the property is a simple property (e.g: model.Name) I can get the name from the expression. But if it is a nested property (e.g: model.AnotherModel.Name) then how can I get full text ("AnotherModel.Name") from the expression. For example, I have the following classes:

public class BaseModel
{
    public ChildModel Child { get; set; }
    public List<ChildModel> ChildList { get; set; }

    public BaseModel()
    {
        Child = new ChildModel();
        ChildList = new List<ChildModel>();
    }
}

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

public void GetExpressionText<T>(Expression<Func<T, object>> expression)
{
    string expText;
    //what to do??
    return expText;
}


GetExpressionText<BaseModel>(b => b.Child); //should return "Child"
GetExpressionText<BaseModel>(b => b.Child.Name); //should return "Child.Name"
GetExpressionText<BaseModel>(b => b.ChildList[0].Name); //should return "ChildList[0].Name"
5-4-K
  • 15
  • 6
  • *Why* do you want to do that? The typical case is to use with INotifyPropertyChanged. There are duplicates that show how to do this, although [CallerMemberNameAttribute](https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.callermembernameattribute(v=vs.110).aspx) and `nameof()` . – Panagiotis Kanavos Jun 07 '17 at 08:31
  • Nested models/viewmodels should provide *their own* INotifyPropertyChanged. This makes binding and notifications a lot easier. Instead of the parent raising INPC for its children, the children themselves do it and notify *their* bindings – Panagiotis Kanavos Jun 07 '17 at 08:37
  • 1
    @PanagiotisKanavos This is a clear example of XY problem... But closing as duplicate of something else is quite wrong... His X was quite clear and was completely contextless about the Y and the question could stand "alone". – xanatos Jun 07 '17 at 08:46
  • @xanatos the code is the same in both cases. Duplicates of *this* question are in the context of INPC. I picked that specific duplicate because it shows most methods used to retrieve the expression text - reflection and `MemberExpression`, CallerMemberName, nameof. A better dupclicate would be one that shows how to handle *all* expressions, which requires a full-length article – Panagiotis Kanavos Jun 07 '17 at 08:50

5 Answers5

1

My first thought was to use expression.Body.ToString() and tweak that a bit, but you would still need to deal with Unary (convert) etc. Assuming this is for logging and you want more control, the below can be used for formatting as wanted (e.g. if you want Child->Name for display purposes, string.Join("->",..) can be used). It may not be complete, but should you find any unsupported types, they should be easy to add.

PS: this post was generated before the question was closed. Just noticed it was reopend and submitting it now, but I haven't checked if particulars have been changed.

public string GetName(Expression e, out Expression parent)
{   
    if(e is MemberExpression  m){ //property or field           
        parent = m.Expression;
        return m.Member.Name;
    }
    else if(e is MethodCallExpression mc){          
        string args = string.Join(",", mc.Arguments.SelectMany(GetExpressionParts));
        if(mc.Method.IsSpecialName){ //for indexers, not sure this is a safe check...           
            return $"{GetName(mc.Object, out parent)}[{args}]";
        }
        else{ //other method calls      
            parent = mc.Object;
            return $"{mc.Method.Name}({args})";                     
        }
    }
    else if(e is ConstantExpression c){ //constant value
        parent = null;
        return c.Value?.ToString() ?? "null";       
    }
    else if(e is UnaryExpression u){ //convert
        parent=  u.Operand;
        return null;
    }
    else{
        parent =null;
        return e.ToString(); 
    }
}

public IEnumerable<string> GetExpressionParts(Expression e){
    var list = new List<string>();
    while(e!=null && !(e is ParameterExpression)){
        var name = GetName(e,out e);
        if(name!=null)list.Add(name);
    }
    list.Reverse();
    return list;
}

public string GetExpressionText<T>(Expression<Func<T, object>> expression) => string.Join(".", GetExpressionParts(expression.Body));
Me.Name
  • 12,259
  • 3
  • 31
  • 48
  • Thanks, it's working for me. I just had to make a little change because I don't have C# 7 (I am assuming `e is UnaryExpression u` is a new feature in C# 7). – 5-4-K Jun 07 '17 at 15:24
  • Yep, pattern matching is new in 7.0. Great feature :) Glad you could get it to work for your version! – Me.Name Jun 07 '17 at 17:06
0

You could use the C# 6.0 feature: nameof(b.Child) "Used to obtain the simple (unqualified) string name of a variable, type, or member." which will also change on renaming. But this will only return the propertyname and not the complete path. Returning a complete path will be difficult, because only one instance is passed.

Jeroen van Langen
  • 21,446
  • 3
  • 42
  • 57
  • nameof isn't what I am looking for. As i said in the question I need the full name. – 5-4-K Jun 07 '17 at 07:24
  • @5-4-K *why* do you want this? Expressions were used to implement INotifyPropertyChanged but this became trivial with CallerMemberName in C# 5 and nameof in C# 6 – Panagiotis Kanavos Jun 07 '17 at 08:29
  • @PanagiotisKanavos it could be useful for logging scenarios e.g. ArgumentExceptions where an issues is on a property of the method parameter. – bugged87 Oct 22 '18 at 23:56
0

Closest i know right now is by simply using expression.Body.ToString() which would result in b.ChildList.get_Item(0).Name as a result.

You would still have to remove the first b. from the string if not wanted, and you could go even further to your intended output with Regex by replacing the get_Item(0) with the typical Index-Accessor.

(Also i had to make the ChildList and the Name-Property of ChildModel public to get it to work)

sydeslyde
  • 135
  • 7
0

This Should get you most of the way there:

    public static string GetFullPath<T>(Expression<Func<T>> action)
    {
        var removeBodyPath = new Regex(@"value\((.*)\).");
        var result = action.Body.ToString();
        var replaced = removeBodyPath.Replace(result, String.Empty);
        var seperatedFiltered = replaced.Split('.').Skip(1).ToArray();
        return string.Join(".", seperatedFiltered);
    }
robjam
  • 969
  • 1
  • 11
  • 24
0

It gets ugly quite quickly...

public static string GetExpressionText<T>(Expression<Func<T, object>> expression)
{
    bool needDot = false;

    Expression exp = expression.Body;

    string descr = string.Empty;

    while (exp != null)
    {
        if (exp.NodeType == ExpressionType.MemberAccess)
        {
            // Property or field
            var ma = (MemberExpression)exp;
            descr = ma.Member.Name + (needDot ? "." : string.Empty) + descr;
            exp = ma.Expression;
            needDot = true;
        }
        else if (exp.NodeType == ExpressionType.ArrayIndex)
        {
            // Array indexer
            var be = (BinaryExpression)exp;

            descr = GetParameters(new ReadOnlyCollection<Expression>(new[] { be.Right })) + (needDot ? "." : string.Empty) + descr;

            exp = be.Left;
            needDot = false;
        }
        else if (exp.NodeType == ExpressionType.Index)
        {
            // Object indexer (not used by C#. See ExpressionType.Call)
            var ie = (IndexExpression)exp;

            descr = GetParameters(ie.Arguments) + (needDot ? "." : string.Empty) + descr;

            exp = ie.Object;
            needDot = false;
        }
        else if (exp.NodeType == ExpressionType.Parameter)
        {
            break;
        }
        else if (exp.NodeType == ExpressionType.Call)
        {
            var ca = (MethodCallExpression)exp;

            if (ca.Method.IsSpecialName)
            {
                // Object indexer 
                bool isIndexer = ca.Method.DeclaringType.GetDefaultMembers().OfType<PropertyInfo>().Where(x => x.GetGetMethod() == ca.Method).Any();

                if (!isIndexer)
                {
                    throw new Exception();
                }
            }
            else if (ca.Object.Type.IsArray && ca.Method.Name == "Get")
            {
                // Multidimensiona array indexer
            }
            else
            {
                throw new Exception();
            }

            descr = GetParameters(ca.Arguments) + (needDot ? "." : string.Empty) + descr;

            exp = ca.Object;
            needDot = false;
        }
    }

    return descr;
}

private static string GetParameters(ReadOnlyCollection<Expression> exps)
{
    var values = new string[exps.Count];

    for (int i = 0; i < exps.Count; i++)
    {
        if (exps[i].NodeType != ExpressionType.Constant)
        {
            throw new Exception();
        }

        var ce = (ConstantExpression)exps[i];

        // Quite wrong here... We should escape string values (\n written as \n and so on)
        values[i] = ce.Value == null ? "null" :
            ce.Type == typeof(string) ? "\"" + ce.Value + "\"" :
            ce.Type == typeof(char) ? "'" + ce.Value + "\'" :
            ce.Value.ToString();
    }

    return "[" + string.Join(", ", values) + "]";
}

The code is quite easy to read, but it is quite long... There are 4 main cases: MemberAccess, that is accessing a property/field, ArrayIndex that is using the indexer of a single-dimensional array, Index that is unused by the C# compiler, but that should be using the indexer of an object (like the [...] of the List<> you are using), and Call that is used by C# for using an indexer or for accessing multi-dimensional arrays (new int[5, 4]) (and for other method calls, but we disregard them).

I support multidimensional arrays, jagged array s(arrays of arrays, new int[5][]) or arrays of indexable objects (new List<int>[5]) or indexable objects of indexable objects (new List<List<int>>). There is even support for multi-property indexers (indexers that use more than one key value, like obj[1, 2]). Small problem: printing the "value" of the indexers: I support only null, integers of various types, chars and strings (but I don't escape them... ugly... if there is a \n then it won't be printed as \n). Other types are not really supported... They will print what they will print (see GetParameters() if you want)

xanatos
  • 109,618
  • 12
  • 197
  • 280