Tag Archives: WCF

Async Made Easy

During the course of working on my current project I’ve had to write many, many WCF web service calls in Silverlight. This means a lot of typing for event subscription and error handling since Silverlight requires that you use asynchronous calls. To make this process as easy as possible I implemented a series of classes centering around one which I named AsyncHandler. This class allows you to make calls to your MVVM model like the following using very few lines of code:

Model.GetUsers(
    lUsers => MyUsers = lUsers,
    lEx => ShowError("Unable to retrieve user list!"));

This style of invocation offers several advantages. First, it’s very brief. Traditionally you have to write the AsyncCompletedEventHandler function and come up with case-by-case logic for processing the error state which can be quite verbose. This also leads us to the second advantage which is that the response processing is located directly after the invocation giving it the look and feel of a synchronous call. Finally, it gives you an easy way to reuse the same model method for different purposes since the callbacks are supplied as parameters. In summation, AsyncHandler allows you to replace the standard event-driven web service model with a condensed and simplified lambda expression driven callback model.

The entire implementation is included in the collapsed code block at the bottom of this post, but I’d like to cover the three core functions that make the AsyncHandler work. The first is the AsyncHandler constructor  which takes three parameters: target is the object reference hosting the asynchronous members (typically the web service client), baseName is an optional parameter indicating the name of the async functions when stripped of their “Async” and “Complete” postfixes, and ignoreRequestId is an optional flag which disables lastest-invocation tracking.

IgnoreRequestID = ignoreRequestId;
Target = target;

// If baseName is null, infer it from the TEventArgs type name.
if (String.IsNullOrWhiteSpace(baseName))
{
    string eventArgsName = typeof(TEventArgs).Name;
    int postfixIndex = eventArgsName.IndexOf("CompletedEventArgs");
    baseName = eventArgsName.Substring(0, postfixIndex);
}

// Use reflection to find the async invocation method.
string asyncFunctionName = baseName + "Async";
MethodInfo function = target.GetType()
    .GetMethods(BindingFlags.Instance | BindingFlags.Public)
    .SingleOrDefault(lMethod =>
        lMethod.Name.Equals(asyncFunctionName,
                            StringComparison.OrdinalIgnoreCase) &&
        FunctionHasUserObjectParameter(lMethod));
if (function == null)
    throw new EntryPointNotFoundException(
        String.Format("Async function \"{0}\" not found",
                      asyncFunctionName));
Function = function;

// Use reflection to find the completed event and subscribe to it.
string completedEventName = baseName + "Completed";
EventInfo completedEvent = target.GetType()
    .GetEvent(completedEventName,
              BindingFlags.Instance | BindingFlags.Public);
if (completedEvent == null)
    throw new EntryPointNotFoundException(
        String.Format("Completed event \"{0}\" not found",
                      completedEventName));

completedEvent.AddEventHandler(Target,
    new EventHandler<TEventArgs>(Target_FunctionCompleted));

In a nutshell this code uses reflection to obtain references to the “Async” and “Complete” members on the supplied target. If baseName is null it will infer a value from the name of the AsyncCompletedEventArgs supplied as the first type parameter to the class. These classes are created automatically by the web service proxy generator so it is guaranteed to match the member names found on the target. The remainder of the function validates the existence of the “Async” method and the “Complete” event then stores it for use when invoking the procedure and subscribes to it respectively. At this point an AsyncHandler can be constructed as simply as this:

MyServiceClient client = new MyServiceClient();
AsyncHandler<GetUsersCompletedEventArgs> handler =
    new AsyncHandler<GetUsersCompletedEventArgs>(client);

The next core function is the Invoke function which is defined as:

public void Invoke(Delegate callback, Action<Exception> failureCallback,
                   params object[] args)
{
    if (!IgnoreRequestID)
        RequestID++;

    AsyncHelperBase helper = BuildHelper(callback);
    helper.FailureCallback = failureCallback;
    helper.IgnoreRequestID = IgnoreRequestID;
    helper.RequestID = RequestID;

    object[] argsWithHelper = null;
    if (args == null)
        argsWithHelper = new object[1];
    else
    {
        argsWithHelper = new object[args.Length + 1];
        for (int i = 0; i < args.Length; i++)
            argsWithHelper[i] = args[i];
    }
    argsWithHelper[argsWithHelper.Length - 1] = helper;

    Function.Invoke(Target, argsWithHelper);
}

