A Smarter WCF Service Client, Part 3

Published on: Author: Michael

In the last article we replaced the standard WCF service client with an improved version. But beyond solving an issue with cleaning up the client we haven’t really improved the calling code any. In this article we are going to look at one approach for making the calling code cleaner. The less code that is needed to call something the cleaner it tends to make code. We will present one approach for making WCF calls one-liners. This is not always the best solution so we will talk about the disadvantages as well.


ServiceClientFactory


ServiceClientFactory is a static class that wraps calls to WCF services. It’s sole purpose is to hide the boilerplate code involved in most service calls. If you analysis a standard WCF call you will see the following pattern.


using (var client = new SomeServiceClientProxy())
{
    //Call a service method
};

Boilerplate code is ideal for encapsulation as it rarely changes, reduces the amount of code that has to be written and eliminates issues with copy/paste coding. ServiceClientFactory wraps all this boilerplate code into a single method call making it easier to manage.


Here is the class boiled down to its core.  It really isn’t that complicated but it dramatically reduces the amount of code that has to been by clients.


public static class ServiceClientFactory
{
    public static ServiceClientWrapper<TChannel> CreateAndWrap<TChannel> ()
                   where TChannel : class
    {
        return new ServiceClientWrapper<TChannel>();
    }

    public static ServiceClientWrapper<TChannel> CreateAndWrap<TChannel> ( 
                                    Binding binding, EndpointAddress remoteAddress 
                                    ) where TChannel : class
    {
        return new ServiceClientWrapper<TChannel>(binding, remoteAddress);
    }

    public static ServiceClientWrapper<TChannel> CreateAndWrap<TChannel> ( 
                                   InstanceContext callbackInstance, Binding binding, 
                                   EndpointAddress remoteAddress 
                                   ) where TChannel : class
    {
        return new ServiceClientWrapper<TChannel>(callbackInstance, binding, remoteAddress);
    }

    public static ServiceClientWrapper<TChannel> CreateAndWrap<TChannel> ( 
                                    InstanceContext callbackInstance, string endpointConfigurationName, 
                                    EndpointAddress remoteAddress ) where TChannel : class
    {
        return new ServiceClientWrapper<TChannel>(callbackInstance, endpointConfigurationName, remoteAddress);
    }

    public static ServiceClientWrapper<TChannel> CreateAndWrap<TChannel> ( 
                                    InstanceContext callbackInstance, string endpointConfigurationName, 
                                    string remoteAddress ) where TChannel : class
    {
        return new ServiceClientWrapper<TChannel>(callbackInstance, endpointConfigurationName, remoteAddress);
    }

    public static void InvokeMethod<TChannel> ( Action<TChannel> invocation ) 
                          where TChannel : class
    {
        if (invocation == null)
            throw new ArgumentNullException("invocation");

        using (var proxy = CreateAndWrap<TChannel>())
        {
            invocation(proxy.Client);
        };
    }

    public static void InvokeMethod<TChannel> ( 
                             Action<TChannel> invocation, 
                             Func<ServiceClientWrapper<TChannel>> initializer 
                             ) where TChannel : class
    {
        if (invocation == null)
            throw new ArgumentNullException("invocation");

        Func<ServiceClientWrapper<TChannel>> init = initializer ?? (() => CreateAndWrap<TChannel>());

        using (var proxy = init())
        {
            invocation(proxy.Client);
        };
    }

    public static TResult InvokeMethod<TChannel, TResult> ( 
                             Func<TChannel, TResult> invocation ) where TChannel : class
    {
        if (invocation == null)
            throw new ArgumentNullException("invocation");

        using (var proxy = CreateAndWrap<TChannel>())
        {
            return invocation(proxy.Client);
        };
    }

    public static TResult InvokeMethod<TChannel, TResult> ( 
                            Func<TChannel, TResult> invocation, 
                            Func<ServiceClientWrapper<TChannel>> initializer
                             ) where TChannel : class
    {
        if (invocation == null)
            throw new ArgumentNullException("invocation");

        Func<ServiceClientWrapper<TChannel>> init = initializer ?? (() => CreateAndWrap<TChannel>());

        using (var proxy = init())
        {
            return invocation(proxy.Client);
        };
    }
}

