Partager via


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:

GET ~/Products

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.
  • etc.

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:

[Queryable(ResultLimit=100)]
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.

Inheritance:

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
modelBuilder
.Entity<Car>()
.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:

modelBuilder
.Entity<Vehicle>()
.Abstract()
.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();
modelBuilder.EntitySet<Vehicle>(“Vehicle”);

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:

builder.IgnoreTypes(``typeof`` (Sportbike));

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

OData Actions:

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:

  1. Defining OData Actions in the model builder.
  2. Advertising bindable and available actions in representations of the entity sent to clients.
  3. Deserializing parameters values when people attempt to invoke an Action.
  4. 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”);
pullWheelie.Parameter<int>(“ForSeconds”);
pullWheelie.Returns<bool>();

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.

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:

~/Vehicles(1)/Drive

or to access an action bound to a derived type like Motorcyles:

~/Vehicles(1)/Namespace.Motorcycle/PullWheelie

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:

[HttpPost]
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.
DoWheelie(motorcycle, numberOfSeconds);

   return true;
}

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

While the core of all these new features are complete, getting them to work together is tricky. That makes checking out the ODataSampleService (part of aspnet.codeplex.com) even more important.

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);
configuration.Formatters.Clear();

// Enable OData
configuration.EnableOData(GetEdmModel());

// Create server
server = new HttpSelfHostServer(configuration);

// Start listening
server.OpenAsync().Wait();
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.
QUERY  
GET ~/EntitySet [Queryable]          Get()    or           Get(ODataQueryOptions)
CRUD  
POST ~/EntitySet Post(Entity)
GET ~/EntitySet(id) GetById(id)
PUT ~/EntitySet(id) Put(id, Entity)
PATCH ~/EntitySet(id) Patch(id, Delta<Entity>)
DELETE ~/EntitySet(id) Delete(id)
NAVIGATION  
GET ~/EntitySet(id)/NavigationSet [Queryable]          GetNavigationProperty()    or           GetNavigationProperty(ODataQueryOptions)
GET ~/EntitySet(id)/NavigationSingle GetNavigationProperty()
GET ~/EntitySet(id)/cast/NavigationSet [Queryable]          GetNavigationPropertyFromCast()    or           GetNavigationPropertyFromCast(ODataQueryOptions)
GET ~/EntitySet(id)/cast/NavigationSingle GetNavigationPropertyFromCast()
MANIPULATING LINKS  
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)
ACTIONS  
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:

GET ~/EntitySet/cast
GET ~/EntitySet(id)/cast
POST ~/Action
POST ~/EntitySet/Action

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 :)

Summary

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!

-Alex