Invoke uses reflection and the MethodInfo reference obtained in the constructor to begin the asynchronous call. The bulk of this method involves attaching tracking values using the optional userState parameter available on all proxy-generated “Async” methods. An AsyncHelperBase object is constructed which stores references to the success and failure callbacks and the current request ID. This object is then spliced onto the end of the args array in the position of the userState parameter and the combined array is supplied to the MethodInfo.Invoke method. One round trip to the server later and we reach our final core function:

public static void HandleComplete(
    AsyncCompletedEventArgs e,
    Action<object> successCallback,
    Action<Exception> failureCallback = null)
{
    if (e.Cancelled || e.Error != null)
    {
        if (failureCallback != null)
            failureCallback(e.Error);
    }
    else if (successCallback != null)
    {
        PropertyInfo resultProperty = e.GetType().GetProperty("Result");
        if (resultProperty == null)
            successCallback(null);
        else
        {
            object result = resultProperty.GetValue(e, null);
            successCallback(result);
        }
    }
}

This method is invoked by the event handler subscribed in the constructor. The AsyncHelperBase object constructed in the Invoke method is extracted from the AsyncCompletedEventArgs.UserState property to supply HandleComplete‘s parameters. It then checks to see if the response is in an error state and either invokes the failure callback or the success callback using reflection to evaluate the EventArgs’ Result property.

One essential feature which isn’t represented in the above code is the request ID processing. The request ID increments with each successive call to the AsyncHandler’s Invoke function. When a response comes back from the server the request ID stored in the AsyncHelperBase object is compared to the AsyncHandler’s request ID. If IgnoreRequestID is set to true or the IDs match, HandleComplete is allowed to execute. Otherwise the result is discarded. This is a very useful feature which prevents varying execution times from causing your callbacks to be invoked with stale data. If you’re intending to receive many results back in rapid succession, for example populating data from a list, it is very important to set the IgnoreRequestID flag to true.

There are a number of subclasses of AsyncHandler to address some additional optimizations and fringe cases. I encourage you to look through the complete code and don’t hesitate to ask if you feel like I skimmed over something important. Thanks for reading!

Complete AsyncHandler Implementation

using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;

namespace CodeThaumaturgy
{
    public static class AsyncTools
    {
        /// <summary>
        /// Determines the response state specified in AsyncCompletedEventArgs and invokes either successCallback
        /// or failureCallback with the appropriate arguments. Logs failures to the debugger log under the
        /// "WebServiceException" key.
        /// </summary>
        public static void HandleComplete(AsyncCompletedEventArgs e, Action<object> successCallback, Action<Exception> failureCallback = null)
        {
            if (e.Cancelled || e.Error != null)
            {
                if (System.Diagnostics.Debugger.IsAttached)
                    System.Diagnostics.Debugger.Log(1, "WebServiceException",
                        e.Error == null ? "Cancelled" : e.Error.ToString());

                if (failureCallback != null)
                    failureCallback(e.Error);
            }
            else if (successCallback != null)
            {
                PropertyInfo resultProperty = e.GetType().GetProperty("Result");
                if (resultProperty == null)
                    successCallback(null);
                else
                {
                    object result = resultProperty.GetValue(e, null);
                    successCallback(result);
                }
            }
        }
    }

    #region Handlers

    /// <summary>
    /// Base implementation of the AsyncHandler framework for processing weakly typed result values. Handles async
    /// event subscription and leverages AsyncHelpers to invoke supplied callbacks.
    /// </summary>
    public class AsyncHandler<TEventArgs> where TEventArgs : AsyncCompletedEventArgs
    {
        public bool IgnoreRequestID { get; protected set; }
        public int RequestID { get; protected set; }
        public object Target { get; protected set; }

        protected MethodInfo Function { get; set; }

