Introduction

This article is intended to illustrate how to implement callback operations in Windows Communication Foundation through a common business scenario where the service needs to notify that some events has happened to the client. During a callback, in many aspects the tables are turned: the service becomes the client, and the client becomes the server. So, we need to develop an interesting application supporting the solution.

Implementing the solution

The first thing to know before implementing this approach is that not all bindings support callback operations and only bidirectional-capable bindings can be used. Due to the connectionless nature of HTTP, this transport protocol cannot be used for callbacks, that's, you cannot use the BasicHttpBinding and WSHttpBinding for this purpose. In order to support callbacks in your application, WCF provides the WSDualHttpBinding, which actually sets up two HTTP channels: one for the calls from the client to the server, and one for the calls from the server to the client.
Now, let's create a console application to host the service in Visual Studio.NET and add a reference to the System.ServiceModel assembly.

Defining the callback contract

The callback operations are part of the service contract. A service contract can have at most one callback contract. Once defined, the clients are required to support the callback and also provide the callback endpoint to service in every call.
In our example, we have a service which provides a math service doing some long term calculation. We want to be notified when the calculation operation begins. The service contract attribute annotates our contract and offers a CallbackContract property of the type Type setting up our callback contract, as shown in Listing 1.
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.ServiceModel;
  5. namespace CallbackApp
  6. {
  7. public interface IMathCalculationCallback
  8. {
  9. [OperationContract]
  10. void OnCalculating();
  11. }
  12. [ServiceContract(CallbackContract = typeof(IMathCalculationCallback))]
  13. public interface IMathCalculation
  14. {
  15. [OperationContract]
  16. void DoLongCalculation(int nParam1, int nParam2);
  17. }
  18. }
Listing 1. Definition of the service contract and the associated callback contract.

The service implementation

In order to invoke the client callback from the service, we need a reference to the callback object. When the client invokes the service operations, it supplies a callback channel for the communication with the server through the callback. This channel can be referenced from the server by calling the GetCallbackChannel operation on the global OperationContext instance, as shown in the Listing 2. The DoLongCalculation operation does some long initialization, then notifies to the client the a complex operation begins now (in our case this complex operation is nParam1 + nParam2, I guess it is joke but with a delay System.Threading.Thread.Sleep(10000)).
  1. [ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Reentrant)]
  2. public class MathCalculationService : IMathCalculation
  3. {
  4. public int DoLongCalculation(int nParam1, int nParam2)
  5. {
  6. System.Threading.Thread.Sleep(10000);
  7. IMathCalculationCallback objCallback = OperationContext.Current.GetCallbackChannel<IMathCalculationCallback>();
  8. if (objCallback != null)
  9. {
  10. objCallback.OnCalculating();
  11. }
  12. System.Threading.Thread.Sleep(10000);
  13. return nParam1 + nParam2;
  14. }
  15. }
Listing 2. The MathCalculationService implementation.
You may notice that the service is invoking the callback reference while executing the operation DoLongCalculation. By default the service class is configure for single-threaded access, thus the service is associated with a lock, and only one thread at a time can own the lock and access the service instance. When the service invokes the callback reference while executing one of its operations, the service thread is blocked, because the thread which is processing the reply message from the client once the callback returns a message response requires ownership of the same lock, so a deadlock situation occurs. To avoid this situation, there are three possibilities:
The application's main workflow is shown in Listing 3.
  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. MathCalculationServiceHost.StartService();
  6. System.Console.WriteLine("Please, press any key to finish ...");
  7. System.Console.Read();
  8. }
  9. }
Listing 3. The service application's main workflow.
And finally the configuration file is shown in Listing 4.
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3. <system.serviceModel>
  4. <services>
  5. <service name="CallbackApp.MathCalculationService" behaviorConfiguration="MathCalculationServiceBeh">
  6. <endpoint contract="CallbackApp.IMathCalculation" binding="wsDualHttpBinding"/>
  7. </service>
  8. </services>
  9. <behaviors>
  10. <serviceBehaviors>
  11. <behavior name="MathCalculationServiceBeh" >
  12. <serviceDebug includeExceptionDetailInFaults="true" />
  13. <serviceMetadata httpGetEnabled="true" />
  14. </behavior>
  15. </serviceBehaviors>
  16. </behaviors>
  17. </system.serviceModel>
  18. </configuration>
Listing 4. The service application's configuration file.

Developing the client side

