How to use Expressions to invoke a method call wit

2019-02-27 16:35发布

We're using the very Excellent ToStringBuilder in our project as a performant, generic backing for our ToString implementations. It worked fine for debugging until I needed to generate a string representation of an object graph to check if it had changed in between loading and closing. Previously I had used a MemoryStream to write the object out to xml, but this seemed heavyweight so I decided to try out using ToStringBuilder, which is where I hit a showstopper...

Our object graph uses generic typed lists heavily, so when the lists are printed out they look like the following:

PropertyName:{System.Collections.Generic.List`1[Namespace.Path.To.MyClassDto]}

Instead of enumerating through the list and invoking ToString on each object, which is fine as that's default behaviour (btw, ToStringBuilder supports object[], but we don't want to retrofit our entire Dto layer just to fix this problem).

I tried to patch the code in question (ToStringBuilder.cs, line 177) to recognise when the type is a generic list, and then invoke string.Join(", ", list), but I couldn't get my head around how the linq reflection API handles generics.

The first thing I tried was to get a handle to the String.Join(IEnumerable<>) method like this:

var stringJoinMethod = typeof(string).GetMethod("Join", new[] { typeof(string), typeof(IEnumerable<>) });

But GetMethod returned null so that didn't work. I eventually found this StackOverflow question that showed me how to get a generic method by signature (call getmethods() instead and filter the results). That got me the correct method handle, so I tried to do something like this:

private void AppendMember(MemberInfo memberInfo)
{
    AppendQuotesIfRequiredForType(memberInfo);

    Type type = GetMemberType(memberInfo);
    var memberAppendMethod = typeof(StringBuilder).GetMethod("Append", new[] { type });
    Expression getMemberValue = Expression.MakeMemberAccess(TargetArgExpression, memberInfo);

    if (type.IsValueType)
    {
        Type appendArgType = memberAppendMethod.GetParameters()[0].ParameterType;
        if (type != appendArgType)
        {
            getMemberValue = Expression.TypeAs(getMemberValue, typeof(object));
        }
        //my code begins here.
        _appendExpressions.Add(Expression.Call(SbArgExpression, memberAppendMethod, getMemberValue));
    }
    else if (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>)))
    {
        // now to emit some code to do the below, you wouldn't think it'd be this hard...
        // string.Join(", ", genericList);
        AppendStartOfMembers();

        //this returns null, because generics are not well supported by the reflection API, boo!
        var stringJoinMethod = typeof(string).GetGenericMethod("Join", new[] { typeof(string), typeof(IEnumerable<>) });
        var CommaSpace = Expression.Constant(", ");

        // this doesn't work, throws an ArgumentException as below
        getMemberValue = Expression.Call(stringJoinMethod, CommaSpace, getMemberValue);


        _appendExpressions.Add(Expression.Call(SbArgExpression, memberAppendMethod, getMemberValue));

        AppendEndOfMembers();
    }
    else
    {
        //primitives like strings
        _appendExpressions.Add(Expression.Call(SbArgExpression, memberAppendMethod, getMemberValue));
    }

    //my code ends here.
    AppendQuotesIfRequiredForType(memberInfo);
}

This errors with the following exception:

System.ArgumentException: "Method System.String Join[T](System.String, System.Collections.Generic.IEnumerable`1[T]) is a generic method definition"
   at System.Linq.Expressions.Expression.ValidateMethodInfo(MethodInfo method)
   at System.Linq.Expressions.Expression.ValidateMethodAndGetParameters(Expression instance, MethodInfo method)
   at System.Linq.Expressions.Expression.Call(MethodInfo method, Expression arg0, Expression arg1)
   at MyNameSpace.Common.ToStringBuilder`1.AppendMember(MemberInfo memberInfo) in C:\myproject\MyNamespace.Common\ToStringBuilder.cs:line 206

I started googling that error message and found people talking about using Expression.Lamba() to wrap calls to generic methods, at which point I realised I was way out of my depth.

So, assuming I have a List mylist, how do I generate an Expression as above that will do the equivalent of string.Join(", ", mylist); ?

thanks!

1条回答
Melony?
2楼-- · 2019-02-27 17:06

To get the "generic type" of your generic list (the type of "T"), you can do

var genericListType= type.GetGenericArguments()[0];

so in your elseif, you could do (it might be easier, I just stay as close as possible to your code)

else if (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>)))
    {
        // now to emit some code to do the below, you wouldn't think it'd be this hard...
        // string.Join(", ", genericList);
        AppendStartOfMembers();

        //this returns null, because generics are not well supported by the reflection API, boo!
        var stringJoinMethod = typeof(string).GetGenericMethod("Join", new[] { typeof(string), typeof(IEnumerable<>) });
        var CommaSpace = Expression.Constant(", ");

        var genericListType= type.GetGenericArguments()[0];
        var genericStringJoinMethod = stringJoinMethod.MakeGenericMethod(new[]{genericListType});

        // this doesn't work, throws an ArgumentException as below
        getMemberValue = Expression.Call(genericStringJoinMethod , CommaSpace, getMemberValue);


        _appendExpressions.Add(Expression.Call(SbArgExpression, memberAppendMethod, getMemberValue));

        AppendEndOfMembers();
    }
查看更多
登录 后发表回答