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
Comments
Anonymous
October 23, 2011
The comment has been removedAnonymous
November 09, 2011
I have to write a client which required to consume few WCF data services, which i was managing at the design time to point to the different data services. Now after introduction of OData library i want to write a generic OData client which can consume any WCF data service at run time. Can i achieve this by OData library ? If yes then please guide me how i can achieve this. Thanks.Anonymous
July 12, 2012
I feel Im using the incorrect version of 'something'. When I try to build the samples using VS 2010 I get the following 2 errors:
- ODataFormat.Json does not exist but ODataFormat.VerboseJson does exist
- HttpMethod could not be found Any help would be appreciated.
Anonymous
July 24, 2012
The sample was written against a "prerelease" version of ODataLib so there are small differences in the shipped version. The JSON format is called VerboseJson (in preparation for the JsonLight format to come soon). The HttpMethod values were moved to ODataConstants. So for example ODataConstants.MethodGet. The source code of ODataLib has been updated on codeplex as of beggining of July.Anonymous
September 24, 2013
Thanks for the sample. What is the best approach to read the response entity into object ?Anonymous
January 15, 2014
In 5.6 the HttpMethod was Private?!