Possible to post ODataQueryOptions from the Http Request body?

淺唱寂寞╮ 提交于 2020-01-01 10:02:10

问题


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?


回答1:


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();
}



回答2:


I've just wrote this quick implementation of ODataQueryOption based on the original version. The difference is that properties for odata are fetched from HttpRequest instead of HttpRequestMessage as in original version. Still I believe it is better just to increase the maximum request uri length in the web server configuration and use GET instead of POST with the default ODataQueryOption , which I did eventually in my own project.

public class ODataQueryOptionsPost<T> : ODataQueryOptions<T>
{
    private RawValues2 rawValues;
    private IAssembliesResolver _assembliesResolver2;
    public FilterQueryOption FilterQueryOption { get; set; }


    public ODataQueryOptionsPost(ODataQueryContext context, HttpRequestMessage request, HttpRequest httpRequest) :
        base(context, request)
    {
        if (context == null)
            throw new Exception(nameof(context));
        if (request == null)
            throw new Exception(nameof(request));
        if (request.GetConfiguration() != null)
            _assembliesResolver2 = request.GetConfiguration().Services.GetAssembliesResolver();
        _assembliesResolver2 =
            this._assembliesResolver2 ?? (IAssembliesResolver) new DefaultAssembliesResolver();
        this.rawValues = new RawValues2();
        var filter = GetValue(httpRequest.Params, "$filter");
        if (!string.IsNullOrWhiteSpace(filter))
        {
            rawValues.Filter = filter;
            FilterQueryOption = new FilterQueryOption(filter, context);
        }

        var orderby = GetValue(httpRequest.Params, "$orderby");
        if (!string.IsNullOrWhiteSpace(orderby))
        {
            rawValues.OrderBy = orderby;
            OrderbyOption = new OrderByQueryOption(orderby, context);
        }

        var top = GetValue(httpRequest.Params, "$top");
        if (!string.IsNullOrWhiteSpace(top))
        {
            rawValues.Top = top;
            TopOption = new TopQueryOption(top, context);
        }

        var skip = GetValue(httpRequest.Params, "$skip");
        if (!string.IsNullOrWhiteSpace(skip))
        {
            rawValues.Skip = skip;
            SkipOption = new SkipQueryOption(skip, context);
        }

        var select = GetValue(httpRequest.Params, "$select");
        if (!string.IsNullOrWhiteSpace(select))
        {
            rawValues.Select = select;
        }

        var inlinecount = GetValue(httpRequest.Params, "$inlinecount");
        if (!string.IsNullOrWhiteSpace(inlinecount))
        {
            rawValues.InlineCount = inlinecount;
            InlineCountOption = new InlineCountQueryOption(inlinecount, context);
        }

        var expand = GetValue(httpRequest.Params, "$expand");
        if (!string.IsNullOrWhiteSpace(expand))
        {
            rawValues.Expand = expand;
        }

        var format = GetValue(httpRequest.Params, "$format");
        if (!string.IsNullOrWhiteSpace(format))
        {
            rawValues.Format = format;
        }

        var skiptoken = GetValue(httpRequest.Params, "$skiptoken");
        if (!string.IsNullOrWhiteSpace(skiptoken))
        {
            rawValues.SkipToken = skiptoken;
        }
    }

    public InlineCountQueryOption InlineCountOption { get; set; }

    public SkipQueryOption SkipOption { get; set; }

    public TopQueryOption TopOption { get; set; }

    public OrderByQueryOption OrderbyOption { get; set; }

    private static string GetValue(NameValueCollection httpRequestParams, string key)
    {
        return httpRequestParams.GetValues(key)?.SingleOrDefault();
    }

