Thursday 5 September 2013

Exploring Web API OData Query Options

As I mentioned in the post on CRUD operations using Web API, OData has a lot of query options. The full listing of standard query options can be found on the official OData site. In this post, we will explore the query options supported by Web API and we will also see how to use these query options from .NET clients through LINQ queries.

Query support on Web API Data can be enabled in following two ways:

  • At the method level, by setting Queryable attribute on the Get method

    [Queryable]
    public IQueryable<Customer> Get()
    {
        .....
    }
  • When application starts, by calling EnableQuerySupport method on HttpConfiguration object

    GlobalConfiguration.Configuration.EnableQuerySupport();

To be able to explore more options, we will be using the Northwind database. Create an ADO.NET Entity Data model with following tables:

  • Customers
  • Orders
  • Employee

Let’s make these entities available to the Web API OData endpoint by adding them to an EDM model. Following statements accomplish this:


    var modelBuilder = new ODataConventionModelBuilder();
    modelBuilder.EntitySet<Customer>("Customers");
    modelBuilder.EntitySet<Order>("Orders");
    modelBuilder.EntitySet<Employees>("Employees");
    Microsoft.Data.Edm.IEdmModel model = modelBuilder.GetEdmModel();
    GlobalConfiguration.Configuration.Routes.MapODataRoute("ODataRoute", "odata", model);

Add a Web API controller named CustomersController and modify the code as:


public class CustomersController : ODataController
{
    NorthwindEntities context = new NorthwindEntities();

    public IQueryable<Customer> Get()
    {
        return context.Customers;
    }
}

Now we are all set to test the service. Run the project and change the URL on the browser to:


 localhost:<port-no>/odata

You should be able to see entries for each entity collection we added.



Now that the service is up and running, create a client project and add the service reference of the service just created (You may refer to my post on consuming OData from .NET clients post for setting up the client). Also, create a container object by passing URL of the Web API OData service.

Now that we have service and the client ready, let’s start exploring the different to query the service.

$filter:

All OData query options are query string based. $filter is the most basic and the most powerful of them. As the name suggests, $filter is used to select data out of a collection based on some conditions. The type conditions vary from simple equal to operator to string and date filtering operations.

As we already know, the following URL fetches details of all customers in the Northwind database:

    http://localhost:<port-no>/odata/Customers

Let’s add a condition to fetch details of customers with title Owner. Following URL gets the values for us:

    http://localhost:<port-no>/odata/Customers?$filter=ContactTitle eq 'Owner'

This filter can be applied from .NET client using the LINQ operator Where. Following is the query:


 var filteredCustomers = container.Customers.Where(c => c.ContactTitle == "Owner");

The above query is not fired in-memory. Instead, it generates the above URL by parsing the expressions passed into the LINQ operators. The generated URL can be viewed during debugging. Following screen-shot shows the URL generated by the above query:



As we see, it is same as the URL that we typed manually.

To select all customers with contact Ana at the beginning of their ContactName can be fetched using following URL:

 http://localhost:<port-no>/odata/Customers?$filter=startswith(ContactName,'Ana') eq true
The above query uses the OData function startswith() to compare initial letters of the field value. The corresponding LINQ query is:
    container.Customers.Where(c => c.ContactName.StartsWith("Ana"))

Similarly, endswith() can be used to compare suffix of a string. We can also check if a string is contained in a field using substringof() function.

 http://localhost:<port-no>/odata/Customers?$filter=substringof('ill',CompanyName)%20eq%20true

The corresponding LINQ query is:

    container.Customers.Where(c => c.CompanyName.Contains("ill"))

To get list of all customers with length of their names greater than 10 and less than 20, the URL is:
 http://localhost:<port-no>/odata/Customers?$filter=length(ContactName) gt 10 and length(ContactName) lt 20


You must have already guessed the LINQ query to achieve the same.

    container.Customers.Where(c => c.ContactName.Length > 10 && c.ContactName.Length < 20)

$filter supports querying based on numbers, dates and even checking types. You can get the full listing on the official site for OData.

$orderby:

Result can be ordered based on a field using $orderby operator. Syntax of specifying the operator is same as that of $filter operator. Following URL and the LINQ query fetch details of customers with names of the countries ordered in ascending order:

    http://localhost:<port-no>/odata/Customers?$orderby=Country 
    container.Customers.OrderBy(c => c.Country)

To make the order descending, we just need to specify the keyword desc at the end of the above URL.

$top and $skip

The server collections can be divided into pages from client using the $top and $skip operators. These operators are assigned with numbers that indicate the number of entries to be selected from the top or the number of entries to be skipped from the beginning of the collection.

Following URL and LINQ query select first 10 customers in the Customers table.


    http://localhost:<port-no>/odata/Customers?$top=10
    container.Customer.Take(10)

Syntax of using $skip is similar to that of $top, as $skip also expects a number to be passed in. Both $skip and $top can be used together to fetch data in several chunks. Following URL fetches 10 customers starting with index 40.

    http://localhost:<port-no>/odata/Customers?$skip=40&$top=10
    container.Customers.Skip(40).Take(10)

