Generic questions, Lambda and reflection [complex]

advertisements

Before I start explaining the code I will first give my use case so you can understand what and why is going on.

Prerequisites:

  • let there be a server (Queue dispatcher / Buffer in client server terminology)
  • let there be one or more managing clients (Producer in client server terminology)
  • let there be clients (Consumers in client server terminology)

Workflow:

  • managing client writes a C# script, sends to server
  • script gets compiled by C# CodeDomProvider
  • script can give back a CallQueue result, or just execute something on server
  • if occurs server caches the CallQueue
  • in the meanwhile other managing clients can send new scripts that are processed
  • some client connects to server and asks for a CallQueue
  • client gets the CallQueue and executes it at the time specified in it

Everything is ok up to this point, and works flawlessly.

Now the technical part:

The CallQueue is a class that uses lambda expressions as input on generic methods, and stores reflection data required to execute the calls in the queue on client side.

Why all this complexity.. lambda generics etc? Type Safety. Managing clients are dumb and need to know only a few script methods to write, not real programmers. So sending an integer instead of a string or naming a property with a typo could be often. This is why the script uses lambdas and generics to restrict what someone can type.

This gets compiled on server and rejected if wrong.

This is a symbolic script that a managing client would write:

    CallQueue cc = new CallQueue(new DateTime(2012,12,21,10,0,0));

    // set property Firstname to "test person"
    cc.AddPropertySet<Person, string>(x => x.FirstName, "test person");

    // call method ChangeDescription with parameter "test order"
    cc.AddVoidMethodCall<Order, string>(x => x.ChangeDescription, "test order");

    // call method Utility.CreateGuid and send result to Person.PersonId
    cc.AddFunctionCallWithDestinationPropertySet<Utility, Guid, Person>(src => src.CreateGuid, dst => dst.PersonId);

What a client would get is a CallQueue instance and execute it like this:

    Order order = new Order();
    Person person = new Person();
    Utility util = new Utility();

    CallQueue cc = /* already got from server */;

    // when you call this execute the call queue will do the work
    // on object instances sent inside the execute method
    cc.Execute(new List<object> { order, person, util });

Up to here everything is fine and typesafe, but there are implications:

  • client exactly know which objects must be sent to the execute method, hardcoded by design
  • managing client can write script which operates on objects that won't be sent into but still compile on server because Types exist

Take for example:

 cc.AddFunctionCall<Int32, string>(x => x.ToString);

This will compile but will fail when the client executes it because it does not send an Int32 into the execute method.

Ok, bla bla bla.... So the question is:

How to restrict those generic methods with a set of allowed Types - not by defining inheritance:

 where T : something

but more like

 where listOftypes.Contains(T)

Or any equivalent solution that restricts what can go into it... I did not find a generic contraint for this...

