Skip to content

OData Query Options

Chris Martinez edited this page Dec 17, 2018 · 6 revisions

OData query option conventions allow you to specify information for your OData services without having to rely solely on .NET attributes. There are a number of reasons why you might uses these conventions. The most common reasons are:

  • Centralized management and application of all OData query options
  • Define OData query options that cannot be expressed with any OData query attributes
  • Apply OData query options to services defined by controllers in external .NET assemblies

The parameter names generated are based on the name of the OData query option and the configuration of the ODataUriResolver. OData supports query options without the system $ prefix. This is enabled or disabled by the ODataUriResolver.EnableNoDollarQueryOptions property.

Attribute Model

The attribute model relies on Model Bound attributes and the EnableQueryAttribute. The EnableQueryAttribute indicates options that cannot otherwise be set such as MaxTop, MaxSkip, allowed functions, and so on. Consider the following model and controller definitions.

using System;
using Microsoft.AspNet.OData.Query;
using static Microsoft.AspNet.OData.Query.SelectExpandType;

[Select]
[Select( "effectiveDate", SelectType = Disabled )]
public class Order
{
    public int Id { get; set; }
    public DateTime CreatedDate { get; set; } = DateTime.Now;
    public DateTime EffectiveDate { get; set; } = DateTime.Now;
    public string Customer { get; set; }
    public string Description { get; set; }
}

ASP.NET Web API with OData

using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Routing;
using Microsoft.Web.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Web.Http.Description;
using static Microsoft.AspNet.OData.Query.AllowedQueryOptions;
using static System.Net.HttpStatusCode;
using static System.DateTime;

[ApiVersion( "1.0" )]
[ODataRoutePrefix( "Orders" )]
public class OrdersController : ODataController
{
    [ODataRoute]
    [Produces( "application/json" )]
    [ProducesResponseType( typeof( ODataValue<IEnumerable<Order>> ), Status200OK )]
    [EnableQuery( MaxTop = 100, AllowedQueryOptions = Select | Top | Skip | Count )]
    public IQueryable<Order> Get()
    {
      var orders = new[]
      {
        new Order(){ Id = 1, Customer = "John Doe" },
        new Order(){ Id = 2, Customer = "John Doe" },
        new Order(){ Id = 3, Customer = "Jane Doe", EffectiveDate = UtcNow.AddDays( 7d ) }
      };

      return orders.AsQueryable();
    }

    [ODataRoute( "({key})" )]
    [Produces( "application/json" )]
    [ProducesResponseType( typeof( Order ), Status200OK )]
    [ProducesResponseType( Status404NotFound )]
    [EnableQuery( AllowedQueryOptions = Select )]
    public SingleResult<Order> Get( int key )
    {
      var orders = new[] { new Order(){ Id = key, Customer = "John Doe" } };
      return SingleResult.Create( orders.AsQueryable() );
    }
}

ASP.NET Core with OData

using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Routing;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using static Microsoft.AspNet.OData.Query.AllowedQueryOptions;
using static Microsoft.AspNetCore.Http.StatusCodes;
using static System.DateTime;

[ApiVersion( "1.0" )]
[ODataRoutePrefix( "Orders" )]
public class OrdersController : ODataController
{
    [ODataRoute]
    [Produces( "application/json" )]
    [ProducesResponseType( typeof( ODataValue<IEnumerable<Order>> ), Status200OK )]
    [EnableQuery( MaxTop = 100, AllowedQueryOptions = Select | Top | Skip | Count )]
    public IQueryable<Order> Get()
    {
      var orders = new[]
      {
        new Order(){ Id = 1, Customer = "John Doe" },
        new Order(){ Id = 2, Customer = "John Doe" },
        new Order(){ Id = 3, Customer = "Jane Doe", EffectiveDate = UtcNow.AddDays(7d) }
      };

      return orders.AsQueryable();
    }

    [ODataRoute( "({key})" )]
    [Produces( "application/json" )]
    [ProducesResponseType( typeof( Order ), Status200OK )]
    [ProducesResponseType( Status404NotFound )]
    [EnableQuery( AllowedQueryOptions = Select )]
    public SingleResult<Order> Get( int key )
    {
      var orders = new[] { new Order(){ Id = key, Customer = "John Doe" } };
      return SingleResult.Create( orders.AsQueryable() );
    }
}

