Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Dynamic Binding Using the Factory Pattern

3.00/5 (1 vote)
1 Nov 2009CPOL11 min read 26.3K   181  
Using the Factory design pattern to hide dynamic binding and use configuration strings to determine which classes should be instantiated.

Contents

Introduction

I need to use a service that exposes an object using an interface (for example, the ITheService interface). For unit testing, I would like to use my own simple implementation of the interface (in the unit test process) so that my unit test does not depend on the actual service implementation. For formal testing and production, the client will connect to the service using .NET TCP remoting. In the future, it is likely that we will want to connect using HTTP. I want to switch between implementations using a configuration string. In this article, I will share an approach that enables this kind of dynamic binding using a Factory design pattern.

Background

Dynamically loading a class into the current process space generally uses some combination of the Assembly.Load and Assembly.CreateInstance methods. This requires knowing the assembly name and the class name. The Assembly.CreateInstance method has capabilities for passing values to the class constructor.

The most direct mechanism to dynamically establish a .NET remoting connection uses the Activator.GetObject method. This requires knowing the type and the URI for the remote object. Only the default constructor can be used. There is not a built-in mechanism for passing values to the constructor.

I will generally ignore the HTTP possibility at this point, except to remember that our configuration string needs to allow for that possibility. Only the default constructor is used.

In the interest of extensible design, it would be nice if the solution would allow adding new activation types without having to recompile the existing assembly.

Basic Solution