Invoking Methods


The core function ServiceClientFactory provides is to call WCF methods. With ServiceClientFactoryit is a single line of code. Internally the code creates an instance of the service client, using the ServiceClientWrapper from the last article, wrapped in a using statement. It then invokes an action provided as a parameter passing the action the client proxy. The client code from previous articles can be rewritten as follows.


ServiceClientFactory.InvokeMethod<ServiceReference1.IEmployeeService>(
              c => c.Update(dlg.Employee)
);

In a good SOA architecture you are taught that service calls should be broad, meaning a single service call should do all the work necessary to handle the request. Unlike traditionally object oriented code it is generally bad if you need to make multiple calls to a service in a single request. Hence ServiceClientFactoryis optimized for the common case where a request needs to call one and only one service method. In the original version it took 4 lines of code to call one method. Now it takes one.


Another benefit of this approach is that the action is working with the service interface and not the proxy, this hides the actual proxy implementation from the client code because all the client sees is the interface. If you need to do more work than just a single method call then use a lambda.


ServiceClientFactory.InvokeMethod<ServiceInterface>(c =>
{
    //Call first method
    //Do some work
    //Call another method
});

Calling a method that returns a value is just as simple, just add the return type as a parameter to the method.


private void ShowEmployeeDetails ( int id )
{
    var employee = ServiceClientFactory.InvokeMethod<ServiceReference1.IEmployeeService, 
                                            Employee>(
                        c => c.Get(id)
        );

    var form = new EmployeeForm();
    form.Employee = employee;

    form.ShowDialog(this);
}

Proxy Generation


The other core function provided by ServiceClientFactory is to create the WCF service client using CreateAndWrap. In the simplest case it just creates an instance of the wrapper but there are various overloads that accept endpoints, bindings and other parameters in the cases where the service is not defined declaratively in the config file. In the rare cases where a client needs to create an explicitly instance of the proxy then it can do so using these methods. The only time this becomes a burden is when you want to create a proxy in a non-standard way and invoke one of its methods. For this special case there is an overload of InvokeMethod that accepts a function that returns the wrapper.


public static TResult InvokeMethod<TChannel, TResult> ( 
                           Func<TChannel, TResult> invocation, 
                           Func<ServiceClientWrapper<TChannel>> initializer
                           ) where TChannel : class
{
    if (invocation == null)
        throw new ArgumentNullException("invocation");

    Func<ServiceClientWrapper<TChannel>> init = initializer ?? (() => CreateAndWrap<TChannel>());

    using (var proxy = init())
    {
        return invocation(proxy.Client);
    };
}

Disadvantages


Seem simple? It should because that is the point. With very little code we can completely eliminate the boilerplate code that is written for something as simple as calling a WCF service. By using our custom service client from last time we also resolve the issue with clean up. And all this is mostly invisible to the actual client code. This is a great first step in refactoring calls to WCF services but we are not done yet. All this code is actually infrastructure code for the final solution that we are working toward. As such it has some disadvantages if you choose to start using it right now.


Perhaps the biggest issue with the above code is the reliance on a static class. Static classes cannot be easily mocked during testing so any code that uses the static class cannot be easily unit tested without the WCF services being available. You can work around this by creating a virtual method that then calls the static class but you will end up with lots of virtual methods (probably one for each service method) just for unit testing purposes.


Another issue is that it tightly couples the code with the WCF service call. It is clear by looking at the code you are making a WCF call which partially defeats the purpose of using WCF to begin with. WCF is about contracts so that is what you should be using, yet the static class prevents code from using just the contract (at least outside the action parameter).


Yet another issue with the code revolves around the proxy creation. WCF provides quite a few different options for creating a client in order to handle the various approaches. This mostly has to be replicated in the factory class as well. This leads to method overloading which can be confusing. There is bound to be some case that the factory doesn’t handle but it is not easy to extend or change the static class without changing the actual source code.


Even with these limitations it is a great first step toward refactoring to the final solution. If you can live with the disadvantages then this code can be used now. But next time we’ll start working toward the final form that client code can use that fixes all these issues.


SmarterWCF.zip