Pergunta

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.

Foi útil?

Solução

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);
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top