Click here to Skip to main content
15,880,543 members
Articles / Programming Languages / C# 4.0

WCF Proxy Manager - Going Configless

Rate me:
Please Sign up or sign in to vote.
4.80/5 (8 votes)
17 Apr 2012CPOL5 min read 99.8K   601   36   14
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)


Written By
Architect ESPN
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAdding Message Headers Pin
Member 423590123-Jun-15 1:08
Member 423590123-Jun-15 1:08 
GeneralMy vote of 5 Pin
jaybto25-Jul-12 7:12
jaybto25-Jul-12 7:12 
GeneralMy vote of 5 Pin
jaybto25-Jul-12 6:16
jaybto25-Jul-12 6:16 
QuestionWhat about using an Async pattern? Pin
jaybto23-Jul-12 16:31
jaybto23-Jul-12 16:31 
AnswerRe: What about using an Async pattern? Pin
K.C. Jones24-Jul-12 3:40
K.C. Jones24-Jul-12 3:40 
GeneralRe: What about using an Async pattern? Pin
jaybto24-Jul-12 5:46
jaybto24-Jul-12 5:46 
GeneralRe: What about using an Async pattern? Pin
jaybto25-Jul-12 12:26
jaybto25-Jul-12 12:26 
Questionwe use a similar approach to this but Pin
Sacha Barber17-Apr-12 21:39
Sacha Barber17-Apr-12 21:39 
You are not getting rid of any config concerns, you still have to manage 3 settings in config, which is in all likelihood are the same 3 settings you would have to change if you specified full binding in config.

C#
var s = ConfigurationHelper.GetKey("HOST", Environment.MachineName);
var port = ConfigurationHelper.GetKey("PORT", "8080");
var binding = ConfigurationHelper.GetKey("BINDING", "HTTP");


These are typically the only 3 you would change if everything was specified in App.Config


Anyway for interest here is our proxy code

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.ServiceModel;
using System.Threading;
using Moneycorp.Common.Logging;
using Moneycorp.Common.Security;
using Moneycorp.Gateway;
using Moneycorp.Omni.UI.ServiceLayer;
using Moneycorp.Common.Data;
using Moneycorp.Omni.UI.ServiceLayer.ServiceProxy;
using Moneycorp.Omni.Gateway.BusinessObjects.Common;
using Moneycorp.BusinessObjects;

namespace Moneycorp.UI.ServiceLayer
{
    public static class Service<T>
    {
        private static IClientChannel proxy;
        private static ChannelFactory<T> channelFactory;

        private static Object createLock;

        static Service()
        {
            try
            {
                channelFactory = new ChannelFactory<T>(ServiceSettingsConfigurationHelper.Instance.DefaultBindingName);

                createLock = new Object();
            }
            catch (Exception e)
            {
                ApplicationException ae = new ApplicationException("Error initiating WCF channel for The Gateway", e);
                LogManager.Log(LogLevel.Error, ae);
                throw ae;
            }
        }

        public static R CallGateway<R>(Request request) where R : Response
        {
            try
            {
                try
                {
                    GetOpenProxy().ExecuteRequest(null);
                }
                catch
                {
                    GetOpenProxy(true);
                }

                return (R)GetOpenProxy().ExecuteRequest(request);
            }
            catch (FaultException<ArgumentOutOfRangeFault> e)
            {
                LogManager.Log(LogLevel.Error, e);
                throw new ApplicationException("A Gateway FaultException<ArgumentOutOfRangeException> occured", e);
            }
            catch (FaultException<ApplicationFault> genericAex)
            {
                //LogManager.Log(LogLevel.Error, genericAex);
                throw new BusinessLogicException(genericAex);
            }
            catch (Exception ex)
            {
                LogManager.Log(LogLevel.Error, ex);
                throw new Exception("A Gateway Exception occured", ex);
            }
        }



        private static IGateway GetOpenProxy()
        {
            return GetOpenProxy(false);
        }

        private static IGateway GetOpenProxy(bool forceOpen)
        {
            lock (createLock)
            {
                if (proxy == null || proxy.State != CommunicationState.Opened || forceOpen)
                {
                    if (proxy != null)
                    {
                        try
                        {
                            proxy.Close();
                        }
                        catch
                        {
                            proxy.Abort();
                        }
                    }

                    proxy = (IClientChannel)channelFactory.CreateChannel();
                    proxy.Open();
                }
            }

            return (IGateway)proxy;
        }
    }
}




Where we use a Request/Response type contract, so we only ever have one OperationContract on the service, which is ExecuteRequest(Request r)

We also use a double call mechanism, such that if it is faulted we simply push through dummy call which ensures new unfaulted proxy is then used
Sacha Barber
  • Microsoft Visual C# MVP 2008-2012
  • Codeproject MVP 2008-2012
Open Source Projects
Cinch SL/WPF MVVM

Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

AnswerRe: we use a similar approach to this but Pin
K.C. Jones18-Apr-12 2:39
K.C. Jones18-Apr-12 2:39 
AnswerRe: we use a similar approach to this but Pin
K.C. Jones18-Apr-12 3:05
K.C. Jones18-Apr-12 3:05 
QuestionCode would be nice... Pin
Dewey9-Apr-12 9:42
Dewey9-Apr-12 9:42 
AnswerRe: Code would be nice... Pin
K.C. Jones9-Apr-12 9:44
K.C. Jones9-Apr-12 9:44 
AnswerRe: Code would be nice... Pin
K.C. Jones9-Apr-12 9:46
K.C. Jones9-Apr-12 9:46 
GeneralRe: Code would be nice... Pin
Dewey9-Apr-12 13:38
Dewey9-Apr-12 13:38 

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

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