Ninja Ferret

The random thoughts of a software developer

WCF Clients Part 2 - IChannelFactory

I talked last time about how I approached building a WCF service and how using "Add Service Reference" was causing me some problems. The next part of my journey into WCF was to look at other ways of managing the client and I came across IChannelFactory<T>.

Creating a proxy client IChannelFactory<T>...

So, to recap, last time I started by defining the following service:
[ServiceContract]
public interface AccountService
{
    [OperationContract]
    [FaultContract(typeof(AccountNotFoundFault))]
    Account Get(int id);

    [OperationContract]
    [FaultContract(typeof(NoMatchingAccountsFault))]
    Account Find(string customerName, AccountType accountType);

    [OperationContract]
    [FaultContract(typeof(InvalidCustomerNameFault))]
    void Create(string customerName, AccountType accountType);
}
The next thing is to look at the original client code within an ASP.NET MVC controller:
public ActionResult AddNewAccount(NewAccountCommand newAccountCommand)
{
    // Validate the command
    ...
    // Create the account using the service
    try
    {
        _service.Create(newAccountCommand.CustomerName, newAccountCommand.AccountType);
     }
    catch (InvalidCustomerNameException e)
    {
        // Handle the error
    }
}
How do we get to using the channel factory? As my client is already referencing the service assemblies and using the channel factory we can implement the interface:
var channelFactory = new ChannelFactory<AccountService>();
var proxy = channelFactory.CreateChannel();
The proxy that is generated can be passed into the ASP.NET MVC controller as the service.

So where is the problem?

The client code above will work... well, it will work in the case where the exceptions are not thrown. When an exception is thrown it will be translated into a FaultException therefore the original exception will not be caught. To fix this we could try:
public ActionResult AddNewAccount(NewAccountCommand newAccountCommand)
{
    // Validate the command
    ...
    // Create the account using the service
    try
    {
        _service.Create(newAccountCommand.CustomerName, newAccountCommand.AccountType);
     }
    catch (InvalidCustomerNameException e)
    {
        // Handle the error
    }
    catch(FaultException<InvalidCustomerNameFault> fault)
    {
        // Handle the error
    }
}
But now the code is less clean, for each error I want to handle I would have to now catch either just the equivalent FaultException or both the normal exception or the FaultException. However, the service is now disposable! I now have to remember to call the Close() or Dispose() methods at some point to release the resources. When do I call it? I have injected this into the controller so the controller cannot dispose it because it does not know whether there are any other references to this object out there. While searching around this problem I came across the Indisposable - WCF Gotcha 1 article on serviceoriented.com. To summarise, a disposable object can be wrapped in a using statement which will automatically call the Dispose() method or you can call the Dispose() method yourself (usually in a finally block). However, the proxy that is generated by ChannelFactory requires that you call Abort() after an exception rather than Dispose(). The pattern that was recommended by the blog article was that we can wrap most of the error handling within a generic method:
public static class Service<T>
{
    public static ChannelFactory<T> _channelFactory = new ChannelFactory<T>(""); 

    public static void Use(UseServiceDelegate<T> codeBlock)
    {
        IClientChannel proxy = (IClientChannel)_channelFactory.CreateChannel();
        bool success = false;
        try
        {
            codeBlock((T)proxy);
            proxy.Close();
            success = true;
        }
        finally
        {
            if (!success)
            {
                proxy.Abort();
            }
        }
    }
}
But that means that rather than making a standard call to the service we now have to change the client code to:
public ActionResult AddNewAccount(NewAccountCommand newAccountCommand)
{
    // Validate the command
    ...
    // Create the account using the service
    try
    {
        Service<AccountService>.Use(service => service.Create(newAccountCommand.CustomerName, newAccountCommand.AccountType);
     }
    catch (InvalidCustomerNameException e)
    {
        // Handle the error
    }
    catch(FaultException<InvalidCustomerNameFault> fault)
    {
        // Handle the error
    }
}
This makes me a little worried because the changes have just made the system untestable, by calling into this static class I have now bound the controller to requiring a WCF service to test.

Crafting my own proxy...

In order to keep my code testable I used the above pattern within my own hand-crafted proxies:
public class AccountServiceProxy : IAccountService
{
    public void Create(string customerName, AccountType accountType)
    {
        try
        {
            Service<AccountService>.Use(service => service.Create(customerName, accountType);
        }
        catch(FaultException<InvalidCustomerNameFault> fault)
        {
            throw new InvalidCustomerNameException(...);
        }
    }

    public Account Get(int id)
    {
        try
        {
            Account account = null;
            Service<AccountService>.Use(service => account = service.Get(id);
            return account;
        }
        catch(FaultException<AccountNotFoundFault> fault)
        {
            throw new AccountNotFoundException(...);
        }
    } 
}
This keeps the client code in tact, it only needs to handle the standard exceptions that it always handled rather than adding anything more because of the faults. A much cleaner solution, in my mind.

So is there still a problem?

I think that there is still a problem, I am now having to manually do all of the work that I was hoping that someone else would do for me and this led me to think that I now had a pattern that I was manually applying to all services therefore it should be possible to automate the generation of these proxies; this leads me on nicely to my next post...

Tags:

blog comments powered by Disqus