.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
ASMX Proxy vs WCF Proxy
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:
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.
is no clear channel stack. The SoapHttpClientProtocol wraps everything
on the client-side. It directly invokes System.Net API HttpWebRequest.
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
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)))
// Error handling code here …
HelloWorldProxy is auto-generated by svcutil.exe and it’s actually a
subclass of ClientBase<IHelloWorld> with the ServiceContract type
.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.
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>
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)
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?
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(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
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);
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
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>:
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 that is created internally is added to 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.
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
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
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.
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.
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.