Possible to post ODataQueryOptions from the Http Request body?

后端 未结 3 2096
盖世英雄少女心
盖世英雄少女心 2021-02-15 16:44

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.

Th

相关标签:
3条回答
  • 2021-02-15 17:02

    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();
    }
    
    0 讨论(0)
  • 2021-02-15 17:10

    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
            );
        }
    
    0 讨论(0)
  • 2021-02-15 17:14

    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"
    }
    
    0 讨论(0)
提交回复
热议问题