WCF verses the ASP.NET Web API

02 March 2013

Introduction

I was playing around with some async web services the other day after having read Nigel Sampson's blog on "Simple golden rules for async / await" (more on this in an up-coming post). Over the last few years, simple REST-based services have become the preferred model, compared to SOAP-based web services and the more "heavy-weight" WCF way of doing things. This got me thinking about the relative merits of RESTful WCF services verses (in my mind) the more "light-weight" ASP.NET Web API. In particular, what sort of performance benefits the ASP.NET Web API would have compared to WCF. Looking around the web I couldn't really find any figures, so I decided to have a quick play at comparing WCF verses the ASP.NET Web API.

The WCF REST Service

WCF supports a REST-based model out-of-the-box, provided you know the (non-obvious) configuration magic required to enable it. Here's the entity model I used for my WCF service:

namespace WcfService4WinRT.Model
{
    [DataContract]
    public class Person
    {
        [DataMember]
        public int Id;

        [DataMember]
        public string Name;

        [DataMember]
        public string Address;
    }
}

The definition of the service contract was as follows:

namespace WcfService4WinRT
{
    [ServiceContract]
    public interface IPersonService
    {
        [OperationContract(Name = "GetAll")]
        [WebGet(UriTemplate = "GetAll", ResponseFormat = WebMessageFormat.Json)]
        List<Person> Get();

        [OperationContract(Name = "GetById")]
        [WebGet(UriTemplate = "Get/{id}", ResponseFormat = WebMessageFormat.Json)]
        Person Get(string id);
    }
}

Notice in the above interface how I defined two HTTP GET operations and configured them to return JSON.

Here's the service's implementation:

namespace WcfService4WinRT
{
    public class PersonService : IPersonService
    {
        public List<Person> People { get; set; }

        public PersonService()
        {
            if (HttpRuntime.Cache == null) return;

            People = HttpRuntime.Cache["ListOfPersonWcf"] as List<Person>;
            if (People != null) return;

            CreatePeople();
            HttpRuntime.Cache.Add(
                "ListOfPersonWcf",
                People,
                null,
                DateTime.Now.AddMinutes(20),
                Cache.NoSlidingExpiration,
                CacheItemPriority.Default, null);        
        }

        private void CreatePeople()
        {
            People = new List<Person>();
            People.Add(new Person { Id = 0, Name = "Fred", Address = "1 The Road" });
            People.Add(new Person { Id = 1, Name = "Mary", Address = "2 The Road" });
            People.Add(new Person { Id = 2, Name = "Jim", Address = "3 The Road" });
            People.Add(new Person { Id = 3, Name = "Bob", Address = "4 The Road" });
            People.Add(new Person { Id = 4, Name = "Sue", Address = "5 The Road" });
            People.Add(new Person { Id = 5, Name = "Steve", Address = "6 The Road" });
            People.Add(new Person { Id = 6, Name = "Jane", Address = "7 The Road" });
            People.Add(new Person { Id = 7, Name = "Harry", Address = "8 The Road" });
            People.Add(new Person { Id = 8, Name = "June", Address = "9 The Road" });
            People.Add(new Person { Id = 9, Name = "John", Address = "10 The Road" });
        }

        public List<Person> Get()
        {
            return People;
        }

        public Person Get(string id)
        {
            return People[int.Parse(id)];
        }
    }
}

Finally, here's the Web.config file that configures the services for REST (for more background info on setting up WCF REST services, see my previous post on the subject):

<?xml version="1.0"?>
<configuration>

  <appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
  </appSettings>
  
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5"/>
  </system.web>
  <system.serviceModel>
    
    <behaviors>
      
      <endpointBehaviors>
          <behavior name="REST">
              <webHttp helpEnabled="true" />
          </behavior>
      </endpointBehaviors>
      
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    
    <bindings>
      <webHttpBinding>
        <binding name="webHttpBindingWithJsonP" crossDomainScriptAccessEnabled="true" />
      </webHttpBinding>
    </bindings>
    
    <services>
      <service name="WcfService4WinRT.PersonService">
        <endpoint address="" 
                  behaviorConfiguration="REST" 
                  binding="webHttpBinding"
                  bindingConfiguration="webHttpBindingWithJsonP" 
                  name="PersonService"
                  contract="WcfService4WinRT.IPersonService" />
        
        <endpoint address="mex" 
                  binding="mexHttpBinding" 
                  name="PersonServiceMetadataExchange"
                  contract="IMetadataExchange" />
        
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost/WcfService4WinRT/PersonService" />
          </baseAddresses>
        </host>
      
      </service>
    </services>
   
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <directoryBrowse enabled="true"/>
  </system.webServer>

