Performance Improvement for WCF Client Proxy Creation in .NET 3.5 and Best Practices


Introduction


In .NET 3.0 SP1, which will be shipped together with .NET 3.5, there is significant performance improvement in WCF client proxy creation. For BasicHttpBinding, the performance is close to that of ASMX proxy creation.


ASMX Proxy vs WCF Proxy


ASMX proxy is much simpler than WCF proxy. The former is a wrapper the type System.Web.Services.Protocols.SoapHttpClientProtocol. In ASMX world, the programming model is “horizontal” in two ways:


·         There is no ServiceContract concept. The signatures of all service operations (WebMethod) are duplicated in the client proxy. The calls to those operations on the client side go through SoapHttpClientProtocol.Invoke. So there is no separation between service interface and the underlying message processing system.


·         There is no clear channel stack. The SoapHttpClientProtocol wraps everything on the client-side. It directly invokes System.Net API HttpWebRequest.


Instead WCF provides a “vertical” programming model. You get the same rich programming features on the client side as that you get on the service-side. Internally WCF proxy is a .NET Remoting transparent proxy which is wrapped inside the type System.ServiceModel.Channels.ServiceChannel. The transparent proxy allows you to perform desired type casting and thus you can use the proxy and call service operations just like you are using the ServiceContract interface.


WCF Proxy Creation Cost


As pointed out in my last blog entry, there are two common ways of creating WCF client proxies (or channels):


1)     Using ClientBase<T> which is the default WCF support and you can use svcutil.exe tool to generate the proxy code.


2)     Using ChannelFactory<T>.CreateChannel().


A common pattern of using ClientBase<T> styled proxies is to perform creation/disposing on each call:


foreach (string msg in myList)


{


    // You may add try/catch block here …


    HelloWorldProxy proxy = new HelloWorldProxy(“MyEndpoint”, new EndpointAddress(address)))


    proxy.Hello(msg);


    proxy.Close();


    // Error handling code here …


}


Here HelloWorldProxy is auto-generated by svcutil.exe and it’s actually a subclass of ClientBase<IHelloWorld> with the ServiceContract type IHelloWorld.


In .NET 3.0, creating/disposing WCF client proxies is a very expensive operation for both approaches. It’s much worse than that for ASMX proxies. The approach 1) involves creating ChannelFactory<T> internally. Each proxy holds a ChannelFactory object as a private field. The lifetime of the ChannelFactory is fully controlled by the proxy. Approach 2) is much better since you have your own control of the ChannelFactory<T> object and thus you may only create it once and thus save the cost for further proxy creation.


Why is creating/disposing ChannelFactory so expensive? This is because it involves the following major operations:



  • Constructing the ContractDescription tree

  • Reflecting all of the required CLR types

  • Constructing the channel stack

  • Disposing all of the resources

In most cases, these data are identical between different proxies which are created in the same way.


In .NET 3.0 SP1, which will be shipped together with .NET 3.5, there are two major performance improvements in this area:


·         ChannelFactory caching inside ClientBase<T>. This significantly improves the performance for approach 1).


·         Channel management logic is improved and thus both 1) and 2) benefits from this.


With these improvements, creation of WCF proxie have quite comparable performance as ASMX ones.


Improvements


The idea of improving the performance of ClientBase<T> is simple: caching ChannelFactory objects. This is similar as the client type cache in ASMX proxy.


ChannelFactory Caching for ClientBase<T>


The key idea is to cache the ChannelFactory object at AppDomain level so that the lifetime of a ChannelFactory object is not controlled by the proxy. The cache is a most recently used (MRU) cache. The cache size is hard-coded as 32 so that the least used ChannelFactory objects are purged from the cache.


 With the ChannelFactory cache, the rough process of creating a ClientBase<T> object is as following:


·         In the constructor of ClientBase<T>, a lookup is performed to find a matched ChannelFactory in the cache.


·         If found, the ref-count of the ChannelFactory is incremented. Otherwise, a new ChannelFactory is created based on the settings.


·         Before the inner channel  (the transparent proxy) of ClientBase<T> is created, the caching logic for the current ClientBase<T> can be disabled if other public properties (such as ChannelFactory, Endpoint, and ClientCredentials) are accessed.


·         Once the innerchannel if created successfully, the ChannelFactory object for the ClientBase<T> is added to the cache if it’s not grabbed from the cache and caching is not disabled.


What is Matched ChannelFactory?


