Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Possible to post ODataQueryOptions from the Http Request body?

I'm implementing a Web API interface to support some fairly complex queries to run against it and have run up against an issue with the maximum request URI length.

The definition of my Web API method looks like this (using Automapper to perform the DTO projections):

public IQueryable<ReportModel> Get(ODataQueryOptions<Report> queryOptions)
{
     var query = DbContext.Query<Report>();

     return (queryOptions.ApplyTo(query) as IQueryable<Report>).WithTranslations().Project(MappingEngine).To<ReportModel>().WithTranslations();
}

My request consists of a dynamically built OData query including a potentially large number of 'Field eq Id' filters which are captured into the ODataQueryOptions parameter which is then applied to the IQueryable database context. For example:

http://example.com/api/Report?$filter=(Field1+eq+1%20or%20Field1+eq+5%20or%20Field1+eq+10%20or%20Field1+eq+15...

The problem is occurring once the length of the request URI reaches a certain limit. Any request with a URI length over that limit results in a 404 error. After some testing, this limit appears to be around the 2KB range (a URI with 2065 characters works fine, while one with 2105 fails using Chrome, IE, or FF).

The simple solution to this seems to be changing the request type from a GET to a POST request sending the search query across in the body as opposed to the URI. I'm running into some issues trying to get this working, however, as I can't seem to get the ODataQueryOptions object to populate correctly from the POST request. My Web API method now looks like this:

public IQueryable<ReportModel> Post([FromBody] ODataQueryOptions<Report> queryOptions)
{
      var query = DbContext.Query<Report>();

      return (queryOptions.ApplyTo(query) as IQueryable<Report>).WithTranslations().Project(MappingEngine).To<ReportModel>().WithTranslations();
}

As you can see, I'm trying to populate the query options from the body of the request as opposed to from the URI. To this point I haven't been able to get the ODataQueryOptions parameter to populate from the request, and the parameter results in being 'null'. If I remove the '[FromBody]' attribute, the query options object will populate correctly from the request URI, but the same URI length issue remains.

Here is an example of how I'm calling the method from the browser (using jQuery):

$.ajax({
       url: "/API/Report",
       type: "POST",
       data: ko.toJSON({
           '$filter': 'Field1+eq+1%20or%20Field1+eq+5%20or%20Field1+eq+10%20or%20Field1+eq+15...'
       }),
       dataType: "json",
       processData: false,
       contentType: 'application/json; charset=utf-8',
});

First, is it possible to do what I am trying to do here (Post ODataQueryOptions in the body of the request)? If so, am I building the POST request correctly? Is there anything else I'm missing here?

like image 202
skaaks Avatar asked Jun 11 '14 16:06

skaaks


People also ask

What is Odataqueryoptions?

OData defines parameters that can be used to modify an OData query. The client sends these parameters in the query string of the request URI. For example, to sort the results, a client uses the $orderby parameter: http://localhost/Products?$orderby=Name. The OData specification calls these parameters query options.

Which OData query does Web API support?

The web API supports these standard OData string query functions. $filter=endswith(name,'Inc. ') You can create paging enabled endpoint which means, if you have a lot of data on database, and the requirement is that client needs to show the data like 10 records per page.


2 Answers

You can pass the raw string value of the query options in the post body, and construct a query option in the post method of the controller.

The code below is just for filter query option. You can add other query options the same way.

public IQueryable<ReportModel> Post([FromBody] string filterRawValue)
{
    var context = new ODataQueryContext(Request.ODataProperties().Model, typeof(Report));
    var filterQueryOption = new FilterQueryOption(filterRawValue, context);
    var query = DbContext.Query<Report>();
    return (filterQueryOption.ApplyTo(query) as IQueryable<Report>).WithTranslations().Project(MappingEngine).To<ReportModel>().WithTranslations();
}
like image 186
Feng Zhao Avatar answered Nov 15 '22 06:11

Feng Zhao


My two cents for dotnet core 2.2. Should also work on dotnet core 3.x but not guaranteed.

Handles all OData query parameters.

This passes the raw argument from ODataActionParameters to the HttpRequest's Query property (excluding the host), or if not present, we create one base of the ODataActionParameters.

Extension for IQueryable{T} which applies OData query options.