        /// <param name="target">The object containing the async function call and the corresponding completed event.</param>
        /// <param name="baseName">
        ///     The name of the async function. This will be "X" for a function with async members named "XAsync"
        ///     and "XCompleted". If this value is supplied or defaulted as null it will be inferred from the type name
        ///     of TEventArgs.
        /// </param>
        /// <param name="ignoreRequestId">
        ///     If this is set to false (the default) only the latest invocation's response will result in a callback in
        ///     the case of multiple concurrent requests. If this is set to true, there will be a 1:1 invocation / callback
        ///     relationship, but as with any asynchronous system there is no guarantee that the results will come back in
        ///     the same order in which the initial invocations were made.
        /// </param>
        public AsyncHandler(object target, string baseName = null, bool ignoreRequestId = false)
        {
            if (target == null)
                throw new ArgumentNullException("target");

            IgnoreRequestID = ignoreRequestId;
            Target = target;

            // If baseName is null, infer it from the TEventArgs type name.
            if (String.IsNullOrWhiteSpace(baseName))
            {
                string eventArgsName = typeof(TEventArgs).Name;
                int postfixIndex = eventArgsName.IndexOf("CompletedEventArgs");
                baseName = eventArgsName.Substring(0, postfixIndex);
            }

            // Use reflection to find the async invocation method.
            string asyncFunctionName = baseName + "Async";
            MethodInfo function = target.GetType()
                .GetMethods(BindingFlags.Instance | BindingFlags.Public)
                .SingleOrDefault(lMethod => lMethod.Name.Equals(asyncFunctionName, StringComparison.OrdinalIgnoreCase) &&
                                            FunctionHasUserObjectParameter(lMethod));
            if (function == null)
                throw new EntryPointNotFoundException(String.Format("Async function \"{0}\" not found", asyncFunctionName));
            Function = function;

            // Use reflection to find the completed event and subscribe to it.
            string completedEventName = baseName + "Completed";
            EventInfo completedEvent = target.GetType()
                .GetEvent(completedEventName, BindingFlags.Instance | BindingFlags.Public);
            if (completedEvent == null)
                throw new EntryPointNotFoundException(String.Format("Completed event \"{0}\" not found", completedEventName));

            completedEvent.AddEventHandler(Target, new EventHandler<TEventArgs>(Target_FunctionCompleted));
        }

        private static bool FunctionHasUserObjectParameter(MethodInfo methodInfo)
        {
            ParameterInfo lastParameter = methodInfo.GetParameters().LastOrDefault();
            return lastParameter != null &&
                   typeof(object).Equals(lastParameter.ParameterType) &&
                   lastParameter.Name.Equals("userState", StringComparison.OrdinalIgnoreCase);
        }

        public void InvalidateRequest() { RequestID++; }

        /// <summary>
        /// Creates the AsyncHelper tracking object and invokes the async method stored by the constructor in the
        /// Function property.
        /// </summary>
        /// <param name="callback">The callback to invoke when a successful response is received.</param>
        /// <param name="failureCallback">The callback to invoke when an error occurs.</param>
        /// <param name="args">The arguments to supply to the async function.</param>
        public void Invoke(Delegate callback, Action<Exception> failureCallback, params object[] args)
        {
            if (!IgnoreRequestID)
                RequestID++;

            AsyncHelperBase helper = BuildHelper(callback);
            helper.FailureCallback = failureCallback;
            helper.IgnoreRequestID = IgnoreRequestID;
            helper.RequestID = RequestID;

            object[] argsWithHelper = null;
            if (args == null)
                argsWithHelper = new object[1];
            else
            {
                argsWithHelper = new object[args.Length + 1];
                for (int i = 0; i < args.Length; i++)
                    argsWithHelper[i] = args[i];
            }
            argsWithHelper[argsWithHelper.Length - 1] = helper;

            try { Function.Invoke(Target, argsWithHelper); }
            catch (Exception ex)
            {
                if (System.Diagnostics.Debugger.IsAttached)
                    System.Diagnostics.Debugger.Log(4, "AsyncHelperInvoke", ex.ToString());
                throw ex;
            }
        }

        protected virtual AsyncHelperBase BuildHelper(Delegate callback)
        {
            if (callback != null && !(callback is Action))
                throw new ArgumentException("Must be of type Action", "callback");

            return new AsyncHelper() { Callback = (Action)callback };
        }

        protected void Target_FunctionCompleted(object sender, TEventArgs e)
        {
            AsyncHelperBase helper = (AsyncHelperBase)e.UserState;
            helper.HandleComplete(RequestID, e);
        }
    }

