WCF Data Services performance

By Mirek on (tags: Data Services, OData, WCF, categories: architecture, code)

The main benefit of using WCF Data Services (formerly ADO.NET Data Services, codename "Astoria) is that this is interoperable, which means it can be accessed from clients created in different technologies. Moreover standard WCF services offers the only exposed set of methods. Data Services, as implementation of OData protocol, offers the Resource to be queried and altered using HTTP methods. That’s are the pros of WCF Data Services. And what about the performance in comparison to WCF services?
In this post I will try to measure this performance.

Testing solution will consists of MVC 3 application which will hosts WCF Data Services service and WCF service, and console clients for OData and wcf service. Both services will expose the collection of Person entity

   1: [DataServiceKey("Id")]
   2: [DataContract]
   3: public class Person
   4: {
   5:     [DataMember]
   6:     public int Id { get; set; }
   7:  
   8:     [DataMember]
   9:     public string Name { get; set; }
  10:     
  11:     [DataMember]
  12:     public DateTime CreatedOn { get; set; }
  13:  
  14:     [DataMember]
  15:     public double FloatNumber { get; set; }
  16:  
  17:     [DataMember]
  18:     public IList<Order> Orders { get; set; }
  19: }

One test will be based on receiving flat list of Person objects without Orders loaded. And at second step the Orders will be filled with some random objects of type

   1: [DataServiceKey("Id")]
   2: [DataContract]
   3: public class Order
   4: {
   5:     [DataMember]
   6:     public int Id { get; set; }
   7:  
   8:     [DataMember]
   9:     public DateTime CreatedOn { get; set; }
  10:  
  11:     [DataMember]
  12:     public int PersonId { get; set; }
  13:  
  14:     [DataMember]
  15:     public string ProductName { get; set; }
  16: }

To avoid any additional performance hit we will use some mock repository for both services which will generate the collection of Person and Order entities

   1: public class PersonContextMock
   2:     {
   3:         private static readonly int CollectionCount = 1000;
   4:  
   5:         private static readonly bool LoadOrders = false;
   6:  
   7:         private static List<Person> people = null;
   8:  
   9:         public static List<Person> GetPeople()
  10:         {
  11:             if (people == null)
  12:             {
  13:                 people = new List<Person>(CollectionCount);
  14:                 DateTime now = DateTime.Now;
  15:                 for (int idx = 1; idx <= CollectionCount; idx++)
  16:                 {
  17:                     people.Add(new Person
  18:                     {
  19:                         Id = idx,
  20:                         Name = "Person" + idx.ToString(),
  21:                         FloatNumber = (double)(idx + 0.123456),
  22:                         CreatedOn = now.AddMilliseconds(idx),
  23:                         Orders = new List<Order>()
  24:                     });
  25:                 }
  26:  
  27:                 if (LoadOrders)
  28:                 {
  29:  
  30:                     for (int idx = 0; idx < CollectionCount; idx++)
  31:                     {
  32:                         people[idx].Orders.Add(new Order
  33:                         {
  34:                             Id = CollectionCount + idx + 1,
  35:                             ProductName = "OrderedProduct" + (CollectionCount + idx + 1).ToString(),
  36:                             CreatedOn = now.AddMilliseconds(idx),
  37:                             PersonId = people[idx].Id
  38:                         });
  39:                         people[idx].Orders.Add(new Order
  40:                         {
  41:                             Id = CollectionCount + idx + 2,
  42:                             ProductName = "OrderedProduct" + (CollectionCount + idx + 2).ToString(),
  43:                             CreatedOn = now.AddMilliseconds(idx),
  44:                             PersonId = people[idx].Id
  45:                         });
  46:                     }
  47:                 }
  48:             }
  49:  
  50:             return people;
  51:         }
  52:         
  53:         public IQueryable<Person> People
  54:         {
  55:             get
  56:             {
  57:                 return GetPeople().AsQueryable<Person>();
  58:             }
  59:         }
  60:  
  61:         public IQueryable<Order> Orders
  62:         {
  63:             get
  64:             {
  65:                 return (from pr in GetPeople()
  66:                         from or in pr.Orders
  67:                         select or).AsQueryable();
  68:             }
  69:         }
  70:         
  71:         public List<Person> GetPerson()
  72:         {
  73:             return GetPeople();
  74:         }
  75:     }

In line 53 and 61 there are IQueryable properties which will be automatically exposed in WCF Data Services. That’s the one of WCF Data Service provider requirements to have public IQueryable collections. Another is that the entity type (here Person) has an DataServiceKeyAttribute applied.

The WCF Data Service looks like this

   1: [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
   2: public class ODataPersonService : DataService<PersonContextMock>
   3: {
   4:     public static void InitializeService(DataServiceConfiguration config)
   5:     {
   6:         config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
   7:         config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
   8:         config.UseVerboseErrors = true;
   9:     }
  10: }

Simple isn’t it?

And WCF service looks like this

   1: [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
   2: public class WCFPersonService : IWCFPersonService
   3: {
   4:     public IList<core.model.Person> GetPeople()
   5:     {
   6:         return PersonContextMock.GetPeople();
   7:     }
   8: }

Also simple. It is standard WCF service using basicHttpBinding so messages are encoded as a text and sent through HTTP. Current functionality of this service is only a fraction of above OData service, but this is not an subject of this post.
I will not show the code of the clients, since it is trivial.

In the tests we will measure the time from request to the response. The standard WCF services will be tested with two message encodings: Text and binary, both using HTTP protocol.
Last row in each the table contains the median of the results.

Test 1

Getting a 1000 of Person without Orders

Try OData (ms) WCF Text encoding (ms) WCF Binary encoding (ms)
1 1717,0811 1642,09 1148,0657
2 647,037 58,0033 50,0029
3 607,0347 79,0045 50,0029
4 581,0332 60,0034 48,0028
5 571,0327 71,0041 57,0033
6 596,0341 71,0041 48,0028
7 568,0325 60,0035 48,0028
8 581,0333 58,0033 47,0027
9 572,0327 58,0033 49,0028
10 589,0337 77,0044 47,0027
11 556,0318 64,0037 49,0028
med 581,03 64,0037 48,0028

Test 2

Getting a 1000 of Person with Orders loaded

The only change required for this test is to set the

   1: private static readonly bool LoadOrders = true;

so all Person entities will be stored with its Orders loaded in mock repository. In WCF there is no change needed while OData service client must be modified, because OData service does not automatically loads the collections. We have to use Expand method on client side like this

   1: var people = client.People.Expand("Orders").ToList();

so the WCF Data Service will load Orders for each Person entity on sever side.

Try OData (ms) WCF Text encoding (ms) WCF Binary encoding (ms)
1 3907,22 1331,0761 1671,096
2 2303,13 131,0074 100,0057
3 2282,13 133,0076 103,0059
4 2278,13 122,007 104,0059
5 2270,13 128,0073 90,0052
6 2252,13 118,0068 124,0071
7 2250,13 122,0069 88,0051
8 2325,13 151,0086 98,0056
9 2264,13 134,0077 101,0057
10 2252,13 116,0066 99,0057
11 2316,13 120,0068 92,0052
med 2278,13 128,01 100,01

Here you can see how bad performance has OData service. Especially Expand functionality is very not efficient.

Test 3

OData service in comparison with direct database access using Entity Framework.

In the last test we will compare the OData service with Entity Framework Code First as a backend data source performance with direct Entity Framework Code First context operation. Test data contains 1000 Person entities without orders loaded. Direct access application has duplicated database in MS Sql Server CE 4.0 format and is a console application. OData client is also an console application and connects to the service hosted in MVC web site.

Try OData (ms) Direct EF access (ms)
1 3378,0799 3190,182
2 716,0409 25,0014
3 650,0372 24,0014
4 599,0342 23,0014
5 532,0304 25,0014
6 544,0311 27,0016
7 483,0276 23,0013
8 482,0275 26,0015
9 497,0284 38,0022
10 685,0392 24,0013
11 629,036 23,0013
med 599,03 25,00