The OData API Explorer will discover and add add the following parameters for an entity set query:

Name Description Parameter Type Data Type
$select Limits the properties returned in the result. The allowed properties are: id, createdDate, customer, description. query string
$top Limits the number of items returned from a collection. The maximum value is 100. query integer
$skip Excludes the specified number of items of the queried collection from the result. query integer

Convention Model

The convention model relies on Model Bound attributes and the new Query Option conventions. The query option conventions configure options that cannot otherwise be set such as MaxTop, MaxSkip, allowed functions, and so on. Consider the following model and controller definitions.

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
}

public class PersonModelConfiguration : IModelConfiguration
{
    public void Apply( ODataModelBuilder builder, ApiVersion apiVersion )
    {
        var person = builder.EntitySet<Person>( "People" ).EntityType;

        person.HasKey( p => p.Id );

        // configure model bound conventions
        person.Select().OrderBy( "firstName", "lastName" );

        if ( apiVersion < ApiVersions.V3 )
        {
            person.Ignore( p => p.Phone );
        }

        if ( apiVersion <= ApiVersions.V1 )
        {
            person.Ignore( p => p.Email );
        }

        if ( apiVersion > ApiVersions.V1 )
        {
            var function = person.Collection.Function( "NewHires" );

            function.Parameter<DateTime>( "Since" );
            function.ReturnsFromEntitySet<Person>( "People" );
        }

        if ( apiVersion > ApiVersions.V2 )
        {
            person.Action( "Promote" ).Parameter<string>( "title" );
        }
    }
}

ASP.NET Web API with OData

using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Routing;
using Microsoft.Web.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using System.Web.Http.Description;
using static Microsoft.AspNet.OData.Query.AllowedQueryOptions;
using static System.Net.HttpStatusCode;
using static System.DateTime;

public class PeopleController : ODataController
{
    [HttpGet]
    [ResponseType( typeof( ODataValue<IEnumerable<Person>> ) )]
    public IHttpActionResult Get( ODataQueryOptions<Person> options )
    {
        var validationSettings = new ODataValidationSettings()
        {
            AllowedQueryOptions = Select | OrderBy | Top | Skip | Count,
            AllowedOrderByProperties = { "firstName", "lastName" },
            AllowedArithmeticOperators = AllowedArithmeticOperators.None,
            AllowedFunctions = AllowedFunctions.None,
            AllowedLogicalOperators = AllowedLogicalOperators.None,
            MaxOrderByNodeCount = 2,
            MaxTop = 100,
        };

        try
        {
            options.Validate( validationSettings );
        }
        catch ( ODataException )
        {
            return BadRequest();
        }

        var people = new[]
        {
            new Person()
            {
                Id = 1,
                FirstName = "John",
                LastName = "Doe",
                Email = "[email protected]",
                Phone = "555-987-1234",
            },
            new Person()
            {
                Id = 2,
                FirstName = "Bob",
                LastName = "Smith",
                Email = "[email protected]",
                Phone = "555-654-4321",
            },
            new Person()
            {
                Id = 3,
                FirstName = "Jane",
                LastName = "Doe",
                Email = "[email protected]",
                Phone = "555-789-3456",
            }
        };

        return this.Success( options.ApplyTo( people.AsQueryable() ) );
    }

    [HttpGet]
    [ResponseType( typeof( Person ) )]
    public IHttpActionResult Get( int key, ODataQueryOptions<Person> options )
    {
        var people = new[]
        {
            new Person()
            {
                Id = key,
                FirstName = "John",
                LastName = "Doe",
                Email = "[email protected]",
                Phone = "555-987-1234",
            }
        };

        var query = options.ApplyTo( people.AsQueryable();
        return this.SuccessOrNotFound( query ).SingleOrDefault() );
    }
}

ASP.NET Core with OData

using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Routing;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using static Microsoft.AspNet.OData.Query.AllowedQueryOptions;
using static Microsoft.AspNetCore.Http.StatusCodes;
using static System.DateTime;

