AutoMapper.Extensions.OData: multiple orderby throws exception

Opened on SO as well: stack overflow

When trying to execute something like /odata/roles?$orderby=IsActive,Name I get the below exception.

"No generic method ‘ThenBy’ on type ‘System.Linq.Queryable’ is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic. "

"System.InvalidOperationException: No generic method 'ThenBy' on type 'System.Linq.Queryable' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic. at System.Linq.Expressions.Expression.FindMethod(Type type, String methodName, Type[] typeArgs, Expression[] args, BindingFlags flags) at System.Linq.Expressions.Expression.Call(Type type, String methodName, Type[] typeArguments, Expression[] arguments) at AutoMapper.AspNet.OData.LinqExtensions.GetOrderByMethod[T](ODataQueryOptions1 options, Expression expression) at AutoMapper.AspNet.OData.LinqExtensions.GetQueryableExpression[T](ODataQueryOptions1 options) at AutoMapper.AspNet.OData.QueryableExtensions.GetAsync[TModel,TData](IQueryable1 query, IMapper mapper, ODataQueryOptions1 options, HandleNullPropagationOption handleNullPropagation) at Mpi.PlanningApp.Web.Features.Roles.RolesController.Get(ODataQueryOptions1 options) in C:\Users\A330429\Source\Repos\VVC\Planning App\src\Mpi.PlanningApp.Web\Features\Roles\RolesController.cs:line 52 at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at System.Threading.Tasks.ValueTask1.get_Result() at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync() at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync() at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync() at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext) at Hellang.Middleware.ProblemDetails.ProblemDetailsMiddleware.Invoke(HttpContext context)"

Posted more code examples on stack overflow. If it’s needed, I can copy it to here.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 15 (15 by maintainers)

Commits related to this issue

Most upvoted comments

Sounds good. I will create a pr with the current fix tonight with the smaller methods you provided. I tested that, and works as it should now.

Get Outlook for Androidhttps://aka.ms/ghei36


