{"id":4362,"date":"2020-12-04T12:48:28","date_gmt":"2020-12-04T19:48:28","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=4362"},"modified":"2020-12-04T12:48:28","modified_gmt":"2020-12-04T19:48:28","slug":"passing-odata-query-options-in-the-request-body","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/passing-odata-query-options-in-the-request-body\/","title":{"rendered":"Passing OData Query Options in the Request Body"},"content":{"rendered":"<p>The query options part of an OData URL can be quite long, potentially exceeding the maximum length of URLs supported by components involved in transmitting or processing the request. HTTP does not impose any limits on the length of a URL, however, many hosting environments (including IIS) impose limitations which may restrict the client&#8217;s ability to request the exact set of data that they are interested in. These limitations would especially pose a big challenge for clients using explicit select statements for example.<\/p>\n<h3>Background<\/h3>\n<p>One way that you can go round these limitations is by wrapping the GET request in a batch request. This approach requires you to be conversant with how to construct a well-formed batch request body. It also requires you to configure the service to <a href=\"https:\/\/devblogs.microsoft.com\/odata\/all-in-one-with-odata-batch\/\">support OData batching<\/a>.<\/p>\n<p>For example, the following GET request can be passed in a batch request as demonstrated further below:<\/p>\n<p>GET: <code>http:\/\/ServiceRoot\/Movies?$select=Id,Name,Classification,RunningTime&amp;$filter=contains(Name, 'li')&amp;$orderby=Name desc<\/code><\/p>\n<p>Wrapped in a batch request&#8230;<\/p>\n<p>POST: <code>http:\/\/ServiceRoot\/$batch<\/code><\/p>\n<p>Content-Type: <code>multipart\/mixed;boundary=changeset_6c67825c-8938-4f11-af6b-a25861ee53cc<\/code><\/p>\n<p>Request Body:<\/p>\n<pre class=\"prettyprint\">--batch_d3bcb804-ee77-4921-9a45-761f98d32029\r\n\r\nContent-Type: multipart\/mixed; boundary=changeset_6c67825c-8938-4f11-af6b-a25861ee53cc\r\n\r\n--changeset_6c67825c-8938-4f11-af6b-a25861ee53cc\r\nContent-Type: application\/http\r\nContent-Transfer-Encoding: binary\r\n\r\nGET http:\/\/ServiceRoot\/Movies?$select=Id,Name,Classification,RunningTime&amp;$filter=contains(Name,%20%27li%27)&amp;$orderby=Name%20desc HTTP\/1.1\r\nOData-Version: 4.0\r\nOData-MaxVersion: 4.0\r\nAccept: application\/json;odata.metadata=minimal\r\nAccept-Charset: UTF-8\r\nUser-Agent: PostmanRuntime\/7.24.1\r\n\r\n--changeset_6c67825c-8938-4f11-af6b-a25861ee53cc--\r\n--batch_d3bcb804-ee77-4921-9a45-761f98d32029--<\/pre>\n<p>R<span style=\"font-size: 1rem;\">esponse Body:<\/span><\/p>\n<pre class=\"prettyprint\">--batchresponse_656768b7-d2d6-49fb-8251-303ec59cc42f\r\nContent-Type: application\/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP\/1.1 200 OK\r\nContent-Type: application\/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8\r\nOData-Version: 4.0\r\n\r\n{\"@odata.context\":\"http:\/\/ServiceRoot\/$metadata#Movies(Id,Name,Classification,RunningTime)\",\"value\":[{\"Id\":11,\"Name\":\"The Equalizer\",\"Classification\":\"R\",\"RunningTime\":132},{\"Id\":9,\"Name\":\"Dolittle\",\"Classification\":\"PG\",\"RunningTime\":101}]}\r\n--batchresponse_656768b7-d2d6-49fb-8251-303ec59cc42f--<\/pre>\n<p>In <a href=\"https:\/\/docs.microsoft.com\/en-us\/odata\/changelog\/webapi-7x#webapi-750\">OData Web API 7.5<\/a>, we introduced an alternative and relatively easier way of achieving the same goal. You can now pass the query options part of the URL in the request body to an endpoint comprising of the resource path appended with <code>\/$query<\/code> \u2013 using the POST verb instead of GET.<\/p>\n<p>Based on our example above, you can now do it this way:<\/p>\n<p>POST: <code>http:\/\/ServiceRoot\/Movies\/$query<\/code><\/p>\n<p>Content-Type: <code>text\/plain<\/code><\/p>\n<p>Request Body:<\/p>\n<pre class=\"prettyprint\">$select=Id,Name,Classification,RunningTime&amp;$filter=contains(Name,%20%27li%27)&amp;$orderby=Name%20desc<\/pre>\n<p>Response Body:<\/p>\n<pre class=\"prettyprint\">{\r\n    \"@odata.context\": \"http:\/\/ServiceRoot\/$metadata#Movies(Id,Name,Classification,RunningTime)\",\r\n    \"value\": [\r\n        {\r\n            \"Id\": 11,\r\n            \"Name\": \"The Equalizer\",\r\n            \"Classification\": \"R\",\r\n            \"RunningTime\": 132\r\n        },\r\n        {\r\n            \"Id\": 9,\r\n            \"Name\": \"Dolittle\",\r\n            \"Classification\": \"PG\",\r\n            \"RunningTime\": 101\r\n        }\r\n    ]\r\n}<\/pre>\n<p>The OData specification prescribes that the query options specified in the request body and those specified in the request URL are processed together. Whereas a query string on a POST request is not a common thing, this feature supports that option. Here&#8217;s how the request would look like:<\/p>\n<p>POST: <code>http:\/\/ServiceRoot\/Movies\/$query?$filter=contains(Name,%20%27li%27)<\/code><code><\/code><\/p>\n<p>Content-Type: <code>text\/plain<\/code><\/p>\n<p>Request Body:<\/p>\n<pre class=\"prettyprint\">$select=Id,Name,Classification,RunningTime$orderby=Name%20desc<\/pre>\n<p>Specifically, note that <code>\/$query<\/code> should be preceded by the resource path. The resource path is <code>\/Movies<\/code> in the above example. This is the one way that the <code>\/$query<\/code> endpoint differs from the <code>\/$batch<\/code> endpoint.<\/p>\n<h3>Feature extensibility<\/h3>\n<p>We support the ability to extend this feature by providing means for the developer to plug in their own query options parser.<\/p>\n<p>Your first step towards accomplishing that is implementing the <code>IODataQueryOptionsParser<\/code> interface. This interface contains two methods that you need to implement:<\/p>\n<ul>\n<li><code>bool CanParse(HttpRequest)<\/code> (or <code>bool CanParse(HttpRequestMessage<\/code>) in .NET FX)<\/li>\n<li><code>Task&lt;string&gt; ParseAsync(Stream)<\/code><\/li>\n<\/ul>\n<p>The logic to analyze the request to determine if the parser is capable of processing the content goes into <code>CanParse(HttpRequest)<\/code> method. Your logic could inspect headers, e.g. <code>Content-Type<\/code> or any other thing in the request to evaluate whether the conditions are right for the parser to process the request content.<\/p>\n<p>The parsing logic goes into the <code>ParseAsync(Stream)<\/code> method. The return value for the method is a string containing the query options part of the URL.<\/p>\n<h3>Implementing a custom query options parser<\/h3>\n<p>In this section, we look at how one can implement and plug in their own parser. We implement a simple parser that reads and extracts query options from a POST with <code>text\/xml<\/code> content.<\/p>\n<pre class=\"prettyprint\">public class TextXmlODataQueryOptionsParser : IODataQueryOptionsParser\r\n{\r\n    private static MediaTypeHeaderValue SupportedMediaType =\r\n            MediaTypeHeaderValue.Parse(\"text\/xml\");\r\n\r\n    public bool CanParse(Microsoft.AspNetCore.Http.HttpRequest request)\r\n    {\r\n        return request.ContentType?.StartsWith(SupportedMediaType.MediaType,\r\n            StringComparison.Ordinal) == true ? true : false;\r\n    }\r\n\r\n    public async Task&lt;string&gt; ParseAsync(Stream requestStream)\r\n    {\r\n        using (var reader = new StreamReader(\r\n                requestStream,\r\n                encoding: Encoding.UTF8,\r\n                detectEncodingFromByteOrderMarks: false,\r\n                bufferSize: 1024,\r\n                leaveOpen: true))\r\n        {\r\n            var content = await reader.ReadToEndAsync();\r\n            var document = XDocument.Parse(content);\r\n            var queryOptions = document.Descendants(\"QueryOption\").Select(d =&gt;\r\n            new {\r\n                Option = d.Attribute(\"Option\").Value,\r\n                d.Attribute(\"Value\").Value\r\n            });\r\n\r\n            return string.Join(\"&amp;\", queryOptions.Select(d =&gt; d.Option + \"=\" + d.Value));\r\n        }\r\n    }\r\n}<\/pre>\n<p>Let us go ahead and plug in this parser into the dependency injection container.<\/p>\n<pre class=\"prettyprint\">var queryOptionsParsers = ODataQueryOptionsParserFactory.Create();\r\nqueryOptionsParsers.Insert(0, new TextXmlODataQueryOptionsParser());\r\n\r\n\/\/ ...\r\napp.UseMvc(routeBuilder =&gt;\r\n{\r\n    \/\/ ...\r\n    routeBuilder.MapODataServiceRoute(\r\n    \"ROUTENAME\",\r\n    \"ROUTEPREFIX\",\r\n    configureAction: containerBuilder =&gt; containerBuilder\r\n        .AddService(Microsoft.OData.ServiceLifetime.Singleton,\r\n            typeof(IEdmModel),\r\n            _ =&gt; modelBuilder.GetEdmModel()) \/\/ YOUR EDM MODEL HERE\r\n        .AddService(Microsoft.OData.ServiceLifetime.Singleton,\r\n            typeof(IODataPathHandler),\r\n            _ =&gt; new DefaultODataPathHandler())\r\n        .AddService(Microsoft.OData.ServiceLifetime.Singleton,\r\n            typeof(IEnumerable&lt;IODataRoutingConvention&gt;),\r\n            _ =&gt; ODataRoutingConventions.CreateDefault())\r\n        .AddService(Microsoft.OData.ServiceLifetime.Singleton,\r\n            typeof(IEnumerable&lt;IODataQueryOptionsParser&gt;),\r\n            _ =&gt; queryOptionsParsers)\r\n    );\r\n});<\/pre>\n<p><strong>Note:<\/strong> Inserting the new parser at index 0 elevates it above any other that could potentially handle the request.<\/p>\n<p>This parser can process the following request:<\/p>\n<p>POST: <code>http:\/\/ServiceRoot\/Movies\/$query<\/code><\/p>\n<p>Content-Type: <code>text\/xml<\/code><\/p>\n<p>Request Body:<\/p>\n<pre class=\"prettyprint\">&lt;QueryOptions&gt;\r\n&lt;QueryOption Option=\"$select\" Value=\"Id,Name,Classification,RunningTime\"\/&gt;\r\n&lt;QueryOption Option=\"$filter\" Value=\"contains(Name,'li')\"\/&gt;\r\n&lt;QueryOption Option=\"$orderby\" Value=\"Name desc\"\/&gt;\r\n&lt;\/QueryOptions&gt;<\/pre>\n<h3>Important notes<\/h3>\n<ol>\n<li>The OData specification prescribes that the same system query option must not be specified more than once. For this reason, be careful not to repeat the same query option on the request URL, or on the request body, or both considered together. The current behaviour when a query option is repeated is a 404 response.<\/li>\n<li>Nothing changes in terms of how you implement controllers. You will still implement a <code>Get<\/code> controller action decorated with <code>EnableQueryAttribute<\/code> or taking a <code>ODataQueryOptions&lt;TEntity&gt;<\/code> parameter.<\/li>\n<\/ol>\n<h3>Summary<\/h3>\n<p>It is my hope that this feature will enrich your experience when dealing with long URLs situations.<\/p>\n<p>If you have 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 <a href=\"https:\/\/github.com\/OData\/WebApi\/issues\">GitHub repo<\/a> for OData Web API. We are more than happy to listen to your feedback and respond to your concerns.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The query options part of an OData URL can be quite long, potentially exceeding the maximum length of URLs supported by components involved in transmitting or processing the request. HTTP does not impose any limits on the length of a URL, however, many hosting environments (including IIS) impose limitations which may restrict the client&#8217;s ability [&hellip;]<\/p>\n","protected":false},"author":25333,"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-4362","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-odata"],"acf":[],"blog_post_summary":"<p>The query options part of an OData URL can be quite long, potentially exceeding the maximum length of URLs supported by components involved in transmitting or processing the request. HTTP does not impose any limits on the length of a URL, however, many hosting environments (including IIS) impose limitations which may restrict the client&#8217;s ability [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4362","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\/25333"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/comments?post=4362"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4362\/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=4362"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=4362"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=4362"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}