Paging can be forced from server side as well. If an entity set contains very huge amount of data, the requested client will have to wait for a long time to get the entire response from the server. This can be prevented by sending data in chunks from the server using server side paging. To get next set of values, the client has to send a new request to the server.

Server side paging:

Paging can be enabled on the server using PageSize property of the Queryable attribute. Following snippet applies a page size of 20 while returning a collection of customers:

    [Queryable(PageSize=20)]
    public IQueryable<Customer> Get()
    {
        return context.Customers;
    }
It can also be applied globally across all entity sets by setting the property to configuration object when the application starts, as follows:
    IActionFilter filter = new QueryableAttribute() { PageSize=20 };
    config.EnableQuerySupport(filter);
If you hit the URL to get all customers on your favorite browser after applying above changes, you will be able to see only 20 entries with a link to next set at the end of the result set, as shown in the following screen-shot:



As the URL is passed as a property of the JSON object, it can be easily extracted in any JavaScript client to send request for next set of values. But it is a bit tricky from .NET client. The collection LINQ query written on the client has to be converted to a DataServiceCollection to get the URL of the next request. It is shown below:


    var firstSetOfCustomers = new DataServiceCollection<Customer>(container.Customers);
    var nextSetOfCustomers = new DataServiceCollection<Customer>(container.Execute<Customer>(firstSetOfCustomers.Continuation.NextLinkUri));

Count of records on all pages can be obtained using the query option $inlinecount operator in the above query.
    http://localhost:<port-no>/odata/Customers?$inlinecount=allpages

    var firstSetOfCustomers = new DataServiceCollection<Customer>(container.Customers.IncludeTotalCount());

$expand:

ASP.NET Web API team added support of $expand and $select options in its latest release, which is version 5.0.0-rc1. It is built into Visual Studio 2013 Preview. In Visual Studio 2012, the NuGet package can be upgraded to the pre-release version.

$expand is used to include values of navigation properties of the queried entity set. The interesting part is the results of the navigation properties of the inner entity set can also be included in the result set. Following URL and LINQ query fetch details of Customers, their Orders and the details of the Employee who placed the Order.

    http://localhost:<port-no>/odata/Customers?$expand=Orders/Employee
    container.Customers.Expand("Orders/Employee");

The above query navigates till two levels down the entity set. Depth of the navigation can be controlled using the MaxExpansionDepth property on the Queryable attribute.
    [Queryable(MaxExpansionDepth=1)]
    public IQueryable<Customer> Get()
    {
        return context.Customers;
    }

This configuration can be applied on HttpConfiguration object when the application starts.

    IActionFilter filter = new QueryableAttribute() { MaxExpansionDepth=1 };
    config.EnableQuerySupport(filter);
After applying the above changes, the client cannot navigate to more than one level down the entity set.

$select:

A client can request for projected results based on its need. A list of properties to be fetched can be specified while querying the service to get only those properties. Following are a sample URL and the LINQ query:


    http://localhost:<port-no>/odata/Customers?$select=CustomerID,ContactName
    container.Customers.Select(c => new { ID = c.CustomerID, Name = c.ContactName });

Happy coding!

7 comments:

  1. Hi Ravi, great article and I'm sure it would really be of great help once I get to try it. However, I'm currently stuck on the same situation as this guy http://stackoverflow.com/questions/18347290/webapi-odata-5-0-beta-accessing-globalconfiguration-throws-security-error.

    Long story short, my application crashes once I update the nuget package into any of the pre-release version (I tried both beta2 and rc1, still nothing). Would you have any thoughts on this?

    ReplyDelete
    Replies
    1. Hi Rigel,

      I too faced the same issue. I answered the question on Stackoverflow. Check it and let me know if it resolves your issue.

      Delete
    2. Hi Ravi!

      Found your answer very helpful. I actually just updated the WebApi package followed by the WebApi.OData package since I'm trying to recreate a service and since my project has no views, pages etc. I'm guessing the rc1 release of WebApi.OData has some dependencies with WebApi package. Weird that Nuget doesn't check it during installation.

      Anyway, thanks a lot! :)

      Delete
    3. Hi Rigel,

      You are welcome :).

      I agree, the NuGet package isn't built properly to automatically update the dependencies. I had a tough time in resolving and making stuff work when I updated the package for the first time.

      Delete
  2. Hello Ravi, Good blog thanks for this. I just wondered, as you are generating Service references whether you have used DataServiceContext class directly in the past? If so do you see any advantage to generating service references as opposed to DataServiceContext + DataServiceQuery created in code?

    ReplyDelete
    Replies
    1. Are comparing the service references generated by WCF Data Service and Web API?

      Delete
    2. No, in the past I used WCF Data Services. Rather than generate service references I used the DataServiceContext class directly. Now I use WebApi and you seem to generate service references for WebApi via the IDE not just connect to the service in code using a client such as DataServiceContext or HttpConnection

      Delete

Note: only a member of this blog may post a comment.