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.
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.
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:
- 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.
- 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. - 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).
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.
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.
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.
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.
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.
public class InProcessActivator : Locator.ILocatorActivator
{
#region ILocatorActivator Members
public object Activate(Type type, Uri uri)
{
string[] pathSegments = uri.LocalPath.Split('/');
if (pathSegments.Length != 3)
{
throw new ArgumentException(uri.ToString());
}
string assemblyName = pathSegments[1];
Assembly assembly = Assembly.Load(assemblyName);
if (null == assembly)
{
throw new ArgumentException(uri.ToString());
}
string className = pathSegments[2];
object response = null;
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.
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.
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:
public class SaoActivator : Locator.ILocatorActivator
{
public interface IInitializer
{
void Intialize(string arg);
}
#region ILocatorActivator Members
public object Activate(Type type, Uri uri)
{
string realUri = (uri.Query.Length == 0) ?
uri.AbsoluteUri :
uri.AbsoluteUri.Substring(0, uri.AbsoluteUri.Length - uri.Query.Length);
object response = Activator.GetObject(type, realUri);
if (uri.Query.Length > 0)
{
string initString = Uri.UnescapeDataString(uri.Query);
if ('?' == initString[0])
{
initString = initString.Remove(0, 1);
}
IInitializer initializer = response as IInitializer;
if (null == initializer)
{
throw new ArgumentException(initString);
}
else
{
initializer.Intialize(initString);
}
}
return response;
}
#endregion
}
The SoaActivator
now provides initialization support, something not directly supported by .NET Remoting.
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:
static Locator()
{
RegisterActivator("local", new InProcessActivator());
RegisterActivator("tcp", new SaoActivator());
}
The attached sample solution has a shared assembly that defines the interface that our services must implement.
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.
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:
ITheService theClass = Locator.Activate(typeof(ITheService),
"local://localhost/Wycoff.Client/Wycoff.Client.TheInProcessService")
as ITheService;
and:
theClass = Locator.Activate(typeof(ITheService),
"local://localhost/Wycoff.Client/Wycoff.Client.TheInProcessService?Hola")
as ITheService;
To create the SoaTheService
proxy object, the client calls:
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.
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
.
[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:
- Create a Binding
- Create an EndPoint
- 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.
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:
Locator.RegisterActivator("HttpTheService",
new WcfHttpActivator<itheservice>());
and call it using:
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.
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.
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.
- 31-Oct-2009 - Initial posting.