2

The system consists of a set of peer to peer connections. Each peer provides a set of 'actions' that it can perform. Each action is represented by a method on an interface, say

public interface IMyCoolActions
{
    int Add(int first, int second);
}

The 'client' peer will create a proxy object for the action interface so that it can invoke the methods on this interface. When one of the interface methods on this proxy is called, the proxy gathers the parameter data, packages it and sends it across the network to the 'server' peer. The 'sever' peer unpacks the data, determines which method was invoked and calls the method, i.e. basically an RPC approach

Now the 'server' peer does not have to have an actual implementation of the IMyCoolActions interface. All it needs is a method that will:

  • Have the same parameters
  • Have the same return type
  • Perform the action that was indicated by the called interface method

So it could have an instance of the following class

public sealed class DoStuff
{
    public int Combine(int first, int second)
    {
        return first + second;
    }
}

Obviously there will need to be a mapping that maps the IMyCoolActions.Add method to the DoStuff.Combine method. The easy way would be to make DoStuff implement the IMyCoolActions interface, however the goal is to disconnect these two so that it is possible to allow the callers to provide parameters that are only used on the local end. e.g. the following should still be mappable

public interface IMyCoolActions
{
    Task<int> Add(int first, int second, [ConnectionTimeoutAttribute]TimeSpan timeout);
}

public sealed class DoStuff
{
    public int Combine([RemoteIdAttribute]IPEndpoint origin, int first, int second)
    {
        return IsAllowedToCommunicate(orgin) ? first + second : int.MaxValue;
    }
}

This mapping should still work because the client uses the timeout value locally (as a .. well time-out) and the server is provided with the origin IP data when the network data is unpacked.

The entire system has been implemented except for the generation of the mapping. It has so far proven illusive to find an appropriate way to create the correct mapping. I have tried the following approach (and derivatives of it):

public interface ICommandMapper<TCommand>
{
    IMethodWithoutResultMapper ForMethodWithResult<T1, T2, T3, TOut>(
        Expression<Func<TCommand, T1, T2, T3, Task<TOut>>> methodCall);
}

public interface IMethodWithResultMapper
{
    void ToMethod<TInstance, T1, T2, T3, TOut>(
        TInstance instance,
        Expression<Func<TInstance, T1, T2, T3, TOut>> methodCall);
}

Which can then called in the following way:

var instance = new DoStuff();

ICommandMapper<IMyCoolActions> map = CreateMap();
map.ForMethodWithoutResult((command, first, second, timeout) => command.Add(first, second, timeout))
    .ToMethod(instance, (ipaddress, first, second) => instance.Combine(ipaddress, first, second));

Unfortunately the C# compiler is not able to infer the different types. While the lack of type inference is solvable it leads to a lot of ugly casting and type specifying.

