Click here to Skip to main content
15,893,622 members
Articles / General Programming / Performance

Tweaking WCF to build highly scalable async REST API

Rate me:
Please Sign up or sign in to vote.
4.92/5 (35 votes)
31 Jul 2011CPOL25 min read 118.8K   1.2K   85  
You can build async REST API using WCF but due to some bug in WCF implementation it does not scale as you would want it to. Here's my journey with Microsoft's WCF team to explore the problem and find the right fix.
/// Copyright (c) Microsoft Corporation.  All rights reserved.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.ServiceModel.Activation;
using Microsoft.ServiceModel.Web;
using System.Linq;
using System.Net;
using System.IO;
using System.Text;
using ProxyStuff;
using System.Web;
using System.Diagnostics;
using System.Threading;
using AsyncServiceLibrary;

// The following line sets the default namespace for DataContract serialized typed to be ""
[assembly: ContractNamespace("", ClrNamespace = "WcfAsyncRestApi")]
namespace WcfAsyncRestApi
{
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall, ConcurrencyMode=ConcurrencyMode.Multiple), 
    AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed), 
    ServiceContract]
    public partial class AsyncService
    {
        /// <summary>
        /// Returns the content from destination URL and caches the response in the browser for specified seconds.
        /// </summary>
        /// <param name="url">URL to fetch</param>
        /// <param name="cacheDuration">Cache duration</param>
        /// <returns></returns>
        [OperationContract(AsyncPattern=true)]
        //[WebGet(BodyStyle=WebMessageBodyStyle.Bare, UriTemplate="/Url?uri={url}&cacheDuration={cacheDuration}")]
        [WebGet(BodyStyle=WebMessageBodyStyle.Bare)]
        public IAsyncResult BeginGetUrl(string url, int cacheDuration, AsyncCallback wcfCallback, object wcfState)
        {
            /// If the url already exists in cache then there's no need to fetch it from the source.
            /// We can just return the response immediately from cache
            if (IsInCache(url))
            {
                return WcfAsyncHelper.BeginSync<WebRequestState>(
                    new WebRequestState
                    {
                        Url = url,
                        CacheDuration = cacheDuration,
                        ContentType = WebOperationContext.Current.IncomingRequest.ContentType,
                    },
                    wcfCallback, wcfState);
            }
            else
            {
                /// The content does not exist in cache and we need to get it from the
                /// original source                 
                HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
                request.Method = "GET";
                //request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
                return WcfAsyncHelper.BeginAsync<WebRequestState>(
                    new WebRequestState
                    {
                        Request = request,
                        ContentType = WebOperationContext.Current.IncomingRequest.ContentType,
                        Url = url,
                        CacheDuration = cacheDuration,
                        StartThreadId = Thread.CurrentThread.ManagedThreadId
                    },
                    wcfCallback, wcfState,
                    (myState, externalServiceCallback, customState) => 
                        myState.Request.BeginGetResponse(externalServiceCallback, customState));

            }
        }

        public Stream EndGetUrl(IAsyncResult asyncResult)
        {
            //Debug.WriteLine("Requested completed on Thread: " + Thread.CurrentThread.ManagedThreadId);
            if (WcfAsyncHelper.IsSync<WebRequestState>(asyncResult))
            {
                return WcfAsyncHelper.EndSync<WebRequestState, Stream>(
                    asyncResult,
                    (myState, completedResult) =>
                    {
                        CacheEntry cacheEntry = GetFromCache(myState.Url);
                        
                        var outResponse = WebOperationContext.Current.OutgoingResponse;
                        SetResponseHeaders(cacheEntry.ContentLength, cacheEntry.ContentType, 
                            cacheEntry.ContentEncoding, 
                            myState, outResponse);               
         
                        return new MemoryStream(cacheEntry.Content);
                    },
                    (exception, myState) => { throw new ProtocolException(exception.Message, exception); },
                    (myState) => { /*Nothing to dispose*/ });
            }
            else
            {
                return WcfAsyncHelper.EndAsync<WebRequestState, Stream>(asyncResult,
                    (myState, serviceResult) =>
                    {
                        myState.EndThreadId = Thread.CurrentThread.ManagedThreadId;
                                                
                        var httpResponse = myState.Request.EndGetResponse(serviceResult) as HttpWebResponse;

                        var outResponse = WebOperationContext.Current.OutgoingResponse;
                        SetResponseHeaders(httpResponse.ContentLength,
                            httpResponse.ContentType, httpResponse.ContentEncoding,
                            myState, outResponse);

                        // If response needs to be cached, then wrap the stream so that
                        // when the stream is being read, the content is stored in a memory
                        // stream and when the read is complete, the memory stream is stored
                        // in cache.
                        if (myState.CacheDuration > 0)
                            return new StreamWrapper(httpResponse.GetResponseStream(),
                                (int)(outResponse.ContentLength > 0 ? outResponse.ContentLength : 8 * 1024),
                                buffer =>
                                {
                                    StoreInCache(myState.Url, myState.CacheDuration, new CacheEntry
                                    {
                                        Content = buffer,
                                        ContentLength = buffer.Length,
                                        ContentEncoding = httpResponse.ContentEncoding,
                                        ContentType = httpResponse.ContentType
                                    });
                                });
                        else
                            return httpResponse.GetResponseStream();
                    },
                    (exception, myState) => { throw new ProtocolException(exception.Message, exception); },
                    (myState) => { /*Nothing to dispose*/ });
            }
        }

        private void SetResponseHeaders(long contentLength, string contentType, string contentEncoding,
            WebRequestState webState,
            OutgoingWebResponseContext outResponse)
        {
            if (!string.IsNullOrEmpty(contentEncoding))
                outResponse.Headers["Content-Encoding"] = contentEncoding;
            outResponse.ContentLength = contentLength;
            outResponse.ContentType = contentType;
            SetCaching(WebOperationContext.Current, DateTime.Now, webState.CacheDuration);            
        }

        private void SetCaching(WebOperationContext context, DateTime lastModifiedDate, int maxCacheAge)
        {
            // set CacheControl header
            HttpResponseHeader cacheHeader = HttpResponseHeader.CacheControl;
            String cacheControlValue = String.Format("max-age={0}, must-revalidate", maxCacheAge);
            context.OutgoingResponse.Headers.Add(cacheHeader, cacheControlValue);

            // set cache validation 
            context.OutgoingResponse.LastModified = lastModifiedDate;

            // No ETag, want this to be cached on browser for good.
            // If you want to emit etag, then do so here.
            //String eTag = context.IncomingRequest.UriTemplateMatch.RequestUri.ToString() + lastModifiedDate.ToString();
            //context.OutgoingResponse.ETag = eTag;
        }

        private bool IsInCache(string url)
        {
            return false;
            //return HttpRuntime.Cache.Get(url) != null;
        }

        private CacheEntry GetFromCache(string url)
        {
            return HttpRuntime.Cache.Get(url) as CacheEntry;
        }

        private void StoreInCache(string url, int cacheDuration, CacheEntry entry)
        {
            return;

            //HttpRuntime.Cache.Add(url, entry, null, DateTime.Now.AddSeconds(cacheDuration), 
            //    System.Web.Caching.Cache.NoSlidingExpiration, 
            //    System.Web.Caching.CacheItemPriority.Normal,
            //   null);
        }

        internal class WebRequestState 
        {
            public HttpWebRequest Request;
            public string Url;
            public int CacheDuration;
            public string ContentType;
            public int StartThreadId;
            public int EndThreadId;
        }

        [Serializable]
        internal class CacheEntry
        {
            public long ContentLength;
            public string ContentType;
            public string ContentEncoding;
            public byte[] Content;
        }
    }

}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect BT, UK (ex British Telecom)
United Kingdom United Kingdom

Comments and Discussions