Dan Rigsby – Coding Up Style

Developer.Speaker.Blogger

Async Operations in Wcf: Canceling Operations

Posted by Dan Rigsby on March 30th, 2008

Async Operations in Wcf Series:
Part 1: Event Based Model
Part 2: IAsyncResult Model (Client-Side)
Part 3: IAsyncResult Model (Server-Side)
Part 4: Canceling Operations
Part 5: Handling Exceptions

This is the fourth in a four part series on asynchronous operations in Wcf.  If you are new to this series, you might want to read the first section of part one to get an introduction to this post: http://www.danrigsby.com/blog/index.php/2008/03/18/async-operations-in-wcf-event-based-model/

What is Canceling Wcf Operations

In some situations, when you submit an asynchronous call to the server, you might want to be able to cancel the operation.  Let’s say you have a query running that will may last over 30 seconds and the user navigates away.  Or suppose you are running an expensive database update and realize that you need to cancel the operation before it completes.

Windows Communication Foundation has no support for canceling operations and some pitfalls in trying to implement it are addressed here: http://blogs.msdn.com/drnick/archive/2007/12/27/cleaning-up-async.aspx.  This article will explain an approach to implement canceling support.  The process explained here uses a custom library called “Rigsby.AsyncOperation” and a class called “PendingOperationController”. This approach has one major pitfall: if an operation has completed, the best chance a cancel operation has may be to cancel the return to the client.  While this can save network bandwidth, the operation is still completed.  This can be undesirable in situations like a database update, but you there is little that can be done once everything in complete.  What this approach does offer is a way to cancel an operation that will try to cancel at multiple stages:

  1. If the operation is just sitting in the thread pool and hasn’t yet been processed, then it will be removed.
  2. If the operation is already running, then it will try to cancel the operation after it is complete.  It does this via a TransactionScope to attempt to rollback the operation.
  3. The operation itself can check the status of the request to check if it has been canceled at any time.  So if you have an operation which does 3 updates to the database, you can double check between each update if the request has been canceled, and if so bail out and let the TransactionScope roll back the pervious updates.

The PendingOperationController can also be used for other server-side asynchronous operations as well.  The controller could just be used to handle queuing of requests and signaling their completion.  This can make server-side code much cleaner while giving you the ability to add canceling support in the future.

What is AsyncOperation library

The AsyncOperation library is a custom library that can be used to assist with asynchronous operations in Wcf and provide support for cancellations.  (You can download the library here, but the code for each file is included inline as well.) The goal of this library and the PendingOperationController is all about extensibility and flexibility to work with many different systems and scenarios. The library contains 5 items:

1. AsyncStatus Enum

To convey the status of a pending operation, this enumeration contains all of the states that a pending operation can be in:

namespace Rigsby.AsyncOperation
{
    /// <summary>
    /// The status for an asynchronous operation.
    /// </summary>
    public enum AsyncStatus
    {
        /// <summary>
        /// The status is unknown.
        /// </summary>
        Unknown = 0,

        /// <summary>
        /// The asynchronous operation has not yet been started, but is queued up.
        /// </summary>
        Queued,

        /// <summary>
        /// The asynchronous operation has been started.
        /// </summary>
        Started,

        /// <summary>
        /// The asynchronous operation has completed.
        /// </summary>
        Completed,

        /// <summary>
        /// The asynchronous operation has been canceled.
        /// </summary>
        Canceled
    }
}

2. AsyncResult Class

This class was introduced in the IAsyncResult Model (Server-Side) article.  You could use the System.Runtime.Remoting.Messaging.AsyncResult, but many times you will want your own implementation of IAsyncResult.  I use a custom AsyncResult base class and inherit from it to add in properties and methods needed by the operations. Here is a base AsyncResult class that you can use:

using System;
using System.Threading;