So what I want is a suggestion / idea for mapping between these methods so that

  • it is possible to determine which interface method and which object method is used (by using reflection, DynamicObject or other means
  • the user doesn't have to wrangle themselves in too many corners.

EDIT

The actual action signatures (i.e. IMyCoolActions) and the implementation of the action (i.e. DoStuff) are in the control of the users of my code. My code is only responsible for the generation of the proxy, transporting of the call data and invocation of the correct action method.

The current demands on signatures are:

  • The signature is defined via an interface which derives from one of my action interfaces.
  • Each interface may only have methods, so no properties or events.
  • Each method must return a Task (in case the action doesn't return a value) or Task<T> (in case the action does return a value). In the latter case T must be serializable.
  • Each method parameter must be serializable.
  • Method parameters used by the proxy, i.e. those that will not be transferred are to be marked with a special attribute.

There are similar (but not identical) requirements for the action implementations.

Petrik
  • 1,961
  • 13
  • 23
  • You seem to have dug a big hole for yourself here. Where are these mappings going to sit? Decorating your server and or client classes with attributes might be way to go. If I was doing this I think I'd have done a dsl and described the desired action and then emitted the correct code at either end, ie coupled through a common description if what I wanted to happen. e.g. Make Thingy Blue. – Tony Hopkinson Apr 22 '14 at 22:11
  • @TonyHopkinson It's a bit of a hole but it should be doable .. I think. The mappings will be on the 'server' end. The 'client' will just send over the details of the interface method + parameter values of those parameters that aren't marked as local by an attribute. Would you be able to be a bit more specific on the DSL approach? – Petrik Apr 22 '14 at 23:06
  • Idea of a dsl is server and client have a common language. e.g the client says "Add(1,2) and the server parses that string and emits new DoStuff().Combine(1,2). Totally decoupled, typing done at either end. Problem with mappings you have is the server will have to know how the client describes the operation, whereas with a dsl, the only thing either end needs to know is the syntax and semantics of the language. – Tony Hopkinson Apr 22 '14 at 23:14
  • @TonyHopkinson Ah right. That is sort of the idea with the interface. The reason I'm having this issue is that I don't control the operations, the user code does. My code is only responsible for the proxy / networking / invocation bits of it. The available operations are determined by user code. I should probably have been more specific about that in my question. I have updated the question with the additional information. – Petrik Apr 22 '14 at 23:45
  • That's not really an issue, Using a dsl would mean your proxy/invocation code would be just passing dsl code between server and client. Basically just straight message passing and perhaps some routing. That said switching to a dsl basically means start again in your layer, and a good bit of work at either end. – Tony Hopkinson Apr 23 '14 at 09:38

1 Answers1

0

For the moment I have solve the issue by accepting that casting will be a fact of life for this problem.

The interfaces have been changed to classes because there should really only be one implementation of each and the methods have been simplified by removing the type parameter for the output. Given that the code deals extracts the MethodInfo from the expression it will still be possible to get the return types without having to define multiple method overloads to be able to have the return type in the method signature.

public sealed class CommandMapper<TCommand>
{
    public MethodMapper For<T1, T2, T3>(Expression<Action<TCommand, T1, T2, T3>> methodCall)
    {
        return CreateMethodMapper(methodCall);
    }
}

public sealed class MethodMapper
{
    public void To<T1, T2, T3>(Expression<Action<T1, T2, T3>> methodCall)
    {
        // Do stuff
    }
}

With this interface the user calls the methods like this:

var map = CommandMapper<IMyCoolActions>.CreateMap();
map.For<int, int, TimeSpan>((command, first, second, timeout) => command.Add(first, second, timeout))
    .To((IPEndpoint ipaddress, int first, int second) => instance.Combine(ipaddress, first, second));

In the CommandMapper the MethodInfo is obtained by:

var methodCall = method.Body as MethodCallExpression;
if (methodCall == null)
{
    throw new InvalidCommandMethodExpressionException();
}

return methodCall.Method;

In the MethodMapper besides the MethodInfo the actual object reference needs to be extracted as well. This is slightly more tricky because the compiler generates a class that holds the actual reference, but fortunately there was a solution on StackOverflow.

var methodCall = method.Body as MethodCallExpression; if (methodCall == null) { throw new InvalidCommandMethodExpressionException(); }

var methodInfo = methodCall.Method;

// if the object on which the method is called is null then it's a static method
object instance = null;
if (methodCall.Object != null)
{
    var member = methodCall.Object as MemberExpression;
    if (member == null)
    {
        throw new InvalidCommandMethodExpressionException();
    }

    // The member expression contains an instance of an anonymous class that defines the member
    var constant = member.Expression as ConstantExpression;
    if (constant == null)
    {
        throw new InvalidCommandMethodExpressionException();
    }

    var anonymousClassInstance = constant.Value;

    // The member of the class
    var calledClassField = member.Member as FieldInfo;

    // Get the field value
    instance = calledClassField.GetValue(anonymousClassInstance);
}

return new Tuple<object, MethodInfo>(instance, methodInfo);
Community
  • 1
  • 1
Petrik
  • 1,961
  • 13
  • 23