Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Using WCF with Automatic Client Proxies

4.71/5 (20 votes)
7 Oct 2008CPOL6 min read 1   565  
Using automatically generated proxies in WCF in a manner similar to .NET Remoting

Introduction

Microsoft has produced Windows Communication Foundation (WCF) as the natural successor to its earlier .NET Web Service and Remoting technologies. From the documentation it initially seems like WCF just uses the same sort of manual client proxy generation as Web Services. However, WCF is equally capable of generating client proxies automatically in a similar manner to Remoting. This article seeks to redress the documentation imbalance.

Background

Web Services were specifically designed for situations in which an interface was being publicly exposed outside the enterprise (where it was impractical to share interface code) and/or where technologies differ (for example, a Java client consuming a .NET Web Service). Clients are typically built by extracting interface metadata from a running Web Service, generating proxy interface code and building it into the client application.

Remoting was designed for use within an enterprise, typically on a single machine or enterprise LAN/WAN. The client(s) and server(s) are effectively part of the same overall application or system, sharing interface assemblies from which client proxies are automatically generated at runtime. Often both client and server are built as part of the same solution or in the same build process, a build which can be completed in a single pass.

Developers moving from Remoting must wonder how to generate WCF proxies automatically from shared assemblies as this subject is not well explained in the WCF documentation. There are strong reasons for keeping automatic proxies, including simpler build process and avoiding interface mismatch errors caused by missed manual proxy generation steps. Automatic proxies also give you common types between client and server which is essential if you want to share other code between the two.

How does WCF provide us with automatic client proxies? Here is a simple example.

The Service Interface

Firstly we define our interface (contract) and decorate it with the normal WCF service and operation attributes. This is then compiled into the shared assembly, in this case ICalculator.dll.

C#
// ICalculator.cs

using System.ServiceModel;

namespace Calculator
{
    [ServiceContract]
    public interface ICalculator : IDisposable
    {
        [OperationContract]
        int Add (int a, int b);
    }
}

This topic is well covered in the WCF documentation, but it is worth pointing out the use of IDisposable, the reasons for which I’ll come to later.

The Service Implementation

The example server here implements the calculator service and also provides a very simple console hosting application.

C#
// CalculatorService.cs

using System;
using System.ServiceModel;

namespace Calculator
{

  // Implementation of CalculatorService

  public class CalculatorService : ICalculator
  {

    public CalculatorService ()
    {
      Console.WriteLine ("Constructing CalculatorService");
    }

    public int Add (int a, int b)
    {
      Console.WriteLine ("Call {0} Adding {1} and {2}", callNo++, a, b);
      return a+b;
    }

    public void Dispose ()
    {
      Console.WriteLine ("Disposing CalculatorService");
    }

    private int callNo = 1;

  }


  // Simple console service host

  public class TestProgram
  {
    public static void Main ()
    {
      ServiceHost svcHost = new ServiceHost (typeof (CalculatorService));
      svcHost.Open();
      Console.WriteLine ("Press a key to exit");
      Console.ReadKey();
      svcHost.Close();
    }
  }

}

CalculatorService implements the shared ICalculator interface and logs its actions to the console. Note that it also implements IDiposable which provides a logical place to put any service clean-up code. The service also has a private counter callNo which illustrates its ability to hold state information.

The very simple console service hosting application above just starts up the ServiceHost and waits for the user to close it down (note that this is .NET 3.5 syntax. .NET 3.0 requires more endpoint detail). The ServiceHost listens for connections on the endpoints defined in the config file:

XML
<!--CalculatorService.config-->

<configuration>
  <system.serviceModel>

    <services>
      <service name="Calculator.CalculatorService">

        <endpoint address="net.tcp://localhost:5000/Calc"
                  binding="netTcpBinding"
                  contract="Calculator.ICalculator" />

        <endpoint address="net.pipe://localhost/Calc"
                  binding="netNamedPipeBinding"
                  contract="Calculator.ICalculator" />

        <endpoint address="http://localhost:8081/Calc"
                  binding="wsHttpBinding"
                  contract="Calculator.ICalculator" />

        <endpoint address="http://localhost:8080/Calc"
                  binding="basicHttpBinding"
                  contract="Calculator.ICalculator" />

      </service>
    </services>

  </system.serviceModel>

</configuration>

In this case I have exposed the same service using four endpoints, which correspond to the four bindings I will explore further below.

The Client Proxy

Since this article is about automatic client proxies, this is where the real action takes place. Firstly of course, the client program must reference the shared interface assembly ICalculator.dll so it can use the ICalculator type.

Creating a proxy does not use new. There are a number of options, but the way which most closely resembles conventional construction is:

C#
ICalculator calc = new ChannelFactory<ICalculator>("TcpCalc").CreateChannel();

"TcpCalc" is the name of the remote endpoint to connect to, as defined in the client config file (below).

This statement creates both a proxy object of type ICalculator in the client and an instance of the CalculatorService service object on the server.

Having created the proxy, the client can simply invoke the methods of the remote service object:

C#
int sum1 = calc.Add (1,2);
int sum2 = calc.Add (3,4);

There is a problem here. Even when we have finished with the client proxy, the channel to the server can remain open (for some protocols) and the remote service object will still exist, tying up valuable network and server resources. Only when the client proxy is garbage collected will the system clean-up.