namespace Rigsby.AsyncOperation
{
    /// <summary>
    /// The result of an asynchronous operation.
    /// </summary>
    public class AsyncResult : IAsyncResult, IDisposable
    {
        private AsyncCallback m_AsyncCallback;
        private object m_State;
        private ManualResetEvent m_ManualResentEvent;

        /// <summary>
        /// Initializes a new instance of the <see cref="AsyncResult"/> class.
        /// </summary>
        /// <param name="callback">The callback.</param>
        /// <param name="state">The state.</param>
        public AsyncResult(
            AsyncCallback callback,
            object state)
        {
            this.m_AsyncCallback = callback;
            this.m_State = state;
            this.m_ManualResentEvent = new ManualResetEvent(false);
        }

        /// <summary>
        /// Completes this instance.
        /// </summary>
        public virtual void OnCompleted()
        {
            m_ManualResentEvent.Set();
            if (m_AsyncCallback != null)
            {
                m_AsyncCallback(this);
            }
        }

        #region IAsyncResult Members
        /// <summary>
        /// Gets a user-defined object that qualifies or contains information about an asynchronous operation.
        /// </summary>
        /// <value></value>
        /// <returns>A user-defined object that qualifies or contains information about an asynchronous operation.</returns>
        public object AsyncState
        {
            get
            {
                return m_State;
            }
        }

        /// <summary>
        /// Gets a <see cref="T:System.Threading.WaitHandle"/> that is used to wait for an asynchronous operation to complete.
        /// </summary>
        /// <value></value>
        /// <returns>A <see cref="T:System.Threading.WaitHandle"/> that is used to wait for an asynchronous operation to complete.</returns>
        public System.Threading.WaitHandle AsyncWaitHandle
        {
            get
            {
                return m_ManualResentEvent;
            }
        }

        /// <summary>
        /// Gets an indication of whether the asynchronous operation completed synchronously.
        /// </summary>
        /// <value></value>
        /// <returns>true if the asynchronous operation completed synchronously; otherwise, false.</returns>
        public bool CompletedSynchronously
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Gets an indication whether the asynchronous operation has completed.
        /// </summary>
        /// <value></value>
        /// <returns>true if the operation is complete; otherwise, false.</returns>
        public bool IsCompleted
        {
            get
            {
                return m_ManualResentEvent.WaitOne(0, false);
            }
        }
        #endregion IAsyncResult Members

        #region IDisposable Members
        private bool m_IsDisposed = false;
        private event System.EventHandler m_Disposed;

        /// <summary>
        /// Gets a value indicating whether this instance is disposed.
        /// </summary>
        /// <value>
        ///     <c>true</c> if this instance is disposed; otherwise, <c>false</c>.
        /// </value>
        [System.ComponentModel.Browsable(false)]
        public bool IsDisposed
        {
            get
            {
                return m_IsDisposed;
            }
        }

        /// <summary>
        /// Occurs when this instance is disposed.
        /// </summary>
        public event System.EventHandler Disposed
        {
            add
            {
                m_Disposed += value;
            }
            remove
            {
                m_Disposed -= value;
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        public void Dispose()
        {
            if (!m_IsDisposed)
            {
                this.Dispose(true);
                System.GC.SuppressFinalize(this);
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    m_ManualResentEvent.Close();
                    m_ManualResentEvent = null;
                    m_State = null;
                    m_AsyncCallback = null;

                    System.EventHandler handler = m_Disposed;
                    if (handler != null)
                    {
                        handler(this, System.EventArgs.Empty);
                        handler = null;
                    }
                }
            }
            finally
            {
                m_IsDisposed = true;
            }
        }

        /// <summary>
        ///    <para>
        ///        Checks if the instance has been disposed of, and if it has, throws an <see cref="ObjectDisposedException"/>; otherwise, does nothing.
        ///    </para>
        /// </summary>
        /// <exception cref="System.ObjectDisposedException">
        ///    The instance has been disposed of.
        ///    </exception>
        ///    <remarks>
        ///    <para>
        ///        Derived classes should call this method at the start of all methods and properties that should not be accessed after a call to <see cref="Dispose()"/>.
        ///    </para>
        /// </remarks>
        protected void CheckDisposed()
        {
            if (m_IsDisposed)
            {
                string typeName = GetType().FullName;

                // TODO: You might want to move the message string into a resource file
                throw new System.ObjectDisposedException(
                    typeName,
                    String.Format(System.Globalization.CultureInfo.InvariantCulture,
                        "Cannot access a disposed {0}.",
                        typeName));
            }
        }
        #endregion IDisposable Members
    }
}

3. PendingAsyncResult Class

This class is an extension of the AsyncResult class.  It is designed to contain all of the information needed as a operation moves through its stages.  Included in this are its status, the input values, and the output values:

