Click here to Skip to main content
15,878,852 members
Articles / Programming Languages / C#
Article

Dynamic Generation of Interfaces Using a Pool of Objects

Rate me:
Please Sign up or sign in to vote.
4.64/5 (9 votes)
3 Oct 20075 min read 32.5K   147   34   5
An article introducing a mechanism for on-the-fly interface generation
Screenshot - Article.gif

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:

C#
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 MethodDescriptors. The MethodDescriptor class defines the following properties:

C#
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 MethodDescriptors 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:

C#
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 MethodDescriptors 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:

C#
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:

C#
CommonInterfaces.DynamicInterface dynamicInterface;
... // code that initializes '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:

C#
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:

C#
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:

C#
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:

C#
public static object InvokeParamsMethod(object callee, 
    System.Reflection.MethodInfo methodInfo, object[] parameters)
{
    /*
     * all parameters, except the last remain the same
     * the last parameter is an array holding all the 
     *parameters that come as the 'params' argument
     */ 
    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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Israel Israel
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.

Comments and Discussions

 
GeneralThank you InvokeParamsMethod() saved my day Pin
Karl Tarbet18-May-11 7:59
Karl Tarbet18-May-11 7:59 
Generalcool Pin
Jay R. Wren10-Oct-07 10:25
Jay R. Wren10-Oct-07 10:25 
AnswerRe: cool Pin
Omer Tripp10-Oct-07 12:32
Omer Tripp10-Oct-07 12:32 
Generalstatic Interface on the client side Pin
adaml4-Oct-07 18:38
adaml4-Oct-07 18:38 
AnswerRe: static Interface on the client side Pin
Omer Tripp11-Oct-07 8:08
Omer Tripp11-Oct-07 8:08 
Hi Adam,

Your idea is very interesting. It's also more powerful than the approach I put forward in the article, as it allows type-safe use of dynamically generated objects. However, I could not make it work.

The main problem is that - as far as I know (and tried) - it's not enough for a class to have the same methods as in the interface (a pure virtual class in the case of C++) it is to be cast to (including order) for the cast to be successful. static_cast<>() fails at compile-time and reinterpret_cast<>() fails at run-time. In C++, polymorphic behavior needs to be announced explicitly (through public inheritance).

Please let me know if there's anything I missed or mis-interpreted in your idea. It sounds very promising.

Omer

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.