OData in WebAPI – RC release
Next week we will release an official RC of the Microsoft ASP.NET WebAPI OData assembly. This marks the third release on our way to RTM.
Although this post talks about code that hasn’t been officially release yet, since all development is happening in public, if you can’t wait till next week, you can always go and get one of the nightly builds or for a more bare metal experience build it yourself from our code repository.
Simplified OData support
To make it easy to support full OData we’ve added a simple extension method HttpConfiguration.EnableOData(IEdmModel) that allows you to easily configure your service to support OData, simply by providing an IEdmModel and optionally supplying an OData route prefix (i.e. ~/api or similar).
The method does a number of key tasks for you, each of which you can do manually if necessary:
- Registers an OData wild card route. Which will have a prefix if you specified a prefix when you called EnableOData(..). For example this:
- HttpConfiguration.EnableOData(model, “api”) will position your OData service under the ~/api url prefix.
- Registers the various ODataMediaTypeFormatters.
- Note today this will stomp on application/json for your whole service. By RTM we aim to make this much more selective, so you get the OData version of application/json only if your request addresses an OData resource.
- Stashes the IEdmModel on the configuration
- Stashes the DefaultODataPathHandler on the configuration.
- Registers the ODataControllerSelector and ODataActionSelectors and configures them to use default OData routing conventions. These selectors only do OData routing when the request is recognized as an OData request, otherwise they delegate to the previously registered selectors.
First class OData routing
In the alpha release to get everything (OData Actions, OData casts, OData navigations etc) working together I updated the sample to do custom routing. This routing was based on a component that understood OData Paths and dispatched to actions based on custom routing conventions. At the time we knew this was a merely a stop gap.
That code has now been refactored, improved and moved into the core code base.
The code consists of:
- A class called ODataPath that represents the path part of an OData Url. An ODataPath is made up of a list of ODataPathSegments, these semantically represent the OData operations you compose to address a resource. For example something like this: ~/Customers(10)/Orders, semantically has 3 instructions that are encoded as ODataPathSegments in an ODataPath:
- Start in the Customers EntitySet
- Then find a single customer with the key 10
- Then Navigate to the related Orders.
- An interface called IODataPathHandler that has two methods:
- ODataPath Parse(string odataPath) –> to take a Url and parse it into an ODataPath
- string Link(ODataPath) –> to take an ODataPath and generate a Url.
- A default implementation of IODataPathHandler called DefaultODataPathHandler, that implements these methods using the OData V3’s built-in conventions.
- A way to register and retrieve a IODataPathHandler on your HttpConfiguration. Two things worth calling out here:
- Normally you don’t have to worry about this, because calling HttpConfiguration.EnableOData(…) registers the DefaultODataPathHandler for you.
- This design means that you don’t like the way OData puts keys in parenthesis (i.e.~/Customers(1)) and would prefer to put keys in their own segments (i.e. ~/Customer/1) then all you need to do is override the DefaultODataPathHandler so that it recognizes this syntax. I’ve tried this personally and doing targeted overloads like this is pretty easy.
- All OData link generation conventions (for example for EditLinks or links to invoke actions) now build an ODataPath to represent the link and then ask the registered IODataPathHandler to convert that to a url using the IODataPathHandler.Link(..) method.
- An ODataControllerSelector and an ODataActionSelector that route OData requests based on a configurable set of IODataRoutingConventions. Out the box EnableOData(..) will register these routing conventions:
- EntitySetRoutingConvention –> for routing request to manipulate and query an EntitySet, for example:
- GET ~/Customers
- GET ~/Customers/Namespace.VIP
- POST ~/Customers
- EntityRoutingConvention –> for routing request to retrieve or manipulate an Entity, for example:
- GET ~/Customer(1)
- GET ~/Customers(1)/Namespace.VIP
- PUT ~/Customer(1)
- PATCH ~/Customers(1)
- DELETE ~/Customers(1)
- NavigationRoutingConvention –> for routing requests to retrieve related items, for example:
- GET ~/Customers(1)/Orders
- GET ~/Customers(1)/Namespace.VIP/Benefits
- MetadataRoutingConvention –> for routing request to retrieve $metadata or the service document
- GET ~/$metadata
- GET ~
- LinksRoutingConvention –> for routing requests to manipulate relationship, for example:
- DELETE ~/Customers(1)/$links/Orders(1)
- POST ~/Customers(1)/$links/Orders
- PUT ~/Customers(1)/$links/RelationshipManager
- ActionRoutingConvention –> for routing requests to invoke an OData Action
- POST ~/Customers(1)/ConvertToVIP
- UnmappedRequestRoutingConvention –> for routing requests that match no other convention to a fall back method, for example:
- GET ~/Customers(1)/RelationshipManager/Customers
- POST ~/Customers(1)/Orders
- EntitySetRoutingConvention –> for routing request to manipulate and query an EntitySet, for example:
- A new ODataPathRouteConstraint class that implements IHttpRouteConstraint an makes sure the ‘wildcard route’ that captures every request against your OData service only matches if the Url is in fact an OData url.
- An EntitySetController<TEntity,TKey> class that provides a convenient starting point for creating controllers for your OData EntitySets. This class provides stub methods that follow the default IODataRoutingConventions will route requests too. Your job is simply to handle the request.
For a complete example of how to use all this new goodness check out the sample OData service which you can find at https://aspnet.codeplex.com in the source tree under: /Samples/Net4/CS/WebApi/ODataService.
One of the most exciting new features is the ability to specify validation rules to be applied to a query. Essentially this allows you to constrain what query options you allow against your [Queryable]. Under the hood this is all implemented via the virtual ODataQueryOptions.Validate(ODataValidationSettings) method, which you can use if you need to.
That said the 90-99% scenario is simply to specify additional parameters to the [Queryable] attribute that the control what is allowed in a query, and then before the query is processed, Web API converts those settings into an ODataValidationSettings object and calls ODataQueryOptions.Validate(..). If anything not supported is found in the AST a validation error results, and your backend Queryable never gets called.
Here are some examples:
[Queryable(AllowedQueryParameters = ODataQueryParameters.Skip| ODataQueryParameters.Top)]
This means only Skip and Top are allowed, i.e. a $filter or $orderby would result in a validation error. By default every thing is allowed, until you mention a particular setting, then only the things you list are supported.
Another example is:
This says the $filter only supports the Equal operator, so for example this:
~/Customer?$filter=Name eq ‘Bob’
will pass validation but this:
~/Customers?$filter=Name ne ‘Bob’
Checkout the new [Queryable] attribute to find out more.
This now supports patching classes that derive from the T too. This is useful if you want both these requests:
to be dispatched to the same method, for example:
public virtual HttpResponseMessage Patch([FromODataUri] TKey key, Delta<Customer> patch)
Now because Delta<Customer> can also hold changes made to a VIP (i.e. a class the derives from Customer) you can route to the same method, and deal with a whole inheritance hierarchy in one action.
Another thing to notice is the new [FromODataUri] attribute, this tells Web API the key parameter is in the URL encoded as an OData Uri Literal.
We now handle DateTime or DateTimeOffset literals or properties for the first time, and we allow you to specify casts in your filter too, for example:
~/Customers?$filter=Name eq ‘Bob’ or Namespace.VIP/RelationshipManager/Name eq ‘Bob’
At this point the only thing that we don’t support (and won’t support for RTM either) is spatial types and custom OData functions inside the filter. These will come later when support for them is added to ODataLib’s ODataUriParser.
Partial support for OData’s new JSON format
This release also includes a little support for the new OData v3 json format, and of course by RTM time we will have full support. This OData V3 json format is more efficient than our older V1/V2 json format (which incidentally the OData team has started calling ‘JSON verbose’, to make sure you know we don’t like it any more :)
The new JSON format has sub types, which allow clients to specify how much metadata they want, ranging from FullMetadata, through MinimalMetadata to NoMetadata. The RC supports the new OData JSON format only for write scenarios (i.e. responding to requests), it doesn’t currently handle read scenarios at all (i.e. request payloads that are in the new JSON format). It supports the most common payload kinds, feeds, entries, errors and service documents, and by RTM we will flesh this out to include other payloads like properties, links etc.
In the RC we don’t support NoMetadata at all, and we treat MinimalMetadata as if it is FullMetadata. We do this because MinimalMetadata means only send metadata and links that the client can’t deduce them by convention, and in the RC we don’t have anyway to tell the formatter that you are indeed following conventions. This forces us to always emit links. By RTM we will add a way to say you are following convention, and that will allow us to support MinimalMetadata properly.
As you can see we’ve been really busy making Web API a first class stack for creating and exposing OData services, ranging from supporting just the OData query syntax, through supporting the majority of the OData protocol, all the way up to supporting extra things not in OData but enabled by Web API, like for example new formats.
As always please let us know what you think.