Now, add another application to the solution and name it CallbackClientApp, and also add a reference to the System.ServiceModel assembly.
In order to generate the proxy class, you need to open command windows and change to the directory of the client application, and run the following line command shown in Listing 5. As you can see the proxy inherits from the class System.ServiceModel.DuplexClientBase.
svcutil http://localhost:8080/CallbackApp/MathCalculationService.svc?wsdl
Listing 5.
  1. The generated proxy is shown in Listing 6.
  2. //---------------------------------------------------------
  3. // <auto-generated>
  4. // This code was generated by a tool.
  5. // Runtime Version:2.0.50727.42
  6. //
  7. // Changes to this file may cause incorrect behavior and will be lost if
  8. // the code is regenerated.
  9. // </auto-generated>
  10. //---------------------------------------------------------
  11. namespace CallbackClientApp
  12. { [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
  13. [System.ServiceModel.ServiceContractAttribute(ConfigurationName = "IMathCalculation", CallbackContract = typeof(IMathCalculationCallback))]
  14. public interface IMathCalculation
  15. {
  16. [System.ServiceModel.OperationContractAttribute(Action = "http://tempuri.org/IMathCalculation/DoLongCalculation", ReplyAction = "http://tempuri.org/IMathCalculation/DoLongCalculationResponse")]
  17. int DoLongCalculation(int nParam1, int nParam2);
  18. } [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
  19. public interface IMathCalculationCallback
  20. {
  21. [System.ServiceModel.OperationContractAttribute(Action = "http://tempuri.org/IMathCalculation/OnCalculating", ReplyAction = "http://tempuri.org/IMathCalculation/OnCalculatingResponse")]
  22. void OnCalculating();
  23. } [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
  24. public interface IMathCalculationChannel : IMathCalculation, System.ServiceModel.IClientChannel
  25. {
  26. }
  27. [System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
  28. public partial class MathCalculationClient : System.ServiceModel.DuplexClientBase<IMathCalculation>, IMathCalculation
  29. {
  30. public MathCalculationClient(System.ServiceModel.InstanceContext callbackInstance)
  31. :base(callbackInstance)
  32. {
  33. }
  34. public MathCalculationClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName)
  35. :base(callbackInstance, endpointConfigurationName)
  36. {
  37. }
  38. public MathCalculationClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress)
  39. :base(callbackInstance, endpointConfigurationName, remoteAddress)
  40. {
  41. }
  42. public MathCalculationClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress)
  43. :base(callbackInstance, endpointConfigurationName, remoteAddress)
  44. {
  45. }
  46. public MathCalculationClient(System.ServiceModel.InstanceContext callbackInstance, System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress)
  47. :base(callbackInstance, binding, remoteAddress)
  48. {
  49. }
  50. public int DoLongCalculation(int nParam1, int nParam2)
  51. {
  52. return base.Channel.DoLongCalculation(nParam1, nParam2);
  53. }
  54. }
  55. }
Listing 6. The generated proxy.
In order to use the callback capabilities the client application needs to create an instance of a class which implements the callback logic, host it in a context, create the proxy and call the service passing the callback instance's reference.
Let's define the callback class as shown in Listing 7.
  1. class MathCalculationCallback : IMathCalculationCallback
  2. {
  3. #region IMathCalculationCallback Members
  4. public void OnCalculating()
  5. {
  6. System.Console.WriteLine("The server begins to calculate, please for a moment ...");
  7. }
  8. #endregion
  9. }
Listing 7. The callback class definition.
Then, we define the client's main workflow as shown in Listing 8.
  1. class Program {
  2. static void Main(string[] args) {
  3. IMathCalculationCallback objCallback = new MathCalculationCallback();
  4. InstanceContext objContext = new InstanceContext(objCallback);
  5. int nResult = 0;
  6. using(MathCalculationClient objProxy = new MathCalculationClient(objContext))
  7. {
  8. nResult = objProxy.DoLongCalculation(1, 2);
  9. }
  10. System.Console.WriteLine("The result is {0}", nResult);
  11. System.Console.WriteLine("Press any key to finish ...");
  12. System.Console.Read();
  13. }
  14. }
Listing 8. The client's main workflow.
And the configuration file is shown in Listing 9.
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <configuration>
  3. <system.serviceModel>
  4. <bindings>
  5. <wsDualHttpBinding>
  6. <binding name="WSDualHttpBinding_IMathCalculation" closeTimeout="00:01:00"
  7. openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
  8. bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
  9. maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
  10. messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true">
  11. <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
  12. maxBytesPerRead="4096" maxNameTableCharCount="16384" />
  13. <reliableSession ordered="true" inactivityTimeout="00:10:00" />
  14. <security mode="Message">
  15. <message clientCredentialType="Windows" negotiateServiceCredential="true"
  16. algorithmSuite="Default" />
  17. </security>
  18. </binding>
  19. </wsDualHttpBinding>
  20. </bindings>
  21. <client>
  22. <endpoint address="http://localhost:8080/CallbackApp/MathCalculationService.svc"
  23. binding="wsDualHttpBinding" bindingConfiguration="WSDualHttpBinding_IMathCalculation"
  24. contract="IMathCalculation" name="WSDualHttpBinding_IMathCalculation">
  25. </endpoint>
  26. </client>
  27. </system.serviceModel>
  28. </configuration>
Listing 9. The configuration file.
Let's see the client application's output in Figure 1.
Image1.jpg
Figure 1: The client application's output.

Conclusion

In this article, I covered the main concepts and strategies to develop a WCF service which notifies events to the client using callback operations, and how the client must implement the logic associated to the event handling.