Support for fetching nested paths in OData Web API

Clément

Background

OData services use nested paths to access properties or entities related to a resource. For example, if you want to access orders of a given customer, you would use a request path like:

http://localhost:8080/Customers/1/Orders

Microsoft.AspNetCore.OData 7.x provides two main approaches for handling such requests: convention and attribute routing.

In the first approach, we create controller actions with conventional naming patterns to handle specific scenarios.

For example, if you want to handle fetching the customers entity set (/Customers), you would implement a Get() method in the CustomersController that returns the collection corresponding to the entity set as follows:

public IActionResult<IQueryable<Customer>> Get()
{
    return Ok(dbContext.Customers);
}

To retrieve a single customer by key, you would implement a Get(int key) method that returns the requested customer as follows:

public IActionResult<Customer> Get([FromODataUri] int key)
{
    return Ok(dbContext.Customers.FirstOrDefault(c => c.Id.Equals(key)));
}

To handle requests like /Customers/1/Orders, you would need to implement GetOrders(int key) method as follows:

public IActionResult<IQueryable<Order>> GetOrders([FromODataUri] int key)
{
     return Ok(dbContext.Customers.SingleOrDefault(c => c.Id.Equals(key))?.Orders);
}

The convention-based approach only allows 2 levels of nesting. Beyond that you need to specify each route explicitly using the ODataRoute attribute. For example, to handle a route like http://localhost:8080/Customers/1/Orders/1/Items you would need to implement an action like:

[ODataRoute("Customers({customerId})/Orders/({orderId})/Items")]
public IActionResult<IQueryable<Order>> GetOrderItems(
    [FromODataUri] int customerId,
    [FromODataUri] int orderId)
{
    return Ok(dbContext.Orders.FirstOrDefault(o => o.Id.Equals(orderId) && o.CustomerId.Equals(customerId))?.Items);
}

With the existing approaches, we would need to implement a different controller action for each property or navigation property we want to access and for each level of nesting we want to support. Sometimes we just want to make it possible for clients to retrieve declared properties without implement special logic for such scenarios. We are introducing a new feature in Microsoft.AspNetCore.OData that makes such scenarios much easier to implement.

Introducing EnableNestedPaths Attribute

Microsoft.AspNetCore.OData 7.5.9 introduces a new EnableNestedPaths attribute that allows you to handle all those requests we have mentioned with only a single Get() method. The Get() action should return a collection (either IQueryable or IEnumerable) corresponding to the entity set for that controller. For example:

 

public class CustomersController
{

    [EnableNestedPaths]

    public IActionResult<IQueryable<Customer>> Get()
    {

        return Ok(dbContext.Customers);

    }

}

 

Now this method will handle all GET requests to with /Customers as the navigation source, with a few exceptions (explained later in this article). However, if you still have actions that handle specific requests like Get(int key), GetOrders(int key), etc., then those actions will take precedence. The EnableNestedPaths attribute only handles requests for which no other controller action is set to handle. To illustrate this point, let us assume we have the following controller in our application:

public class CustomersController
{

    [EnableNestedPaths]

    public IActionResult<IQueryable<Customer>> Get() { /*... */}

    public IActionResult<Customer> Get(int key) { /* ... */ }

    public IActionResult<IQueryable<Order>> GetOrders(int key) {/* ... */}

}

The following table shows which controller actions will receive the specified requests:

Path Action
/Customers Get()
/Customers(1) Get(int key)
/Customers(1)/Orders GetOrders(int key)
/Customers(1)/Name Get()
/Customers(1)/Orders(2)/Items Get()

 

The feature also works for singleton controllers. In this case, you should use the SingleResult<T> wrapper to wrap the object returned by your controller action in an IQueryable:

public class BestCustomerController {
    [EnableNestedPaths]
    public IActionResult<IQueryable<Customer>> Get() {
        return Ok(new SingleResult<Customer>.Create(new [] { this.BestCustomer }.AsQueryable()});
    }
}

The action should either be called Get or Get{NavigationSource} (e.g. GetCustomers for the entity set, GetBestCustomer for the singleton).

The EnableNestedPaths attribute applies query transformations to the collection returned by your controller action to retrieve the data that matches the path being requested. You can use EnableNestedPaths alongside EnableQuery attribute to allow query options to be automatically applied to your results. The query transformations from EnableNestedPaths are applied before those from EnableQuery.

Current Limitations

  • EnableNestedPaths currently does not accept any further configurations. In particular, you cannot limit how deeply nested paths can be, you cannot limit which properties or navigation properties can be accessed, etc.
  • EnableNestedPaths only handles GET requests with entity set or singleton navigation sources. It does not handle functions or actions.
  • EnableNestedPaths does not handle $ref requests (i.e. GET /Customers/1/Orders/1/$ref will not be routed to the Get() method with EnableNestedPaths)
  • This feature is currently only supported on .NET Core (Microsoft.AspNetCore.OData)
  • This feature is not yet available in Microsoft.AspNetCore.OData 8.x but plans to port the feature to 8.x are underway.

 

This feature is still a work in progress and we count on your feedback to help us improve it. If you any questions, comments, concerns or if you run into any issues using this feature, feel free to reach out on this blog post or report the issue on the GitHub repository for OData Web API. We are more than happy to listen to your feedback and respond to your concerns.

 

0 comments

Leave a comment