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.
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.
using System;
using System.ServiceModel;
namespace Calculator
{
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;
}
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:
<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:
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:
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:
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
):
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
using System;
using System.ServiceModel;
namespace Calculator
{
class TestClient
{
static void Main ()
{
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);
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);
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);
}
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:
<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.