Introducing the OData Library

 

This blog talks about a new feature delivered in the WCF Data Services October CTP that can be downloaded here.

WCF Data Services’ latest CTP includes a new stand-alone library for working directly with OData. The library makes public some underpinnings of WCF Data Services (the server and client library), and we made this library stand-alone to allow its use independent from WCF Data Services. The library provides a low-level implementation of some components needed to build an OData producer/consumer. Specifically, we focused on the core tasks of reading/writing OData from streams in the library’s first version, and in the future we hope to add more fundamental OData functionality (possibly OData Uri reading and writing). However, we haven’t made any final plans on what we will add, and we welcome your feedback.

I want to take a minute to explain this library’s relation to the existing WCF Data Services products; this library doesn’t replace WCF Data Services. If you want a great end-to-end solution for creating and exposing your data via an OData endpoint, then the WCF Data Services server library is (and will continue to be) the way to go. If you want a great OData Feed-consuming client with auxiliary support, like code generation and LINQ translation, then WCF Data Services’ client library is still your best bet. However, we also recognize that people are exploring creative possibilities with OData, and to help them build their own solutions from scratch we made the components we use as part of the WCF Data Services stack available as a stand-alone library.

We have published the OData library’s latest source code for the on codeplex (https://odata.codeplex.com) as shared source for developers on .NET and other platforms

The CodePlex source code includes the samples that I have attached to this blog post, and I’ll walk through a couple of those samples to illustrate reading and writing OData.

Writing a Single Entity

To hide the library’s details of stream-reading/writing the OData Library uses an abstraction called a Message, which consists of stream and header interfaces (IODataRequestMessage, IODataResponseMessage). The example below walks through the basics of single-entry writing using an implementation of the messages that work over HTTPClient (this implementation is available in the samples project).

The library uses a class called the ODataMessageWriter to write the actual body of a single ODataMessage (request or response). The ODataMessageWriter has a bunch of methods on it that can be used for writing small non-streaming payloads, such as single properties or individual complex-type values. For larger payloads (such as entities and collections of entities) the ODataMessageWriter has methods that create streaming writers for each payload type. The example shows how to use the ODataMessageWriter methods to create an ODataEntryWriter that can be used to write a single OData entry.

Finally, the sample goes on to use the ODataEntryWriter to write a single Customer entry along with four primitive properties and two deferred links. The samples project includes a few samples that show how to write an expanded navigation link as well.

Writing Full Sample

  1: HTTPClientRequestMessage message = new HTTPClientRequestMessage(uri);
  2: message.SetHeader("Accept", formatKind == ODataFormat.Json ?           "application/json" : "application/atom+xml");
  3: message.Method = HttpMethod.Post;
  4: message.SetHeader("MaxDataServiceVersion", maxVersion.ToHeaderValue());
  5:  
  6: // create the writer, indent for readability of the examples.
  7: ODataMessageWriterSettings writerSettings =           new ODataMessageWriterSettings() { 
  8:     Indent = true,             //pretty printing
  9:     CheckCharacters = false,   //sets this flag on the XmlWriter for ATOM
  10:     BaseUri = new Uri(baseUri),//set the base uri to use in relative links
  11:     Version = version //set the Odata version to use when writing the entry
  12: };
  13: writerSettings.SetContentType(formatKind);
  14:  
  15: //create message writing for the message
  16: using (ODataMessageWriter messageWriter =            new ODataMessageWriter(message, writerSettings))
  17: {
  18:     //creates a streaming writer for a single entity
  19:     ODataWriter writer = messageWriter.CreateODataEntryWriter(); 
  20:  
  21:     // start the entry
  22:     writer.WriteStart(new ODataEntry()
  23:     {
  24:         // the edit link is relative to the  //baseUri set on the writer in the case
  25:         EditLink = new Uri("/Customers('" +                        dataSource.Customers.First().CustomerID +                       "')", UriKind.Relative),
  26:       Id = "Customers('" + dataSource.Customers.First().CustomerID + "')",
  27:         TypeName = "NORTHWNDModel.Customer",
  28:         Properties = new List<ODataProperty>(){
  29:             new ODataProperty(){ Name = "CustomerID", Value =                                   dataSource.Customers.First().CustomerID },
  30:             new ODataProperty(){ Name = "CompanyName", Value =                                   dataSource.Customers.First().CompanyName },
  31:             new ODataProperty(){ Name = "ContactName", Value =                                   dataSource.Customers.First().ContactName },
  32:             new ODataProperty(){ Name = "ContactTitle", Value =                                   dataSource.Customers.First().ContactTitle }
  33:         }
  34:     });
  35:  
  36:     //create a non-expanded link for the orders navigation property
  37:     writer.WriteStart(new ODataNavigationLink()
  38:     {
  39:         IsCollection = true,
  40:         Name = "Orders",
  41:         Url = new Uri("https://microsoft.com/Customer(" +                      dataSource.Customers.First().CustomerID + ")/Orders")
  42:     });
  43:     writer.WriteEnd(); //ends the orders link
  44:  
  45:     //create a non-expanded link for the employees navigation property
  46:     writer.WriteStart(new ODataNavigationLink()
  47:     {
  48:         IsCollection = true,
  49:         Name = "Employees",
  50:         Url = new Uri(
  51:             "https://microsoft.com/Customer(" +                   dataSource.Customers.First().CustomerID + ")/Employees")
  52:     });
  53:     writer.WriteEnd(); //ends the Employees link
  54:  
  55:     writer.WriteEnd(); //tells the writer we are done writing the entity
  56:     writer.Flush(); //always flush at the end
  57: }

Reading a Single Entity

Let’s look at an example that shows OData deserialization via the library. The example method below demonstrates how to issue a request to the Netflix OData feed for the set of Genres and parse the response.

The example below makes use of the same ODataMessage classes as the previous example (the HTTPClientMessage), but first creates an HTTPClientRequestMessage that targets the Genres URL for the OData Netflix feeds, and then executes the request to get an HTTPClientResponseMessage that represents the response returned by the Netflix services. For readability, the example just outputs the data in the response to a text file afterwards.

The example below uses an IEdmModel not used in the writer example above. When the ODataMessageReader is created an IEdmModel is passed in as a parameter – the IEdmModel is essentially an in-memory representation of the metadata about the service that is exposed via the $metadata url. For a client component the easiest way to create the IEdmModel is to use the ReadMetadata method in the OData Library that creates an in-memory IEdmModel by parsing a $metadata document from the server. For a server, you would generally use the APIs included in the Edm Library (Microsoft.Edm.dll) to craft a model. Providing a model for OData parsing provides key benefits:

o The reader will validate that the entities and properties in the documents being parsed conform to the model specified

o Parsing is done with full type fidelity (i.e. that wire types are converted to the model types when parsed); this is especially important when parsing JSON because the JSON format only preserves 4 types and the OData protocol supports many more. There are configuration options to change how this is done, but I won’t discuss them here for space reasons.

o If the service defines feed customizations, the model contains their definitions and the readers (and writers) will only know to apply them correctly if provided a model.

o JSON can only be parsed when a model is provided (this is a limitation of the library and we may add JSON parsing without a model at some point in the future). ATOM parsing without a model is supported.

In the example below an ODataFeedReader is created out of the ResponseMessageReader to read the contents of the response stream. The reader works like the XmlReader in the System.XML library, with which many of you will be familiar. Calling the Read() method moves the reader through the document, and each time Read() is called the reader changes to a specific state that depends on what the reader is currently reading, which is represented by an “Item”. For instance, when the reader reads an entry in the feed, it will go to the StartEntry state, and the Item on the reader will be the ODataEntry being read –there are similar states for Feeds and Links. Importantly, when the reader is in a start state (StartEntry, StartFeed, StartLink, etc) the reader will have an Item it has created to hold the Entry/Feed/Link that it is reading, but the Item will be mostly empty because the reader has not actually read it yet. It’s only when the reader gets to the end states (EndEntry, EndFeed, EndLink) that the Item will be fully populated with data.  

  1: public void ExecuteNetflixRequest(IEdmModel model, string fileName)
  2: {
  3:     //we are going to create a GET request to the OData Netflix Catalog
  4:     HTTPClientRequestMessage message = new HTTPClientRequestMessage(                   "https://odata.netflix.com/v2/Catalog/Genres");
  5:     message.SetHeader("Accept", "application/json");
  6:     message.SetHeader("DataServiceVersion",                    ODataUtils.ODataVersionToString(ODataVersion.V2));
  7:     message.SetHeader("MaxDataServiceVersion",                    ODataUtils.ODataVersionToString(ODataVersion.V2));
  8:  
  9:     //create a text file to write the response to and create a textwriter
  10:     string filePath = fileName;
  11:     using (StreamWriter outputWriter = new StreamWriter(filePath))
  12:     {
  13:         //use an indented text writer for readability
  14:         this.writer = new IndentedTextWriter(outputWriter, " ");
  15:  
  16:         //issue the request and get the response as an ODataMessage.  //Create an ODataMessageReader over the response 
  17:         //we will use the model when creating the reader  //as this will tell the library to validate when parsing
  18:         using (ODataMessageReader messageReader =                   new ODataMessageReader(message.GetResponse(), 
  19:             new ODataMessageReaderSettings(), model))
  20:         {
  21:             //create a feed reader 
  22:             ODataReader reader = messageReader.CreateODataFeedReader();
  23:             while (reader.Read())
  24:             {
  25:                 switch (reader.State)
  26:                 {
  27:                     case ODataReaderState.FeedStart: 
  28:                         {
  29:                             //this is just the beginning of the feed,  //data will not be parsed yet
  30:                             ODataFeed feed = (ODataFeed)reader.Item;
  31:                             this.writer.WriteLine("ODataFeed:");
  32:                             this.writer.Indent++;
  33:                         }
  34:  
  35:                         break;
  36:  
  37:                     case ODataReaderState.FeedEnd:
  38:                         {
  40:                             ODataFeed feed = (ODataFeed)reader.Item;
  41:                             if (feed.Count != null)
  42:                             {
  43:                                 //if there is an inlinecount value // write the value out
  44:                                 this.writer.WriteLine("Count: " +                                              feed.Count.ToString());
  45:                             }
  46:                             if (feed.NextPageLink != null)
  47:                             {
  48:                                 //if there is a next link  //write that link as well
  49:                                 this.writer.WriteLine("NextPageLink: " +                                              feed.NextPageLink.AbsoluteUri);
  50:                             }
  51:  
  52:                             this.writer.Indent--;
  53:                         }
  54:  
  55:                         break;
  56:  
  57:                     case ODataReaderState.EntryStart:
  58:                         {
  59:                             //this is just the start of the entry. 
  60:                             //Properties of the entity will not be parsed yet
  61:                             ODataEntry entry = (ODataEntry)reader.Item;
  62:                             this.writer.WriteLine("ODataEntry:");
  63:                             this.writer.Indent++;
  64:                         }
  65:  
  66:                         break;
  67:  
  68:                     case ODataReaderState.EntryEnd:
  69:                         {
  70:                             //at the point the whole entry has been read
  71:                             //and the properties of the entity are available
  72:                             ODataEntry entry = (ODataEntry)reader.Item;
  73:                             this.writer.WriteLine("TypeName: "                                          + (entry.TypeName ?? "<null>"));
  74:                             this.writer.WriteLine("Id: "                                          + (entry.Id ?? "<null>"));
  75:                             if (entry.ReadLink != null)
  76:                             {
  77:                                 this.writer.WriteLine("ReadLink: "                                             + entry.ReadLink.AbsoluteUri);
  78:                             }
  79:  
  80:                             if (entry.EditLink != null)
  81:                             {
  82:                                 this.writer.WriteLine("EditLink: "                                             + entry.EditLink.AbsoluteUri);
  83:                             }
  84:  
  85:                             if (entry.MediaResource != null)
  86:                             {
  87:                                 this.writer.Write("MediaResource: ");
  88:                                 this.WriteValue(entry.MediaResource);
  89:                             }
  90:  
  91:                             this.WriteProperties(entry.Properties);
  92:  
  93:                             this.writer.Indent--;
  94:                         }
  95:  
  96:                         break;
  97:  
  98:                     case ODataReaderState.NavigationLinkStart:
  99:                         {
  100:                             //navigation links have their own states. 
  101:                             //This could be an expanded link and include  //an entire expanded entry or feed.
  102:                             ODataNavigationLink navigationLink =                                        (ODataNavigationLink)reader.Item;
  103:                             this.writer.WriteLine(navigationLink.Name                                        + ": ODataNavigationLink: ");
  104:                             this.writer.Indent++;
  105:                         }
  106:  
  107:                         break;
  108:  
  109:                     case ODataReaderState.NavigationLinkEnd:
  110:                         {
  111:                             ODataNavigationLink navigationLink =                                       (ODataNavigationLink)reader.Item;
  112:                             this.writer.WriteLine("Url: " + 
  113:                                 (navigationLink.Url == null ? "<null>"                                        : navigationLink.Url.AbsoluteUri));
  114:                             this.writer.Indent--;
  115:                         }
  116:  
  117:                         break;
  118:                 }
  119:             }
  120:         }
  121:     }
  122: }

This is a quick introduction to the new OData Library included in this CTP. The post’s attached samples walk through the basics of OData feed creation and consumption via the library. We welcome any feedback you have on the library so don’t hesitate to contact us.

Shayne Burgess
Program Manager – OData Team

ODataLib.Samples.zip