</configuration>

Testing access to the WCF Service

The easiest way to test our GET services is via the browser. In this case, because I configured help to be available I can go to the following URL and have an API help page auto-generated for me by ASP.NET:

You can see from the above screenshot that the help page shows the URIs (and the required HTTP verb) that each of the services is available on. I can go ahead and access these service URIs from the browser, or I can use Fiddler:


Creating a Windows 8 Store App Client

Creating a Windows 8 Store App client is very straightforward. You simply need the same interface or class (Person in my case) available in the client that's being used in the service. Here's an example of the four lines of code required to read JSON from a WCF rest-based service:

var httpClient = new HttpClient();
var response = await httpClient.GetAsync("http://localhost/WcfService4WinRT/PersonService.svc/Get");  
var jsonText = await response.Content.ReadAsStringAsync();
var list = JsonConvert.DeserializeObject<List<Person>>(jsonText);  // Use Newtonsoft.Json to deserialize

The ASP.NET Web API Service

To create an ASP.NET Web API start with the Visual Studio MVC 4 Web Application template, then pick Web API:

In my tests, I used the same Person entity model for both the WCF and ASP.NET Web API projects. So, all I really needed to do was add a PersonController class like so:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Http;
using MvcWebApiDemo.Models;
using System.Web.Caching;

namespace MvcWebApiDemo.Controllers
{
    public class PersonController : ApiController
    {
        private List<Person> People { get; set; }

        public PersonController()
        {
            if (HttpRuntime.Cache == null) return;

            People = HttpRuntime.Cache["ListOfPerson"] as List<Person>;
            if (People != null) return;

            CreatePeople();
            HttpRuntime.Cache.Add(
                "ListOfPerson", 
                People, 
                null, 
                DateTime.Now.AddMinutes(20), 
                Cache.NoSlidingExpiration, 
                CacheItemPriority.Default, null);
        }

        private void CreatePeople()
        {
            People = new List<Person>();
            People.Add(new Person {Id = 0, Name = "Fred", Address = "1 The Road"});
            People.Add(new Person {Id = 1, Name = "Mary", Address = "2 The Road"});
            People.Add(new Person {Id = 2, Name = "Jim", Address = "3 The Road"});
            People.Add(new Person {Id = 3, Name = "Bob", Address = "4 The Road"});
            People.Add(new Person {Id = 4, Name = "Sue", Address = "5 The Road"});
            People.Add(new Person {Id = 5, Name = "Steve", Address = "6 The Road"});
            People.Add(new Person {Id = 6, Name = "Jane", Address = "7 The Road"});
            People.Add(new Person {Id = 7, Name = "Harry", Address = "8 The Road"});
            People.Add(new Person {Id = 8, Name = "June", Address = "9 The Road"});
            People.Add(new Person {Id = 9, Name = "John", Address = "10 The Road"});
        }

        // URL: GET localhost/MvcWebApiDemo/api/person/ 
        // The GetAll() method begins with "Get" and there are no params so the Web API maps 
        // HTTP GET requests to this method. This is the "default" GET method
        public List<Person> GetAll()
        {
            return People;
        }

        // URL: GET localhost/MvcWebApiDemo/api/person/0
        // or:  GET localhost/MvcWebApiDemo/api/person?id=0
        // Method begins with "Get" and expects one param, so the Web API maps HTTP GET
        // requests with one param to this method
        public Person Get(int id)
        {
            var person = from p in People where p.Id == id select p;
            if (person == null) throw new HttpResponseException(HttpStatusCode.NotFound);
            return person.First();
        }

