Service Stack vs WCF Data Services

In this post I’m going to kick the tires on WCF Data Services and Service Stack. Service Stack offers claims of a much cleaner programming model than traditional WCF services in addition to impressive performance numbers compared to the Entity Framework. I’m going to compare how the two frameworks expose the same data model and test how they perform in this real world scenario.

The Scenario

I want a Service which given the following data model, takes a user id and returns the  user along with that users contacts  in JSON format.

Service 1: WCF Data Services & Entity Framework

WCF Data Services exposes a data model in a RESTful style using the OData protocol. It integrates seamlessly with the Entity Framework. I’m going to use these frameworks to expose the data model and do all the heavy lifting for us. The resulting Entity Framework Model is as follows:

2-Tiered Architecture

WCF Data Services and Entity Framework integrate together very easily. Once a SQL Server database schema is created, an Entity Framework  model can be automatically generated. Exposing this data model to consumers requires only the code listed below. Consumers are then able to generate rich OData queries to extract the information they need from the model.

This flexibility and ease of use comes with the downside of tight coupling between consumers and the database. The Entity Model  maps closely to database schema and the OData entities consumers interact with map very closely to the Entity Model.

Code

[JSONPSupportBehavior]
 public class ODataService : DataService
 {
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("Contacts", EntitySetRights.AllRead);
config.SetEntitySetAccessRule("ContactGroups", EntitySetRights.AllRead);
           config.SetEntitySetAccessRule("Users", EntitySetRights.AllRead);

           config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
 }

The User with Id=32 would be retrieved as follows:
http://localhost:200/ODataService.svc/Users(32L)?$expand=Contacts/ContactGroups&$format=json

Service 2: Optimized – WCF Data Services and Entity Framework

After digging into WCF Data Services, I found the following optimization improves query performance when using OData expansions:


[JSONPSupportBehavior]
 public class ODataService : DataService, IServiceProvider, IExpandProvider
 {
 public static void InitializeService(DataServiceConfiguration config)
 {
 config.SetEntitySetAccessRule("Contacts", EntitySetRights.AllRead);
 config.SetEntitySetAccessRule("ContactGroups", EntitySetRights.AllRead);
 config.SetEntitySetAccessRule("Users", EntitySetRights.AllRead);

 config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
 }

  public IEnumerable ApplyExpansions(IQueryable queryable, ICollection expandPaths)
  {
     if (queryable == null) throw new ArgumentNullException("queryable");

     dynamic query = queryable;
     return query.Execute(MergeOption.AppendOnly);
  }

  public object GetService(Type serviceType)
  {
     if (serviceType == null) throw new ArgumentNullException("serviceType");

     return (serviceType == typeof(IExpandProvider) ? this : null);
  }

}

Note: Querying for a user works the same as the un-optimized OData query.

Service 3: Service Stack

