Friday 12 December 2008

Taming the "Broken" IDisposable Implementation for WCF Clients

One of the early lessons we learn in Microsoft .NET development is to always use the using statement on objects that implement the IDisposable interface.  The effect is that the Dispose() method is called at the end of the using statement to ensure that resources are cleaned up.

Unfortunately, this pattern fails for Windows Communication Foundation (WCF) clients for the reason that is explained quite well in the posts, WCF Clients and the "Broken" IDisposable Implementation by David Barrett and Indisposable: WCF Gotcha #1 by Jesse Ezell. A Microsoft WCF development team member, Brian McNamara, explained the thinking behind this design decision in a post at the MSDN WCF forum Why does ClientBase Dispose need to throw on faulted state?

David and Jesse provide some quite elegant workarounds to this problem. I have adapted their solutions and applied them to a simple WCF service to illustrate their use. Let's take a step by step approach.

Imagine we have a simple WCF Calculator service.  This contains a single operation that adds two integers and returns the result.

Accessing the Calculator Service the Wrong Way

We can access the Calculator service using either a ChannelFactory<T> or a client proxy (a generated ClientBase<T> derived class). In that case, the client code might look like this.

private const string Uri = 
"net.pipe://localhost/CalculatorService/Calculator";

private static void RunUsingChannelFactoryWithUsingStatement()
{
NetNamedPipeBinding binding = new NetNamedPipeBinding();
EndpointAddress ep = new EndpointAddress(Uri);

ICalculator channel =
ChannelFactory<ICalculator>.CreateChannel(
binding, ep
);

using (channel as IDisposable)
{
int result = channel.Add(1, 2);
Console.WriteLine(result);
}
}

private static void RunUsingProxyWithUsingStatement()
{
NetNamedPipeBinding binding = new NetNamedPipeBinding();
EndpointAddress endpoint = new EndpointAddress(Uri);

using (
CalculatorClient client =
new CalculatorClient(binding, endpoint)
)
{
int result = client.Add(1, 2);
Console.WriteLine(result);
}
}

However,  we should not do this since Dispose() calls Close() and Close() can throw an exception.  Instead we have to be more long-winded.


Accessing the Calculator Service the Correct Way

private static void RunUsingChannelFactoryWithTryFinally()
{
NetNamedPipeBinding binding = new NetNamedPipeBinding();
EndpointAddress endpoint = new EndpointAddress(Uri);
ChannelFactory<ICalculator> channelFactory =
new ChannelFactory<ICalculator>(binding, endpoint);

IClientChannel channel =
channelFactory.CreateChannel() as IClientChannel;

bool closed = false;
try
{
int result = (channel as ICalculator).Add(1, 2);
Console.WriteLine(result);
channel.Close();
closed = true;
}
finally
{
if (!closed)
{
channel.Abort();
}
}
}

private static void RunUsingProxyWithTryFinally()
{
NetNamedPipeBinding binding = new NetNamedPipeBinding();
EndpointAddress endpoint = new EndpointAddress(Uri);
ClientBase<ICalculator> client =
new CalculatorClient(binding, endpoint);

bool closed = false;
try
{
int result = (client as CalculatorClient).Add(1, 2);
Console.WriteLine(result);
client.Close();
closed = true;
}
finally
{
if (!closed)
{
client.Abort();
}
}
}

Can we improve on this? Yes.


Improved Solution 1 - Implement IDisposable in Proxy Partial Class

public partial class CalculatorClient : IDisposable
{
void IDisposable.Dispose()
{
try
{
Close();
}
catch (CommunicationException)
{
Abort();
}
catch (TimeoutException)
{
Abort();
}
}
}

With this in place we can force the using statement to do the right thing. This is now valid.

using (CalculatorClient client = new CalculatorClient(binding, endpoint))
{
int result = client.Add(1, 2);
Console.WriteLine(result);
}

This is an elegant solution but it has two disadvantages.



  1. It only works for proxy-based client access, not ChannelFactory-based access.
  2. The client must write the partial class for each new service they wish to access (though this could be automated with code generation).

Improved Solution 2 - Use Generics and Delegates


First we define these two delegates that represent code blocks for proxy-based and ChannelFactory-based client access respectively.

/// <summary>
///
Represents a code block containing WCF proxy client method calls.
/// </summary>
public delegate void
UseProxyDelegate<TInterface, TClass>(TClass proxy);
/// <summary>
///
Represents a code block containing WCF client method calls
/// using a <see cref="ChannelFactory"/>.
/// </summary>
public delegate void
UseChannelFactoryDelegate<TInterface>(TInterface channel);

Then we define a class that abstracts away the closing of the connection for us.

public static class Service
{
/// <summary>
///
Uses a WCF client via its proxy.
/// </summary>
public static void UseProxy<TInterface, TClass>(
ClientBase<TInterface> proxy,
UseProxyDelegate<TInterface, TClass> codeBlock
)
where TInterface : class
where
TClass : ClientBase<TInterface>, TInterface
{
if (proxy == null)
throw new ArgumentNullException(
"proxy", "proxy is null."
);
if (codeBlock == null)
throw new ArgumentNullException(
"codeBlock", "codeBlock is null."
);

bool closed = false;
try
{
codeBlock(proxy as TClass);
proxy.Close();
closed = true;
}
finally
{
if (!closed)
{
proxy.Abort();
}
}
}

/// <summary>
///
Uses a WCF client via its <see cref="ChannelFactory"/>.
/// </summary>
public static void UseChannelFactory<TInterface>(
ChannelFactory<TInterface> channelFactory,
UseChannelFactoryDelegate<TInterface> codeBlock
)
where TInterface : class
{
if (channelFactory == null)
throw new ArgumentNullException(
"channelFactory", "channelFactory is null."
);
if (codeBlock == null)
throw new ArgumentNullException(
"codeBlock", "codeBlock is null."
);

IClientChannel channel =
channelFactory.CreateChannel() as IClientChannel;
bool closed = false;
try
{
codeBlock(channel as TInterface);
channel.Close();
closed = true;
}
finally
{
if (!closed)
{
channel.Abort();
}
}
}
}

With this in place we can now proceed to the final solution.

private static void RunUsingChannelFactory()
{
NetNamedPipeBinding binding = new NetNamedPipeBinding();
EndpointAddress endpoint = new EndpointAddress(Uri);
ChannelFactory<ICalculator> channelFactory =
new ChannelFactory<ICalculator>(binding, endpoint);

Service.UseChannelFactory<ICalculator>(
channelFactory,
delegate(ICalculator calculator)
{
int result = calculator.Add(1, 2);
Console.WriteLine(result);
}
);
}

private static void RunUsingProxy()
{
NetNamedPipeBinding binding = new NetNamedPipeBinding();
EndpointAddress endpoint = new EndpointAddress(Uri);
ClientBase<ICalculator> client =
new CalculatorClient(binding, endpoint);

Service.UseProxy<ICalculator, CalculatorClient>(
client,
delegate(CalculatorClient calculator)
{
int result = calculator.Add(1, 2);
Console.WriteLine(result);
}
);
}

If we are using C# 3.0 we can replace the anonymous delegates with statement lambdas but there is no real gain in expressiveness in doing so in this case.


This final solution is more general purpose but is straightforward to use.

No comments:

Post a Comment