WCF Data Service – OData 1 Year Later

A year ago I began a new project and we investigated what frameworks and tools to use. OData promised a simple and stanardized RESTful interface for CRUD operations. It leveraged many widely used and standard web technologies such as ATOM, HTTP and JSON. It looked promising and I thought my n-tiered service would be saved from large pots of boiler plate service and repository code. It has worked with mixed results – given the choice again I would not build this application again with OData.

Problem 1: Poor Client Code

WCF Data Services generates a rich client interface for interacting with an OData Service. Where it comes up short is readability and ease of use. Although you can do everything, you will likely spend time figuring out how to do it… or more importantly, how to maintain it. Here’s an example highlighting the poor usability of client code.

This is what I would expect. This fails:

    [Test]
    [ExpectedException(typeof(DataServiceRequestException))]
    public void GoodInterface_ThrowsException()
    {
        var engine = new Engine {Cylinders = 6, Horsepower = 200};
        var tires = new TirePackage {Size = 16, Weight = 40};
        var car = new PurchasedCar {CustomerName = "Me", Engine = engine, Tires = tires};

        var context = GetEntityContext();
        context.AddToPurchasedCars(car);

        // BAD: Throws "Resource not found for the segment 'PurchasedCar'" error
        context.SaveChanges();
    }

This is what works:


    [Test]
    public void NonIntuitive_ButSucceeds()
    {
        var engine = new Engine { Cylinders = 6, Horsepower = 200 };
        var tires = new TirePackage { Size = 16, Weight = 40 };
        var car = new PurchasedCar { CustomerName = "Me" };

        var context = GetEntityContext();

        // BAD: SetLink is not type safe and has poor readability
        // Note: Order is important AddToEngines and AddToPurcasedCars must come first.
        // Note: SetLink must come after calling AddTo*() on the context
        context.AddToEngines(engine);
        context.AddToTirePackages(tires);
        context.AddToPurchasedCars(car);
        context.SetLink(car, "Engine", engine);
        context.SetLink(car, "Tires", tires);

        context.SaveChanges(SaveChangesOptions.Batch);
    }

Problem 2: Poor Encapsulation of Service Logic

WCF Data Services and Entity Framework both provide extension points. What they don’t provide is a place to put Service Tier or Business logic. Here’s a very simple example of a Service Tier with some business logic. WCF Data Services does not have good extension points for this operation.

Here’s what I want to do, but can not with OData:

    public enum CarType
    {
        Basic,
        Deluxe
    }

    [WebMethod]
    public void PurchaseCar(CarType type)
    {
        var tires = new TirePackage {Size = 16, Weight = 40};

        Engine engine = type == CarType.Basic
                            ? new Engine {Cylinders = 4, Horsepower = 120}
                            : new Engine {Cylinders = 8, Horsepower = 200};

        var car = new PurchasedCar {CustomerName = "Me", Engine = engine, Tires = tires};

        var context = new Entities();
        context.AddToPurchasedCars(car);
        context.SaveChanges();
    }

An alternate implementation:

    [WebMethod]
    [XmlInclude(typeof(DeluxeCar))]
    public void Purchase(Car car)
    {
        PurchasedCar purchasedCar = car.ToPurchasedCar();

        var context = new Entities();
        context.AddToPurchasedCars(purchasedCar);
        context.SaveChanges();
    }

    public class Car
    {
        internal PurchasedCar ToPurchasedCar()
        {
            var tires = new TirePackage { Size = 16, Weight = 40 };
            var car = new PurchasedCar { CustomerName = "Me", Engine = GetEngine(), Tires = tires };

            return car;
        }

        virtual internal Engine GetEngine()
        {
            return new Engine {Cylinders = 4, Horsepower = 120};
        }
    }

    public class DeluxeCar : Car
    {
        internal override Engine GetEngine()
        {
            return new Engine {Cylinders = 8, Horsepower = 200};
        }
    }
}

An OData solution limitation:

As a possible solution, I tried to add a property to the Entity classes as in this example. But it throws a cryptic error message.

    public enum CarType
    {
        Basic,
        Deluxe
    }

    public partial class PurchasedCar
    {

        // This does not work. Causes cryptic error:
        // "The server encountered an error processing the request. See server logs for more details."
        [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)]
        [DataMember]
        public CarType Type { get; set; }
    }

This rather simple example can be implemented with OData Service Operations. However Service Operations come with their own caveat: all parameters must be passed in the query string. They are not passed in the body of the HTTP request. Query Strings have length limitations and IIS and Apache log query strings which can be a security concern.

Some options that I did not try (and do not want to try):
1. A SQL View, mapped to the Entity framework with CREATE and UPDATE triggers on the view. I don’t want my logic in the database.
2. An Entity Framework Defining Query – a client side view implemented in the Entity Framework. This is basically the same as #1.

Problem 3: Complex XML or JSON

The XML and JSON for all operations are hideous.

Here’s an entry for a HTTP GET on a single entity:


  <entry>
    <id>http://localhost:65191/ODataService.svc/PurchasedCars(2)</id>
    <title type="text"></title>
    <updated>2012-09-06T03:10:54Z</updated>
    <author>
      <name />
    </author>
    <link rel="edit" title="PurchasedCar" href="PurchasedCars(2)" />
    <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Engine" type="application/atom+xml;type=entry" title="Engine" href="PurchasedCars(2)/Engine" />
    <link rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Tires" type="application/atom+xml;type=entry" title="Tires" href="PurchasedCars(2)/Tires" />
    <category term="Model.PurchasedCar" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
    <content type="application/xml">
      <m:properties>
        <d:Id m:type="Edm.Int32">2</d:Id>
        <d:CustomerName>Me</d:CustomerName>
        <d:EngineId m:type="Edm.Int32">4</d:EngineId>
        <d:TireId m:type="Edm.Int32">4</d:TireId>
      </m:properties>
    </content>
  </entry>

