Introduction
This article demonstrates a pattern to make a durable IClientChannel
that does not require many layers of configuration to get it to execute. This eases deployment, increases reliability, and frankly makes the code easy to develop for the front line developer.
Background
WCF has really changed the way .NET tiers communicate. While being revolutionary in its extensibility, it has left many developers pining for the good old days of Web Services. One of the major drawbacks
to WCF is also its greatest asset. It really is a very configurable communication vehicle. Often however, that configuration turns into its greatest challenge when deploying and developing code. There is also the fragile nature of the
IClientChannel
. Once a channel is created, it is very easy to disrupt or set into a faulted state.
Using the code
The diagram
The implementation
In this example, I have created two simple WCF services, defaulting the binding to HTTP and the port to 8080. A caveat here is that if you intend on using this pattern using TCP and a self-hosted WCF service, you will have to utilize the .NET
Port Sharing Service. I then generated out two proxy files using svcutil. Then create a new public class called
Proxies
.
namespace HelloWorldServiceProxies
{
public class Proxies
{
public static IHelloWorld HelloWorldService
{
get { return ProxyManager.GetProxy<IHelloWorld>(); }
}
public static IGoodByeWorld GoodByeWorldService
{
get { return ProxyManager.GetProxy<IGoodByeWorld>(); }
}
}
}
As you can see, there are two static properties that represent the proxies I want to access. Here is a sample of how easy the code is to invoke:
var helloReturn = Proxies.HelloWorldService.Hello();
There was no need to wrap the code in a using
block at all. Why, you ask? Let's see what really happened? First we will take
a look under the hood at the
GetProxy
method that was invoked.
public static T GetProxy<T>()
{
var t = typeof (T);
if (!_proxyCache.ContainsKey(t))
{
try
{
_proxyCache.Add(t, createProxy<T>());
}
catch (Exception ex)
{
throw new Exception("Failed to create provider: " + ex.Message, ex);
}
}
var s = (ProxyBase<T>) _proxyCache[t];
var ic = (IClientChannel) s.InnerChannel;
ic.Abort();
s.SetChannel();
return s.InnerChannel;
}
It is pretty simple, we create a key by doing the typeof
on the type parameter. Then we see if the proxy wrapper is cached. If not we create it. Once we have it, we cache it and then return its inner channel (the Client Channel) after we have reset its underlying connection. This is a real important item as this makes the cached proxy durable. If you have a network interruption between call 1 and then call 2, using this guarantees that there should be no issue.
I had experimented with doing a ping()
method on each service and invoking that but found there was no easy way to do this generically, and if the ping failed, we would have to do this. I would have to reset the channel anyway. Seeing how the reset channel only takes 4-5 milliseconds (about the same time to return a ping), why not just ensure we get a clean channel?
Now let’s dive a little deeper into the scary world of channel factories, or as I like to call it the Voodoo Science of WCF. Let’s look at the
CreateProxy
code.
internal static ProxyBase<T> createProxy<T>()
{
var t = typeof (T);
ChannelFactory channelFactory;
lock (_channelFactoryCache)
{
if (_channelFactoryCache.ContainsKey(t))
{
channelFactory = (ChannelFactory) _channelFactoryCache[t];
}
else
{
channelFactory = createChannelFactory<T>();
_channelFactoryCache.Add(t, channelFactory);
}
}
EndpointAddress endpoint = null;
var s = ConfigurationHelper.GetKey("HOST", Environment.MachineName);
var port = ConfigurationHelper.GetKey("PORT", "8080");
var binding = ConfigurationHelper.GetKey("BINDING", "HTTP");
var serviceName = typeof (T).ToString();
if (serviceName[0] == char.Parse("I"))
{
serviceName = serviceName.Remove(0, 1);
}
string server;
switch (binding)
{
case "TCP":
server = string.Format("net.tcp://" + getIPAddress(s) + ":{0}/{1}", port, serviceName);
endpoint = new EndpointAddress(server);
break;
case "HTTP":
server = string.Format("http://" + getIPAddress(s) + ":{0}/{1}", port, serviceName);
endpoint = new EndpointAddress(server);
break;
}
var pb = new ProxyBase<T>((ChannelFactory<T>) channelFactory, endpoint);
return pb;
}
The CreateProxy
method by itself is pretty straightforward. As you can see, we first create a key and then get the cached Channel factory or create one as needed.
private static ChannelFactory createChannelFactory<t>()
{
Binding b = null;
switch (ConfigurationHelper.GetKey("BINDING", "HTTP"))
{
case "HTTP":
b = new BasicHttpBinding();
break;
case "TCP":
b = new NetTcpBinding();
break;
}
if (b != null)
{
var factory = new ChannelFactory<t>(b);
ApplyContextToChannelFactory(factory);
return factory;
}
return null;
}
The next area is an example of some simple configuration options. HOST, PORT, and BINDING are
three simple keys I have made configurable. These keys are used to build the endpoint and create the proper channel factory.
if (serviceName[0] == char.Parse("I"))
{
serviceName = serviceName.Remove(0, 1);
}
As you can see above, by using the Interface Name minus the “I”, we should be able to reliably create the default URI for a specific service. Once we have our endpoint, we pass the channel factory and endpoint to the proxy wrapper. That class will use the channel factory and endpoint to set the Channel.
Performance
Parameters: Two service calls, one calling HelloService and one calling GoodbyeService. Each call is made 1000 times.
Numbers
- Traditional methodology : Average 4.1 Milliseconds per 2 calls total time for 1000 iterations 6748 milliseconds.
- Proxy Manager: Average 9.1 milliseconds for 2 calls total time for 1000 runs 9551 milliseconds.
Let us take a look at the numbers. Let no good code go untested. What we find when we run some performance metrics is that the average time to create a default WCF channel and make a
simple call hovers at just about 4 milliseconds. The Proxymanager
code introduces a bit of a
performance draw. Its average for a similar call is around 9 milliseconds.
Sounds terrible? Folks, we are talking milliseconds. In the included source, I have
two test harness projects. One important note: using a traditional approach and one with my
proxy manager approach. Note that in my new methodology, you do not have to worry about disposing channels as they get recreated each time you want to use it. This increases your ease of coding dramatically.
Points of Interest
I don't know about you, but three simple appsettings are a lot easier to manage. I'm sure there are those wondering, why bother? Well, when you have 1 or 2 places to deploy code to and you
are doing the deployment, then I can see your point.
Built in a large enterprise where I have dozens of test / uat / srt type environments, the configuration becomes
truly a burden.
Now as you might imagine, this only really works if you are not trying to manually override your service names. In another article, I will discuss how to
programmatically host up your contracts and even add compression to the messaging.
Follow UP Question
There was a some question regarding the creation of the ChannelFactory. The question was a good one, "Why have it at all". I have included
an example of why. There are times when you need specific bindings options. Usually this is handled by the config file options.
In our scenario that's not possible without hard coding them. In the following code example I'm doing just that to demonstrate what type
of options you can set on the channel factory.
protected override ChannelFactory CreateChannelFactory<I, T>(T context)
{
if (object.Equals(context, default(T)))
{
throw new ArgumentException("The argument 'context' cannot be null.");
}
ChannelFactory<I> factory = null;
var t = new NetTcpBinding
{
CloseTimeout = new TimeSpan(0, 0, 10, 0),
OpenTimeout = new TimeSpan(0, 0, 10, 0),
ReceiveTimeout = new TimeSpan(0, 1, 0, 0),
SendTimeout = new TimeSpan(0, 0, 10, 0),
TransactionFlow = false,
TransferMode = TransferMode.Buffered,
TransactionProtocol = TransactionProtocol.OleTransactions,
HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
ListenBacklog = 500,
MaxBufferPoolSize = 524288,
MaxBufferSize = 2147483647,
MaxConnections = 2000,
MaxReceivedMessageSize = 2147483647,
ReaderQuotas =
{
MaxDepth = 128,
MaxStringContentLength = int.MaxValue,
MaxArrayLength = 2147483647,
MaxBytesPerRead = 16384,
MaxNameTableCharCount = 16384
}
};
t.ReliableSession.Ordered = true;
t.ReliableSession.InactivityTimeout = new TimeSpan(0, 0, 10, 0);
t.ReliableSession.Enabled = false;
factory = new ChannelFactory<I>(t);
ApplyContextToChannelFactory<T>(context, factory);
return factory;
}