Let's begin with the configuration string and work out from that. How can I represent the needed information in a single string? The URI provides a nice format for the configuration string that can represent all of these conditions.

  • Assembly name, class name, and a string for the constructor can be encoded into a URI style string (local://localhost/AssemblyName/ClassName?ConsructorString).
  • .NET remoting uses a URI style string (tcp://localhost:9000/TheClass). .NET remoting always uses the default constructor, but it would be nice if we could develop a convention to allow class initialization.
  • It is reasonable to assume that HTTP can use some form of a URI as well.

In the configuration strings above, the scheme (protocol) portion of the URI determines how the string should be interpreted. The rest gives information about how the class should be instantiated. A common creational design pattern for mapping some kind of information to class creation is the Factory pattern. This seems like a great candidate for this solution, mapping a scheme to a specialized class that can create an instance of the desired class (or proxy to the desired class) from information in the string.

Our solution will have three basic parts:

  1. A singleton class that will act as the factory. We will call this class the Locator. When we want to create one of the configuration specified objects, we will pass the configuration string to the Locator. The Locator will select the specialized class that knows how to create that kind of object and ask the specialized object to create it.
  2. An interface that all of our specialized classes will derive from. We will call this interface ILocatorActivator. It will specify the method and signature that the factory requires in order to request that an object be created.
  3. At least one concrete implementation of the ILocatorActivator interface (one implementation of the specialized class). In this case, we will start with two implementations. One for Assembly.CreateInstance (in process) and one for Activator.GetObject (.NET Remoting).

Note: I have chosen to nest the ILocatorActivator interface inside of the Locator class implementation. I did this for two reasons. First, it reduces the number of files in the solution. That is not necessarily a good reason, but when uploading a sample, it is helpful. Second, it only has meaning within the context of the factory. It is intended to be used only to create specialized classes for this factory. Some coding standards would recommend against nesting. Follow your coding standards (they are there to help).

The Factory

Next, let's consider the factory. The factory is responsible for turning a URI string into an object. In this example, the factory is a class called the Locator. We will start with the basic structure of the class and fill in the guts of the methods as we go along. The Locator class should:

  • Have singleton behavior,
  • Have a method for creating an object (Activate),
  • Have a method for adding ILocatorActivator implementations (RegisterActivator).

This factory depends on all activator classes (the classes that will actually create the objects from the URI string) to have a common interface (ILocatorActivator), so we will define it here as well.

C#
public class Locator
{
    public interface ILocatorActivator
    {
        object Activate(Type type, Uri uri);
    }

    private static Dictionary<string,> _activators
        = new Dictionary<string,>();

    static Locator()
    {
    }

    public static void RegisterActivator(string scheme, 
                  ILocatorActivator activator)
    {
    }

    public static object Activate(Type type, string locator)
    {
    }
}

All static methods and a static constructor were used for the singleton behavior. The class has a collection to map schemes to implementations of the ILocatorActivator interface. Since this collection will be used by static methods, it is static as well.

I would have preferred not to include type as an Activate method parameter, but the Activator.GetObject method requires it, so we have to add it for all. In the long run, it might be helpful for other ILocatorActivator implementations. The ILocatorActivator implementation does not have to use it.

Let's go ahead and add some meat to the RegisterActivator and Activate methods. RegisterActivator adds an instance of an ILocatorActivator implementation to our collection for the given scheme.

C#
public static void RegisterActivator(string scheme, ILocatorActivator activator)
{
    if (_activators.Keys.Contains(scheme.ToLower()))
    {
        throw new ArgumentException(scheme);
    }
    else
    {
        _activators.Add(scheme.ToLower(), activator);
    }
}

For this simple implementation, we will not allow you to add the same scheme twice. The Activate method is responsible for locating the appropriate ILocatorActivator implementation for the scheme and asking the mapped object to create the desired object.

C#
public static object Activate(Type type, string locator)
{
    object response = null;

    Uri uri = new Uri(locator);
    if (_activators.Keys.Contains(uri.Scheme.ToLower()))
    {
        ILocatorActivator activator = _activators[uri.Scheme.ToLower()];
        response = activator.Activate(type, uri);
    }

    return response;
}

If an ILocatorActivator implementation for this scheme cannot be found, null is returned. Otherwise, the ILocatorActivator instance is used to create an instance of the object.

In Process Activation Using Assembly.CreateInstance

Now, let's look at the Assembly.CreateInstance (in process) ILocatorActivator implementation. We will call this class InProcessActivator. All this class knows how to do is Assembly.CreateInstance an object (in the current process space) from a configuration string, in the "local://host/AssemblyName/ClassName?ConsructorString" format.

An instance of this class will be associated in the Locator collection with the scheme "local" using the Locator.RegisterActivator method, so the URI string must start with "local". We will ignore the host value, but it is required as a place holder in the URI string. The URI.LocalPath property is used to determine the assembly name and the class name. The correct version of Assembly.CreateInstance is selected depending on if a constructor string value is provided.

C#
public class InProcessActivator : Locator.ILocatorActivator
{
    #region ILocatorActivator Members

    public object Activate(Type type, Uri uri)
    {
        // Uri must be in the format
        // local://host/AssemblyName/ClassName or
        // local://host/AssemblyName/ClassName?ConstructorString
        string[] pathSegments = uri.LocalPath.Split('/');
        if (pathSegments.Length != 3)
        {
            throw new ArgumentException(uri.ToString());
        }

        // path starts with slash, so element zero is empty
        string assemblyName = pathSegments[1];
        Assembly assembly = Assembly.Load(assemblyName);
        if (null == assembly)
        {
            throw new ArgumentException(uri.ToString());
        }

        string className = pathSegments[2];

        object response = null;

        //Do we have a constructor string?
        if (uri.Query.Length > 0)
        {
            string initString = Uri.UnescapeDataString(uri.Query);
            if ('?' == initString[0])
            {
                initString = initString.Remove(0, 1);
            }
            object[] initParam = { initString };

            response = assembly.CreateInstance(className, 
                false,
                BindingFlags.CreateInstance,
                null,
                initParam,
                null,
                null);
        }
        else
        {
            response = assembly.CreateInstance(className);
        }

        return response;
    }

    #endregion
}

With the exception of the URI parsing code, it is just normal System.Reflection code.

Remote Activation Using Activator.GetObject

Next is the Activator.GetObject implementation (.NET remoting) of ILocatorActivator. We will call this class SaoActivator, for Server Activated Object Activator. All this class knows how to do is call Activator.GetObject to create a proxy object from a URI string in the "tcp://localhost:9000/TheClass" format.

An instance of this class will be associated in the Locator collection with the scheme "tcp" using the Locator.RegisterActivator method, so the URI string must start with "tcp". Refer to the .NET remoting documentation for additional information on this string.

C#
public class SaoActivator : Locator.ILocatorActivator
{
    #region ILocatorActivator Members

    public object Activate(Type type, Uri uri)
    {
        return Activator.GetObject(type, uri.ToString());
    }

    #endregion
}

Earlier, I pointed out that Activator.GetObject uses the default constructor only. It would be nice to extend this and allow object initialization before it is passed back to the caller. This can only be accomplished by adding some kind of Initialize method to the object we are creating, but that is not a part of the ILocatorActivator interface, and we do not want to force this method on all ILocatorActivator implementations. To accomplish this, we need to develop a convention that will only optionally apply to objects that will be created through the SoaActivator. This convention will be codified in an interface called IInitializer that has a Initialize method. Classes that may be created through the SoaActivator and that want to support initialization can implement this interface. For objects that support that interface, the Initialize method can be called. For objects that do not support this interface and where an initialization parameter has been provided, we will throw an exception. This will change the SaoActivator class to look like this:

C#
public class SaoActivator : Locator.ILocatorActivator
{
    public interface IInitializer
    {
        void Intialize(string arg);
    }

    #region ILocatorActivator Members

    public object Activate(Type type, Uri uri)
    {
        // if an initializatin parameter has been provided, it must be
        // stripped off before calling Activator.GetObject
        string realUri = (uri.Query.Length == 0) ?
            uri.AbsoluteUri :
            uri.AbsoluteUri.Substring(0, uri.AbsoluteUri.Length - uri.Query.Length);

        // Create the object
        object response = Activator.GetObject(type, realUri);

        // If an initiaization parameter has been provided, process it.
        if (uri.Query.Length > 0)
        {
            // if necessary, remove the "?"
            string initString = Uri.UnescapeDataString(uri.Query);
            if ('?' == initString[0])
            {
                initString = initString.Remove(0, 1);
            }

            IInitializer initializer = response as IInitializer;

            if (null == initializer)
            {
                // the user expected IInitializer support
                throw new ArgumentException(initString);
            }
            else
            {
                initializer.Intialize(initString);
            }
        }

        return response;
    }

    #endregion
}

The SoaActivator now provides initialization support, something not directly supported by .NET Remoting.

For Convenience

Since I have packed the IProcessActivator and the SoaActivator in the same assembly as the Locator, I would prefer to have these be default supported types. To accomplish this, I will automatically register these types in the Locator's static constructor. This makes the constructor look like this:

C#
static Locator()
{
    RegisterActivator("local", new InProcessActivator());
    RegisterActivator("tcp", new SaoActivator());
}

Demo Code

The attached sample solution has a shared assembly that defines the interface that our services must implement.

C#
public interface ITheService
{
    string Hello(string name);
}

The SoaHost project implements the ITheService interface and the IInitializer interface (to support initialization) through the SoaTheService class. It then makes this class remotable.

C#
public class SoaTheService : MarshalByRefObject, 
                             ITheService, SaoActivator.IInitializer
{
    private string _salutation = "Hello ";
    #region ITheService Members

    public string Hello(string name)
    {
        return string.Format("{0} {1}", _salutation, name);
    }

    #endregion

    #region IInitializer Members

    public void Intialize(string arg)
    {
        _salutation = arg;
    }

    #endregion
}

The client project implements the ITheService interface through the TheInProcessService class. It then creates an instance of the TheInProcessService and the SoaTheService classes using the Locator. For ease of reading and debugging, the URI strings are in the code rather than in a configuration file.

To create the TheInProcessService object in the current process, the client calls:

C#
ITheService theClass = Locator.Activate(typeof(ITheService), 
    "local://localhost/Wycoff.Client/Wycoff.Client.TheInProcessService") 
    as ITheService;

and:

C#
theClass = Locator.Activate(typeof(ITheService), 
    "local://localhost/Wycoff.Client/Wycoff.Client.TheInProcessService?Hola") 
    as ITheService;

To create the SoaTheService proxy object, the client calls:

C#
theClass = Locator.Activate(typeof(ITheService), 
    "tcp://localhost:9000/TheService?xyz") 
    as ITheService;

There we go. The only difference between the lines of code is the string values. These strings could have been easily obtained from the configuration file.

Enhanced Solution

WCF puts a different wrapper around remoting. Let's go ahead and create an ILocatorActivator implementation that answers the HTTP Remoting need using WCF. To do this, I have to make a small change to our shared interface ITheService.

C#
[ServiceContract]
public interface ITheService
{
    [OperationContract]
    string Hello(string name);
}

While this will force a recompile of everything that uses the interface, it does not force a change to any of the Locator related classes. The WCF attributes will not negatively impact any of the other ITheInterface implementations that do not use WCF.

The next challenge comes when we try and create the remote object. To connect to a remote object using WCF, we:

  1. Create a Binding
  2. Create an EndPoint
  3. Create the object using ChannelFactory<>.CreateChannel

Notice that the ChannelFactory is a generic class, so it must be provided a type at compile time rather than runtime. This forces the same requirement to bubble up to the ILocatorActivator implementation that we are creating. This means two things:

  • Our ILocatorActivator implementation will be a generic class that captures the type that will be used by the ChannelFactory generic.
  • Where our other ILocatorActivator implementations could create any kind of object, an instance of this ILocatorActivator will only be able to create one kind of object.

Let's look at that second one a little more closely. If I want to create two different kinds of objects using HTTP WCF, I must have two instances of this class. Previously, the URI scheme was sufficient to determine which ILocatorActivator implementation would be used. The rest of the URI string determined the kind of object. Since the scheme is the key to the mapping, we need to use a different scheme for each instance of our generic class. Internally, the ILocatorActivator implementation can change the scheme to what it should be since this implementation will only know how to communicate using WCF over HTTP.

Taking these things into consideration, here is the code.

C#
public class WcfHttpActivator<t> : Locator.ILocatorActivator
{
    #region ILocatorActivator Members

    public object Activate(Type type, Uri uri)
    {
        string correctedUri = null;
        if (uri.IsDefaultPort)
        {
            correctedUri = string.Format("http://{0}{1}",
             uri.Host, uri.PathAndQuery);
        }
        else
        {
            correctedUri = string.Format("http://{0}:{1}{2}",
             uri.Host, uri.Port, uri.PathAndQuery);
        }

        BasicHttpBinding binding = new BasicHttpBinding();
        EndpointAddress address = new EndpointAddress(correctedUri);
        object proxy = ChannelFactory<t>.CreateChannel(binding, address);

        return proxy;
    }

    #endregion
}

The demo program can now register the new ILocatorActivator implementation using:

C#
Locator.RegisterActivator("HttpTheService", 
    new WcfHttpActivator<itheservice>());

and call it using:

C#
theClass = Locator.Activate(typeof(ITheService), 
        "HttpTheService://localhost:8080/RemoteTheService") 
        as ITheService;

Like .NET Remoting, WCF only uses the default constructor. I have not chosen to implement initialization, but I think you can figure out how to get there if you need it.

Running the Demo Program

The demo program is really made to run in the debugger so you can see the changes to the "s" variable. In order to run the Client project, you must also run the SaoHost and WcfHttpHost projects. I find that the easiest way to accomplish this is to right click on the solution (in the Solution Explorer) and use "Set StartUp Projects" from the context menu. You then have the ability to start all three projects automatically when you press F5 to debug the application.

Wrap Up

Using the Factory design pattern, I can hide the dynamic binding in some simple classes and use configuration to determine which implementation of an interface best suits my needs. All I need to do is make sure that the Activation assembly is included in my project and use the Locator class to create the objects that need to be dynamically bound. This lets me unit test with an implementation that is in my process and absolutely predictable, and to QA test and promote to production using the appropriate services, changing only the configuration. Additionally, I can initialize my remote objects as long as they support the interface provided.

One other benefit of this approach is that I can defer some decisions about what should be included in which process. If there are questions about which processes should host which objects, development can continue without a concrete answer.

History

  • 31-Oct-2009 - Initial posting.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)