public class PeopleController : ODataController
{
    [Produces( "application/json" )]
    [ProducesResponseType( typeof( ODataValue<IEnumerable<Person>> ), Status200OK )]
    public IActionResult Get( ODataQueryOptions<Person> options )
    {
        var validationSettings = new ODataValidationSettings()
        {
            AllowedQueryOptions = Select | OrderBy | Top | Skip | Count,
            AllowedOrderByProperties = { "firstName", "lastName" },
            AllowedArithmeticOperators = AllowedArithmeticOperators.None,
            AllowedFunctions = AllowedFunctions.None,
            AllowedLogicalOperators = AllowedLogicalOperators.None,
            MaxOrderByNodeCount = 2,
            MaxTop = 100,
        };

        try
        {
            options.Validate( validationSettings );
        }
        catch ( ODataException )
        {
            return BadRequest();
        }

        var people = new[]
        {
            new Person()
            {
                Id = 1,
                FirstName = "John",
                LastName = "Doe",
                Email = "[email protected]",
                Phone = "555-987-1234",
            },
            new Person()
            {
                Id = 2,
                FirstName = "Bob",
                LastName = "Smith",
                Email = "[email protected]",
                Phone = "555-654-4321",
            },
            new Person()
            {
                Id = 3,
                FirstName = "Jane",
                LastName = "Doe",
                Email = "[email protected]",
                Phone = "555-789-3456",
            }
        };

        return Ok( options.ApplyTo( people.AsQueryable() ) );
    }

    [Produces( "application/json" )]
    [ProducesResponseType( typeof( Person ), Status200OK )]
    [ProducesResponseType( Status404NotFound )]
    public IActionResult Get( int key, ODataQueryOptions<Person> options )
    {
        var people = new[]
        {
            new Person()
            {
                Id = key,
                FirstName = "John",
                LastName = "Doe",
                Email = "[email protected]",
                Phone = "555-987-1234",
            }
        };

        var person = options.ApplyTo( people.AsQueryable() ).SingleOrDefault();

        if ( person == null )
        {
            return NotFound();
        }

        return Ok( person );
    }
}

Conventions

OData does not provide a mechanism to express some OData query options by convention; for example MaxTop. These conventions, however, can be expressed in the OData API Explorer options:

using static Microsoft.AspNet.OData.Query.AllowedQueryOptions;

.AddODataApiExplorer( options =>
{
    var queryOptions = options.QueryOptions;

    // configure query options (which cannot otherwise be configured by OData conventions)
    queryOptions.Controller<V2.PeopleController>()
                .Action( c => c.Get( default( ODataQueryOptions<Person> ) ) )
                    .Allow( Skip | Count ).AllowTop( 100 );

    queryOptions.Controller<V3.PeopleController>()
                .Action( c => c.Get( default( ODataQueryOptions<Person> ) ) )
                    .Allow( Skip | Count ).AllowTop( 100 );
} );

The OData API Explorer will discover and add add the following parameters for an entity set query:

Name Description Parameter Type Data Type
$select Limits the properties returned in the result. query string
$orderby Specifies the order in which results are returned. The allowed properties are: firstName, lastName. query string
$top Limits the number of items returned from a collection. The maximum value is 100. query integer
$skip Excludes the specified number of items of the queried collection from the result. query integer

Parameter Descriptions

While each OData query option has a default provided description, the description can be changed by providing a custom description. Descriptions are generated by the IODataQueryOptionDescriptionProvider:

public interface IODataQueryOptionDescriptionProvider
{
    string Describe(
        AllowedQueryOptions queryOption,
        ODataQueryOptionDescriptionContext context );
}

Note: Although AllowedQueryOptions is a bitwise enumeration, only a single query option value is ever passed

You can change the default description by implementing your own IODataQueryOptionDescriptionProvider or extending the built-in DefaultODataQueryOptionDescriptionProvider. The implementation is updated in the OData API Explorer options using:

.AddODataApiExplorer( options =>
{
    var queryOptions = options.QueryOptions;
    queryOptions.DescriptionProvider = new MyODataQueryOptionDescriptionProvider();
} );

Custom Conventions

You can also define custom conventions via the IODataQueryOptionsConvention interface and add them to the builder:

public interface IODataQueryOptionsConvention

    void ApplyTo( ApiDescription apiDescription );
}
.AddODataApiExplorer( options =>
{
    options.QueryOptions.Add( new MyODataQueryOptionsConvention() );
} );
Clone this wiki locally