Question

I have a rather complicated issue. I am trying to get a unique key from a method and its formal and actual parameters. The goal of the method, is to take a method call, and return a unique key based on 1) The name of the class and method and 2) The name and values of the parameters it is called with.

The method looks like this (sorry for all the details, but I can't find a sensible way to make the example smaller yet still explain my problem)

 public class MethodKey
    {
        public static string GetKey<T>(Expression<Func<T>> method, params string[] paramMembers)
        {
            var keys = new Dictionary<string, string>();
            string scope = null;
            string prefix = null;
            ParameterInfo[] formalParams = null;
            object[] actual = null;

            var methodCall = method.Body as MethodCallExpression;
            if (methodCall != null)
            {
                scope = methodCall.Method.DeclaringType.FullName;
                prefix = methodCall.Method.Name;

                IEnumerable<Expression> actualParams = methodCall.Arguments;
                actual = actualParams.Select(GetValueOfParameter<T>).ToArray();
                formalParams = methodCall.Method.GetParameters();
            }
            else
            {
                // TODO: Check if the supplied expression is something that makes sense to evaluate as a method, e.g. MemberExpression (method.Body as MemberExpression)

                var objectMember = Expression.Convert(method.Body, typeof (object));
                var getterLambda = Expression.Lambda<Func<object>>(objectMember);
                var getter = getterLambda.Compile();
                var m = getter();


                var m2 = ((System.Delegate) m);

                var delegateDeclaringType = m2.Method.DeclaringType;
                var actualMethodDeclaringType = delegateDeclaringType.DeclaringType;
                scope = actualMethodDeclaringType.FullName;
                var ar = m2.Target;
                formalParams = m2.Method.GetParameters();
                //var m = (System.MulticastDelegate)((Expression.Lambda<Func<object>>(Expression.Convert(method.Body, typeof(object)))).Compile()())

                //throw new ArgumentException("Caller is not a method", "method");
            }


            // null list of paramMembers should disregard all parameters when creating key.
            if (paramMembers != null)
            {
                for (var i = 0; i < formalParams.Length; i++)
                {
                    var par = formalParams[i];
                    // empty list of paramMembers should be treated as using all parameters 
                    if (paramMembers.Length == 0 || paramMembers.Contains(par.Name))
                    {
                        var value = actual[i];
                        keys.Add(par.Name, value.ToString());
                    }
                }

                if (paramMembers.Length != 0 && keys.Count != paramMembers.Length)
                {
                    var notFound = paramMembers.Where(x => !keys.ContainsKey(x));
                    var notFoundString = string.Join(", ", notFound);

                    throw new ArgumentException("Unable to find the following parameters in supplied method: " + notFoundString, "paramMembers");
                }
            }

            return scope + "¤" + prefix +  "¤" + Flatten(keys);

        }


        private static object GetValueOfParameter<T>(Expression parameter)
        {
            LambdaExpression lambda = Expression.Lambda(parameter);
            var compiledExpression = lambda.Compile();
            var value = compiledExpression.DynamicInvoke();
            return value;
        }
}

Then, I have the following test, which works OK:

        [Test]
        public void GetKey_From_Expression_Returns_Expected_Scope()
        {
            const string expectedScope = "MethodNameTests.DummyObject";
            var expected = expectedScope + "¤" + "SayHello" + "¤" + MethodKey.Flatten(new Dictionary<string, string>() { { "name", "Jens" } });

            var dummy = new DummyObject();

            var actual = MethodKey.GetKey(() => dummy.SayHello("Jens"), "name");

            Assert.That(actual, Is.Not.Null);
            Assert.That(actual, Is.EqualTo(expected));
        }

However, if I put the () => dummy.SayHello("Jens") call in a variable, the call fails. Because I then no longer get a MethodCallExpression in my GetKey method, but a FieldExpression (subclass of MemberExpression. The test is:

        [Test]
        public void GetKey_Works_With_func_variable()
        {
            const string expectedScope = "MethodNameTests.DummyObject";
            var expected = expectedScope + "¤" + "SayHello" + "¤" + MethodKey.Flatten(new Dictionary<string, string>() { { "name", "Jens" } });

            var dummy = new DummyObject();

            Func<string> indirection = (() => dummy.SayHello("Jens"));

            // This fails. I would like to do the following, but the compiler
            // doesn't agree :)
            // var actual = MethodKey.GetKey(indirection, "name");
            var actual = MethodKey.GetKey(() => indirection, "name");

            Assert.That(actual, Is.Not.Null);
            Assert.That(actual, Is.EqualTo(expected));
        }

The Dummy class SayHello method definitions are trivial:

 public class DummyObject
    {
        public string SayHello(string name)
        {
            return "Hello " + name;
        }

        public string Meet(string person1, string person2 )
        {
            return person1 + " met " + person2;
        }
    }

I have two questions:

  1. Is there any way to send the variable indirection to MethodKey.GetKey, and get it as a MethodCallExpression type?
  2. If not, how can I get the name and value of the method supplied if I get a MemberExpression instead? I have tried a few bits in the "else" part of the code, but haven't succeeded.

Any help is appreciated.

Thanks in advance, and sorry for the long post.

Was it helpful?

Solution

The problem is you are putting it into the wrong type of variable. Your method expects Expression<Func<T>> and you are using a variable of type Func<string> to store it. The following should fix your problem:

Expression<Func<string>> foo = () => dummy.SayHello("Jens");
var actual = MethodKey.GetKey<string>(foo, "name");

converting a .net Func<T> to a .net Expression<Func<T>> discusses the differences between a Func and an Expression<Func> and converting between the two and at a glance it says don't. The compiler makes them into totally different things. So make it the right thing at compile time and it should work fine.

If this isn't an option then possibly an overload that takes a Func instead of an Expression might work for you.

Note that in both cases I would pass the variable directly rather than trying to make it into a new expression in your call.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top