Fortunately, it just so happens that proxy objects constructed by ChannelFactory always implement IDisposable as well as their generic type (ICalculator). Since ICalculator also happens to implement IDisposable we can simply dispose it:

C#
calc.Dispose();

This seems just like a proxy call to a corresponding service Dispose() operation and it appears to have this effect, but Dispose() has not been defined as an [OperationContract] so this cannot be. In reality the client Dispose() sends a 'close' message to the service and shuts down any open connections. On receipt of the close message the service calls Dispose() on the service object.

Of course it is good practice to employ the using block (or use Dispose() with try...finally):

C#
using (ICalculator calc = new ChannelFactory<ICalculator>("TcpCalc").CreateChannel())
{
  int sum1 = calc.Add (1,2);
  int sum2 = calc.Add (3,4);
  . . .
}

IMHO this last code snippet really illustrates the way that WCF clients should look when both client and service originate within the same organisation.

Example Client

C#
// CalculatorClient.cs

using System;
using System.ServiceModel;

namespace Calculator
{
  class TestClient
  {

    static void Main ()
    {

      // Create a proxy via TCP which is not closed until calc1 is garbage collected

      ICalculator calc1 = new ChannelFactory<ICalculator>("TcpCalc").CreateChannel();
      int sum1 = calc1.Add (1,2);
      int sum2 = calc1.Add (3,4);
      Console.WriteLine ("Sum1 = {0}  Sum2 = {1}", sum1, sum2);

      // Create a proxy via named pipe which explicitly closed by Dispose()

      ICalculator calc2 = new ChannelFactory<ICalculator>("PipeCalc").CreateChannel();
      sum1 = calc2.Add (5,6);
      sum2 = calc2.Add (7,8);
      calc2.Dispose();
      Console.WriteLine ("Sum1 = {0}  Sum2 = {1}", sum1, sum2);

      // Create a proxy via WS HTTP binding which closes at the end of the 'using' block

      using (ICalculator calc3 = new ChannelFactory<ICalculator>(
          "WsHttpCalc").CreateChannel())
      {
        sum1 = calc3.Add (9,10);
        sum2 = calc3.Add (11,12);
        Console.WriteLine ("Sum1 = {0}  Sum2 = {1}", sum1, sum2);
      }

      // Create a proxy via basic HTTP binding. Each call closes itself & 'using'
      // does nothing

      using (ICalculator calc4 = new ChannelFactory<ICalculator>(
          "HttpCalc").CreateChannel())
      {
        sum1 = calc4.Add (13,14);
        sum2 = calc4.Add (15,16);
        Console.WriteLine ("Sum1 = {0}  Sum2 = {1}", sum1, sum2);
      }

    }

  }
}

A number of interesting points here:

  • In the case of calc1, the client proxy is not explicitly disposed. The log output from the service shows that Dispose() is only called later in program execution when calc1 is garbage collected.
  • Calc2 and calc3 are properly disposed after use and the corresponding service Dispose() is called immediately.
  • In the case of calc4 using the basic http binding, the service object is constructed and disposed for every call of Add(). Since there is a different service object for each method call, no state is retained between calls.
  • By contrast, services connected via the TCP, Named Pipe and WS HTTP bindings do retain state between method calls, as shown by the incrementing value of callNo. Given that behaviour varies with network transport then there is risk in using state and/or using the basic HTTP binding. That's for the developer to judge.

The corresponding client config file defines the four named service endpoints:

XML
<!--CalculatorClient.config-->

<configuration>
  <system.serviceModel>
    <client>

      <endpoint address="net.tcp://localhost:5000/Calc"
                binding="netTcpBinding"
                contract="Calculator.ICalculator"
                name="TcpCalc" />

      <endpoint address="net.pipe://localhost/Calc"
                binding="netNamedPipeBinding"
                contract="Calculator.ICalculator"
                name="PipeCalc" />

      <endpoint address="http://localhost:8081/Calc"
                binding="wsHttpBinding"
                contract="Calculator.ICalculator"
                name="WsHttpCalc" />

      <endpoint address="http://localhost:8080/Calc"
                binding="basicHttpBinding"
                contract="Calculator.ICalculator"
                name="HttpCalc" />

    </client>
  </system.serviceModel>
</configuration>

Conclusion

In my experience, communications between clients and services within the enterprise is more common than exposing external interfaces to third parties. WCF provides an effective mechanism for this situation by automatically generating proxies using the ChannelFactory which is simpler, easier to code and less prone to manual build process errors. The lack of clear examples has hampered its adoption.

The lack of documentation may also have deterred some Remoting users from moving to WCF. While WCF automatic proxies don’t solve all the issues of migration, they do knock down one of the barriers to adoption. In many cases WCF is the effective successor to Remoting, exhibiting comparable performance and a similar choice of protocols.

Finally, some readers may be thinking that sharing types breaks the rules of SOA. This is not so. The 3rd tenet of SOA is "share schema/contract, not class". The shared interface types define the contract -- they are not classes (which would represent the implementation). I would argue that this approach is actually more service oriented than manually generated proxies. The first tenet of SOA is that boundaries should be explicit and the automatic creation of the client proxy via ChannelFactory is much more explicit than just using new with a manually generated proxy. In any event there is nothing to prevent the same service being accessed both internally using an automatic proxy and externally via a manual one.

History

First version, 06/10/08.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)