Here is the CallQueue class:

   [Serializable]
   public class CallQueue : List<CallQueue.Call>
    {
        [Serializable]
        public struct Call {
            public MethodInfo Method;
            public MethodInfo DestinationProperty;
            public object[] Parameters;
            public Call(MethodInfo m, MethodInfo d, object[] p) {
                Method = m;
                Parameters = p;
                DestinationProperty = d;
            }
        }

        public CallQueue(DateTime when) {
            ScheduledTime = when;
        }

        public DateTime ScheduledTime
        {
            get;
            set;
        }

        public void AddFunctionCall<TSrcClass, TResult>(Expression<Func<TSrcClass, Func<TResult>>> expr)
        {
            MethodResolver((LambdaExpression)expr, null, new object[] {});
        }

        public void AddFunctionCallWithDestinationPropertySet<TSrcClass, TResult, TDest>(Expression<Func<TSrcClass, Func<TResult>>> expr, Expression<Func<TDest, TResult>> dest)
        {
            MethodResolver((LambdaExpression)expr, dest,  new object[] { });
        }

        public void AddFunctionCallWithDestinationPropertySet<TSrcClass, TParam1, TResult, TDest>(Expression<Func<TSrcClass, Func<TParam1, TResult>>> expr, TParam1 param, Expression<Func<TDest, TResult>> dest)
        {
            MethodResolver((LambdaExpression)expr, dest, new object[] { param });
        }

        public void AddFunctionCall<TSrcClass, TParam1, TResult>(Expression<Func<TSrcClass, Func<TParam1, TResult>>> expr, TParam1 param)
        {
            MethodResolver((LambdaExpression)expr, null, new object[] {param});
        }

        public void AddFunctionCallWithDestinationPropertySet<TSrcClass, TParam1, TParam2, TResult, TDest>(Expression<Func<TSrcClass, Func<TParam1, TParam2, TResult>>> expr, TParam1 param, TParam2 param2, Expression<Func<TDest, TResult>> dest)
        {
            MethodResolver((LambdaExpression)expr, dest, new object[] { param, param2 });
        }

        public void AddFunctionCall<TSrcClass, TParam1, TParam2, TResult>(Expression<Func<TSrcClass, Func<TParam1, TParam2, TResult>>> expr, TParam1 param, TParam2 param2)
        {
            MethodResolver((LambdaExpression)expr, null, new object[] { param, param2 });
        }

        public void AddVoidMethodCall<TSrcClass, TParam>(Expression<Func<TSrcClass, Action<TParam>>> expr, TParam param)
        {
            MethodResolver((LambdaExpression)expr, null, new object[] { param });
        }

        public void AddVoidMethodCall<TSrcClass, TParam1, TParam2>(Expression<Func<TSrcClass, Action<TParam1, TParam2>>> expr, TParam1 param, TParam2 param2)
        {
            MethodResolver((LambdaExpression)expr, null, new object[] { param, param2 });
        }

        public void AddVoidMethodCall<TSrcClass, TParam1, TParam2, TParam3>(Expression<Func<TSrcClass, Action<TParam1, TParam2, TParam3>>> expr, TParam1 param, TParam2 param2, TParam3 param3)
        {
            MethodResolver((LambdaExpression)expr, null, new object[] { param, param2, param3 });
        }

        public void AddPropertySet<TSrcClass, TParam1>(Expression<Func<TSrcClass, TParam1>> expr, TParam1 param)
        {
            PropertyResolver((LambdaExpression)expr, new object[] {param});
        }

        public void Execute(List<object> instances) {
            foreach (var call in this) {
                var owner = instances.Find(o => o.GetType() == call.Method.DeclaringType);
                if (call.DestinationProperty != null)
                {
                    // execute method get result and set to destination property
                    object res = call.Method.Invoke(owner, call.Parameters);
                    var destOwner = instances.Find(o => o.GetType() == call.DestinationProperty.DeclaringType);
                    call.DestinationProperty.Invoke(destOwner, new object[] {res});
                }
                else
                {
                    // just execute method
                    call.Method.Invoke(owner, call.Parameters);
                }
            }
        }

        private void MethodResolver(LambdaExpression expr, LambdaExpression dest, object[] param)
        {
            var body = (UnaryExpression)expr.Body;
            var methodCall = (MethodCallExpression)body.Operand;
            var constant = (ConstantExpression)methodCall.Arguments[2];
            var method = (MethodInfo)constant.Value;

            MethodInfo dmethod = null;

            if (dest != null)
            {
                var prop = (MemberExpression)dest.Body;
                var propMember = (PropertyInfo)prop.Member;
                dmethod = propMember.GetSetMethod();
            }

            this.Add(new Call(method, dmethod, param));
            Console.WriteLine(method.Name);
        }

        private void PropertyResolver(LambdaExpression expr, object[] param)
        {
            var prop = (MemberExpression)expr.Body;
            var propMember = (PropertyInfo)prop.Member;
            var method = propMember.GetSetMethod();
            this.Add(new Call(method, null, param));
            Console.WriteLine(method.Name);
        }
    }

Thank you very much. Cheers!


So there are two things you could do. One is make sure all your types in listOftypes derive from the same baseclass, or implement the same interface, in which case you could just use where.

Given that your question seems to indicate this is not what you want, you might be able to get better error reporting at runtime by looking at typeof(T) and seeing if that type is contained within listOfTypes. Not as nice as what you want, but you are limited.