    public override IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings)
    {
        if (query == null)
            throw new Exception(nameof(query));
        if (querySettings == null)
            throw new Exception(nameof(querySettings));
        IQueryable queryable = query;
        if (this.FilterQueryOption != null)
            queryable = this.FilterQueryOption.ApplyTo(queryable, querySettings, this._assembliesResolver2);
        if (this.InlineCountOption != null && !this.Request.ODataProperties().TotalCount.HasValue)
        {
            long? entityCount = this.InlineCountOption.GetEntityCount(queryable);
            if (entityCount.HasValue)
                this.Request.ODataProperties().TotalCount = new long?(entityCount.Value);
        }

        OrderByQueryOption orderBy = this.OrderbyOption;
        if (querySettings.EnsureStableOrdering &&
            (this.Skip != null || this.Top != null || querySettings.PageSize.HasValue))
            orderBy = orderBy == null
                ? GenerateDefaultOrderBy(this.Context)
                : EnsureStableSortOrderBy(orderBy, this.Context);
        if (orderBy != null)
            queryable = (IQueryable) orderBy.ApplyTo(queryable, querySettings);
        if (this.SkipOption != null)
            queryable = this.SkipOption.ApplyTo(queryable, querySettings);
        if (this.TopOption != null)
            queryable = this.TopOption.ApplyTo(queryable, querySettings);
        if (this.SelectExpand != null)
        {
            this.Request.ODataProperties().SelectExpandClause = this.SelectExpand.SelectExpandClause;
            queryable = this.SelectExpand.ApplyTo(queryable, querySettings);
        }

        if (querySettings.PageSize.HasValue)
        {
            bool resultsLimited;
            queryable = LimitResults(queryable as IQueryable<T>, querySettings.PageSize.Value, out resultsLimited);
            if (resultsLimited && this.Request.RequestUri != (Uri) null &&
                (this.Request.RequestUri.IsAbsoluteUri && this.Request.ODataProperties().NextLink == (Uri) null))
                this.Request.ODataProperties().NextLink =
                    GetNextPageLink(this.Request, querySettings.PageSize.Value);
        }

        return queryable;
    }

    private static OrderByQueryOption GenerateDefaultOrderBy(ODataQueryContext context)
    {
        string rawValue = string.Join(",",
            GetAvailableOrderByProperties(context)
                .Select<IEdmStructuralProperty, string>(
                    (Func<IEdmStructuralProperty, string>) (property => property.Name)));
        if (!string.IsNullOrEmpty(rawValue))
            return new OrderByQueryOption(rawValue, context);
        return (OrderByQueryOption) null;
    }

    private static OrderByQueryOption EnsureStableSortOrderBy(OrderByQueryOption orderBy, ODataQueryContext context)
    {
        HashSet<string> usedPropertyNames = new HashSet<string>(orderBy.OrderByNodes.OfType<OrderByPropertyNode>()
            .Select<OrderByPropertyNode, string>((Func<OrderByPropertyNode, string>) (node => node.Property.Name)));
        IEnumerable<IEdmStructuralProperty> source = GetAvailableOrderByProperties(context)
            .Where<IEdmStructuralProperty>(
                (Func<IEdmStructuralProperty, bool>) (prop => !usedPropertyNames.Contains(prop.Name)));
        if (source.Any<IEdmStructuralProperty>())
        {
            orderBy = new OrderByQueryOption(orderBy.RawValue, context);
            foreach (IEdmStructuralProperty structuralProperty in source)
                orderBy.OrderByNodes.Add((OrderByNode) new OrderByPropertyNode((IEdmProperty) structuralProperty,
                    OrderByDirection.Ascending));
        }

        return orderBy;
    }

    private static IEnumerable<IEdmStructuralProperty> GetAvailableOrderByProperties(ODataQueryContext context)
    {
        IEdmEntityType elementType = context.ElementType as IEdmEntityType;
        if (elementType != null)
            return (IEnumerable<IEdmStructuralProperty>) (elementType.Key().Any<IEdmStructuralProperty>()
                    ? elementType.Key()
                    : elementType.StructuralProperties()
                        .Where<IEdmStructuralProperty>(
                            (Func<IEdmStructuralProperty, bool>) (property => property.Type.IsPrimitive())))
                .OrderBy<IEdmStructuralProperty, string>(
                    (Func<IEdmStructuralProperty, string>) (property => property.Name));
        return Enumerable.Empty<IEdmStructuralProperty>();
    }

    internal static Uri GetNextPageLink(HttpRequestMessage request, int pageSize)
    {
        return GetNextPageLink(request.RequestUri, request.GetQueryNameValuePairs(), pageSize);
    }

    internal static Uri GetNextPageLink(Uri requestUri, IEnumerable<KeyValuePair<string, string>> queryParameters,
        int pageSize)
    {
        StringBuilder stringBuilder = new StringBuilder();
        int num = pageSize;
        foreach (KeyValuePair<string, string> queryParameter in queryParameters)
        {
            string key = queryParameter.Key;
            string str1 = queryParameter.Value;
            switch (key)
            {
                case "$top":
                    int result1;
                    if (int.TryParse(str1, out result1))
                    {
                        str1 = (result1 - pageSize).ToString((IFormatProvider) CultureInfo.InvariantCulture);
                        break;
                    }

                    break;
                case "$skip":
                    int result2;
                    if (int.TryParse(str1, out result2))
                    {
                        num += result2;
                        continue;
                    }

                    continue;
            }

            string str2 = key.Length <= 0 || key[0] != '$'
                ? Uri.EscapeDataString(key)
                : 36.ToString() + Uri.EscapeDataString(key.Substring(1));
            string str3 = Uri.EscapeDataString(str1);
            stringBuilder.Append(str2);
            stringBuilder.Append('=');
            stringBuilder.Append(str3);
            stringBuilder.Append('&');
        }

        stringBuilder.AppendFormat("$skip={0}", (object) num);
        return new UriBuilder(requestUri)
        {
            Query = stringBuilder.ToString()
        }.Uri;
    }
}

public class RawValues2
{
    public string Filter { get; set; }
    public string OrderBy { get; set; }
    public string Top { get; set; }
    public string Skip { get; set; }
    public string Select { get; set; }
    public string InlineCount { get; set; }
    public string Expand { get; set; }
    public string Format { get; set; }
    public string SkipToken { get; set; }
}

To use it we're going to need current request object

    [HttpPost]
    public async Task<PageResult<TypeOfYourViewModel>> GetDataViaPost(ODataQueryOptions<TypeOfYourViewModel> options)
    {
        IQueryable<TypeOfYourViewModel> result = await GetSomeData();

        var querySettings = new ODataQuerySettings
        {
            EnsureStableOrdering = false,
            HandleNullPropagation = HandleNullPropagationOption.False
        };


        var optionsPost = new ODataQueryOptionsPost<TypeOfYourViewModel>(options.Context, Request, HttpContext.Current.Request);
        var finalResult = optionsPost.ApplyTo(result, querySettings);

        var uri = Request.ODataProperties().NextLink;
        var inlineCount = Request.ODataProperties().TotalCount;
        var returnedResult = (finalResult as IQueryable<T>).ToList();
        return new PageResult<TypeOfYourViewModel>(
            returnedResult,
            uri,
            inlineCount
        );
    }


来源:https://stackoverflow.com/questions/24168213/possible-to-post-odataqueryoptions-from-the-http-request-body

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!