Duplex service over TCP with WCF

Akos Nagy
May 27, 2019

I was asked by one of my clients that I consult for to help with a TCP-based duplex communicaton solution using WCF. It's been a while since I have blogged about WCF (actually, it's been a while since I have used it), so I though it might make a good post.

Whenever you need to implement a WCF solution, you have to take care of a lot of things. But you can always break it down into three parts: the address, the binding and the contract. The where, how and what of WCF services. When you have that, you're almost done, all you need is an actual implementation to handle requests.

The contract

[ServiceContract(CallbackContract = typeof(IServiceCallback))]
public interface IMyService
{
  [OperationContract]
  void Register(string name);
}

public interface IServiceCallback
{
  [OperationContract]
  void DoWork(int param);
}

Basically, you need two contract interfaces: one "regular" contract interface, that the clients can call and is implemented on the server (I guess client and server are not very good terms here, but I'll stick with it for simplicity). You have to decorate this interface with the ServiceContractAttribute and in that attribute specify the interface type that are implemented on the client and can be called back by the server.

The binding and the address

<services>
  <service name="ServerApp.MyService">
    <endpoint address="" binding="netTcpBinding" contract="ServerApp.IMyService">
      <identity>
        <dns value="localhost" />
      </identity>
    </endpoint>
    <endpoint address="mex" binding="mexTcpBinding" contract="IMetadataExchange" />
    <host>
      <baseAddresses>
        <add baseAddress="net.tcp://localhost:8732/MyService/"/>
      </baseAddresses>
    </host>
  </service>
</services>

You have to add an endpoint with nettcpbinding and specify the server-interface as the contract (the identity here is just for the demo's sake). It's also a good idea to add a mex endpoint so that the client side code can be generated from Visual Studio. You can also see the baseaddress specified in the host and no relative address for the service.

Implementing the server

The last thing that you need is some actual code to handle the requests. Basically, you have to implement the server interface:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Reentrant)]
public class MyService : IMyService
{
       
   private readonly ConcurrentDictionary<string, IServiceCallback> connections = new ConcurrentDictionary<string, IServiceCallback>();
        
   public void Register(string name)
   {   
    this.connections.TryAdd(name, OperationContext.Current.GetCallbackChannel<IServiceCallback>());
    Console.WriteLine($"{name} registered");
  }
}

That's the server side implementation. You can use the OperationContext to get the channel that you can later use to communicate with the other side. It's very important to use a thread-safe collection to store these and also to specify ConcurrencyMode.Reentrant in the ServiceBehaviorAttribute to avoid potential deadlocks.

Implementing the client

On the client (well, the other) side, it's probably best to just generate via the mex endpoint your starting code. When you have that, you have to implement the client-side interface:

[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]
public class CallbackService : IMyServiceCallback
{
  public void DoWork(int param)
  {
    Console.WriteLine($"Work received: {param}");
  }
}

Note the reentrant concurrency mode here as well. And now, you can open a connection to the other side:

var instanceContext = new InstanceContext(new CallbackService());
using (var service = new ServiceReference1.MyServiceClient(instanceContext)) 
{
  Console.WriteLine("Press enter to register");
  Console.ReadLine();
  var id = Guid.NewGuid().ToString();
  service.Register(id);
  Console.WriteLine($"Registration complete with id {id}, receiving work; press enter to shutdown");
  Console.ReadLine();            

Notice the InstanceContext object that you have to wrap the client-side interface implementation into. This object is then passed to the proxy. You have to be very careful to only dispose the proxy object when you are done communicating, because on the server-side, the elements in the dictionary that identify the connected clients call back to these objects. If you dipose of them, the server-side cannot call back and the communication fails.
Also be careful the properly implement service calls from the UI thread and use async-await whenever you can to avoid UI deadlocks.

Akos Nagy
Posted in WCF