{"id":4665,"date":"2021-08-26T02:25:56","date_gmt":"2021-08-26T09:25:56","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=4665"},"modified":"2021-08-26T02:25:56","modified_gmt":"2021-08-26T09:25:56","slug":"support-for-fetching-nested-paths-in-odata-web-api","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/support-for-fetching-nested-paths-in-odata-web-api\/","title":{"rendered":"Support for fetching nested paths in OData Web API"},"content":{"rendered":"<h2>Background<\/h2>\n<p>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:<\/p>\n<p><code>http:\/\/localhost:8080\/Customers\/1\/Orders<\/code><\/p>\n<p><code>Microsoft.AspNetCore.OData<\/code> 7.x provides two main approaches for handling such requests: convention and attribute routing.<\/p>\n<p>In the first approach, we create controller actions with conventional naming patterns to handle specific scenarios.<\/p>\n<p>For example, if you want to handle fetching the customers entity set (<code>\/Customers<\/code>), you would implement a <code>Get()<\/code> method in the <code>CustomersController<\/code> that returns the collection corresponding to the entity set as follows:<\/p>\n<pre class=\"prettyprint\">public IActionResult&lt;IQueryable&lt;Customer&gt;&gt; Get()\r\n{\r\n    return Ok(dbContext.Customers);\r\n}<\/pre>\n<p>To retrieve a single customer by key, you would implement a <code>Get(int key)<\/code> method that returns the requested customer as follows:<\/p>\n<pre class=\"prettyprint\">public IActionResult&lt;Customer&gt; Get([FromODataUri] int key)\r\n{\r\n    return Ok(dbContext.Customers.FirstOrDefault(c =&gt; c.Id.Equals(key)));\r\n}<\/pre>\n<p>To handle requests like <code>\/Customers\/1\/Orders<\/code>, you would need to implement <code>GetOrders(int key)<\/code> method as follows:<\/p>\n<pre class=\"prettyprint\">public IActionResult&lt;IQueryable&lt;Order&gt;&gt; GetOrders([FromODataUri] int key)\r\n{\r\n     return Ok(dbContext.Customers.SingleOrDefault(c =&gt; c.Id.Equals(key))?.Orders);\r\n}<\/pre>\n<p>The convention-based approach only allows 2 levels of nesting. Beyond that you need to specify each route explicitly using the <code>ODataRoute<\/code> attribute. For example, to handle a route like <code>http:\/\/localhost:8080\/Customers\/1\/Orders\/1\/Items<\/code> you would need to implement an action like:<\/p>\n<pre class=\"prettyprint\">[ODataRoute(\"Customers({customerId})\/Orders\/({orderId})\/Items\")]\r\npublic IActionResult&lt;IQueryable&lt;Order&gt;&gt; GetOrderItems(\r\n    [FromODataUri] int customerId,\r\n    [FromODataUri] int orderId)\r\n{\r\n    return Ok(dbContext.Orders.FirstOrDefault(o =&gt; o.Id.Equals(orderId) &amp;&amp; o.CustomerId.Equals(customerId))?.Items);\r\n}\r\n\r\n<\/pre>\n<p>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 <code>Microsoft.AspNetCore.OData<\/code> that makes such scenarios much easier to implement.<\/p>\n<h2>Introducing <code>EnableNestedPaths<\/code> Attribute<\/h2>\n<p><code>Microsoft.AspNetCore.OData<\/code> 7.5.9 introduces a new <code>EnableNestedPaths<\/code> attribute that allows you to handle all those requests we have mentioned with only a single <code>Get()<\/code> method. The <code>Get()<\/code> action should return a collection (either <code>IQueryable<\/code> or <code>IEnumerable<\/code>) corresponding to the entity set for that controller. For example:<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"prettyprint\">public class CustomersController\r\n{\r\n\r\n    [EnableNestedPaths]\r\n\r\n    public IActionResult&lt;IQueryable&lt;Customer&gt;&gt; Get()\r\n    {\r\n\r\n        return Ok(dbContext.Customers);\r\n\r\n    }\r\n\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<p>Now this method will handle all <code>GET<\/code> requests to with <code>\/Customers<\/code> as the navigation source, with a few exceptions (explained later in this article). However, if you still have actions that handle specific requests like <code>Get(int key)<\/code>, <code>GetOrders(int key)<\/code>, etc., then those actions will take precedence. The <code>EnableNestedPaths<\/code> 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:<\/p>\n<pre class=\"prettyprint\">public class CustomersController\r\n{\r\n\r\n    [EnableNestedPaths]\r\n\r\n    public IActionResult&lt;IQueryable&lt;Customer&gt;&gt; Get() { \/*... *\/}\r\n\r\n    public IActionResult&lt;Customer&gt; Get(int key) { \/* ... *\/ }\r\n\r\n    public IActionResult&lt;IQueryable&lt;Order&gt;&gt; GetOrders(int key) {\/* ... *\/}\r\n\r\n}<\/pre>\n<p>The following table shows which controller actions will receive the specified requests:<\/p>\n<table style=\"border-collapse: collapse; width: 100%; height: 178px;\">\n<tbody>\n<tr style=\"height: 28px;\">\n<td style=\"width: 50%; height: 28px;\"><strong>Path<\/strong><\/td>\n<td style=\"width: 50%; height: 28px;\"><strong>Action<\/strong><\/td>\n<\/tr>\n<tr style=\"height: 28px;\">\n<td style=\"width: 50%; height: 28px;\"><code>\/Customers<\/code><\/td>\n<td style=\"width: 50%; height: 28px;\"><code>Get()<\/code><\/td>\n<\/tr>\n<tr style=\"height: 28px;\">\n<td style=\"width: 50%; height: 28px;\"><code>\/Customers(1)<\/code><\/td>\n<td style=\"width: 50%; height: 28px;\"><code>Get(int key)<\/code><\/td>\n<\/tr>\n<tr style=\"height: 28px;\">\n<td style=\"width: 50%; height: 28px;\"><code>\/Customers(1)\/Orders<\/code><\/td>\n<td style=\"width: 50%; height: 28px;\"><code>GetOrders(int key)<\/code><\/td>\n<\/tr>\n<tr style=\"height: 28px;\">\n<td style=\"width: 50%; height: 28px;\"><code>\/Customers(1)\/Name<\/code><\/td>\n<td style=\"width: 50%; height: 28px;\"><code>Get()<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 50%;\"><code>\/Customers(1)\/Orders(2)\/Items<\/code><\/td>\n<td style=\"width: 50%;\"><code>Get()<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>&nbsp;<\/p>\n<p>The feature also works for singleton controllers. In this case, you should use the <code>SingleResult&lt;T&gt;<\/code> wrapper to wrap the object returned by your controller action in an <code>IQueryable<\/code>:<\/p>\n<pre class=\"prettyprint\">public class BestCustomerController {\r\n    [EnableNestedPaths]\r\n    public IActionResult&lt;IQueryable&lt;Customer&gt;&gt; Get() {\r\n        return Ok(new SingleResult&lt;Customer&gt;.Create(new [] { this.BestCustomer }.AsQueryable()});\r\n    }\r\n}<\/pre>\n<p>The action should either be called <code>Get<\/code> or <code>Get{NavigationSource}<\/code> (e.g. <code>GetCustomers<\/code> for the entity set, <code>GetBestCustomer<\/code> for the singleton).<\/p>\n<p>The <code>EnableNestedPaths<\/code> 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 <code>EnableNestedPaths<\/code> alongside <code>EnableQuery<\/code> attribute to allow query options to be automatically applied to your results. The query transformations from <code>EnableNestedPaths<\/code> are applied before those from <code>EnableQuery<\/code>.<\/p>\n<h2>Current Limitations<\/h2>\n<ul>\n<li><code>EnableNestedPaths<\/code> 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.<\/li>\n<li><code>EnableNestedPaths<\/code> only handles <code>GET<\/code> requests with entity set or singleton navigation sources. It does not handle functions or actions.<\/li>\n<li><code>EnableNestedPaths<\/code> does not handle <code>$ref<\/code> requests (i.e. <code>GET \/Customers\/1\/Orders\/1\/$ref<\/code> will not be routed to the <code>Get()<\/code> method with <code>EnableNestedPaths<\/code>)<\/li>\n<li>This feature is currently only supported on .NET Core (<code>Microsoft.AspNetCore.OData<\/code>)<\/li>\n<li>This feature is not yet available in <code>Microsoft.AspNetCore.OData<\/code> 8.x but plans to port the feature to 8.x are underway.<\/li>\n<\/ul>\n<p>&nbsp;<\/p>\n<p>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 <a href=\"https:\/\/github.com\/OData\/WebApi\">GitHub repository for OData Web API<\/a>. We are more than happy to listen to your feedback and respond to your concerns.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":20326,"featured_media":3253,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4665","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-odata"],"acf":[],"blog_post_summary":"<p>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 [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4665","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/users\/20326"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/comments?post=4665"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4665\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media\/3253"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media?parent=4665"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=4665"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=4665"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}