Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OData Options

The ODataApiExplorerOptions extends the above options with the following additional settings:

Use Qualified Names

The OData API Explorer is responsible for building URLs that refer to your entity sets, functions, and actions. This property determines whether the constructed URLs use qualified names. The default value is false. The ODataUriResolver instance configured for your application must be configured to match the generated URLs (ex: UnqualifiedCallAndEnumPrefixFreeResolver).

Query Options

This option allows you to configure OData query options. The configuration for query options can be expressed purely by convention, through the use of supported OData query attribute, or both. The default behavior will always apply conventions from OData query attributes without additional configuration. For more information see the OData query options topic.

Metadata Options

This option allows you to determine whether the OData metadata ($metadata) and service document (/) are explored as available endpoints. The available options are: None, ServiceDocument, Metadata, or All. The default value is None.

Ad Hoc Model Builder

This property returns an VersionedODataModelBuilder that can be used for building ad hoc Entity Data Models (EDMs) that are used when defining the query options for APIs that do not use the full OData stack. Some OData query options can only be set via Model Bound settings. This builder constructs an ad hoc EDM that will contain those settings solely for the purposes of API exploration and without opting into any other OData-specific features. For more information see the OData query options topic.

This option enables you to specify the description for OData related entity links. The default value is "The identifier of the related entity." OData related entity links appear in $ref requests. This description is used to describe dynamic parameters such as the $id query parameter.

Query Options

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 settings attributes and the EnableQueryAttribute. The EnableQueryAttribute indicates API-specific options that might be too restrictive or not applicable to specific models. 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; }
}
using Asp.Versioning;
using Asp.Versioning.OData;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using static Microsoft.AspNetCore.Http.StatusCodes;
using static Microsoft.AspNetCore.OData.Query.AllowedQueryOptions;
using static System.DateTime;

[ApiVersion( 1.0 )]
public class OrdersController : ODataController
{
    [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();
    }

    [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() );
    }
}

Convention Model

The convention model relies on Model Bound settings via the fluent API of the ODataModelBuilderand the EnableQueryAttribute. The EnableQueryAttribute indicates API-specific options that might be too restrictive or nonapplicable to specific models. 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, string routePrefix )
    {
        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" );
        }
    }
}
using Asp.Versioning;
using Asp.Versioning.OData;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using static Microsoft.AspNetCore.Http.StatusCodes;
using static Microsoft.AspNetCore.OData.Query.AllowedQueryOptions;
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 = "john.doe@somewhere.com",
                Phone = "555-987-1234",
            },
            new Person()
            {
                Id = 2,
                FirstName = "Bob",
                LastName = "Smith",
                Email = "bob.smith@somewhere.com",
                Phone = "555-654-4321",
            },
            new Person()
            {
                Id = 3,
                FirstName = "Jane",
                LastName = "Doe",
                Email = "jane.doe@somewhere.com",
                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 = "john.doe@somewhere.com",
                Phone = "555-987-1234",
            }
        };

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

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

        return Ok( person );
    }
}

Conventions

If you only define OData query options imperatively using ODataQuerySettings and ODataValidationSettings, then there are no attributes or Entity Data Model (EDM) data annotations to explore the query options from. In this scenario, you can use the conventions in the API Explorer extensions to document any query option setting that can be defined by ODataQuerySettings or ODataValidationSettings.

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

    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 the following parameters for an entity set query:

NameParameter TypeData TypeDescription
$selectquerystringLimits the properties returned in the result.
$orderbyquerystringSpecifies the order in which results are returned. The allowed properties are: firstName, lastName.
$topqueryintegerLimits the number of items returned from a collection. The maximum value is 100.
$skipqueryintegerExcludes the specified number of items of the queried collection from the result.

Parameters

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 => options.QueryOptions.DescriptionProvider = new MyQueryOptionDescriptor() );

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() ) );

Partial OData

OData supports query capabilities without using the full OData stack. Consider the following controller, which is not an OData controller, but uses OData query options:

[ApiVersion( 1.0 )]
[ApiController]
[Route( "[controller]" )]
public class BooksController : ControllerBase
{
    [HttpGet]
    [Produces( "application/json" )]
    [ProducesResponseType( typeof( IEnumerable<Book> ), 200 )]
    public IActionResult Get( ODataQueryOptions<Book> options ) =>
        Ok( options.ApplyTo( books.AsQueryable() ) );
}
[ApiVersion( 1.0 )]
[ApiController]
[Route( "[controller]" )]
public class BooksController : ControllerBase
{
    [HttpGet]
    [Produces( "application/json" )]
    [ProducesResponseType( typeof( IEnumerable<Book> ), 200 )]
    public IActionResult Get( ODataQueryOptions<Book> options ) =>
        Ok( options.ApplyTo( books.AsQueryable() ) );
}

When OData query capabilities are used this way, query options can be discovered via EnableQueryAttribute or via the API Explorer extensions. Unfortunately, these are both ultimately limited to what can be expressed via ODataQuerySettings and ODataValidationSettings, which does not cover the gambit of all possible OData query options; for example, the allowable $filter properties. These other properties can be configured via Model Bound settings, but without using the full OData stack there is no Entity Data Model (EDM) to retrieve these annotations from.

To address this limitation, OData query options can now also be explored using an ad hoc EDM. This EDM only exists for the purposes of query option exploration. Using an ad hoc EDM does not opt into other OData feature and only exists during exploration. Applying Model Bound settings to an ad hoc model is almost identical to the normal method. If you want to use attributes, just apply them to your model.

[Filter( "author", "published" )]
public class Book
{
    public string Id { get; set; }
    public string Author { get; set; }
    public string Title { get; set; }
    public int Published { get; set; }
}

Every action that appears to be OData-like will automatically be discovered and its model explored. Discovered models are registered as a complex type by default. If you prefer to use entities or need additional control over the applied settings, you can use conventions as well.

AddODataApiExplorer(
    options =>
    {
        options.AdHocModelBuilder.DefaultModelConfiguration = (builder, version, prefix) =>
        {
            builder.ComplexType<Book>().Filter( "author", "published" );
        };
    } 
)

The AdHocModelBuilder is part of the ODataApiExplorerOptions as opposed to ODataApiVersioningOptions. If you have numerous models and would like to break the settings into different configurations, you can still use IModelConfiguration. IModelConfiguration instances are automatically discovered and injected the same way as they are when using the full OData stack.

public class BookConfiguration : IModelConfiguration
{
    public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix )
    {
        builder.EntitySet<Book>( "Books" ).EntityType.Filter( "author", "published" );
    }
}

Model configuration for an ad hoc model; the routePrefix will always be null.

There is no distinction between an IModelConfiguration that is used for ad hoc EDM exploration versus normal model registration. It is unlikely that you would be mixing the full and partial OData stack. If you are mixing use cases, then you can tell the difference between models from the provided API version. There should be no scenario where a model is registered two different ways for the same API version.