Did I say “matched” ChannelFactory in the last section? Yes, you have to make sure that you can differentiate different ChannelFactory objects based on the input settings from ClientBase<T>’s constructors. There are two types of constructors: those have Binding as a parameter and those don’t. Here are those that don’t:



  • ClientBase();

  • ClientBase(string endpointConfigurationName);

  • ClientBase(string endpointConfigurationName, string remoteAddress);

  • ClientBase(string endpointConfigurationName, EndpointAddress remoteAddress);

  • ClientBase(InstanceContext callbackInstance);

  • ClientBase(InstanceContext callbackInstance, string endpointConfigurationName);

  • ClientBase(InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress);

  • ClientBase(InstanceContext callbackInstance, string endpointConfigurationName, EndpointAddress remoteAddress);

For these constructors, all arguments (including default ones) are in the following list:


·         InstanceContext callbackInstance


·         string endpointConfigurationName


·         EndpointAddress remoteAddress


As long as these three arguments are the same when ClientBase<T> is constructed, we can safely assume that the same ChannelFactory can be used. Fortunately, String and EndpointAddress types are immutable, i.e., we can make simple comparison to determine whether two arguments are the same. For InstanceContext, we can use Object reference comparison. The type EndpointTrait<TChannel> is thus used as the key of the MRU cache.


 


Here are the two constructors which have Binding as a parameter:



  • ClientBase(Binding binding, EndpointAddress remoteAddress);

  • ClientBase(InstanceContext callbackInstance, Binding binding, EndpointAddress remoteAddress);

Since Binding object is mutable, i.e., people can use it for one proxy, modify it and use it for another one, we can not safely detect whether there is a change to a binding object or not. Thus when the Binding is supplied in the constructors, the caching mechanism is disabled for that proxy.


Disabling caching


WCF supports extensibility in the way that people can change the settings of the ChannelFactory and the ContractDescription before the ChannelFactory is opened. Such change to a newly created proxy would prevent it from sharing the same ChannelFactory with other proxies. Same logic applies to other properties for the ClientBase<T>:



  • ChannelFactory

  • Endpoint

  • ClientCredentials

When any of these properties of ClientBase<T> is accessed before the inner channel is created and when ClientBase<T>.Open is called, the caching is disabled for this proxy. This is natural since you don’t want one proxy reuses the ChannelFactory for another proxy which has different contract or security settings.


Channel Management


Each channel that is created internally is added to the System.ServiceModel.Channels.CommunicationObjectManager. The performance of adding/removing an item from this class is improved in .NET 3.5. The fix was to switch from List<T> to Hashtable internally. This is why you will see performance improvement in approach 2) above.


Best Practices


Now that I have talked about the internals, I want to summarize how you can achieve best performance using WCF client proxies.


Reuse the same proxy


In many cases, you would want to reuse the same proxy. This has the best performance. It is especially true when you use security features since the initial security negotiation can have high cost.


Don’t forget to open the proxy explicitly before using it as I mentioned that in my previous blog entry.


Use proxy that enables caching


As mentioned above, you can either use ChannelFactory<T>.CreateChannel to create your proxy or you can use auto-generated proxies. If you use the latter, you need to be aware of the following in order to get ChannelFactory cached:


·         Don’t use the constructors of the proxy that takes Binding as an argument.


·         Don’t access the public properties ChannelFactory, Endpoint, and ClientCredentials of the proxy.


Disabling caching


If you really want to disable caching, you can simply do the reverse of above. For example, you can access the ChannelFactory property of the proxy before it is first used.


Proxy/Channel pooling


The last resort of achieving high performance is through proxy caching. In some scenarios, you may not want all threads to use the same proxy due to the following two possible reasons:


·         The context of the channels does not allow multiple threads to access.


·         There is some bottleneck in the Channel stack that hurts performance when a single proxy is used.


The logic of pooling can be from very simple to very complicated. Some ideas to keep in mind are as following:


·         You need to implement the right synchronization logic for managing the proxies.


·         You need to make sure the proxies are used equally. Sometimes, you may want to implement a round-robin pattern for the proxies.


·         You need to handle exceptions and retries for the pool.


·         The pool size needs to be limited and configurable.


·         You may need to be able to create proxies even if when no proxy is available from the pool.


I don’t have a concrete example about why you need to implement the pool. If you have such a scenario, please do let me know.


 