        // Create a json request body and request (e.g. in Fiddler) as follows:
        //
        //      POST localhost/MvcWebApiDemo/api/person
        //      Host: localhost
        //      Content-Length: 61
        //      Content-Type: application/json
        //      Request Body: { "id" : 11, "name" : "paul", "address" : "123" }
        //
        // Here I put the [HttpPost] attribute - it's optional because I've called the method
        // "Post", the Web API will infer the POST verb by convention
        [HttpPost]
        public HttpResponseMessage Post(Person person)
        {
            var newId = People.Count;
            var newPerson = new Person { Id = ++newId, Name = person.Name, Address = person.Address };
            People.Add(newPerson);

            // The Request object will only be null if we're running unit tests...
            if(this.Request == null) return new HttpResponseMessage(HttpStatusCode.Created);
               
            // Return a 201 (Created) status code, along with a link to the newly created object and the new object
            var response = this.Request.CreateResponse(HttpStatusCode.Created, newPerson);
            var uri = Url.Link("DefaultApi", new {id = newId});
            response.Headers.Location = new Uri(uri);

            return response;
        }
    }
}

Notice that I added a POST method so I could play around with load testing it later.

Using Fiddler to access the GetAll() method was just like accessing the WCF service. However, one of the really nice things about the ASP.NET Web API is that it supports content negotiation out of the box. So, to modify the data format returned, the client just changes the ACCEPT header - no changes to the Web API code are required:

Running load tests

With both the WCF and Web API services in-place, I used Visual Studio 2012 to create load tests that that would simulate a number of concurrent users accessing the "GetAll" service. For more information on creating load tests, see my previous post on the subject.

The load test for each service had the following characteristics:

  • Both services were compiled to a release build
  • No attempt was made to "tune" the performance of either the WCF or Web API services, or IIS
  • In both services, data was cached (at the service level, using HttpRuntime.Cache)
  • A step load test was performed, where every 10 seconds an additional 10 concurrent users were added, up to a maximum load of 300
  • A 5-seconds warm-up was done at the start of each test
  • All tests ran for 5-minutes on a Windows Server 2012 64-bit machine with 8 GB RAM, and 8 logical CPU cores
  • A dedicated IIS App Pool was created for testing
  • The small amount of data returned by both services was constant (~460 KB)
  • Support for session state was turned off

The results of the first test run was not what I expected:

WCF Web API
Run time (mins) 5 5
Max users 300 300
Test failures 0 0
Avg pages/sec 860 736
Avg page time (msecs) 7.6 92
% Processor time 41 56

That's not what I expected at all!

Look at the Avg page time figure - the Web API service was a factor of ten slower than the WCF service! This was most decided not what I had expected!!

After a bit of head-scratching I happened to look in the WebApiConfig.cs file and noticed a seemingly innocence comment:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        // To disable tracing in your application, please comment out or remove the following line of code
        // For more information, refer to: http://www.asp.net/web-api
        config.EnableSystemDiagnosticsTracing();
    }
}

Hmmm... diagnostic tracing - that sounds as if it would have a negative impact on performance! Sure enough, after commenting out config.EnableSystemDiagnosticsTracing() and re-running the tests, things looked much more as I'd expected:

WCF Web API Web API (diag tracing OFF)
Run time (mins) 5 5 5
Max users 300 300 300
Test failures 0 0 0
Avg pages/sec 860 736 939
Avg page time (msecs) 7.6 92 2.8
% Processor time 41 56 42

After disabling diagnostic tracing, the Avg page time figure in the Web API service came down to 2.8 milli seconds - much better!

I then played around with some more performance tests on the ASP.NET Web API, this time increasing the number of concurrent users to 5,000, and then finally doing a POST operation:

Web API - GET Web API - POST
Run time (mins) 10 5
Max users 5,000 500
Test failures 0 0
Avg pages/sec 1,102 1,137
Avg page time (msecs) 5.3 5.8
% Processor time 57 53

The following graph shows key performance indicators for the Web API (GET operation, 5,000 users):

As you can see from the above results, in my simplistic load tests, the throughput of requests being serviced by the Web API remains acceptable and the load on the CPU is only "moderate" (around 50%).

Conclusion

My simple load tests show that, as expected, the more light-weight ASP.NET Web API out-performs WCF, provided that you remember to turn off diagnostic tracing!