-
Notifications
You must be signed in to change notification settings - Fork 197
Advanced OData tutorial
This tutorial describes advanced OData scenario, both for the most recent OData protocol (V4) and earlier versions. The examples for this tutorial were inspired by the Advanced OData Tutorial article published at the odata.org. The original article describes the HTTP communication level: it shows how to build URLs, which verbs to use and what response to expect. Most of developers prefer using some kind of library to encapsulate OData HTTP communication, and we are going to study how to solve the advanced OData scenarios using Simple.OData.Client. This library supports all OData protocol versions and can be installed as a NuGet package for either OData V1-3, V4 or as version-agnostic client. All library versions are packaged as Portable Class Library with support for .NET 4.x, Windows Store, Windows Phone 8, Silverlight 5, Xamarin iOS and Xamarin Android platforms.
To communicate with OData service we need an instance of ODataClient. It can be created either from its URL string or using ODataClientSettings. Here's the simplest version:
var client = new ODataClient("http://services.odata.org/V4/TripPinServiceRW/");
And here is a slightly more complicated example where we instruct the client not to raise exceptions on 404 error and trace all requests so we can check the HTTP communication:
var client = new ODataClient(new ODataClientSettings("http://services.odata.org/V4/TripPinServiceRW/")
{
IgnoreResourceNotFoundException = true,
OnTrace = (x, y) => Console.WriteLine(string.Format(x, y)),
});
In the examples above we used an address of Microsoft's TripPin sample service. This is an OData V4 service that exposes most of the essential OData features, and this is the one we will be using through this article.
Despite the word "Simple" in its title, Simple.OData.Client actually supports three API flavors: typed, dynamic and untyped.
Typed API is what most developers expect when writing C# code. OData protocol is about exposing domain-specific resources over HTTP, and if we provide a set of corresponding C# entity classes, the client will perform necessary conversions of both OData query strings and query results. At the present time Simple.OData.Client doesn't include entity generation tool, so you will have to write entity classes yourself. Note that classes generated by WCF Data Services proxy generator are not compatible with Simple.OData.Client because they require use of context, and Simple.OData.Client is as stateless as REST protocol itself.
Dynamic API is works without entity classes, using dynamic C# syntax. It is best for OData service exploration, when all you need to consume OData feed is just a few lines of code. Moreover, the syntax of this code will be very similar to the syntax of typed API (and in fact it is the same API, just different overloads), so you can later easily convert your dynamic code to become typed.
Finally, untyped API is what the client uses internally where all data are stored in collections of IDictionary<string,object>
. This syntax is also exposed and fits exploration and ad-hoc access scenarios. In our examples we will be using typed API flavors but will show for couple of examples its dynamic counterpart. They are interchangeable.
To retrieve entries from an OData service collection, we only need to know the collection name. If the collection name matches the name of our entity type (either in singular or plural form), the client will manage to figure out the name of the corresponding OData resource. Otherwise we can send the name of the collection with the method call. Here how we can retrieve the entries from People collection:
var people = await client
.For<Person>()
.FindEntriesAsync(annotations);
Setting the collection name explicitly to CustomPeople
var customPeople = await client
.For<CustomPerson>("CustomPeople")
.FindEntriesAsync(annotations);
For an entry-level introduction to OData there probably wouldn't be more to say about how to retrieve top-level OData collection elements, but this is an advanced tutorial, so we need to complicate the things. If you checked the content of the sample TripPin database, you would discover 20 items in People collection. And if you check the count of items returned by code above, you will find that it's only 8. So where is the rest?
By inspecting the HTTP traffic we can discover some additional data returned together with People items, among them so called OData annotations. Annotations supply extra information about the data being retrieved like links to endpoints that can be accessed to perform further operations with the response data. But sometimes these links are also essential to fully perform the current operation. To prevent the service from being overloaded by unconstrained requests (e.g. requesting all data from a particular collection), the service may issue its own constraints on such requests together with a hint about how to retrieve the rest of data. Simple.OData.Client can handle such annotations, so the code to obtain all People entries will look like this:
var annotations = new ODataFeedAnnotations();
var people = await client
.For<Person>()
.FindEntriesAsync(annotations)
.ToList();
while (annotations.NextPageLink != null)
{
people.AddRange(await _client
.For<Person>()
.FindEntriesAsync(annotations.NextPageLink, annotations));
}
If you look at TripPin service metadata, you will see that People collection refer to other collections: emails, addresses, friends, trips and photos. OData protocol is lazy, it doesn't fetch all associated data unless explicitly requested. Here how we can expand People key lookup result to include Trips and Friends:
var person = await client
.For<Person>()
.Key("russellwhyte")
.Expand(x => new { x.Trips, x.Friends })
.FindEntryAsync();
In this example we expanded the result containing a single person data, but expansions can also be applied to requests for multiple items.
In case you have opted for a dynamic syntax, here's the corresponding code:
var x = ODataDynamic.Expression;
var person = await client
.For(x.People)
.Key("russellwhyte")
.Expand(x.Trips, x.Friends)
.FindEntryAsync();
As you can see, the dynamic syntax is very similar and differs in the following:
- You need to define somewhere in the code an instance of OData dynamic expression (ODataDynamic.Expression). You don't need to define it every time you make a call, actually it's sufficient to define it once for the whole application. This variable is an intermediate placeholder for expressions.
- Once a variable of ODataDynamic.Expression is instantiated, you can use it anywhere you would use any typed expression, typically generic type specifiers and lambda expressions.
Results expansions enrich results with associated information, but what if we only need that associated information and are not interested in the root level data. Continuing exploration of People, we will now lookup a person, navigate to one of his trips and then fetch all plan items registered on that trip. Here's how this can be achieved:
var planItems = await client
.For<Person>()
.Key("russellwhyte")
.NavigateTo(x => x.Trips)
.Key(1003)
.NavigateTo(x => x.PlanItems)
.FindEntriesAsync();
Note that we had to call NavigateTo method twice: first to advance from the People entry to Trips collection (thus abandoning People details like if we started from Trips collection), and then advancing to PlanItems collection (again, abandoning Trips details). The result will contain a collection of PlanItems data, just like if it was a top-level collection, only that if will contain only plan items for the specific trip of the specific person.
Now that we know how to retrieve OData collections on different levels, let's have a look at how we can filter them using Any and All quantors. Suppose we want to fetch people information that includes their trips but only if the trip budget was over $10000. Here's what we do:
var flights = await client
.For<Person>()
.Filter(x => x.Trips
.All(y => y.Budget > 10000d))
.Expand(x => x.Trips)
.FindEntriesAsync();
To make the search criteria more complex, let's include both Any and All clauses. We will now search for people with trips all of them would contain at least one plan item with duration longer than 4 hours:
var duration = TimeSpan.FromHours(4);
var flights = await client
.For<Person>()
.Filter(x => x.Trips
.All(y => y.PlanItems
.Any(z => z.Duration < duration)))
.FindEntriesAsync();
And here's the same operation written using dynamic C# syntax:
var x = ODataDynamic.Expression;
var duration = TimeSpan.FromHours(4);
var flights = await client
.For(x.People)
.Filter(x.Trips
.All(x.PlanItems
.Any(x.Duration < duration)))
.FindEntriesAsync();
Note that we didn't have to use different variables in different lambda-expressions: the "x" dynamic expression variable is recycled after each use so it can be used again in the next clause.
Singleton concept is a newcomer to OData. What if one particular element of the OData collection has a distinct role of being the one and only when it comes to certain quality? A company that runs a Web shop may be registered in the Company database together with its clients, but it might be convenient to expose its details in a way that makes it easy to refer to that company without hard-coding its ID in the client application. This can achieved using OData singletons.
A singleton in OData is a top-level collection that contains only one item. You can't search for items within the singleton, it doesn't make sense. You can only grab that only record it contains.
var person = await client
.For<Person>("Me")
.FindEntryAsync();
Note that we only had to specify a different collection name ("Me") and the command doesn't have a query filter or key, otherwise it looks just like a request to People collection. And the singleton quality only applies to the top-level element - the "Me" person may refer to other collections that can be navigated in the same way like demonstrated earlier.
Subclassing is one of the less known features of OData, but it's quite powerful. In addition to grouping data into different collections, it is also possible to categorize them within each collection as being data of different subtype of the collection type. Moreover, it is also possible to search for data of the specific type. Collections of subclassed data are called derived collections.
Suppose we want to retrieve the given persons's plan items but we also want to restrict the plan items by only flights and include flight details not exposed by the PlanItem collection. Simple.OData.Client has a clause As that serves exactly this purpose:
var flights = await client
.For<Person>()
.Key("russellwhyte")
.NavigateTo(x => x.Trips)
.Key(1003)
.NavigateTo(x => x.PlanItems)
.As<Flight>()
.FindEntriesAsync();
Flight definition includes additional fields like FlightNumber and Airline that don't exist in PlanItem definition, and these extra properties will be retrieved by the client as long as it uses As clause. And once we navigate to the subclass, we can write query expressions that include fields defined by that subclass, like in the example below where we lookup for a flight with the specific flight number among the given person's flights:
var flights = await client
.For<Person>()
.Key("russellwhyte")
.NavigateTo(x => x.Trips)
.Key(1003)
.NavigateTo(x => x.PlanItems)
.As<Flight>()
.Filter(x => x.FlightNumber == "FM1930")
.FindEntriesAsync();
Ability to specify derived collections in search criteria is quite useful, but there must also be a way to create, updated and delete subclassed data. Deletion doesn't require additional functionality because OData resources are always deleted by key. They are also updated by key, but updating derived collections need special attention since a client should be able to send values of the properties that are only defined in the subclass. The same goes for insertion where a client needs to send derived collection property values.
Here is the syntax of the statement that inserts into person's PlanItems an entry of a type Event. Event type is derived from PlanItem and adds a couple of properties (Descrption and OccursAt).
var tripEvent = client
.For<Person>()
.Key("russellwhyte")
.NavigateTo<Trip>()
.Key(1003)
.NavigateTo(x => x.PlanItems)
.As<Event>()
.Set(CreateEventDetails())
.InsertEntryAsync();
To make the code easier to read, I extracted the method CreateEventDetails that initialized properties of the Event instance:
private Event CreateEventDetails()
{
return new Event
{
ConfirmationCode = "4372899DD",
Description = "Client Meeting",
Duration = TimeSpan.FromHours(3),
EndsAt = DateTimeOffset.Parse("2014-06-01T23:11:17.5479185-07:00"),
OccursAt = new EventLocation()
{
Address = "100 Church Street, 8th Floor, Manhattan, 10007",
BuildingInfo = "Regus Business Center",
City = new Location.LocationCity()
{
CountryRegion = "United States",
Name = "New York City",
Region = "New York",
}
},
PlanItemId = 33,
StartsAt = DateTimeOffset.Parse("2014-05-25T23:11:17.5459178-07:00"),
};
}
Updating the event uses the same technique:
var tripEvent = client
.For<Person>()
.Key("russellwhyte")
.NavigateTo<Trip>()
.Key(1003)
.NavigateTo(x => x.PlanItems)
.As<Event>()
.Key(33)
.Set(new { Description = "This is a new description" })
.UpdateEntryAsync();
Note that the reason we need to navigate to PlanItems collection via Person is that plan items don't have a top-level collection - they are contained in their respective person's details. Otherwise we could start right from PlanItems.
OData has supported so called service operations for a long time, but the version 4 of OData protocol has a clear distinction between functions and actions. Functions don't have side effects and are usually executed using GET command (although the service may support sending function arguments as POST content). Actions has side effect and following REST principles are executed using POST. Both function and actions can be bounded and unbounded which means that they are applied to a collection or the whole service respectively.
Here's how Simple.OData.Client can execute unbound function GetNearestAirport that takes geolocation as an input and returns the closest airport to the specified coordinates:
var airport = await client
.Unbound<Airport>()
.Function("GetNearestAirport")
.Set(new { lat = 100d, lon = 100d })
.ExecuteAsSingleAsync();
And this is an example for unbound action:
await client
.Unbound()
.Action("ResetDataSource")
.ExecuteAsync();
To execute a bound function or action, we need to specify the collection. In the following example we execute an action ShareTrip that shares the person's trip information with another person.
await client
.For<Person>()
.Key("russellwhyte")
.Action("ShareTrip")
.Set(new { userName = "scottketchum", tripId = 1003 })
.ExecuteAsSingleAsync();
Bounded function and actions don't need to be applied to top-level collection. The OData service metadata specifies only the type the function or action is bound to, not an entity set.
Last but not least we will show how to use Simple.OData.Client with batch operations. Developers sometimes think of OData batches as transactions, but this is not correct. Batches in OData are used to optimize HTTP traffic and reduce the number of roundtrips. You can always cancel the batch while it is being built (and it will cancel all its operations) but once the batch is sent to the server, you can not ensure all-or-nothing behavior. The OData service executes batch operations one by one without ability to rollback earlier executed operations in case of a failure. But that's not all: there is no guarantee that the outcome of the current operation will be available for the next one, neither there is a guarantee that it will not. It all depends on how the service works, so when executing a series of OData operations as a batch, don't put any transactional semantics in it without checking the service implementation.
The following example executes a batch that consists of three operations: retrieving all airlines, creating a new airline and retrieving all airlines again. If you run this batch on the TripPin service example, you will find that although a new airline is created successfully, it will not be included in the list of airlines returned by the next batch operation call. This is not an error, just a service behavior, as mentioned above.
IEnumerable<Airline> airlines1 = null;
IEnumerable<Airline> airlines2 = null;
var batch = new ODataBatch(client);
batch += async c => airlines1 = await c
.For<Airline>()
.FindEntriesAsync();
batch += c => c
.For<Airline>()
.Set(new Airline() { AirlineCode = "TT", Name = "Test Airline"})
.InsertEntryAsync(false);
batch += async c => airlines2 = await c
.For<Airline>()
.FindEntriesAsync();
await batch.ExecuteAsync();
Also note that you only need to await batch operations in case you need to get hold of its result. The operation in the middle (insertion) doesn't need to store its result, so it doesn't need to be awaited.