简体   繁体   中英

How to pass arbitrary lambdas as params to function (to get naming hierarchy)?

I have some (what I think are...) MemberExpression 's wrapped in lambas.

void Main()
{
    Foo<Person>(x => x.Name, x => x.Id, x => x.Address);
}

void Foo<TSource>(params Expression<Func<TSource, TValue>>[] lambdas)
{
    foreach (var lambda in lambdas)
    {
        Console.WriteLine(GetHierarchicalName(lambda));
    }
}

string GetHierarchicalName<TSource, TValue>(Expression<Func<TSource, TValue>> lambda)
{
    var member = lambda.Body as MemberExpression;
    var hierarchy = new Stack<string>();

    if (member == null)
    {
        throw new ArgumentException("You need to pass a lambda which references a member, silly!");
    }

    do
    {
        hierarchy.Push(member.Member.Name);
    } while (member.Expression.NodeType == ExpressionType.MemberAccess && (member = member.Expression as MemberExpression) != null);

    return String.Join("", hierarchy.ToArray());
}

My end goal is that Foo will output "Name", "Id" and "Address" (additionally, when passing a lambda such as x => x.Foo.Bar.Baz , "FooBarBaz" will be output).

However, at the moment I'm not specifying TValue for Foo ; I can't, since each of the lambda's could return a different value... but I don't care, since all I need is the property path they're referencing.

I tried using object in place of TValue , but when a lambda returns an int , the lambda passed to GetHierarchicalName ends up being a Convert , rather than a MemberExpression .

How can I get around not specifying TValue , such that I can pass arbitrary lambdas to Foo() , and have it output the path to the member each lambda is referencing?

int values need to be boxed to be represented as objects, that's why you're getting a Convert expression. You have to get the Convert expression's Operand instead of Body :

    var member = lambda.Body as MemberExpression;
    if (member == null && lambda.Body is UnaryExpression && lambda.Body.NodeType == ExpressionType.Convert)
    {
       member = (lambda.Body as UnaryExpression).Operand as MemberExpression;
    }

You'll have to declare a bunch of overloads of the form Foo<TSource, T1, ..., TN> , similar to how Action and Func themselves have overloads for up to 16 arguments. For example:

void Foo<TSource, T1, T2, T3>(Expression<Func<TSource, T1>> m1, Expression<Func<TSource, T2>> m2, Expression<Func<TSource, T3>> m3)
{
    Console.WriteLine(GetHierarchicalName(m1));
    Console.WriteLine(GetHierarchicalName(m2));
    Console.WriteLine(GetHierarchicalName(m3));
}

This can then be called as:

Foo<string, int, string>(x => x.Name, x => x.Id, x => x.Address);

To get the compiler to infer types, Foo has to accept an extra argument of type TSource :

Foo<TSource, T1, T2, T3>(TSource source, Expression<Func<TSource, T1>> m1, ...) { ... }

so it can be called as:

Foo(person, x => x.Name, x => x.Id, x => x.Address);

But all of that is a lot of work for little gain.

A conversion is represented by a UnaryExpression with NodeType ExpressionType.Convert . In this case, its Operand property contains the MemberExpression you're looking for.

You need some code that can code with any expression, you don't need to specify it is a typed expression so you can just use the base Expression class.

Here is some code from the utilities class I use, it has some code to handle the Converts and strip them out. Note I wrote this in LinqPad, so you may need to replace Dump() with something like Console.WriteLine if you want to run it elsewhere.

    void Main()
    {               
        Foo(x => x.Name, x => x.Id, x => x.Address);
    }

    void Foo(params Expression<Func<Bar, object>>[] lambdas)
    {
        foreach (var lambda in lambdas)
        {
            ExpressionToString(lambda).Dump();
        }
    }

    public static string ExpressionToString(Expression selector)
    {
        string left, right, result;
        switch (selector.NodeType)
        {
            case ExpressionType.MemberAccess:
                var memberExpression = selector as MemberExpression;
                right = (memberExpression.Member as PropertyInfo).Name;
                if (memberExpression.Expression.NodeType == ExpressionType.MemberAccess)
                {
                    left = ExpressionToString(memberExpression.Expression);
                    result = left + right;
                }
                else
                {
                    result = right;
                }

                break;
            case ExpressionType.Call:
                var method = selector as MethodCallExpression;
                left = ExpressionToString(method.Arguments[0]);
                right = ExpressionToString(method.Arguments[1]);
                result = left + right;
                break;
            case ExpressionType.Lambda:
                var lambda = selector as LambdaExpression;
                result = ExpressionToString(lambda.Body);
                break;
            case ExpressionType.Quote:
            case ExpressionType.Convert:
                var unary = selector as UnaryExpression;
                result = ExpressionToString(unary.Operand);
                break;
            default:
                throw new InvalidOperationException("Expression must be MemberAccess, Call, Quote, Convert or Lambda");
        }

        return result;
    }

    public class Bar
    {
        public String Name { get; set; }

        public String Id { get; set; }

        public String Address { get; set; }
    }

This will produce:

Name    
Id    
Address

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM