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) orTask<T>
(in case the action does return a value). In the latter caseT
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.