【问题标题】:Possible to post ODataQueryOptions from the Http Request body?可以从 Http 请求正文中发布 ODataQueryOptions 吗?
【发布时间】:2014-08-01 19:44:54
【问题描述】:

我正在实现一个 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 查询,其中可能包含大量“Field eq Id”过滤器,这些过滤器被捕获到 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 参数,并且该参数导致为“空”。如果我删除“[FromBody]”属性,查询选项对象将从请求 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 请求?我这里还有什么遗漏的吗?

【问题讨论】:

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


    【解决方案1】:

    您可以在帖子正文中传递查询选项的原始字符串值, 并在控制器的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();
    }
    

    【讨论】:

    • 有没有办法从客户端发送所有选项,如 OrderBy、Filter、Skip、Top、Count 等?
    【解决方案2】:

    我刚刚在原始版本的基础上编写了 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
            );
        }
    

    【讨论】:

      【解决方案3】:

      dotnet core 2.2 我的两分钱。也应该在 dotnet core 3.x 上工作,但不能保证。

      处理所有 OData 查询参数。

      这会将ODataActionParameters 中的raw 参数传递给HttpRequestQuery 属性(不包括主机),或者如果不存在,我们创建ODataActionParameters 的一个基数。

      IQueryable{T} 的扩展,它应用 OData 查询选项。

      /// <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 的扩展名,如下所示:

      // <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();
      
      1. 在控制器中添加操作:
      [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));
      }
      

      HTTP Post 请求示例:

      网址:/odata/examples/getexamples 内容:

      {
        "raw": "http://localhost/odata/examples?%24filter%3Dname%20eq%20%27test%27"
      }
      
      {
        "filter": "name eq 'test'",
        "skip": "20",
        "count": "true"
      }
      

      【讨论】:

        猜你喜欢
        • 2016-03-12
        • 2015-03-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-08
        • 1970-01-01
        • 2015-04-01
        相关资源
        最近更新 更多