可以从Http Request主体发布ODataQueryOptions吗?

时间:2014-06-11 16:41:52

标签: jquery odata asp.net-web-api2

我正在实现一个Web API接口,以支持一些相当复杂的查询来运行它,并且遇到了最大请求URI长度的问题。

我的Web API方法的定义如下(使用Automapper执行DTO投影):

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

我的请求包含一个动态构建的OData查询,其中包含可能大量的&lt; field eq Id&#39;捕获到ODataQueryOptions参数中的过滤器,然后应用于IQueryable数据库上下文。例如:

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

一旦请求URI的长度达到某个限制,就会出现问题。任何URI长度超过该限制的请求都会导致404错误。经过一些测试后,此限制似乎在2KB范围内(具有2065个字符的URI工作正常,而使用Chrome,IE或FF的2105个则无法使用)。

对此的简单解决方案似乎是将请求类型从GET更改为POST请求,在主体中发送搜索查询而不是URI。我遇到了一些试图使其工作的问题,因为我似乎无法从POST请求中正确填充ODataQueryOptions对象。我的Web API方法现在看起来像这样:

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

正如您所看到的,我尝试从请求正文填充查询选项而不是URI。到目前为止,我还没有能够从请求中填充ODataQueryOptions参数,并且该参数导致为&#39; null&#39;。如果我删除了[FromBody]&#39;如果属性,查询选项对象将从请求URI中正确填充,但仍存在相同的URI长度问题。

以下是我如何从浏览器调用该方法的示例(使用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',
});

首先,是否有可能做我在这里尝试做的事情(在请求正文中发布ODataQueryOptions)?如果是这样,我正确构建POST请求吗?我还有什么别的吗?

3 个答案:

答案 0 :(得分:2)

您可以在帖子正文中传递查询选项的原始字符串值, 并在控制器的post方法中构造一个查询选项。

以下代码仅适用于过滤查询选项。 您可以用同样的方式添加其他查询选项。

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

答案 1 :(得分:0)

我刚刚基于原始版本编写了ODataQueryOption的这种快速实现。区别在于odata的属性是从HttpRequest而不是原始版本的HttpRequestMessage中获取的。仍然我相信,最好增加Web服务器配置中的最大请求uri长度,并使用GET而不是POST和默认的ODataQueryOption,这是我最终在自己的项目中完成的。

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

要使用它,我们将需要当前的请求对象

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

答案 2 :(得分:0)

我的2美分是dotnet core 2.2的价格。应该也可以在dotnet core 3.x上工作,但不能保证。

处理所有OData查询参数。

这会将raw参数从ODataActionParameters传递到HttpRequest的{​​{1}}属性(不包括主机),或者如果不存在,我们将创建一个Query

适用于OData查询选项的ODataActionParameters的扩展名。

IQueryable{T}

在下面的示例中,您将需要/// <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); } } 的扩展名,如下所示:

ActionConfiguration

示例用法:

  1. 创建如下操作:
// <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;
    }
}
  1. 在控制器中添加操作:
builder.EntityType<ExampleEntity>()
   .Collection
   .Action(nameof(ExampleController.GetExamples))
   .ReturnsCollectionFromEntitySet<ExampleEntity>("Examples")
   .AddODataParameters();

HTTP Post请求示例:

URL:/ odata / examples / getexamples 内容:

[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));
}
{
  "raw": "http://localhost/odata/examples?%24filter%3Dname%20eq%20%27test%27"
}