  1. String Id property:  This is a simple identifier to distinguish the operation.  This should be a System.Guid value exposed as a string.  We can’t just use the IAsyncResult to identify the operation because it the SendAsyncResult class (discussed in the IAsyncResult Model (Server-Side) article) is not serializable.  Since we need to be able to identify the operation in order to cancel it or get it’s status, this is pretty important.
  2. Generic Input property: This property represents the input values.  It is quite possible and likely to have multiple input values for the operation you want this class to represent.  However to make this class be as flexible as possible, there is only a single generic input class.  If you have more than one input value, you should create an intermediate class that contains the different input values and use that class as the TInput template.  For example, if you have an “Addition” operation that takes in two numbers to add, you might have an Input class that contains two integers.
  3. Generic Output property: This property represents the out value of the operation.  This will always be a single value since that is all a method returns.  The type of the output value should be represented with the TOutput template.
  4. Events for status changes: For each of the major statuses there is an event that is fired for each.  You could feasibly change this to be a single event for all status changes, if you so choose.
  5. Methods for moving through statuses: To change statuses there is a method to move between the major statuses.  To move to the Canceled status, you would call the OnCanceled method, etc.
using System;

namespace Rigsby.AsyncOperation
{
    /// <summary>
    /// A pending asynchronous operation.
    /// </summary>
    public class PendingAsyncResult<TInput, TOutput> : AsyncResult
    {
        private string m_Id = String.Empty;
        private TInput m_Input;
        private TOutput m_Output;

        private EventHandler m_Canceled;
        private EventHandler m_Queued;
        private EventHandler m_Started;

        /// <summary>
        /// A generic object to perform a 'lock' on.
        /// </summary>
        protected object m_SyncRoot = new object();

        /// <summary>
        /// Protected implmenetation of <see cref="AsyncStatus"/>.
        /// </summary>
        protected AsyncStatus m_AsyncStatus;

        /// <summary>
        /// Gets or sets the async status.
        /// </summary>
        /// <value>The async status.</value>
        public AsyncStatus AsyncStatus
        {
            get
            {
                return m_AsyncStatus;
            }
        }

        /// <summary>
        /// Gets or sets the id.
        /// </summary>
        /// <value>The id.</value>
        public string Id
        {
            get
            {
                return m_Id;
            }
            set
            {
                m_Id = value;
            }
        }

        /// <summary>
        /// Gets or sets the input for the operation.
        /// </summary>
        /// <value>The input.</value>
        public TInput Input
        {
            get
            {
                return m_Input;
            }
            set
            {
                m_Input = value;
            }
        }

        /// <summary>
        /// Gets or sets the output of the operation.
        /// </summary>
        /// <value>The output.</value>
        public TOutput Output
        {
            get
            {
                return m_Output;
            }
            set
            {
                m_Output = value;
            }
        }

        #region Public Events
        /// <summary>
        /// Occurs when the operation has been canceled.
        /// </summary>
        public event EventHandler Canceled
        {
            add
            {
                m_Canceled += value;
            }
            remove
            {
                m_Canceled -= value;
            }
        }