Comments

  • Anonymous
    November 04, 2012
    If it's model have the following implementations :public class BaseEntity{   public  object Id {get; set;}}public class Entity1 : BaseEntity{   public  string Name {get; set;}}so, when one request is generated in the client side application :$.getJSON("api/Entity1?$filter=Id eq '" + id + "'",function (data) {       ...   });my application encounter with the following error :{"Message":"The query specified in the URI is not valid.","ExceptionMessage":"Type 'Entity 1' does not have a property 'Id'."}
  • Anonymous
    November 04, 2012
    Whether configurations are needed in the ODataModelBuilder?
  • Anonymous
    November 05, 2012
    It's because object type is not a supported type in OData. You should make your Id as odata primitive types like string.
  • Anonymous
    November 05, 2012
    If I make my Id as string, Whether configurations are needed in the ODataModelBuilder?
  • Anonymous
    November 05, 2012
    hongyes, Thank for your response.I found it, I make my Id as string and no configurations are needed in the OData ModelBuilder.
  • Anonymous
    November 06, 2012
    When will the Web API OData package support $inlinecount?
  • Anonymous
    November 11, 2012
    I'm trying to implement paging via web.api (using the [Queryable(ResultLimit=x)] attribute) but when i make a http request I cannot find the "next link" in the response. Where should it be?
  • Anonymous
    November 12, 2012
    Patching doesn't work with complex  objects/types either..  This is a major issue: aspnetwebstack.codeplex.com/.../562Also please add the following code to the v3 release: aspnetwebstack.codeplex.com/.../386
  • Anonymous
    November 13, 2012
    I'm hosting OData in ASP.NET WebAPI project. I tried to update to this latest version but I'm getting following errors from NuGet:Updating 'Microsoft.Data.OData.Contrib 5.1.0.51016-rc2' to 'Microsoft.Data.OData.Contrib 5.1.0.51109' failed. Unable to find a version of 'Microsoft.AspNet.WebApi.OData' that is compatible with 'Microsoft.Data.OData.Contrib 5.1.0.51109'.Is there a way to resolve this?
  • Anonymous
    November 13, 2012
    @Andrea,Are you using the ODataMediaTypeFormatter?Currently the nextlink is only written in OData formats.I recommend you raise an issue (here: aspnetwebstack.codeplex.com/.../advanced ) if you want a next link in raw json/xml etc.-Alex
  • Anonymous
    November 13, 2012
    @SzymonI'm asking around at the moment. We are in a weird state at the moment as we migrate the Uri Parser from ODataContrib into ODataLib (I.e. where all the RTM qualify code goes)... so any problems should be short lived. Either way hopefully I'll have a better answer for you soon.-Alex
  • Anonymous
    November 13, 2012
    The comment has been removed
  • Anonymous
    November 13, 2012
    @Andrea Maruccia :Right now only the ODataFormatter supports putting next page links in the response payload. json and xml formatters don't really have a concept of next page link and hence we decided not to put these in the response by default for these formatters.That said, the next page link we generate is available on the HttpRequestMessage.Properties property bag(key is 'MS_NextPageLink'). So, you can always write a message handler or an action filter that looks at this property and puts it in the response the way it suits you and your clients.
  • Anonymous
    November 13, 2012
    @Alex,Thanks for responding to my previous question. I run into another issue defining the navigation properties. I posted it here: odata.codeplex.com/.../4-Szymon
  • Anonymous
    November 13, 2012
    @Alex,One more question regarding routing. I'm using ASP.NET MVC project with WebAPI template. Default OData routes are registered in the root of the website. How I can change them to prefix path with '/api/...'  ?For example by an Images folder was created by default. But I also have Images EntitySet and currently there is a naming conflict. Also I wanted to put some regular website pages in the same project.
  • Anonymous
    November 19, 2012
    @szymon,I'm not sure yet... although by the time this feature RTM's we will allow you to setup route-prefixes. Best bet is to keep an eye on the nightlies :)-Alex
  • Anonymous
    November 19, 2012
    How to do specialized queries, like spatial?[Queryable]IQueryable<BusStop> GetByLocation(double lat1, double lon1, double lat2, double lon2) {var location = DbGeography.FromText(string.Format("POINT({0} {1})", lat1, lon1), 4326); // TODO: Polygon with 2nd pointreturn from s in EntityCtx.BusStops where s.Intersects(location) select s;}This is powerful, as BusStop could have 8 other properties which can be used with OData dynamic filter support.  Can I do this just by adding my own route?
  • Anonymous
    November 19, 2012
    @Alex,Where can I get the nightly builds?
  • Anonymous
    November 26, 2012
    Hi Alex,Which OData protocol version is this framework initially targeting?I am specifically interested in the JSON format which changes significantly from V1 to V2/3.
  • Anonymous
    November 26, 2012
    @Gav,The latest and greatest, i.e. V3... :)-Alex
  • Anonymous
    November 26, 2012
    @SzymonAdd this Nuget source - www.myget.org/.../aspnetwebstacknightly
  • Anonymous
    November 29, 2012
    @Alex - Can you expose a singleton for setting the default options on Queryable?  I saw your last post that includes the options for null propagation handling.  There are many more LINQ providers than just EF, L2S and L2O.  For example, we can use WebApi with OData to query against RavenDB with their LINQ provider.  But for it to work properly, I have to set HandleNullPropogationOption.False on every single controller.  There should be a simpler way to set the default globally.
  • Anonymous
    November 29, 2012
    @mj123451 (great name BTW).I'm not sure on this. Queryable is registered as an ActionFilter so there is a chance you can do this globally. I've sent an email to people who know this code better than me, hopefully they'll weigh in soon!-Alex
  • Anonymous
    November 29, 2012
    @mj123451you could enable it by using the extension method we added EnableQuerySupport.    config.EnableQuerySupport(new QueryableAttribute { HandleNullPropagation = HandleNullPropagationOption.False });The side-effect is that query-composition is enabled for all actions that return IQueryable<T>.An alternative way is to inherit the QueryableAttribute and in the ctor set HandleNullPropagation to False like,   public class NonNullPropagationQueryableAttribute : QueryableAttribute   {       public NonNullPropagationQueryableAttribute()       {           HandleNullPropagation = HandleNullPropagationOption.False;       }   }
  • Anonymous
    November 29, 2012
    @RaghuRam - Thanks, I like the idea of inheriting it into my own attribute.  Thanks.  (BTW - I hadn't updated my MSDN profile yet to match my others.  I still don't like how the "display name" has to be unique.  Prevents me from using my real name)
  • Anonymous
    December 03, 2012
    I've run into an issue getting some specific entities up and running on odata/webapi. For the most part everything runs great, but I have a hierarchy of entities that have composite keys - think something like { county number, town number }  or { ingredient , recipe }Is there a relatively easy way to get all the URL goodies that are baked into the currently expected URLs that take id, to take multi-part keys in the id? stuff like /books(15)  ie: /books(15,1337) ?
  • Anonymous
    December 05, 2012
    Wow... I have spent the past few hours catching up on this project and I have to say: Alex D James, you are my HERO! :)  Thanks for being super responsive, attentive and massively thorough on these posts.  I can only imagine how irritating some of these comments/bug reports can be.This is an amazing project.  I am a fan.  Goodbye RIA Services. :)  Please do keep up the amazing work, Alex!
  • Anonymous
    December 05, 2012
    Thanks so much for the quick updates.  You guys are flyin.  I'm looking for $inlineCount support for use in the Infragistics IgniteUI grid.  Any good resources on putting that in myself with the current webAPI abilities?
  • Anonymous
    January 07, 2013
    Is there any reason that writing the code in VB.NET may not allow correct uri routing?I have two solutions side by side ... identical as far as I can tell, one written in VisualBasic.Net, one in C#.If I write the code in C# it seems to route to the Get() or Get(Guid key) functions in my apicontroller as you describe but the VB.NET version always routes to the Get() function with no parameters.GET ~/Products   -> Routes to Get()GET ~/Products(1)  -> Also Routes to Get() even though I have a function Get(<FromODataUri>id as integer) availableThere may be other issues using VB.NET but this was the simplest failing case I found.
  • Anonymous
    January 23, 2013
    Hi Alex,Is there an opportunity to implement entity set permissions, ie // Give full access to all of the entities.     config.SetEntitySetAccessRule("*", EntitySetRights.All); as in WCF DS? thx.