Introduction
This article introduces a .NET-based mechanism for on-the-fly generation of interfaces on the basis of a pool of objects. In a client-server setting, this allows clients to specify the interfaces they require at run-time, without the server having to know its clients' needs at compile-time. The internals of the mechanism can be broken into the following steps:
- The client-side passes a full description of the set of methods it requires on to the server-side.
- A pool of server-side objects that are registered as service providers are scanned for corresponding method implementations.
- If all the methods requested by the client-side are matched by corresponding implementations, then a type providing all of these method implementations is created on-the-fly and an instance of this type is returned to the client. Otherwise, the return value is
null
.
Background
A tempting thought which may pass through one's mind when reading about dynamic interfaces is, "Why not define a static interface and somehow compile it and pass it to the client?" This way, we'd win it all. The client would be able to reference a compile-time interface, thus preventing all the dangers and uncertainties inherent in late binding. Also, the server would still support an interface-on-demand policy. The problem is that the interface created on demand needs to be known to the client at compile-time for it to be able to use it in a type-safe way. Put differently, it's asking too much! The server can expose new types to its clients (i.e. types that were not known to them at compile-time), but the clients cannot use them as if they knew them at compile-time.
The approach I have taken does the most to ensure that the client understands what it's going to get back. As I explain below, the client has to specify exactly which methods it is after, which is done through specification of the methods' names, return types, parameter types, calling conventions, etc. Then -- when getting access to a proxy that exposes the requisite methods -- the client is expected to make correct use of them, which is a reasonable expectation given that the client knows what it has asked for.
Using the Code
So, now that we have outlined the solution, how does it actually work? In the demo code attached to this article, I based the communication between the client-side and the server-side on .NET Remoting. The server-side exposes a Remoting server that implements the IBridge
interface:
public interface IBridge
{
T GetServiceProvider<T>() where T : class;
DynamicInterface GetServiceProvider(
params MethodDescriptor[] methodDescriptors);
}
Since IBridge
needs to be known to both sides at compile-time, it is defined in CommonInterfaces
, which is referenced by both the client and the server. The first method defined in IBridge
-- the templated overload of GetServiceProvider()
-- should be used in cases where the client asks for an interface that is known to both sides at compile-time. I included this overload for completeness. More interesting is the second overload of GetServiceProvider()
, which is also the focus of this article. It allows clients to request dynamic interfaces through specification of MethodDescriptor
s. The MethodDescriptor
class defines the following properties:
public string Name { get; }
public Type ReturnType { get; }
public bool IsParams { get; }
public Type[] ParameterTypes { get; }
These are used by the server-side in its attempt to find matching implementations for the MethodDescriptor
s provided by the client. The server-side class responsible for this process is ProxyFactory
, which is a static class. As its name suggests, ProxyFactory
is responsible for the creation of proxy objects to be consumed by the client-side. In the case of dynamic interfaces, the proxy is created by the CreateFacade()
method:
static CommonInterfaces.DynamicInterface CreateFacade(
object[] serviceProviders,
params CommonInterfaces.MethodDescriptor[] methodDescriptors);
CreateFacade()
does the following:
- It creates an
Assembly
in the current AppDomain
. - It defines a new type inside the new
Assembly
. The new type inherits from MarshalByRefObject
and is Serializable
, as required by .NET Remoting. - It then iterates over all the
MethodDescriptor
s provided by the client and -- for each MethodDescriptor
-- it scans serviceProviders
for a corresponding implementation. If the search is successful, then a matching method is defined on the newly created type (we'll delve into the details of this operation in a moment). Otherwise, the process stops and the return value is null
.
So... how do we define new methods of the type we've just created? It's actually quite simple. All we have to do is use a bit of what Reflection.Emit
has to offer:
public static void DefineMethod(TypeBuilder proxyBuilder, string methodName,
Type returnType, Type[] parameterTypes, object from,
CallingConventions callingConvention, bool isParamsMethod)
{
System.Reflection.Emit.MethodBuilder methodBuilder =
proxyBuilder.DefineMethod(
methodName,
MethodAttributes.Public | MethodAttributes.Virtual,
callingConvention,
returnType,
parameterTypes
);
ILGenerator methodGenerator = methodBuilder.GetILGenerator();
if (isParamsMethod)
{
ParameterBuilder parameterBuilder = null;
for (int index = 0; index < parameterTypes.Length; ++index)
parameterBuilder =
methodBuilder.DefineParameter(index + 1,
ParameterAttributes.None, parameterTypes[index].Name);
parameterBuilder.SetCustomAttribute(
new CustomAttributeBuilder(
typeof(ParamArrayAttribute).GetConstructors()[0],
new object[0]
)
);
}
for (int index = 0; index <= parameterTypes.Length; ++index)
methodGenerator.Emit(OpCodes.Ldarg_S, index);
methodGenerator.EmitCall(OpCodes.Callvirt,
from.GetType().GetMethod(methodName), null);
methodGenerator.Emit(OpCodes.Ret);
}
First, we create a MethodBuilder
. Then -- if the method we're defining allows a variable number of arguments in its invocation -- we make sure to set ParamArrayAttribute
on its last parameter. Finally, we load all the arguments passed on to the method to the call stack and emit a (virtual) call to an object that was found to support the mehtod (from among serviceProviders
). That's it! Our proxy is ready to leave the factory...
Now that our proxy is out of the factory, our clients can write code that uses it:
CommonInterfaces.DynamicInterface dynamicInterface;
...
dynamicInterface["PrintStringAndNumberParamArray"].Invoke(
"Wow", 7, 8, 9, 10);
What happens behind the scenes is quite simple. DynamicInterface
has -- as one of its members -- a dictionary that maps between method names and IInvocable
objects. IInvocable
is defined as follows:
public interface IInvocable
{
object Invoke(params object[] parameters);
}
Thus, all we're left to do is define an indexer on DynamicInterface
that gets a string
and returns an IInvocable
using the dictionary:
public IInvocable this[string methodName]
{
get
{
if (_methodDescriptorsDictionary.ContainsKey(methodName))
{
if (!_invocablesDict.ContainsKey(methodName))
_invocablesDict.Add(methodName,
new Invocable(_serviceProvider, methodName));
return _invocablesDict[methodName];
}
else
{
return null;
}
}
}
Points of Interest
The implementation of IInvocable
-- in the form of an internal class of DynamicInterface
called Invocable
-- proved to be a bit trickier than expected. On the face of it, there's little it should do. Using the .NET Reflection toolbox, there should be no more than 2-3 lines of code in the implementation of Invoke()
. However, soon after I started my testing, I realized that things are not that simple.
The problem I witnessed was that a TargetParameterCountException
was thrown every time I tried to invoke a method that supports a variable number of arguments. After tweaking my implementation in all sorts of ways, I found a way out. If all the arguments that go with the last parameter are grouped together into an array, then the invocation succeeds. Otherwise, the method is seen to be invoked with a wrong number of parameters. This is the result:
public object Invoke(params object[] parameters)
{
System.Reflection.MethodInfo methodInfo =
_callee.GetType().GetMethod(_methodName);
object retVal = null;
if (methodInfo != null)
{
if (MethodDescriptor.IsParamsMethod(methodInfo))
retVal = InvokeParamsMethod(_callee, methodInfo, parameters);
else
retVal = methodInfo.Invoke(_callee, parameters);
}
return retVal;
}
InvokeParamsMethod()
is implemented as follows:
public static object InvokeParamsMethod(object callee,
System.Reflection.MethodInfo methodInfo, object[] parameters)
{
System.Reflection.ParameterInfo[] pInfos = methodInfo.GetParameters();
object[] finalParameters = new object[pInfos.Length];
for (int index = 0; index < pInfos.Length - 1; ++index)
finalParameters[index] = parameters[index];
Array paramsArray =
Array.CreateInstance(
Type.GetType(pInfos[pInfos.Length - 1].ParameterType.ToString(
).Replace("[]", "")),
parameters.Length - (pInfos.Length - 1)
);
System.Array.Copy(
parameters,
pInfos.Length - 1,
paramsArray,
0,
paramsArray.Length
);
finalParameters[pInfos.Length - 1] = paramsArray;
return methodInfo.Invoke(callee, finalParameters);
}
History
- 3 October, 2007 -- Original version posted
I am a software engineer, as well as a research student at The Hebrew University. I love coding, math, computer theory, reading, tennis, guitar, swimming, animals, and - most importantly - my wife and family.