/// <summary>
/// Extensions for <see cref="IQueryable{T}" /> interface.
/// </summary>
public static class IQueryableExtensions
{
    /// <summary>
    /// Apply the individual query to the given IQueryable in the right order, based on provided <paramref name="actionParameters" />.
    /// </summary>
    /// <param name="self">The <see cref="IQueryable{TEntity}" /> instance.</param>
    /// <param name="request">The <see cref="HttpRequest" /> instance.</param>
    /// <param name="actionParameters">The <see cref="ODataRawQueryOptions" /> instance.</param>
    /// <param name="serviceProvider">The service provider.</param>
    /// <param name="odataQuerySettings">The <see cref="ODataQuerySettings" /> instance.</param>
    /// <typeparam name="TEntity">The entity type.</typeparam>
    /// <returns>Returns <see cref="IQueryable{TEntity}" /> instance.</returns>
    public static IQueryable ApplyOData<TEntity>(this IQueryable<TEntity> self, HttpRequest request, ODataActionParameters actionParameters, IServiceProvider serviceProvider, ODataQuerySettings odataQuerySettings = default)
    {
        var queryOptionsType = typeof(ODataQueryOptions);

        if (self is null)
        {
            throw new ArgumentNullException(nameof(self));
        }

        if (actionParameters is null)
        {
            throw new ArgumentNullException(nameof(actionParameters));
        }

        if (odataQuerySettings is null)
        {
            odataQuerySettings = new ODataQuerySettings();
        }

        var rawQuery = string.Empty;
        if (actionParameters.ContainsKey("raw"))
        {
            rawQuery = HttpUtility.UrlDecode(actionParameters["raw"].ToString());
            actionParameters.Remove("raw");

            if (Uri.TryCreate(rawQuery, UriKind.Absolute, out Uri absRawQuery))
            {
                rawQuery = absRawQuery.Query;
            }

            request.Query = new QueryCollection(HttpUtility.ParseQueryString(rawQuery).ToDictionary<string, StringValues>());
        }
        else
        {
            request.Query = new QueryCollection(actionParameters.ToDictionary(k => $"${HttpUtility.UrlDecode(k.Key)}", v => new StringValues(HttpUtility.UrlDecode(v.Value.ToString()))));
        }

        //// request.QueryString = new QueryString("?" + string.Join("&", request.Query.Select(x => x.Key + "=" + x.Value)));

        var edmModel = serviceProvider.GetRequiredService<IEdmModel>();
        var odataQueryContext = new ODataQueryContext(edmModel, typeof(TEntity), null);
        var odataQueryOptions = new ODataQueryOptions<TEntity>(odataQueryContext, request);
        var queryOptionParser = new ODataQueryOptionParser(
            edmModel,
            edmModel.FindType(typeof(TEntity).FullName).AsElementType(),
            edmModel.FindDeclaredNavigationSource(typeof(TEntity).FullName),
            request.Query.ToDictionary(k => k.Key, v => v.Value.ToString()),
            serviceProvider);

        return odataQueryOptions.ApplyTo(self, odataQuerySettings);
    }
}

In the example below you will need an extension for ActionConfiguration like this:

// <summary>
/// Extensions for <see cref="ActionConfiguration" />.
/// </summary>
public static class ActionConfigurationExtensions
{
    /// <summary>
    /// Adds OData parameters to the <see cref="ActionConfiguration" />.
    /// </summary>
    /// <param name="actionConfiguration">The <see cref="ActionConfiguration" /> instance.</param>
    /// <returns>Returns current <see cref="ActionConfiguration" /> instance.</returns>
    public static ActionConfiguration AddODataParameters(this ActionConfiguration actionConfiguration)
    {
        foreach (var name in typeof(ODataRawQueryOptions).GetProperties().Select(p => p.Name.ToLower()))
        {
            actionConfiguration
                .Parameter<string>(name)
                .Optional();
        }

        actionConfiguration
                .Parameter<string>("raw")
                .Optional();

        return actionConfiguration;
    }
}

Example how to use it:

  1. Create an action like follow:
builder.EntityType<ExampleEntity>()
   .Collection
   .Action(nameof(ExampleController.GetExamples))
   .ReturnsCollectionFromEntitySet<ExampleEntity>("Examples")
   .AddODataParameters();
  1. Add an action in your controller:
[HttpPost]
public ActionResult<IQueryable<ExampleEntity>> GetExamples(ODataActionParameters parameters, [FromServices] IServiceProvider serviceProvider)
{
   if (parameters is null)
   {
       throw new ArgumentNullException(nameof(parameters));
   }

   if (serviceProvider is null)
   {
       throw new ArgumentNullException(nameof(serviceProvider));
   }

   return this.Ok(this.Repository.GetAll<ExampleEntity>().ApplyOData(this.Request, parameters, serviceProvider));
}

Example HTTP Post requests:

URL: /odata/examples/getexamples CONTENT:

{
  "raw": "http://localhost/odata/examples?%24filter%3Dname%20eq%20%27test%27"
}
{
  "filter": "name eq 'test'",
  "skip": "20",
  "count": "true"
}
like image 44
TheAifam5 Avatar answered Nov 15 '22 08:11

TheAifam5