    /// <summary>
    /// Subclass of AsyncHandler for invoking async calls with a void return type.
    /// </summary>
    public class AsyncHandler : AsyncHandler<AsyncCompletedEventArgs>
    {
        public AsyncHandler(object target, string baseName, bool ignoreRequestId = false) : base(target, baseName, ignoreRequestId) { }
    }

    /// <summary>
    /// Subclass of AsyncHandler which returns strongly typed results.
    /// </summary>
    public class AsyncHandler<TEventArgs, TReturn> : AsyncHandler<TEventArgs> where TEventArgs : AsyncCompletedEventArgs
    {
        public AsyncHandler(object target, bool ignoreRequestId = false)
            : base(target, null, ignoreRequestId)
        {
        }

        protected override AsyncHelperBase BuildHelper(Delegate callback)
        {
            if (callback != null && !(callback is Action<TReturn>))
                throw new ArgumentException(String.Format("Must be of type Action<{0}> callback", typeof(TReturn).FullName));

            return new AsyncHelper<TReturn>() { Callback = (Action<TReturn>)callback };
        }

        public void TypedInvoke(Action<TReturn> callback, Action<Exception> failureCallback, params object[] parameters)
        {
            Invoke(callback, failureCallback, parameters);
        }
    }

    #endregion Handlers

    #region Helpers

    /// <summary>
    /// Stores references to callback delegates. Provides a generalized callback invocation method to support callbacks
    /// with varying type parameters and logic for discarding stale results using an externally populated RequestID property.
    /// </summary>
    public abstract class AsyncHelperBase
    {
        public Action<Exception> FailureCallback { get; set; }
        public bool IgnoreRequestID { get; set; }
        public int RequestID { get; set; }

        public void HandleComplete(int requestId, AsyncCompletedEventArgs e)
        {
            if (!IgnoreRequestID && RequestID != requestId)
                return;

            AsyncTools.HandleComplete(e, (result) => { InvokeCallback(result); }, FailureCallback);
        }

        protected abstract void InvokeCallback(object result);
    }

    public class AsyncHelper : AsyncHelperBase
    {
        public Action Callback { get; set; }

        protected override void InvokeCallback(object result)
        {
            if (Callback != null)
                Callback.Invoke();
        }
    }

    public class AsyncHelper<T> : AsyncHelperBase
    {
        public Action<T> Callback { get; set; }

        protected override void InvokeCallback(object result)
        {
            if (Callback != null)
                Callback.Invoke((T)result);
        }
    }

    #endregion Helpers
}

MVVM Model Examples

public class ExampleModel : IExampleModel
{
    private ExampleServiceClient _exampleClient;
    private ExampleServiceClient ExampleClient
    {
        get
        {
            if (_exampleClient == null)
                _exampleclient = new ExampleServiceClient();
            return _exampleClient;
        }
    }

    // This method uses a subclass of AsyncHandler<TEventArgs> which takes
    // a second type parameter of the return type. This allows for the
    // easy use of strongly typed callback delegates.
    private AsyncHandler<GetUsersCompletedEventArgs, IEnumerable<User>> _getUsersHandler;
    public void GetUsers(Action<IEnumerable<User>> callback,
                         Action<Exception> failureCallback = null)
    {
        if (_getUsersHandler == null)
            _getUsersHandler = new AsyncHandler<GetUsersCompletedEventArgs, IEnumerable<User>>(ExampleClient);
        _getUsersHandler.TypedInvoke(callback, failureCallback);
    }

    // This method uses another subclass which uses the base
    // AsyncCompletedEventArgs type parameter indicating that there is a
    // null return type. The success callback is optional in this case.
    // Note that the ignoreRequestId flag is set to true so the callback
    // will be invoked for every save attempted.
    private AsyncHandler _saveUserHandler;
    public void SaveUser(User user,
                         Action callback = null,
                         Action<Exception> failureCallback = null)
    {
        if (_saveUserHandler == null)
            _saveUserHandler =
                new AsyncHandler(ExampleClient, "SaveUser", true);
        _saveUserHandler.Invoke(callback, failureCallback, user);
    }
}