Comments (20)

  1. Interessantissimo postdi Wenlong Dong, membro del team WCF Performance, che descrive i miglioramenti

  2. .Net 3.0 SP1 e WCF performance improvement

  3. With .NET 3.5 and Visual Studio 2008 being quite close to RTM these days, I thought it would be time…

  4. omario says:

    "You need to handle exceptions and retries for the pool."

    In what conditions proxy should not be returned to the pool? Is it enough for proxy to be in Faulted state?

  5. I was in a meeting last week with a few WCF users at Microsoft when they asked about performance issues

  6. RobertTostevin says:

    Hi

    In .NET3 we are using a WCF service to talk to a WF. The WCF service is being called by an ASP page request. In order to optimize performance we are using a singleton proxy class which is shared by all page requests on the same server. The GUID is obviously part of the params passed to the WCF service which then routes the Event to the right WF. In .NET 3.5 if we were to switch to using the new WorkflowServiceHost & the new Receive Activity the GUID is part of the contextchannel & therefore part of the message being sent by the proxy class. My understanding is that reuse of the proxy via our singleton will fail as each Page would need to block the other pages requests whilst it makes its call (as it would need to change the GUID in the context)ie we lose our scalability.

    … Is that Correct ?

    If so how can this be made more scalable so that each page request doesn’t have to go through the expense of creating a brand new proxy each time ?

    Thanks

    Rob.

  7. wdong says:

    Hi Rob, that’s a good question. I have just posted another blog entry for this: http://blogs.msdn.com/wenlong/archive/2007/11/08/how-to-use-a-singleton-wcf-proxy-to-call-different-workflow-service-instances-in-net-3-5.aspx

    Please let me know if this answers your question above. Thanks!

  8. Introduction Ideally we should not need to pool WCF client proxies as I mentioned in my previous blog

  9. Introduction Ideally we should not need to pool WCF client proxies as I mentioned in my previous blog

  10. MCGP Peters says:

    "If you have such a scenario, please do let me know."

    A scenario would be an intermediate, where a WCF service is used to forward calls to other service end-points. You would like to keep a pool of channels to the previous called end-points…

  11. Introduction This week it was asked to me to find some elegant and practical way to use WCF. After some

  12. The using statement in C#, seems to be quite useful especially to a developer as theoretically, you not

  13. marc@wittke-web.de says:

    Hi Wenlong,

    what exaclty do you mean with

    "Don’t access the public properties ChannelFactory, Endpoint, and ClientCredentials of the proxy"

    in the listing what to be aware to not implicitly disable the caching? Is this valid for set only or also for get? I’m asking because we have an application that writes some addressing stuff to the trace, and is accessing the above mentioned properties. If this prevents the caching, we could get over the missing trace as tradeoff against better performance.

    Thanks,

    Marc

  14. I don’t think I ever wrote about the changes made to WCF generated typed proxies in Orcas, although Wenlong

  15. This article is part of a series; · WCSF Application Architecture 1: Introduction · WCSF Application

  16. tmanthey says:

    Unfortunately the caching of the ChannelFactory has become a reference example why caching is dangerous. Still 3.5 SP1 contains a subtle yet nasty bug. It surfaces once you try to call the ClientBase<T>.Open() method. This can be preferable if you try to reduce the (tremendous) time a first call to a service consumes. The call is forwarded to the ChannelFactory which in case it was cached can be already in "Opened" state, while the ClientBase object is still in "Created" state. I see it once in a while, but have not yet been able to reproduce it in a reliable manner.

    In this case you see the exception:

    The communication object, System.ServiceModel.ChannelFactory`1[IMyService], cannot be modified while it is in the Opened state.

    To work around check the state of the ClientBase<T>.ChannelFactory to be "Created" property before calling ClientBase<T>.Open()

    http://connect.microsoft.com/wcf/feedback/ViewFeedback.aspx?FeedbackID=456192

  17. UrmilShah says:

    I am not sure why caching proxy / channel will give better output. Shouldn’t it create bottleneck in high traffic website?

    Webservices / WCF over IIS are kind of websites only.

    For example If I am accesing 10 request using one browser vs requesting 10 request in 10 different browser (multithreaded way).

    How Cahing of proxy / channel will react for Async calls? Appreciate any help here.

  18. finnlake says:

    Hi Wenlong,

    We have a scenario where we have created an adapter to wrap the wcf client proxy. By doing so we separate all the wcf stuff from the application that uses the adapter.

    We use fxcop and coverage and exclude those for the adapter project since the generated proxy does not play well with fxcop rules and coverage.

    The only problem we have now is that all, applications using the adapter must have the wcf information defined in their config file.

    We started by putting all configuration in code but like you have described this disabled the proxy caching.

    If we want to have clean configuration and not have to copy the configuration all around is the only option

    we have to implement our own caching?

  19. uhclstud says:

    I'm using nettcp binding and I believe, this protocol does connection pooling internally. Is it worth to cache the channel, as socket connections are already pooled ?