From: Blaise Taylor notifications@github.com Sent: Friday, January 31, 2020 11:42:05 AM To: AutoMapper/AutoMapper.Extensions.OData AutoMapper.Extensions.OData@noreply.github.com Cc: Xavier tuffen_91@hotmail.com; Author author@noreply.github.com Subject: Re: [AutoMapper/AutoMapper.Extensions.OData] multiple orderby throws exception (#3)

The problem with paging is that we handle paging by generating the Queryable.Skip().Take() expression but OData also queries the result i.e. you have two records in the database. Our query skips the first one and OData’s EnableQuery skips the second one. Remove [EnableQuery()] from your controller method for paging to work. I’ll create 3 separate issues for:

  • Handling CountQueryOption without OData’s EnableQuery.
  • Handling selection of a subset of fields on the server.
  • Mention this issue of possible double evaluation because of EnableQuery.

We’ll only address the ThenBy() bug in this issue.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHubhttps://github.com/AutoMapper/AutoMapper.Extensions.OData/issues/3?email_source=notifications&email_token=AIMW3QV5EHPTFKXHGGWD5ZLRAP573A5CNFSM4KMU3IWKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKOIERA#issuecomment-580682308, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AIMW3QTQBJTU5WSKQUVK4XLRAP573ANCNFSM4KMU3IWA.

This is nice. If you plan to create a PR consider shorter methods like below.

You won’t need the appyTo (you’re creating the whole lambda expression from scratch). Also no need for ParameterExpression arg. That already lives in GetQueryableExpression<T>. I don’t know if OData supports sorting on child properties of references but hence the GetMemberInfoFromFullName below.

        public static MethodCallExpression GetOrderByMethod<T>(this ODataQueryOptions<T> options, Expression expression)
        {
            if (options.OrderBy == null && options.Top == null)
                return null;

            if (options.OrderBy == null)
            {
                return Expression.Call
                (
                    typeof(Queryable),
                    "Take",
                    new[] { typeof(T) }, expression, Expression.Constant(options.Top.Value)
                );
            }

            //IQueryable queryable = Enumerable.Empty<T>().AsQueryable();
            //queryable = options.OrderBy.ApplyTo(queryable, new ODataQuerySettings());
            //MethodCallExpression mce = (MethodCallExpression)queryable.Expression;

            //mce = Expression.Call(typeof(Queryable), mce.Method.Name, new Type[] { typeof(T), mce.Arguments[1].GetReturnType() }, expression, mce.Arguments[1]);

            //if (options.Skip != null)
            //    mce = Expression.Call(typeof(Queryable), "Skip", new[] { typeof(T) }, mce, Expression.Constant(options.Skip.Value));
            //if (options.Top != null)
            //    mce = Expression.Call(typeof(Queryable), "Take", new[] { typeof(T) }, mce, Expression.Constant(options.Top.Value));

            //return mce;

            return options.OrderBy.OrderByNodes.Aggregate(null, (MethodCallExpression mce, OrderByNode orderByNode) =>
            {
                OrderByPropertyNode propertyNode = (OrderByPropertyNode)orderByNode;
                return mce == null
                    ? expression.GetOrderByCall(propertyNode.Property.Name, orderByNode.Direction)
                    : mce.GetThenByCall(propertyNode.Property.Name, orderByNode.Direction);
            })
            .GetSkipCall(options.Skip)
            .GetTakeCall(options.Top);
        }

        public static MethodCallExpression GetSkipCall(this MethodCallExpression expression, SkipQueryOption skip)
        {
            if (skip == null) return expression;

            return Expression.Call
            (
                typeof(Queryable),
                "Skip",
                new[] { expression.GetUnderlyingElementType() },
                expression,
                Expression.Constant(skip.Value)
            );
        }

        public static MethodCallExpression GetTakeCall(this MethodCallExpression expression, TopQueryOption top)
        {
            if (top == null) return expression;

            return Expression.Call
            (
                typeof(Queryable),
                "Take",
                new[] { expression.GetUnderlyingElementType() },
                expression,
                Expression.Constant(top.Value)
            );
        }

        public static MethodCallExpression GetOrderByCall(this Expression expression, string memberFullName, OrderByDirection sortDirection, string selectorParameterName = "a")
        {
            Type sourceType = expression.GetUnderlyingElementType();
            MemberInfo memberInfo = sourceType.GetMemberInfoFromFullName(memberFullName);
            return Expression.Call
            (
                typeof(Queryable),
                sortDirection == OrderByDirection.Ascending ? "OrderBy" : "OrderByDescending",
                new Type[] { sourceType, memberInfo.GetMemberType() },
                expression,
                memberFullName.GetTypedSelector(sourceType, selectorParameterName)
            );
        }

        public static MethodCallExpression GetThenByCall(this MethodCallExpression expression, string memberFullName, OrderByDirection sortDirection, string selectorParameterName = "a")
        {
            Type sourceType = expression.GetUnderlyingElementType();
            MemberInfo memberInfo = sourceType.GetMemberInfoFromFullName(memberFullName);
            return Expression.Call
            (
                typeof(Queryable),
                sortDirection == OrderByDirection.Ascending ? "ThenBy" : "ThenByDescending",
                new Type[] { sourceType, memberInfo.GetMemberType() },
                expression,
                memberFullName.GetTypedSelector(sourceType, selectorParameterName)
            );
        }

        public static Type GetUnderlyingElementType(this Expression expression)
            => expression.Type.GetUnderlyingElementType();

        public static MemberInfo GetMemberInfoFromFullName(this Type type, string propertyFullName)
        {
            if (propertyFullName.IndexOf('.') < 0)
            {
                return type.GetMemberInfo(propertyFullName);
            }

            string propertyName = propertyFullName.Substring(0, propertyFullName.IndexOf('.'));
            string childFullName = propertyFullName.Substring(propertyFullName.IndexOf('.') + 1);

            return GetMemberInfoFromFullName(type.GetMemberInfo(propertyName).GetMemberType(), childFullName);
        }