Friday, April 27, 2012

How to implement a Duplex Service between ASP.NET and WCF

Un/fortunately, I am working on a Microsoft technology based project for a year now. While the .NET environment looks good on the surface, if you start doing something cool, then you have to either pay for their support or lean on the shoulders of community MVPs. Because the project has no budget for premium Microsoft support, I had to lean a lot on community MVPs and Google mostly (I'm not a Microsoft fan, as you can see).

 I was trying to do something that should be fairly easy, that is implementing a Duplex Service back end for an ASP.NET front end. In this setup, the back end service does some heavy computational processing that reports updates regarding its status to the ASP.NET front end. A normal basicHttpBinding can also be used, but I wanted the back end service to be scale-able. The strategy is that possibly multiple front-end services can send requests to possibly multiple back end services and will only report updates on the status of the request to front-end services that sent them. Also, I'm too lazy to manually configure each back-end service for every front-end service that exists. Now that I have explained the main drive for this, I should be getting back to the topic at hand..

MSDN [1] is actually a good place to start for finding resources on how to quickly create working prototypes. Basically, we just need to create a ServiceContract that has a callback interface and required session mode.

    [Serializable]
    public class TaskResponse
    {
        public string TaskID;
        public bool Successful;
        public string Message;
    }

    /// Contract for Task Service
    [ServiceContract(Namespace = "Example.Service.Duplex/", SessionMode = SessionMode.Required,CallbackContract = typeof(ITaskServiceCallback))]
    public interface ITaskService
    {
        /// Order slave to do pushups
        [OperationContract]
        TaskResponse DoPushUps(int number);
    }

    /// Callback for Task Service contract
    public interface ITaskServiceCallback
    {
        /// Report the result of the task
        [OperationContract(IsOneWay = true)]
        void TaskComplete(TaskResponse result);
    }

On the server side, we need to set the InstanceContextMode to PerSession and use the OperationContext of the current session to use the callback.

    /// Provides services for a task master
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
    public class TaskService : ITaskService
    {
        private static int timerBase = 1000;
        private static Object randSync = new Object();
        private static Random rand = new Random();

        public TaskService()
        {
        }

        /// Quickly get the operation context of the request that sent this request
        ITaskServiceCallback Callback
        {
            get
            {
                return OperationContext.Current.GetCallbackChannel();
            }
        }

        #region ITaskService Members
        public TaskResponse DoPushUps(int number)
        {
            // Create push up task
            string id = Guid.NewGuid().ToString();
            var pushUpTask = new PushUps(id, Callback)
            {
                NumberOfPushUps = number
            };
            // Do task in another thread
            Thread t = new Thread(new ThreadStart(() => { TaskSlave(pushUpTask); }));
            t.Start();
            return new TaskResponse()
            {
                TaskID = pushUpTask.ID,
                Successful = true,
                Message = ""
            };
        }
        #endregion

        /// Task slave to order around!
        private void TaskSlave(Task t)
        {
            // do push ups!
            var pushUp = t as PushUps;
            if (pushUp != null)
            {
                int duration;
                lock (randSync)
                {
                    duration = (timerBase * rand.Next(3)) * pushUp.NumberOfPushUps;
                }
                Thread.Sleep(duration); // simulate long processing task
                pushUp.Callback.TaskComplete(new TaskResponse() { 
                    TaskID = pushUp.ID,
                    Successful = true,
                    Message = "I am tired master"
                });
                return;
            }
            // slave doesn't know the task
            pushUp.Callback.TaskComplete(new TaskResponse()
            {
                TaskID = pushUp.ID,
                Successful = false,
                Message = "I could not understand your order master"
            });
        }
    }

That's it for the server side! For the client side, we need to create a realization for the callback. It is important to note here that for this to work on ASP.NET, we must add a callback behavior property[2].


        [CallbackBehavior(UseSynchronizationContext = false)]
        class TaskServiceCallbackHandler : ITaskServiceCallback
        {
            public void TaskComplete(TaskResponse result)
            {
                Console.WriteLine("Task Complete for " + result.TaskID+ " with result: " + result.Successful);
            }
        }

Then to call the server, you can just extend from ClientBase but you must also specify an InstanceContext.


        class TaskServiceClient : ClientBase, ITaskService
        {
            public TaskServiceClient(InstanceContext instanceContext, string endPointName, string address)
                : base(instanceContext, endPointName, address)
            {

            }

            public TaskResponse DoPushUps(int number)
            {
                return base.Channel.DoPushUps(number);
            }

        }

        InstanceContext ic = new InstanceContext(new TaskServiceCallbackHandler());
        Target = new TaskServiceClient(ic, "Example.Service.TaskService.Dual", "http://localhost:50190/DuplexService.svc");

For the configuration file, you must use a binding that supports sessions like the wsDualHttpBinding and specify the clientBaseAddress. The following is an example configuration:



Please take note of the timeouts. The receiveTimeout of wsDualHttpBinding is important. By default it is set to 10 minutes. If there are no communication within the timeout, the session will expire and the client will throw a ApplicationException. [3] In order to deal with this, the client must reconnect again with a new InstanceContext.

Also, you might encounter exceptions telling that the application is not authorized bind to a URI or something similar. To handle this, just run the following in a shell with an Administrator privilege:

netsh http add urlacl url=http://+:[port number]/[service name] user=[user]

Just replace the text in between [] with a correct one.

I have created a sample project with a unit test of the service. You can download it here.

References
[1] http://msdn.microsoft.com/en-us/library/ms731184.aspx
[2] Calling a Duplex Contract From ASP.NET
[3] http://blogs.msdn.com/b/madhuponduru/archive/2007/02/24/how-to-change-wcf-session-time-out-value.aspx