        /// <summary>
        /// Occurs when the operation has been started.
        /// </summary>
        public event EventHandler Started
        {
            add
            {
                m_Started += value;
            }
            remove
            {
                m_Started -= value;
            }
        }

        /// <summary>
        /// Occurs when the operation has been eueued.
        /// </summary>
        public event EventHandler Queued
        {
            add
            {
                m_Queued += value;
            }
            remove
            {
                m_Queued -= value;
            }
        }
        #endregion Public Events

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="CancellableAsyncResult"/> class.
        /// </summary>
        /// <param name="callback">The callback.</param>
        /// <param name="state">The state.</param>
        public PendingAsyncResult(
            AsyncCallback callback,
            object state) : this(default(TInput), String.Empty, callback, state)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CancellableAsyncResult"/> class.
        /// </summary>
        /// <param name="callback">The callback.</param>
        /// <param name="state">The state.</param>
        public PendingAsyncResult(
            string id,
            AsyncCallback callback,
            object state) : this(default(TInput), id, callback, state)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CancellableAsyncResult"/> class.
        /// </summary>
        /// <param name="input">The input.</param>
        public PendingAsyncResult(
            TInput input,
            string id,
            AsyncCallback callback,
            object state) : base(callback, state)
        {
            m_Input = input;

            if (String.IsNullOrEmpty(id))
            {
                Id = Guid.NewGuid().ToString();
            }
            else
            {
                Id = id;
            }
        }
        #endregion Constructors