And the HTTP CREATE in my functional test:


--batch_04c83645-5fea-4b4c-85c2-798a9cc1273d
Content-Type: multipart/mixed; boundary=changeset_accb4e67-839d-414f-bc85-0380364eb914

--changeset_accb4e67-839d-414f-bc85-0380364eb914
Content-Type: application/http
Content-Transfer-Encoding: binary

POST http://localhost.:65191/ODataService.svc/Engines HTTP/1.1
Content-ID: 1
Content-Type: application/atom+xml;type=entry
Content-Length: 714

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
  <category scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" term="Model.Engine" />
  <title />
  <author>
    <name />
  </author>
  <updated>2012-09-06T03:27:46.3653179Z</updated>
  <id />
  <content type="application/xml">
    <m:properties>
      <d:Cylinders m:type="Edm.Int32">6</d:Cylinders>
      <d:Horsepower m:type="Edm.Int32">200</d:Horsepower>
      <d:Id m:type="Edm.Int32">0</d:Id>
    </m:properties>
  </content>
</entry>
--changeset_accb4e67-839d-414f-bc85-0380364eb914
Content-Type: application/http
Content-Transfer-Encoding: binary

POST http://localhost.:65191/ODataService.svc/TirePackages HTTP/1.1
Content-ID: 2
Content-Type: application/atom+xml;type=entry
Content-Length: 701

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
  <category scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" term="Model.TirePackage" />
  <title />
  <author>
    <name />
  </author>
  <updated>2012-09-06T03:27:46.3983198Z</updated>
  <id />
  <content type="application/xml">
    <m:properties>
      <d:Id m:type="Edm.Int32">0</d:Id>
      <d:Size m:type="Edm.Int32">16</d:Size>
      <d:Weight m:type="Edm.Int32">40</d:Weight>
    </m:properties>
  </content>
</entry>
--changeset_accb4e67-839d-414f-bc85-0380364eb914
Content-Type: application/http
Content-Transfer-Encoding: binary

POST http://localhost.:65191/ODataService.svc/PurchasedCars HTTP/1.1
Content-ID: 3
Content-Type: application/atom+xml;type=entry
Content-Length: 751

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
  <category scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" term="Model.PurchasedCar" />
  <title />
  <author>
    <name />
  </author>
  <updated>2012-09-06T03:27:46.3983198Z</updated>
  <id />
  <content type="application/xml">
    <m:properties>
      <d:CustomerName>Me</d:CustomerName>
      <d:EngineId m:type="Edm.Int32">0</d:EngineId>
      <d:Id m:type="Edm.Int32">0</d:Id>
      <d:TireId m:type="Edm.Int32">0</d:TireId>
    </m:properties>
  </content>
</entry>
--changeset_accb4e67-839d-414f-bc85-0380364eb914
Content-Type: application/http
Content-Transfer-Encoding: binary

PUT $3/$links/Engine HTTP/1.1
Content-ID: 4
Content-Type: application/xml
Content-Length: 143

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<uri xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">$1</uri>
--changeset_accb4e67-839d-414f-bc85-0380364eb914
Content-Type: application/http
Content-Transfer-Encoding: binary

PUT $3/$links/Tires HTTP/1.1
Content-ID: 5
Content-Type: application/xml
Content-Length: 143

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<uri xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">$2</uri>
--changeset_accb4e67-839d-414f-bc85-0380364eb914--
--batch_04c83645-5fea-4b4c-85c2-798a9cc1273d--

Problem 4: Poor Discoverability

The built in WCF Data Service interface does not expose Service Operations. It also doesn’t expose the permissions on each entity. Permissions can be one of the following and which setting is specified greatly impacts how you can interface with the OData service.

ASMX Discoverability:

OData Discoverability:

Here are the flags which have a big impact on the API, but are not queryable.

    [Flags]
    public enum EntitySetRights
    {
        None = 0,
        ReadSingle = 1,
        ReadMultiple = 2,
        AllRead = ReadMultiple | ReadSingle,
        WriteAppend = 4,
        WriteReplace = 8,
        WriteDelete = 16,
        WriteMerge = 32,
        AllWrite = WriteMerge | WriteDelete | WriteReplace | WriteAppend,
        All = AllWrite | AllRead,
    }

The Good

I have listed several problems with OData. I do want to provide some balance to this post. OData and WCF Data Services is a good fit for a simple Data Model. A simple data model with no need for a Service Tier is a good fit for OData. Ad hoc queries overall work very well. CUD operations are where it comes up short. They do work, you just have to work at it more than you’d expect.

OData V3: The Solution to the… OData problem?

I haven’t dug into OData v3 yet. It does come with a cleaner JSON format. There’s also enumeration support with Vocabularies which will be very nice. Maybe they have significantly improved usability as well. I wasn’t able to see a good summarized Release Notes on the WCF Data Service Team Blog or on http://www.odata.org. For now I’m going to play it safe – For a simple domain I may consider OData, but for a complex Domain Model I’ll stick to traditional services frameworks.

All Code for this can be found on here: https://github.com/marksl/service-examples

Advertisements
This entry was posted in .Net and tagged , , , . Bookmark the permalink.

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