Service Stack (http://www.servicestack.net/) is an Opensource REST Web Services framework. It is positioned as a different and perhaps better approach to developing services than WCF. It does take more code to get a service running than WCF Data Services, but it provides more flexibility in the model it exposes.

3-Tier Architecture

The loose coupling between the consumers and the data model is a big benefit of Service Stack over WCF Data Services. The consumer Entity Model can change with ease independently of the underlying data store. It’s also only a small amount of work to swap out the data tier, which in this sample application is OrmLite.

Code

Below are the OrmLite mappings. These classes map to the tables by naming Convention. They can all be made internal which makes the intention that these are not to be used externally very clear!

namespace Service.Data
{
    internal class ContactGroup : IHasId
    {
        public long ContactId { get; set; }

        public string Name { get; set; }

        public long Id { get; set; }
    }

    internal class ContactGroup : IHasId
    {
        public long ContactId { get; set; }

        public string Name { get; set; }

        public long Id { get; set; }
    }

    internal class User : IHasId
    {
        public string Email { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }

        public long Id { get; set; }

        public Model.User ToModel(List contacts, Dictionary> contactGroups)
        {
            var modelUser = new Model.User
                                {
                                    Email = Email,
                                    FirstName = FirstName,
                                    LastName = LastName,
                                    Contacts = GetContacts(contacts, contactGroups)
                                };

            return modelUser;
        }

        private static List GetContacts(IEnumerable contacts,
                                                       Dictionary> contactGroups)
        {
            return contacts.Select(c => new Model.Contact
                                            {
                                                Email = c.Email,
                                                Name = c.Name,
                                                Groups = contactGroups.ContainsKey(c.Id) ? contactGroups[c.Id] : null
                                            }).ToList();
        }
    }

Next is the service mapping. We need to define our Restful service. This is one of the downsides of Service Stack vs WCF Data Services: we must hand-code each API. Arguably this is a feature since it forces you to think about how consumers will be interacting with your service. It is also easier to document method signatures than documenting entities and their relationships to one another.

    internal class UserService : IService
    {
        public IDbConnectionFactory ConnectionFactory { get; set; }

        public object Execute(UserRequest request)
        {
            var response = new UserResponse();

            using (IDbConnection dbConn = ConnectionFactory.OpenDbConnection())
            using (IDbCommand dbCmd = dbConn.CreateCommand())
            {
                var user = dbCmd.GetById(request.Id);
                if (user != null)
                {
                    List contacts = dbCmd.Select(x => x.UserId == user.Id);

                    Dictionary> contactGroups = dbCmd.GetLookup(
                        "select cg.ContactId, cg.Name from ContactGroup cg join Contact c on cg.ContactId = c.Id where c.UserId = {0}",
                        user.Id);

                    response.User = user.ToModel(contacts, contactGroups);
                }
            }

            return response;
        }
    }

Finally there is the Entity Model. These are the entities which consumers interact with. As you can see they are all POCO’s which are very testable and clean! They do not need to be decorated with virtual methods or attributes for testing and serialization! Best of all, they have no direct relationship to the database schema!

    public class Contact
    {
        public string Email { get; set; }

        public string Name { get; set; }

        public List Groups { get; set; }
    }

    public class User
    {
        public string Email { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }

        public List Contacts { get; set; }
    }

    public class UserRequest
    {
        public int Id { get; set; }
    }

    public class UserResponse
    {
        public ResponseStatus ResponseStatus { get; set; }

        public User User { get; set; }
    }

Testing:

The following functional tests randomly generate 5000 user requests. Each of the three services was exercised with these tests and the performance findings are outlined below. The three sites were installed in IIS 7.5 and Windows 7. The computer is a  Intel(R) Core(TM) i7-2600K CPU @ 3.40GHz, 3701 Mhz, 4 Core(s), 8 Logical Processor(s) with 8.00 GB of ram.

using System;
using System.IO;
using System.Net;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Service.Test
{
    [TestClass]
    public class PerformanceTest
    {
        [TestMethod]
        public void TestServiceStack()
        {
            Test("http://localhost:100/user/{0}?format=json");
        }

        [TestMethod]
        public void TestODataOptimized()
        {
            Test("http://localhost:300/ODataService.svc/Users({0}L)?$expand=Contacts/ContactGroups&$format=json");
        }

        [TestMethod]
        public void TestOData()
        {
            Test("http://localhost:200/ODataService.svc/Users({0}L)?$expand=Contacts/ContactGroups&$format=json");
        }

        private void Test(string url)
        {
            int i = 2000;

            var rand = new Random();

            DateTime start = DateTime.UtcNow;
            int errorCount = 0;
            while (i-- > 0)
            {

                int randUser = rand.Next(0, 100000);

                var request = (HttpWebRequest) WebRequest.Create(string.Format(url, randUser));

                try
                {
                    // execute the request
                    using (var response = (HttpWebResponse) request.GetResponse())
                    {
                        using (var sr = new StreamReader(response.GetResponseStream()))
                        {
                            sr.ReadToEnd();
                        }
                    }
                }
                catch (Exception)
                {
                    errorCount++;
                }
            }

            var timeSpan = DateTime.UtcNow.Subtract(start);

            Assert.Inconclusive("test took: " + timeSpan + " - Error Count:" + errorCount);
        }
    }
}

Results:

Service Stack took slightly more CPU usage, but was considerably faster than the others.

CPU Utilization

Time to complete 2000 requests

Final Notes:

Service Stack is considerably faster and provides better encapsulation in a N-Tiered environment. WCF Data Services with the Entity Framework provide a rapid method of exposing a rich Service Model. In an environment where performance and TDD is paramount I would go with Service Stack. When exposing a large data model to a large number of consumers, WCF Data Services is a good choice.

Full code can be found on github: https://github.com/marksl/servicestackdemo

Advertisements
This entry was posted in Architecture. Bookmark the permalink.

3 Responses to Service Stack vs WCF Data Services

  1. Gui says:

    One big benefit is that in odata you have linq over url, in servicestack you must write each query by hand…

  2. Ethan Nelson says:

    What is it about the “optimization” that optimizes? Is it switching to JSONP or one of the other things?

    • codealoc says:

      Nope it wasn’t the jsonp change.

      The two changes that ‘optimized’ the query are the two unclear code blocks in ApplyExpansions and GetService. Those eager load the properties rather then performing expansions on each entity. I recall this worked out to roughly 3 queries if three tables were involved rather then AxBxC queries for A entities with B children each having C children.

      I admit It is scary voodoo code only discovered after digging through the WCF source with dotpeak. I was at the time stuck with OData and this worked nicely and we needed some sort of solution. My full solution also involved mapping only the select very-slow queries to use this ‘optimized method’.

      Also noteworthy is that this was all using Entity Framework 4 and OData 2.0. There have been significant performance improvements in both frameworks since then.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s