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 handlesGET
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 theGet()
method withEnableNestedPaths
)- 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