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

WCF Proxy Manager - Going Configless

4.80/5 (8 votes)
17 Apr 2012CPOL5 min read 100.9K   601  
Making WCF Config Lite, easy for developers, and durable ... yup durable without ping.

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

Image 1

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.

C#
namespace HelloWorldServiceProxies
{
    public class Proxies
    {
        /// <summary>
        /// </summary>
        public static IHelloWorld HelloWorldService
        {
            get { return ProxyManager.GetProxy<IHelloWorld>(); }
        }

        /// <summary>
        /// </summary>
        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:

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

C#
// Return an Encapsulated IClientChannel
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;
    //here is the key Abort anything and force dispose
    ic.Abort();
    // Recreate the channel there is a small amount of overhead here about 4-5 milliseconds 
    // Well worth the ease of deployment / durability
    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.

C#
/// <summary>
///   This is where we new up an encapsulated IClientChannel
/// </summary>
/// <typeparam name="T"> </typeparam>
/// <returns> </returns>
internal static ProxyBase<T> createProxy<T>()
{
    var t = typeof (T);
    // Get The Channel Factory or create it if needed
    ChannelFactory channelFactory;
    lock (_channelFactoryCache)
    {
        if (_channelFactoryCache.ContainsKey(t))
        {
            channelFactory = (ChannelFactory) _channelFactoryCache[t];
        }
        else
        {
            channelFactory = createChannelFactory<T>();
            _channelFactoryCache.Add(t, channelFactory);
        }
    }
    EndpointAddress endpoint = null;
    //get Configuration
    var s = ConfigurationHelper.GetKey("HOST", Environment.MachineName);
    var port = ConfigurationHelper.GetKey("PORT", "8080");
    var binding = ConfigurationHelper.GetKey("BINDING", "HTTP");
    var serviceName = typeof (T).ToString();
    //Ser the correct service name Defaults to the interface name minus the I
    if (serviceName[0] == char.Parse("I"))
    {
        serviceName = serviceName.Remove(0, 1);
    }
    //Create the URI
    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;
    }
    //Create the Enapsulated IClientChanenel
    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.

/// <summary>
/// </summary>
/// <typeparam name="> </typeparam />
/// <returns /> </returns />
/// <exception cref="ArgumentException" />
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);
        // This is super important
        // Why ? This is where you can add
        // Custom behaviors to your outgoing calls
        // like Message Inspectors ... or behaviors.
        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.

//the correct service name Defaults to the interface name minus the I
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.

C#
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;
}

License

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