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.
In this post I will cover what is new since the Alpha, if you need a more complete view of what we is currently supported I recommend you read about the August Release and the Alpha release as well.
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.
Query validation
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:
[Queryable(AllowedLogicalOperators=ODataLogicalOperators.Equal)]
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’
will fail.
Checkout the new [Queryable] attribute to find out more.
Delta<T> improvements
This now supports patching classes that derive from the T too. This is useful if you want both these requests:
PATCH ~/Customers(1)
PATCH ~/Customers(1)/Namespace.VIP
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.
$filter improvements
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.
Read What is JSON Light and Enabling JSON Light from WCF DS to learn more about the new OData JSON format.
Summary
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.
-Alex
Comments
- Anonymous
December 07, 2012
Wow... looks like I jumped on this train right in time. This is really great news, Alex. Is there an official forum for this project? You answer so many questions on your blog here that I'm tempted to continue the tradition. :)Is there a known way to support multiple Gets on a particular controller? I've looked around and can't find any example/guidance. That is, I'd like to have a Get() method and GetByAnother() method that both return IQueryable's.Thanks for any assistance you can provide,Michael - Anonymous
December 07, 2012
@MichaelDThis is possible. See AttributeRouting on Nuget. - Anonymous
December 08, 2012
Thanks, Christopher. I am familiar with AttributeRouting, but from what I understand it doesn't play well with the routes generated by WebAPI OData (and more specifically the data client generated by Add Service Reference). Perhaps I'm missing something... would love a reference/example! - Anonymous
December 10, 2012
Hi,not sure that the samples are fully up to date; in ProductFamiliesController.cs for example there's this:public override HttpResponseMessage CreateLink(int id, string navigationProperty, [FromBody] Uri link)but currently, EntitySetController.cs now returns void:public virtual void CreateLink([FromODataUri] TKey key, string navigationProperty, [FromBody] Uri link) - Anonymous
December 13, 2012
Is it possible to use the $filter query to generate parameterized sql queries when using EF?Currently I have a proof of concept where filtering is working great but the sql queries are not parameterized, which the db gurus are not keen on!Cheers - Anonymous
December 17, 2012
Does this release support $inlinecount? - Anonymous
December 17, 2012
MichaelD. I have a similar need, to support multiple Gets. But in my case, it's OK for me to limit the difference between the method signatures to custom query params (as opposed to explicitly calling a different action name). In one case, I'm returning an IQueryable based on direct repository access (no args) and the other based on a "google like" search (with a arg called "criteria"). Based on whether I provide the query param, the correct controller action is automatically being used. - Anonymous
December 18, 2012
I understand the WebApi OData support is not fully complete and it's very much work in progress at present; however I have a few questions with regards to the OData implementation and ApiController as they are at preset; is there any plans for the routing for apicontroller/odata to areas? and/or will odata support multiple EntityDataModels which map to different uri?Configuration.EnableOData(BusinessModel,"api/odata/B2B");Configuration.EnableOData(CustomerModel,"api/odata/B2C");ThanksGrahame Horner - Anonymous
December 19, 2012
I have an entity with 20 fields, in ODataModelBuilder just 3 of them are defined/mapped.However, when I use JSON format, all 20 fields are serialized.What am I doing wrong?Also, if I do projection ($select), WebApi says it's not supported... In RC no full-spec OData? - Anonymous
December 23, 2012
@Lex, have you tried [JsonIgnore] ? - Anonymous
January 01, 2013
Does EntitySetController<TEntity, TKey> eliminate the api help page details? It seems to for me. - Anonymous
January 02, 2013
@Nigel,I asked around about this and got this back from HongMei ( a dev lead on the Web API effort):Yes, this is a known issue. The help page will not work well with EntitySetController since all the controllers will have the same api description returned by our EntitySetController.For odata, we use the $metadata to tell how the server look like. We have fixed this issue by excluding the OData controllers from help page. I saw the fix has been checked in.[ApiExplorerSettings(IgnoreApi = true)]public abstract class ODataController : ApiControllerSo it sounds like the nightly builds have a fix already.-Alex - Anonymous
January 02, 2013
@Grahame,At present the current API implies that you can do what you want, i.e.Configuration.EnableOData(BusinessModel,"api/odata/B2B");Configuration.EnableOData(CustomerModel,"api/odata/B2C");Unfortunately that is not the case today. That said we are working on a hook to allow you to support something like this, and will modify the API before RTM to make sure that the API doesn't lead you to draw the wrong conclusions. I'd keep an eye on the nightlies if I was you, something should make it into the code base pretty soon.-Alex - Anonymous
January 06, 2013
Unfortunately comments are closed on the Action blog, so I'll post here in the hope it helps someone else.Although it's not clear from the OData spec Actions don't have to run against an individual entity, they can also run against a set.All the examples take the form:/api/Product(1)/ActionHowever, the following is valid (and very useful!):/api/Product/SetActionThere aren't a lot of good examples, and it would be nice if the samples were updated to include Actions for a set.For anyone else looking, the trick is to call Action not on EntityType, but on EntityType.Collection. Another gotcha is to specify the return type correctly otherwise the formatters will fail rather obscurely.In my case, I wanted to return a collection of entities (which would be quite common from a set-based action), the ActionConfiguration class has a ReturnsCollectionFromEntitySet method that makes that possible.Hope that helps someone else! - Anonymous
January 18, 2013
Where can I find information about the anticipated RTM date for this project? - Anonymous
January 22, 2013
The comment has been removed - Anonymous
January 23, 2013
I figured out the Sample changes (had to update NuGet to the nightly builds)Do you expect $expand support to be in this release?If not, is there any way to force the output to expand a navigation property? I understand supporting the generic case is hard, but we have a few specific scenarios in our API specification where $expand is really important... - Anonymous
February 05, 2013
Error : Method not found: 'Int32 Microsoft.Data.OData.Query.SemanticAst.QueryNode.get_Kind()'.orderby/top are working but I get always the error when i use http://localhost:3517/api/Product?$filter=Id eq 1Is ther a simple easy step by step tutorial to get odata support with simple poco classes. On this page there are to much information for beginners? - Anonymous
February 17, 2013
when using (HttpClient) client.PostAsJsonAsync to endpoint, there is null object recieved to protected override CreateEntity on service side. - Anonymous
February 18, 2013
Hi Alex,I like the way OData handles the keys. However, I may have to implement keys in separate segments. Can you point me to the best way to do this?"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"