        /// <summary>
        /// Called when [queued].
        /// </summary>
        public virtual void OnQueued()
        {
            lock (m_SyncRoot)
            {
                if (m_AsyncStatus != AsyncStatus.Completed
                    && m_AsyncStatus != AsyncStatus.Canceled)
                {
                    m_AsyncStatus = AsyncStatus.Queued;
                }
            }

            EventHandler eventHandler = m_Queued;
            if (eventHandler != null)
            {
                eventHandler(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Called when [started].
        /// </summary>
        public virtual void OnStarted()
        {
            lock (m_SyncRoot)
            {
                if (m_AsyncStatus != AsyncStatus.Completed
                    && m_AsyncStatus != AsyncStatus.Canceled)
                {
                    m_AsyncStatus = AsyncStatus.Started;
                }
            }

            EventHandler eventHandler = m_Started;
            if (eventHandler != null)
            {
                eventHandler(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Cancels this instance.
        /// </summary>
        public virtual void OnCanceled()
        {
            lock (m_SyncRoot)
            {
                if (m_AsyncStatus != AsyncStatus.Completed)
                {
                    m_AsyncStatus = AsyncStatus.Canceled;
                }
            }

            EventHandler eventHandler = m_Canceled;
            if (eventHandler != null)
            {
                eventHandler(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Completes this instance.
        /// </summary>
        public override void OnCompleted()
        {
            lock (m_SyncRoot)
            {
                m_AsyncStatus = AsyncStatus.Completed;
            }

            base.OnCompleted();
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Null out event to remove subscribers
                m_Started = null;
                m_Queued = null;
                m_Canceled = null;
            }

            base.Dispose(disposing);
        }
    }
}

One possible addition to this class could be to extend the concept of “status” to include more such as a “percent complete”.  It would be up to you how you would calculate the completion percent.

4. PendingOperationEventHandler Delegate

This delegate represents the method that the PendingOperationController will call when it is actually ready to do its work.  This method uses the same TInput and TOutput templates discussed in the PendingAsyncResult class.  For instance if this operation is a database insert, the method represented in this delegate would be the method that actually performs the database insert.

namespace Rigsby.AsyncOperation
{
    /// <summary>
    /// Represents a method that is called from a <see cref="PendingOperationController"/>.
    /// </summary>
    public delegate TOutput PendingOperationEventHandler<TInput, TOutput>(TInput data);
}

5. PendingOperationController Class

This is the heart of the AyncOperation library.  This class controls and monitors pending operations.  You could use this class if you implement canceling abilities or not.  This class is designed such that there is one PendingOperationController for each type of operation.  To instantiate the class you must specify the types for TInput and TOuput as well as the PendingOperationEventHandler delegate that represents the method that this class should call when executing the operation.  Once the class has been instantiated there three methods that can be called:

  1. AddPendingOperation: This method queues up an operation to be processed.  Each pending operation is added to the thread pool and the method is returned.  When a thread is available in the thread pool, the Callback method is called which checks to see if the operation has yet been canceled, then calls the method represented in the PendingOperationEventHandler delegate with the TInput and TOuput values from the PendingAsyncResult object.  The Callback method implements a TransactionScope.  At the end of the method, it checks one last time to see if the operation has been canceled.  If it has, then the the transaction is rolled back, otherwise the transaction completes and the client is signaled that the operation is complete.
  2. GetPendingOperationStatus:  This method returns the status of a pending operation in the queue.  If the operation is not found the “Unknown” result is returned.  This method may be called by the method represented in the PendingOperationEventHandler delegate to see if the operation has been canceled before proceeding on through the lines in the method.
  3. CancelPendingOperation: This method canceling pending operation.  It returns true if the operation has been canceled, and false if the operation cannot be canceled.  If this returns false, then the operation has probably already completed.
using System;
using System.Collections.Generic;
using System.Threading;

namespace Rigsby.AsyncOperation
{
    /// <summary>
    /// Controls pending asynchronous operations.
    /// </summary>
    /// <typeparam name="TInput">The type of the input.</typeparam>
    /// <typeparam name="TOutput">The type of the output.</typeparam>
    public class PendingOperationController<TInput, TOutput>
    {
        public ReaderWriterLockSlim m_PendingOperationsLock =
            new ReaderWriterLockSlim();
        private static Dictionary<string, PendingAsyncResult<TInput, TOutput>> m_PendingOperations =
            new Dictionary<string, PendingAsyncResult<TInput, TOutput>>();

        private PendingOperationEventHandler<TInput, TOutput> m_Operation;

        /// <summary>
        /// Initializes a new instance of the <see cref="PendingOperationController&lt;TInput, TOutput&gt;"/> class.
        /// </summary>
        /// <param name="operation">The operation that is called when a pending operation is running.</param>
        public PendingOperationController(
            PendingOperationEventHandler<TInput, TOutput> operation)
        {
            m_Operation = operation;
        }

        /// <summary>
        /// Adds the pending operation.
        /// </summary>
        /// <param name="asyncResult">The async result.</param>
        public void AddPendingOperation(
            PendingAsyncResult<TInput, TOutput> asyncResult)
        {
            m_PendingOperationsLock.EnterWriteLock();
            try
            {
                if (!m_PendingOperations.ContainsKey(asyncResult.Id))
                {
                    m_PendingOperations.Add(asyncResult.Id, asyncResult);
                }
                else
                {
                    m_PendingOperations[asyncResult.Id] = asyncResult;
                }
                asyncResult.OnQueued();
            }
            finally
            {
                m_PendingOperationsLock.ExitWriteLock();
            }

            ThreadPool.QueueUserWorkItem(
                new WaitCallback(
                    Callback), asyncResult);
        }

        /// <summary>
        /// Gets the pending operation status.
        /// </summary>
        /// <param name="operationId">The operation id.</param>
        /// <returns></returns>
        public AsyncStatus GetPendingOperationStatus(
            string operationId)
        {
            AsyncStatus status = AsyncStatus.Unknown;

            m_PendingOperationsLock.EnterReadLock();
            try
            {
                if (m_PendingOperations.ContainsKey(operationId))
                {
                    status = m_PendingOperations[operationId].AsyncStatus;
                }
            }
            catch (KeyNotFoundException)
            {
                // ignore
            }
            finally
            {
                m_PendingOperationsLock.ExitReadLock();
            }

            return status;
        }

        /// <summary>
        /// Cancels the pending operation.
        /// </summary>
        /// <param name="operationId">The operation id.</param>
        /// <returns>Whether or not the operation was canceled.  
        /// If false, then the operation has probably already completed or the operationId was not valid.</returns>
        public bool CancelPendingOperation(
            string operationId)
        {
            bool result = false;

            PendingAsyncResult<TInput, TOutput> asyncResult = null;
            m_PendingOperationsLock.EnterReadLock();
            try
            {
                if (m_PendingOperations.ContainsKey(operationId))
                {
                    asyncResult = m_PendingOperations[operationId];
                }
            }
            catch (KeyNotFoundException)
            {
                // ignore
            }
            finally
            {
                m_PendingOperationsLock.ExitReadLock();
            }

            if (asyncResult != null)
            {
                asyncResult.OnCanceled();
                result = true;
                RemovePendingOperation(operationId);
                asyncResult.Dispose();
            }

            return result;
        }

        /// <summary>
        /// Gets the pending operation result.
        /// </summary>
        /// <param name="asyncResult">The async result.</param>
        /// <returns></returns>
        public TOutput GetOperationResult(
            IAsyncResult asyncResult)
        {
            TOutput result;

            PendingAsyncResult<TInput, TOutput> myAsyncResult =
                (PendingAsyncResult<TInput, TOutput>)asyncResult;
            result = myAsyncResult.Output;
            myAsyncResult.AsyncWaitHandle.WaitOne();
            myAsyncResult.Dispose();

            return result;
        }

        private void Callback(
            object state)
        {
            PendingAsyncResult<TInput, TOutput> asyncResult =
                state as PendingAsyncResult<TInput, TOutput>;

            // Make sure the operation isn't canceled yet.
            if (asyncResult.AsyncStatus != AsyncStatus.Canceled
                && m_PendingOperations.ContainsKey(asyncResult.Id))
            {
                using (System.Transactions.TransactionScope transactionScope =
                    new System.Transactions.TransactionScope())
                {
                    try
                    {
                        asyncResult.OnStarted();
                        asyncResult.Output = m_Operation.Invoke(asyncResult.Input);
                    }
                    finally
                    {
                        if (asyncResult.AsyncStatus != AsyncStatus.Canceled)
                        {
                            // Mark transaction completed
                            transactionScope.Complete();

                            asyncResult.OnCompleted();
                        }

                        RemovePendingOperation(asyncResult.Id);
                    }
                }
            }
        }

        private void RemovePendingOperation(
            string operationId)
        {
            m_PendingOperationsLock.EnterWriteLock();
            try
            {
                if (m_PendingOperations.ContainsKey(operationId))
                {
                    PendingAsyncResult<TInput, TOutput> asyncResult = m_PendingOperations[operationId];
                    m_PendingOperations.Remove(operationId);
                }
            }
            catch (KeyNotFoundException)
            {
                // ignore
            }
            finally
            {
                m_PendingOperationsLock.ExitWriteLock();
            }
        }
    }
}

How to implement it

Continuing with the previous articles in this series, the example implementation in this article be a service that calculates a square root.  Normally this operation is very fast, so the service implements a 4 second thread sleep, so that we can show the asynchronous operations and allow time to cancel a pending operation.  To better understand these example you might want to look over the previous articles especially: IAsyncResult Model (Server-Side).

Contract

The contract is much like that in previous articles with the following changes:

  1. The BeginGetSquareRoot now takes in the operationId.  The client determines the operation id so that it can use that identifier to reference the pending operation in the future.  This parameter can be anywhere in the parameter list, but I recommend that it be the last parameter for the AysncCallback and object parameters which must always be the last two parameters in an asynchronous operation.  If your method takes in more or one parameters, they should be displayed first.
  2. The synchronous and asynchronous versions of the operation must have different names (not including the “Begin” prefix on the asynchronous method).  We couldn’t have a method called GetSquareRoot and BeginGetSquareRoot.  This would cause a runtime exception because both the synchronous and asynchronous versions of a method must have the same parameters.  Since we have added the operationId, the method names must be different.  If you need to keep the same names, you could pass in the operationId to the synchronous method as well, but this doesn’t make sense.  One option I recommend is to just append “Async” to the asynchronous versions of the methods.  So you could have GetSquareRoot and BeginGetSquareRootAsync.
  3. We have added a CancelGetSquareRoot method that can be used to cancel an asychronous operation.  This method returns a boolean value that represents if the cancellation succeeded or not.
[ServiceContract]
public interface IMyService
{
    [OperationContract]
    double FindSquareRoot(double value);

    [OperationContract(AsyncPattern=true)]
    IAsyncResult BeginGetSquareRoot(double value, string operationId, AsyncCallback callback, object state);

    double EndGetSquareRoot(IAsyncResult asyncResult);

    [OperationContract]
    bool CancelGetSquareRoot(
        string asyncResultID);
}

Service

Our service is actually much simpler in this version than in previous articles since much of the work has been pushed to the PendingOperationController.  In the constructor of the service we build the PendingOperationController for our GetSquareRoot operations.  The FindSquareRoot method is the method that actually does our calculations.  This is the method that the PendingOperationEventHandler delegate represents, and this is the method passed into the constructor of the PendingOperationController.  The Begin, End, and Cancel operations all just call into the corresponding methods in the PendingOperationController.  The Begin method does have to create the intial PendingAsyncResult object, but otherwise the code in this methods should be very short.

[ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Multiple)]
public class MyService : IMyService
{
    private PendingOperationController<double, double> m_GetSquareRootOperationController;

    public MyService()
    {
        // Create the pending operation controller
        m_GetSquareRootOperationController =
            new PendingOperationController<double, double>(
                new PendingOperationEventHandler<double, double>(FindSquareRoot));
    }

    #region IMyService Members
    public double FindSquareRoot(double value)
    {
        Thread.Sleep(4000); // Wait 4 seconds
        return Math.Sqrt(value);
    }

    public IAsyncResult BeginGetSquareRoot(double value, string operationId, AsyncCallback callback, object state)
    {
        // Create pending result
        PendingAsyncResult<double, double> asyncResult =
            new PendingAsyncResult<double, double>(value, operationId, callback, state);

        // Queue pending result
        m_GetSquareRootOperationController.AddPendingOperation(asyncResult);

        return asyncResult;
    }

    public double EndGetSquareRoot(IAsyncResult asyncResult)
    {
        // Get pending result
        return m_GetSquareRootOperationController.GetOperationResult(asyncResult);
    }

    public bool CancelGetSquareRoot(string operationId)
    {
        // Cancel pending result
        return m_GetSquareRootOperationController.CancelPendingOperation(operationId);
    }
    #endregion IMyService Members
}

Example Client

For our example client, we have a basic windows forms application where the user can enter a value, send it to the server, and the result will be displayed.  We have added a cancel button as well that can be used while the operation is being processed:

client

Here is the code behind this page.  The only tricky things here are the callback method that is passed to the BeginGetSquareRoot method and the call to the EndGetSquareRoot method from that callback method.  I explained these two methods in more depth in the IAsyncResult Model (Server-Side) article.  We have extended this by adding in a cancel method.  The form now needs to store the Id for the operation being run, so that this Id can be used to cancel the operation.

public partial class Form1 : Form
{
    IMyService m_Client;
    string m_MyOperationId;

    public Form1()
    {
        InitializeComponent();

        ChannelFactory<IMyService> factory = new ChannelFactory<IMyService>("netTcp");
        factory.Open();
        m_Client = factory.CreateChannel();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        double value = 0;
        Double.TryParse(m_ValueTextBox.Text, out value);

        m_MyOperationId = Guid.NewGuid().ToString();

        m_Client.BeginGetSquareRoot(value, m_MyOperationId, new AsyncCallback(OnEndGetSquareRoot), null);

        m_StartButton.Enabled = false;
        m_CancelButton.Enabled = true;
        m_ResultTextBox.Text = "Loading...";
    }

    private void m_CancelButton_Click(object sender, EventArgs e)
    {
        bool canceled = m_Client.CancelGetSquareRoot(m_MyOperationId);

        m_StartButton.Enabled = canceled;
        m_CancelButton.Enabled = false;

        if (canceled)
        {
            m_ResultTextBox.Text = "Canceled";
        }
    }

    public void OnEndGetSquareRoot(IAsyncResult asyncResult)
    {
        if (this.IsHandleCreated && !this.IsDisposed)
        {
            this.Invoke(
                new MethodInvoker(delegate()
                {
                    m_ResultTextBox.Text = m_Client.EndGetSquareRoot(asyncResult).ToString();

                    m_StartButton.Enabled = true;
                    m_CancelButton.Enabled = false;
                }));
        }
    }
}

Notice that in the OnEndGetSquareRoot method, we can invoking the operation using “this.Invoke”.  This ensures that the operation is called on the UI thread since we are updating a UI element.  We know that OnEndGetSquareRoot is called by the signal on the server, so the method is always called by a thread from the server.

You can download a complete sample of the code discussed in this article here: http://www.danrigsby.com/Files/Rigsby.WcfAsyncOperation.zip

9 Responses to “Async Operations in Wcf: Canceling Operations”

  1. Dan Rigsby » Async Operations in Wcf: IAsyncResult Model (Client-Side) Says:

    [...] Async Operations in Wcf: Canceling Operations [...]

  2. Dan Rigsby » Async Operations in Wcf: IAsyncResult Model (Server-Side) Says:

    [...] Async Operations in Wcf: Canceling Operations [...]

  3. Dan Rigsby » Async Operations in Wcf: Event Based Model Says:

    [...] Async Operations in Wcf: Canceling Operations [...]

  4. Alex Says:

    Hi Dan,
    excellent ! Please cast your contributions in a book. I will be one of the first buyer !
    Thanks,
    Alex

  5. Robby Says:

    How would I use your code if I have a service function that has no inputs?

  6. heinsk Says:

    Hi Dan,

    What´s happend when implements this example in not Service Host ?, example: publish WCF Service on the Web.

    How to maintenance single instance for PendingOperationController?

  7. Anoosh Says:

    Hi Dan,

    Great blog. Thanks. Getting AsyncStatus is good but any thoughts on how one would implement a progress interface for the Async operation? I am invoking an Async file transfer as part of the Async operation and would like to respond to requests for progresss and display in UI on the client side.

  8. Nemphys Says:

    Hi Dan,

    great work on the classes you made, but there is a flaw (at least with 3.5sp1, with which I am trying):
    The cancellation works fine, but after 1 minute (or the configured timeout value of the channel), the callback function on the client throws a timeout exception.
    I thought of just handling this exception, but I don’t think this would be the best practice (since, the client will be actually waiting for a reply for each canceled call; if you try to even close the channel during this time, it just blocks waiting for the call to complete).
    Do you think there is a better practice (eg. notify the client by issuing the callback immediately after cancel with a null or something)?

    Since you are apparently the only one on the net seriously dealing with the cancellation of asyc wcf calls, your help would be mostly appreciated.

  9. Henrik Says:

    Hi Dan and Nemphys

    I also have the problem with timeout one minute after the call begun when client cancles? (VS2008 SP1 Fx 3.5 SP1)

    Do you guys came up with any solution for this problem?

    Thanks for great articles!

    /Henrik

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>