OData in WebAPI – Microsoft ASP.NET Web API OData 0.2.0-alpha release
Since my last set of blog posts on OData support in WebAPI (see parts 1 & 2) we’ve been busy adding support for Server Driven Paging, Inheritance and OData Actions. Our latest alpha release on Nuget has preview level support for these features. Lets explore the new features and a series of extensions you can use to get them working…
Server Driven Paging:
Our code has supported client initiated paging using $skip and $top for some time now. However there are situations where the server wants to initiate paging. For example if a naïve (or potentially malicious) client makes a request like this:
and you have a lot of products, you are supposed to return all the products (potentially thousands, millions, billions) to the client. This uses a lot of computing resources, and this single request ties up all those resources. This is unfortunate because your client:
- Might simply be malicious
- Might be naïve, perhaps it only needed 20 results?
- Might lockup waiting for all the products to come over the wire.
Thankfully OData has a way to initiate what we call server driven paging, this allows the server to return just a ‘page’ of results + a next link, which tells the client how to retrieve the next page of data. This means naïve clients only get the first page of data and servers have the opportunity to throttle requests from potentially malicious clients because to get all the data multiple requests are required.
This is now really easy to turn on in WebAPI using the Queryable attribute, like this:
public IQueryable<Product> Get()
This code, tells WebAPI to return the first 100 matching results, and then add an OData next-link to the results that when followed will re-enter the same method and continue retrieving the next 100 matching results. This process continues until either the client stops following next-links or there is no more data.
If the strategy [Queryable] uses for Server Driven Paging is not appropriate for your data source, you can also drop down and use ODataQueryOptions and ODataResult<T> directly.
The OData protocol supports entity type inheritance, so one entity type can derive from another, and often you’ll want to setup inheritance in your service model. To support OData inheritance we have:
- Improved the ModelBuilder – you can explicitly define inheritance relationships or you can let the ODataConventionModelBuilder infer them for you automatically.
- Improved our formatters so we can serialize and deserialize derived types.
- Improved our link generation to include needed casts.
- and we need to improve our controller action selection so needed casts are routed correctly.
Model Builder API
You can explicitly define inheritance relationships, with either the ODataModelBuilder or the ODataConventionModelBuilder, like this:
// define the Car type that inherits from Vehicle
.DerivesFrom<Vehicle>() .Property(c => c.SeatingCapacity);
// define the Motorcycle type `` modelBuilder
.Entity<Motorcycle>() .DerivesFrom<Vehicle>() .Property(m => m.CanDoAWheelie);
With inheritance it occasionally makes sense to mark entity types as abstract, which you can do like this:
.HasKey(v => v.ID) .Property(v => v.WheelCount);``
Here we are telling the model builder that Vehicle is an abstract entity type.
When working with derived types you can explicitly define properties and relationship on derived types just as before using EntityTypeConfiguration<TDerivedType>.Property(..), EntityTypeConfiguration<TDerivedType>.HasRequired(…) etc.
Note: In OData every entity type must have a key, either declared or inherited, whether it is abstract or not.
ODataConventionModelBuilder and inheritance
The ODataConventionModelBuilder, which is generally recommended over the ODataModelBuilder, will automatically infer inheritance hierarchies in the absence of explicit configuration. Then once the hierarchy is inferred, it will also infer properties and navigation properties too. This allows you to write less code, focusing on where you deviate from our conventions.
For example this code:
ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
Will look for classes derived from Vehicle and go ahead and create corresponding entity types.
Sometimes you don’t want to have entity types for every .NET type, this is easy to achieve you instruct the model builder to ignore types like this:
With this code in place the implicit model discovery will not add an entity type for Sportbike, even though it derives from Vehicle (in this case indirectly i.e. Sportbike –> Motorcycle –> Vehicle).
Known inheritance issues
In this alpha our support for Inheritance is not quite complete. You can create a service with inheritance in it but there are a number of issues we plan to resolve by RTM:
- Delta<T>doesn’t currently support derived types. This means issuing PATCH requests against instances of a derived type is not currently working.
- Type filtering in the path is not currently supported. i.e. ~/Vehicles/NS.Motorcycles?$filter=…
- Type casts in $filter is not currently supported. i.e. ~/Vehicles?$filter=NS.Motorcyles/Manufacturer/Name eq ‘Ducati’
- Type casts in $orderby is not currently supported. i .e. ~/Vehicles?$filter=Name, NS.Motorcycle/Manufacturer/Name
The other major addition since the august preview is support for OData Actions. Quoting the OData blog:
“Actions … provide a way to inject behaviors into an otherwise data centric model without confusing the data aspects of the model, while still staying true to the resource oriented underpinnings of OData."
Adding OData actions support to the WebAPI involves 4 things:
- Defining OData Actions in the model builder.
- Advertising bindable and available actions in representations of the entity sent to clients.
- Deserializing parameters values when people attempt to invoke an Action.
- Routing requests to invoke OData Actions to an appropriate controller action.
Model Builder API
Firstly we added a new class called ActionConfiguration. You can construct this directly if necessary, but generally you use factory methods that simplify configuring the most common kinds of OData Actions, namely those that bind to an Entity or a collection of Entities. For example:
ActionConfiguration pullWheelie = builder.Entity<Motorcycle>().Action(“PullWheelie”);
defines an Action called ‘PullWheelie’ that binds to a Motorcycle, and that takes an integer parameter “ForSeconds” indicating how long to hold the wheelie, and returns true/false indicating whether the wheelie was successful.
You can also define an action that binds to a Collection of entities like this:
ActionConfiguration pullWheelie = builder.Entity<Motorcycle>().Collection.Action(“PullWheelie”);
Calling this action would result in a collection of Motorcycles all attempting to ‘Pull a Wheelie’ at the same time :)
There is currently no code in the ODataConventionModelBuilder to infer OData Actions, so actions have to be explicitly added for now. That might change as we formalize our conventions more, but if that happens it won’t be until after the first RTM release.
Controlling Action links and availability
When serializing an entity with Actions the OData serializer calls the delegate you pass to ActionConfiguration.HasActionLink(…) for each action. This delegate is responsible for returning a Uri to be embedded in the response sent to the client. The Uri when present tells clients how to invoke the OData Action bound to the current entity. Basically this is hypermedia.
If you are using the ODataConventionModelBuilder, by default the HasActionLink is automatically configured to generate links in the form: ~/entityset(key)[/cast]/action, or for example:
or to access an action bound to a derived type like Motorcyles:
OData also allows you define actions that are only occasionally bindable. For example you might not be able to ‘Stop’ a Virtual Machine if it has already been stopped. This makes ‘Stop’ a transient action. Use the TransientAction() method, which like Action hangs off EntityTypeConfiguration<T>, to define your transient actions.
Finally to make your action truly transient you need to pass a delegate to HasActionLink that returns null when the action is in fact not available.
Handing requests to invoke ODataActions
The OData specification says that OData Actions must be invoked using a POST request, and that parameters to the action (excluding the binding parameter) are passed in the body of post in JSON format. This example shows an implementation of the PullWheelie action that binds to a Motorcycle:
public bool PullWheelieOnMotorcycle(int boundId, ODataActionParameters parameters)
// retrieve the binding parameter, in this case a motorcycle, using the boundId.
// i.e. POST ~/Vehicles(boundId)/Namespace.Motorcycle/PullWheelie
Motorcycle motorcycle = _dbContext.Vehicles.OfType<Motorcycle>().SingleOrDefault(m => m.Id == boundId);
// extract the ForSeconds parameter
int numberOfSeconds = (int) parameters[“ForSeconds”];
// left as an exercise to the reader.
As you can see there is a special class here called ODataActionParameters, this is configured to tell the ODataMediaTypeFormatter to read the POST body as an OData Action invocation payload. The ODataActionParameters class is essentially a dictionary from which you can retrieve the parameters used to invoke the action. In this case you can see we are extracting the ‘ForSeconds’ parameter. Finally because the PullWheelie action was configured to return Bool when we defined it, we simply return Bool and the ODataMediaTypeFormatter takes care of the rest.
The only remaining change is setting up routing to handle Actions, Inheritance, NavigationProperties and all the other OData conventions. Unfortunately this problem is a little too involved for standard WebAPI routing. Which means integrating all these new features is pretty hard.
We realized this and started fleshing something called System.Web.Http.OData.Futures to help out…
Integrating everything using System.Web.Http.OData.Futures
The sample service itself is starting to look a lot simpler: you don’t need to worry about setting up complicated routes, registering OData formatters etc. In fact all you need to do is call EnableOData(…) on your configuration, passing in your model:
// Create your configuration (in this case a selfhost one).
HttpSelfHostConfiguration configuration = new HttpSelfHostConfiguration(_baseAddress);
// Enable OData
// Create server
server = new HttpSelfHostServer(configuration);
// Start listening
Console.WriteLine("Listening on " + _baseAddress);
As you can see this is pretty simple.
For a description of how GetEdmModel() works checkout my earlier post.
As you can imagine EnableOData(…) is doing quite a bit of magic, it:
- Registers the ODataMediaTypeFormatter
- Registers a wild card route, for matching all incoming OData requests
- Registers OData routes for generating links in responses (these will probably disappear by RTM).
- Registers custom Controller and Actions selectors that parse the incoming request path and dispatch all well understood OData requests by convention. The Action selector dispatches deeper OData requests (i.e. ~/People(1)/BestFriend/BestFriend) to a ‘catch all method’ called HandleUnmappedRequest(…) which you can override if you want.
All of this is implemented in System.Web.Http.OData.Futures, which includes:
- OData Route information.
- OData specific Controller and Actions selectors – These classes help avoid routing conflicts. They are necessary because WebAPI’s built-in routing is not sophisticated enough to handle OData’s context sensitive routing needs.
- ODataPathParser and ODataPathSegment – These classes help our custom selectors establish context and route to Controller actions based on conventions.
- EntitySetController<T> – This class implements the conventions used by our custom selectors, and provides a convenient base class for your controllers when supporting OData.
System.Web.Http.OData.Futures is currently only at sample quality, but it is basically required to creating OData services today, so we plan on merging it into System.Web.Http.OData for the RTM release.
OData WebAPI Action Routing Conventions:
The OData ActionSelector is designed to work best with the EntitySetController<TEntity,TKey> and it relies on a series of routing conventions to dispatch OData requests to Controller Actions. You don’t actually need to use the EntitySetController class, so long as you follow the conventions that the OData ActionSelector uses.
The conventions currently defined in Futures and used by the sample are:
|Request||Routed to Controller.|
|GET ~/EntitySet||[Queryable] Get() or Get(ODataQueryOptions)|
|PUT ~/EntitySet(id)||Put(id, Entity)|
|PATCH ~/EntitySet(id)||Patch(id, Delta<Entity>)|
|GET ~/EntitySet(id)/NavigationSet||[Queryable] GetNavigationProperty() or GetNavigationProperty(ODataQueryOptions)|
|GET ~/EntitySet(id)/cast/NavigationSet||[Queryable] GetNavigationPropertyFromCast() or GetNavigationPropertyFromCast(ODataQueryOptions)|
|POST ~/EntitySet(id)/$links/NavigationSet||CreateLink(id, navigationProperty, [FromBody] Uri)|
|PUT ~/EntitySet(id)/$links/NavigationSingle||CreateLink(id, navigationProperty, [FromBody] Uri)|
|DELETE ~/EntitySet(id)/$links/NavigationSingle||DeleteLink(id, navigationProperty, [FromBody] Uri)|
|DELETE ~/EntitySet(id)/$links/NavigationSet(relatedId)||DeleteLink(id, relatedId, navigationProperty)|
|POST ~/EntitySet(boundId)/Action||Action(boundId, ODataActionParameters)|
|POST ~/EntitySet(boundId)/cast/Action||ActionOnCast(boundId, ODataActionParameters)|
These conventions are not complete, in fact by RTM we expect to add a few more, in particular to handle:
We are also experimenting with the idea that anything that doesn’t match one of these conventions will get routed to:
|POST, PATCH, PUT, DELETE, GET *||HandleUnmappedRequest(ODataPathSegment)|
If we did this you would be able to override the default implementation of this and potentially handle OData requests at arbitrary depths :)
As you can see we’ve made a lot of progress, and our OData support is getting more complete and compelling all the time. We still have a number of things to do before RTM, including bringing the ideas from System.Web.Http.OData.Futures into System.Web.Http.OData, finalizing conventions, enabling JSON light, working on performance and fixing bugs etc. That said I hope you’ll agree that things are taking shape nicely?
As always we are keen to hear what you think, even more so if you've kicked the tires a little!