Supporting OData Query Options in ASP.NET Web API 2
by Mike Wasson
OData defines parameters that can be used to modify an OData query. The client sends these parameters in the query string of the request URI. For example, to sort the results, a client uses the $orderby parameter:
http://localhost/Products?$orderby=Name
The OData specification calls these parameters query options. You can enable OData query options for any Web API controller in your project — the controller does not need to be an OData endpoint. This gives you a convenient way to add features such as filtering and sorting to any Web API application.
Before enabling query options, please read the topic OData Security Guidance.
- Enabling OData Query Options
- Example Queries
- Server-Driven Paging
- Limiting the Query Options
- Invoking Query Options Directly
- Query Validation
## Enabling OData Query Options
Web API supports the following OData query options:
Option | Description |
---|---|
$expand | Expands related entities inline. |
$filter | Filters the results, based on a Boolean condition. |
$inlinecount | Tells the server to include the total count of matching entities in the response. (Useful for server-side paging.) |
$orderby | Sorts the results. |
$select | Selects which properties to include in the response. |
$skip | Skips the first n results. |
$top | Returns only the first n the results. |
To use OData query options, you must enable them explicitly. You can enable them globally for the entire application, or enable them for specific controllers or specific actions.
To enable OData query options globally, call EnableQuerySupport on the HttpConfiguration class at startup:
[!code-csharpMain]
1: public static void Register(HttpConfiguration config)
2: {
3: // ...
4:
5: config.EnableQuerySupport();
6:
7: // ...
8: }
The EnableQuerySupport method enables query options globally for any controller action that returns an IQueryable type. If you don’t want query options enabled for the entire application, you can enable them for specific controller actions by adding the [Queryable] attribute to the action method.
[!code-csharpMain]
1: public class ProductsController : ApiController
2: {
3: [Queryable]
4: IQueryable<Product> Get() {}
5: }
This section shows the types of queries that are possible using the OData query options. For specific details about the query options, refer to the OData documentation at www.odata.org.
For information about $expand and $select, see Using $select, $expand, and $value in ASP.NET Web API OData.
Client-Driven Paging
For large entity sets, the client might want to limit the number of results. For example, a client might show 10 entries at a time, with “next” links to get the next page of results. To do this, the client uses the $top and $skip options.
http://localhost/Products?$top=10&$skip=20
The $top option gives the maximum number of entries to return, and the $skip option gives the number of entries to skip. The previous example fetches entries 21 through 30.
Filtering
The $filter option lets a client filter the results by applying a Boolean expression. The filter expressions are quite powerful; they include logical and arithmetic operators, string functions, and date functions.
Return all products with category equal to “Toys”. | http://localhost/Products?$filter=Category eq ‘Toys’ |
---|---|
Return all products with price less than 10. | http://localhost/Products?$filter=Price lt 10 |
Logical operators: Return all products where price >= 5 and price <= 15. | http://localhost/Products?$filter=Price ge 5 and Price le 15 |
String functions: Return all products with “zz” in the name. | http://localhost/Products?$filter=substringof('zz',Name) |
Date functions: Return all products with ReleaseDate after 2005. | http://localhost/Products?$filter=year(ReleaseDate) gt 2005 |
Sorting
To sort the results, use the $orderby filter.
Sort by price. | http://localhost/Products?$orderby=Price |
---|---|
Sort by price in descending order (highest to lowest). | http://localhost/Products?$orderby=Price desc |
Sort by category, then sort by price in descending order within categories. | http://localhost/odata/Products?$orderby=Category,Price desc |
If your database contains millions of records, you don’t want to send them all in one payload. To prevent this, the server can limit the number of entries that it sends in a single response. To enable server paging, set the PageSize property in the Queryable attribute. The value is the maximum number of entries to return.
[!code-csharpMain]
1: [Queryable(PageSize=10)]
2: public IQueryable<Product> Get()
3: {
4: return products.AsQueryable();
5: }
If your controller returns OData format, the response body will contain a link to the next page of data:
[!code-jsonMain]
1: {
2: "odata.metadata":"http://localhost/$metadata#Products",
3: "value":[
4: { "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
5: { "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },
6: // Others not shown
7: ],
8: "odata.nextLink":"http://localhost/Products?$skip=10"
9: }
The client can use this link to fetch the next page. To learn the total number of entries in the result set, the client can set the $inlinecount query option with the value “allpages”.
http://localhost/Products?$inlinecount=allpages
The value “allpages” tells the server to include the total count in the response:
[!code-jsonMain]
1: {
2: "odata.metadata":"http://localhost/$metadata#Products",
3: "odata.count":"50",
4: "value":[
5: { "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
6: { "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },
7: // Others not shown
8: ]
9: }
[!NOTE] Next-page links and inline count both require OData format. The reason is that OData defines special fields in the response body to hold the link and count.
For non-OData formats, it is still possible to support next-page links and inline count, by wrapping the query results in a PageResult<T> object. However, it requires a bit more code. Here is an example:
[!code-csharpMain]
1: public PageResult<Product> Get(ODataQueryOptions<Product> options)
2: {
3: ODataQuerySettings settings = new ODataQuerySettings()
4: {
5: PageSize = 5
6: };
7:
8: IQueryable results = options.ApplyTo(_products.AsQueryable(), settings);
9:
10: return new PageResult<Product>(
11: results as IEnumerable<Product>,
12: Request.GetNextPageLink(),
13: Request.GetInlineCount());
14: }
Here is an example JSON response:
[!code-jsonMain]
1: {
2: "Items": [
3: { "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
4: { "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },
5:
6: // Others not shown
7:
8: ],
9: "NextPageLink": "http://localhost/api/values?$inlinecount=allpages&$skip=10",
10: "Count": 50
11: }
The query options give the client a lot of control over the query that is run on the server. In some cases, you might want to limit the available options for security or performance reasons. The [Queryable] attribute has some built in properties for this. Here are some examples.
Allow only $skip and $top, to support paging and nothing else:
[!code-csharpMain]
1: [Queryable(AllowedQueryOptions=
2: AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
Allow ordering only by certain properties, to prevent sorting on properties that are not indexed in the database:
[!code-csharpMain]
1: [Queryable(AllowedOrderByProperties="Id")] // comma-separated list of properties
Allow the “eq” logical function but no other logical functions:
[!code-csharpMain]
1: [Queryable(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
Do not allow any arithmetic operators:
[!code-csharpMain]
1: [Queryable(AllowedArithmeticOperators=AllowedArithmeticOperators.None)]
You can restrict options globally by constructing a QueryableAttribute instance and passing it to the EnableQuerySupport function:
[!code-csharpMain]
1: var queryAttribute = new QueryableAttribute()
2: {
3: AllowedQueryOptions = AllowedQueryOptions.Top | AllowedQueryOptions.Skip,
4: MaxTop = 100
5: };
6:
7: config.EnableQuerySupport(queryAttribute);
## Invoking Query Options Directly
Instead of using the [Queryable] attribute, you can invoke the query options directly in your controller. To do so, add an ODataQueryOptions parameter to the controller method. In this case, you don’t need the [Queryable] attribute.
[!code-csharpMain]
1: public IQueryable<Product> Get(ODataQueryOptions opts)
2: {
3: var settings = new ODataValidationSettings()
4: {
5: // Initialize settings as needed.
6: AllowedFunctions = AllowedFunctions.AllMathFunctions
7: };
8:
9: opts.Validate(settings);
10:
11: IQueryable results = opts.ApplyTo(products.AsQueryable());
12: return results as IQueryable<Product>;
13: }
Web API populates the ODataQueryOptions from the URI query string. To apply the query, pass an IQueryable to the ApplyTo method. The method returns another IQueryable.
For advanced scenarios, if you do not have an IQueryable query provider, you can examine the ODataQueryOptions and translate the query options into another form. (For example, see RaghuRam Nadiminti’s blog post Translating OData queries to HQL, which also includes a sample.)
The [Queryable] attribute validates the query before executing it. The validation step is performed in the QueryableAttribute.ValidateQuery method. You can also customize the validation process.
Also see OData Security Guidance.
First, override one of the validator classes that is defined in the Web.Http.OData.Query.Validators namespace. For example, the following validator class disables the ‘desc’ option for the $orderby option.
[!code-csharpMain]
1: public class MyOrderByValidator : OrderByQueryValidator
2: {
3: // Disallow the 'desc' parameter for $orderby option.
4: public override void Validate(OrderByQueryOption orderByOption,
5: ODataValidationSettings validationSettings)
6: {
7: if (orderByOption.OrderByNodes.Any(
8: node => node.Direction == OrderByDirection.Descending))
9: {
10: throw new ODataException("The 'desc' option is not supported.");
11: }
12: base.Validate(orderByOption, validationSettings);
13: }
14: }
Subclass the [Queryable] attribute to override the ValidateQuery method.
[!code-csharpMain]
1: public class MyQueryableAttribute : QueryableAttribute
2: {
3: public override void ValidateQuery(HttpRequestMessage request,
4: ODataQueryOptions queryOptions)
5: {
6: if (queryOptions.OrderBy != null)
7: {
8: queryOptions.OrderBy.Validator = new MyOrderByValidator();
9: }
10: base.ValidateQuery(request, queryOptions);
11: }
12: }
Then set your custom attribute either globally or per-controller:
[!code-csharpMain]
1: // Globally:
2: config.EnableQuerySupport(new MyQueryableAttribute());
3:
4: // Per controller:
5: public class ValuesController : ApiController
6: {
7: [MyQueryable]
8: public IQueryable<Product> Get()
9: {
10: return products.AsQueryable();
11: }
12: }
If you are using ODataQueryOptions directly, set the validator on the options:
[!code-csharpMain]
1: public IQueryable<Product> Get(ODataQueryOptions opts)
2: {
3: if (opts.OrderBy != null)
4: {
5: opts.OrderBy.Validator = new MyOrderByValidator();
6: }
7:
8: var settings = new ODataValidationSettings()
9: {
10: // Initialize settings as needed.
11: AllowedFunctions = AllowedFunctions.AllMathFunctions
12: };
13:
14: // Validate
15: opts.Validate(settings);
16:
17: IQueryable results = opts.ApplyTo(products.AsQueryable());
18: return results as IQueryable<Product>;
19: }
|