Click here to Skip to main content
Click here to Skip to main content

Dynamic Generation of Interfaces Using a Pool of Objects

, 3 Oct 2007
Rate this:
Please Sign up or sign in to vote.
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:

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:

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:

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:

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;
... // 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:

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)
{
    /*
     * 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

About the Author

Omer Tripp
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 PinmemberKarl Tarbet18-May-11 7:59 
Generalcool PinmemberJay R. Wren10-Oct-07 10:25 
AnswerRe: cool PinmemberOmer Tripp10-Oct-07 12:32 
Hi Jay,
 
Shared interfaces are a very elegant solution (they are essential to .Net Remoting). The only downside is that they have to be declared at compile-time, not only of the server-side but also of the client-side. This leads to tight coupling between the server and its clients. If a change needs to be made in the public interface of a Remoting server, then the corresponding (shared) interface has to be changed, and then one of two things will necessarily happen:
 
1) Clients of the said Remoting server will be re-compiled, or
2) Clients of the said Remoting server will break.
 
In many real life situations, the developer of the Remoting server does not have complete control over its clients, and here is (one case) where shared interfaces stop doing the job for you...
 
Having said that, I agree that shared interfaces are cool Smile | :)
 
Omer

Generalstatic Interface on the client side Pinmemberadaml4-Oct-07 18:38 
AnswerRe: static Interface on the client side PinmemberOmer Tripp11-Oct-07 8:08 

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

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

| Advertise | Privacy | Mobile
Web01 | 2.8.140709.1 | Last Updated 3 Oct 2007
Article Copyright 2007 by Omer Tripp
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid