Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OrderBy string column name #53

Closed
ovation22 opened this issue Oct 22, 2020 · 39 comments
Closed

OrderBy string column name #53

ovation22 opened this issue Oct 22, 2020 · 39 comments

Comments

@ovation22
Copy link

Is there any easy way to pass a string column name to OrderBy or OrderByDescending, or provide an extension method to do so?

Expected:
Query.OrderBy("Name");

Would effectively be the same as calling:
Query.OrderBy(x => x.Name);

This would allow for passing in different column names into the specification and not having to modify, if/else, or switch on columns.

@fiseni
Copy link
Collaborator

fiseni commented Oct 22, 2020

Hi @ovation22

That would be handy actually, but we're constrained by the functionalities that are provided from LINQ. There is no method that accepts string. All methods stricly accept keySelectors.

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer);
public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);

In order this to be implemented through this package, we effectively have to find the property by the string that is given. The other way is easy, but this one is a tedious task which will incorporate reflection, generating the predicate and a lot of other details.

@fiseni
Copy link
Collaborator

fiseni commented Oct 22, 2020

Are you OK with the answer, should we close the issue here?
Indeed, it would be useful, but if we want the functionality, it would be better if we make a PR to .NET, not here. They might elaborate even further and analyze all edge cases, and they might have other reasons why this was not implemented in the first place.

@ardalis
Copy link
Owner

ardalis commented Oct 22, 2020

Some options here that might be helpful:
https://stackoverflow.com/questions/31955025/generate-ef-orderby-expression-by-string

@fiseni
Copy link
Collaborator

fiseni commented Oct 22, 2020

Indeed, we find the property by reflection, and then dynamically generate the expression.
You want to include this as part of the package here? Or you meant as an extension that users can use on their own?

@ovation22
Copy link
Author

ovation22 commented Oct 22, 2020

Thanks, I've got it working with a switch/Expression<Func for now. I'll explore the extension method/expression trees some more.

Thanks for the help!

Feel free to close.

@ardalis
Copy link
Owner

ardalis commented Oct 22, 2020

I think users can create their own extensions. Thanks all!

@ardalis ardalis closed this as completed Oct 22, 2020
@lankymart
Copy link

Do we have any examples of how you implement an extension like this for ISpecificationBuilder<T>? Understand how it works for an IQueryable<T> but Query isn't an IQueryable<T> or am I missing something obvious?

@fiseni
Copy link
Collaborator

fiseni commented Feb 6, 2021

Yea you're right, we have no examples of that. Before answering that, we should clarify few topics first.
Specification class is nothing else than just a bag of properties. It doesn't have any behavior by itself, the main purpose is to just hold some information. You may consider it as DTO. Then, during the evaluation, we utilize this saved data to construct the query for the ORM (in our case EF). So, in this context, the builder has no importance, we could simply set all those properties directly and populate the specification in that way.

In order to enforce some rules and to avoid wrong usages, we thought it's wise to create a mechanism for populating the specification in a more safe manner. And that's what the builder is. It's just a helper to correctly populate the specification, and it has no significance other than that. So, the specification is a data object which additionally provides a special mechanism how that data should be populated.

The other benefits of the builder, as implemented in our case

  • Collects/groups all available assignment operations under one single construct.
  • It has a fluent design, and it resembles LINQ. The users are quite familiar with the syntax.
  • Since we used the same LINQ method names, you can just copy/paste the expressions. This is valuable during the transition phase if you want to apply this pattern in an existing project. For example, let's assume this EF expression
var customers = await dbContext.Customers
                            .Where(x => x.IsActive)
                            .Include(x => x.Stores)
				.ThenInclude(x=>x.Addresses)
                            .OrderBy(x => x.DisplayName)
                            .Skip(20)
                            .Take(5)
                            .ToListAsync();

Now, you can literally copy the expressions and define them in the specification, and it will work as it is.

public CustomerSpec()
{
	Query.Where(x => x.IsActive)
		.Include(x => x.Stores)
                    .ThenInclude(x=>x.Addresses)
		.OrderBy(x => x.DisplayName)
		.Skip(20)
		.Take(5);
}

How to write extensions

For demonstration purposes, let's add support for AsSplitQuery feature (recently requested).
First of all, we need the base specification to be able to hold information whether this is enabled for a particular specification or not. So, inherit the Specification class and create your own base specification. Then, use the newly created one as a base for all the defined specifications in your project.

public class MyBaseSpecification<T> : Specification<T>
{
	public bool AsSplitQuery { get; set; }
}

Basically, that's all you need, you don't even have to modify the builder, just set this property directly if you wish.

public CustomerSpec()
{
	Query.Where(x => x.IsActive);
	
	AsSplitQuery = true;
}

Now, that you have this extra information, use it during the evaluation to add this extra expression in the EF query.

Probably, soon we can add step by step tutorial on how to write extensions, including how to extend the builder. We recently refactored the internal infrastructure, and all you need is to write an extension method to the builder interface.

@fiseni
Copy link
Collaborator

fiseni commented Feb 6, 2021

@ardalis probably we might add this to the Readme as well.

@lankymart
Copy link

@fiseni This is all really good information, however I’m still unclear how you would use this to pass an OrderBy via field name instead of Lambda. Steve’s answer was just a bit vague;

I think users can create their own extensions. Thanks all!

@fiseni
Copy link
Collaborator

fiseni commented Feb 7, 2021

Regarding this issue, as I stated in the first reply it's not a quite straightforward implementation. The LINQ itself doesn't have support for ordering by string. And that's by a design and for a good reason. It's not a strongly typed definition.

Anyhow, here is the extension for that. You can use it in your projects but we don't see it as part of the package.

LINQ extensions

First of all, let's write extensions for LINQ

public static class LinqExtensions
{
    private static PropertyInfo GetPropertyInfo(Type objType, string name)
    {
        var properties = objType.GetProperties();
        var matchedProperty = properties.FirstOrDefault(p => p.Name == name);
        if (matchedProperty == null)
            throw new ArgumentException("name");

        return matchedProperty;
    }
    private static LambdaExpression GetOrderExpression(Type objType, PropertyInfo pi)
    {
        var paramExpr = Expression.Parameter(objType);
        var propAccess = Expression.PropertyOrField(paramExpr, pi.Name);
        var expr = Expression.Lambda(propAccess, paramExpr);
        return expr;
    }

    public static IQueryable<T> OrderBy<T>(this IQueryable<T> query, string name)
    {
        var propInfo = GetPropertyInfo(typeof(T), name);
        var expr = GetOrderExpression(typeof(T), propInfo);

        var method = typeof(Queryable).GetMethods().FirstOrDefault(m => m.Name == "OrderBy" && m.GetParameters().Length == 2);
        var genericMethod = method.MakeGenericMethod(typeof(T), propInfo.PropertyType);
        return (IQueryable<T>)genericMethod.Invoke(null, new object[] { query, expr });
    }
}

Specification extensions

Now that we have the appropriate LINQ method, lets create a new base specification class and write the extensions. Use this base class as a base for all your specifications in your project.

namespace Ardalis.Specification
{
    public class MySpecification<T> : Specification<T>
    {
        public string OrderByStringExpression { get; set; }
    }

    public static class MyExtensions
    {
        public static ISpecificationBuilder<T> OrderByColumnName<T>(
            this ISpecificationBuilder<T> specificationBuilder,
            string columnName)
        {
            ((MySpecification<T>)specificationBuilder.Specification).OrderByStringExpression = columnName;

            return specificationBuilder;
        }
    }
}
namespace Ardalis.Specification.EntityFrameworkCore
{
    public class MySpecificationEvaluator<T> : SpecificationEvaluator<T> where T : class
    {
        public IQueryable<T> GetQuery(IQueryable<T> inputQuery, MySpecification<T> specification)
        {
            var query = base.GetQuery(inputQuery, specification);

            if (!string.IsNullOrEmpty(specification.OrderByStringExpression))
            {
                query = query.OrderBy(specification.OrderByStringExpression);
            }

            return query;
        }
    }
}

Note: Ordering after pagination may lead to wrong results, so instead of inheriting from the base evaluator, you may want to fully implement your own evaluator with the appropriate evaluation order. Here is the evaluator. Copy the content and add the above logic accordingly.

@lankymart
Copy link

@fiseni This is great thank you very much, wasn't expecting such a detailed response but it's greatly appreciated and I'm sure it will help others. Would even go as far as suggesting it might be worth adding to the documentation in relation to extending Specification perhaps?

@fiseni
Copy link
Collaborator

fiseni commented Feb 9, 2021

Sure, soon we'll improve the documentation and add information regarding these tasks.

@lankymart
Copy link

lankymart commented Feb 10, 2021

@fiseni Just looking at this and the evaluation order is going to be important for pagination, so would it not make more sense if implementing your own evaluator instead of inheriting the base evaluator to convert the orderby string into the existing OrderExpressions property instead of adding a new one? That way we don't have to adjust the existing evaluation order or mess with the base class.

@fiseni
Copy link
Collaborator

fiseni commented Feb 10, 2021

Well, yea you can do it, but in that case, instead of building upon the IQueryable, you should construct Expression<Func<T, object>>. All you need in that case is just writing an extension to ISpecificationBuilder<T>.

Here you go, this is all you need. But you should test this thoroughly (I didn't test it).

public static class MyExtensions
{
    public static IOrderedSpecificationBuilder<T> OrderByColumnName<T>(
        this ISpecificationBuilder<T> specificationBuilder,
        string columnName)
    {
        var matchedProperty = typeof(T).GetProperties().FirstOrDefault(p => p.Name == columnName);
        if (matchedProperty == null)
            throw new ArgumentException("name");

        var paramExpr = Expression.Parameter(typeof(T));
        var propAccess = Expression.PropertyOrField(paramExpr, matchedProperty.Name);
        var expr = Expression.Lambda<Func<T, object?>>(propAccess, paramExpr);

        ((List<(Expression<Func<T, object?>> OrderExpression, OrderTypeEnum OrderType)>)specificationBuilder.Specification.OrderExpressions)
            .Add((expr, OrderTypeEnum.OrderBy));

        var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification);

        return orderedSpecificationBuilder;
    }
}

@lankymart
Copy link

lankymart commented Feb 10, 2021

@fiseni This is exactly the approach I'm taking, the only difference being I'm allowing a comma separated list of column names something like "field1,-field2" etc (the - denoting descending OrderTypeEnum to be applied). This is very helpful thank you.

@lankymart
Copy link

lankymart commented Feb 10, 2021

@fiseni Just wanted to say have implemented this and I'm testing it thoroughly, works really well. Thanks again for pointing me in the right direction.

public static class MyExtensions
{
    public static IOrderedSpecificationBuilder<T> OrderBy<T>(
        this ISpecificationBuilder<T> specificationBuilder,
        string orderByFields)
    {
        var fields = ParseOrderBy(orderByFields);
        if (fields != null)
        {
            foreach (var field in fields)
            {
                var matchedProperty = typeof(T).GetProperties().FirstOrDefault(p => p.Name.ToLower() == field.Key.ToLower());
                if (matchedProperty == null)
                    throw new ArgumentNullException("name");
    
                var paramExpr = Expression.Parameter(typeof(T));
                var propAccess = Expression.PropertyOrField(paramExpr, matchedProperty.Name);
                var expr = Expression.Lambda<Func<T, object?>>(propAccess, paramExpr);
                ((List<(Expression<Func<T, object?>> OrderExpression, OrderTypeEnum OrderType)>)specificationBuilder.Specification.OrderExpressions)
                    .Add((expr, field.Value));
            }
        }
        var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification);
    
        return orderedSpecificationBuilder;
    }
    
    private static IDictionary<string, OrderTypeEnum> ParseOrderBy(string orderByFields)
    {
        if (orderByFields is null) return null;
        var result = new Dictionary<string, OrderTypeEnum>();
        var fields = orderByFields.Split(',');
        for (var index = 0; index < fields.Length; index++)
        {
            var field = fields[index];
            var orderBy = OrderTypeEnum.OrderBy;
            if (field.StartsWith('-')) orderBy = OrderTypeEnum.OrderByDescending;
            if (index > 0)
            {
                orderBy = OrderTypeEnum.ThenBy;
                if (field.StartsWith('-')) orderBy = OrderTypeEnum.ThenByDescending;
            }
            if (field.StartsWith('-')) field = field.Substring(1);
            result.Add(field, orderBy);
        }
        return result;
    }
}

@fiseni
Copy link
Collaborator

fiseni commented Feb 10, 2021

Great, I'm glad it worked out!
Cheers!

@lkroliko
Copy link

lkroliko commented Aug 7, 2021

Code from Fiseni is helpfull (Thanks!) but when I try order by decimal column I get excepiotn "Expression of type 'System.Decimal' cannot be used for return type 'System.Object'". I was able to fix this code and I also optimized using reflection in loop.

public static class SpecificationBuilderExtensions
    {
        public static IOrderedSpecificationBuilder<T> OrderBy<T>(this ISpecificationBuilder<T> specificationBuilder, string orderByFields)
        {
            var fields = ParseOrderBy(orderByFields);
            if (fields != null)
            {
                foreach (var field in fields)
                {
                    //var matchedProperty = typeof(T).GetProperties().FirstOrDefault(p => p.Name.ToLower() == field.Key.ToLower());
                    var matchedProperty = typeof(T).GetProperty(field.Key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                    if (matchedProperty == null)
                        throw new ArgumentNullException(field.Key);

                    //Expression conversion = Expression.Convert(expression, typeof(object));
                    //func = Expression.Lambda<Func<T, Object>>(conversion, parameterExpression).Compile();

                    var paramExpr = Expression.Parameter(typeof(T));
                    var propAccess = Expression.PropertyOrField(paramExpr, matchedProperty.Name);
                    var propAccess2 = Expression.Convert(propAccess, typeof(object));
                    var expr = Expression.Lambda<Func<T, object>>(propAccess2, paramExpr);
                    ((List<(Expression<Func<T, object>> OrderExpression, OrderTypeEnum OrderType)>)specificationBuilder.Specification.OrderExpressions).Add((expr, field.Value));
                }
            }
            var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification);

            return orderedSpecificationBuilder;
        }

        private static IDictionary<string, OrderTypeEnum> ParseOrderBy(string orderByFields)
        {
            //Write your own implementation
        }
}

@ShadyNagy
Copy link
Contributor

@ardalis @fiseni
Could we add this to the package?

@davidhenley
Copy link
Contributor

davidhenley commented Sep 16, 2021

@lkroliko How did you fix the error?

System.ArgumentException: Expression of type 'System.Int32' cannot be used for return type 'System.Object'
System.ArgumentException: Expression of type 'System.Decimal' cannot be used for return type 'System.Object'

Is this the only addition needed?

var propAccess2 = Expression.Convert(propAccess, typeof(object));

Would it be easy to add a Dictionary that can map to properties?

var fieldToLambda = new Dictionary
{
  { "name", x => Name },
  { "address", x => x.Address.AddressLine1 }
};

Just trying to remove all this code:

           if (query.Order == SortOrder.ASC)
            {
                switch (query.Sort)
                {
                    case "type":
                        Query.OrderBy(x => x.Type.Name);
                        break;
                    case "id":
                        Query.OrderBy(x => x.Id);
                        break;
                    case "alphaId":
                        Query.OrderBy(x => x.AlphaId);
                        break;
                    case "city":
                        Query.OrderBy(x => x.PhysicalAddress.City);
                        break;
                    default: 
                        Query.OrderBy(x => x.Name);
                        break;
                }
            }
            else
            {
                switch (query.Sort)
                {
                    case "type":
                        Query.OrderByDescending(x => x.Type.Name);
                        break;
                    case "id":
                        Query.OrderByDescending(x => x.Id);
                        break;
                    case "alphaId":
                        Query.OrderByDescending(x => x.AlphaId);
                        break;
                    case "city":
                        Query.OrderByDescending(x => x.PhysicalAddress.City);
                        break;
                    default:
                        Query.OrderByDescending(x => x.Name);
                        break;
                }
            }

@fiseni
Copy link
Collaborator

fiseni commented Sep 16, 2021

You need the conversion part to be able to use int (also for decimal, guid, etc.)
If you're trying to order by only one field, then this might do the trick for you. You can pass a newly created enum in your app, or just "asc", "desc" string parameters. On the other hand, if you wanna have a list of fields as parameter, then try one of the above answers.

public enum OrderByEnum
{
    Ascending = 1,
    Descending = 2
}

public static class MyExtensions
{
    public static IOrderedSpecificationBuilder<T> OrderBy<T>(
        this ISpecificationBuilder<T> specificationBuilder,
        string fieldName,
        string orderBy)
    {
        if (orderBy.Equals("desc", StringComparison.InvariantCultureIgnoreCase))
        {
            return OrderBy(specificationBuilder, fieldName, OrderByEnum.Descending);
        }
        else
        {
            return OrderBy(specificationBuilder, fieldName, OrderByEnum.Ascending);
        }
    }

    public static IOrderedSpecificationBuilder<T> OrderBy<T>(
        this ISpecificationBuilder<T> specificationBuilder,
        string fieldName,
        OrderByEnum orderByEnum)
    {
        var matchedProperty = typeof(T).GetProperty(fieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
        if (matchedProperty == null)
        {
            //Actually you can decide no to throw here. Ordering is not a crucial task.
            throw new ArgumentException("name");
        }

        var paramExpr = Expression.Parameter(typeof(T));
        var propAccess = Expression.PropertyOrField(paramExpr, matchedProperty.Name);
        var propAccessConverted = Expression.Convert(propAccess, typeof(object));
        var expr = Expression.Lambda<Func<T, object?>>(propAccessConverted, paramExpr);

        if (orderByEnum == OrderByEnum.Descending)
        {
            ((List<(Expression<Func<T, object?>> OrderExpression, OrderTypeEnum OrderType)>)specificationBuilder.Specification.OrderExpressions)
                .Add((expr, OrderTypeEnum.OrderByDescending));
        }
        else
        {
            // Ascending would be default.
            ((List<(Expression<Func<T, object?>> OrderExpression, OrderTypeEnum OrderType)>)specificationBuilder.Specification.OrderExpressions)
                .Add((expr, OrderTypeEnum.OrderBy));
        }

        var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification);

        return orderedSpecificationBuilder;
    }
}

@davidhenley
Copy link
Contributor

davidhenley commented Sep 17, 2021

You are a godsend @fiseni thank you!

I have much to learn in writing Expressions

You have changed my code from:

            if (query.Order == SortOrder.ASC)
            {
                switch (query.Sort)
                {
                    case "id":
                        Query.OrderBy(x => x.Id);
                        break;
                    case "county":
                        Query.OrderBy(x => x.County);
                        break;
                    case "tax":
                        Query.OrderBy(x => x.Tax);
                        break;
                    default: 
                        Query.OrderBy(x => x.Name);
                        break;
                }
            }
            else
            {
                switch (query.Sort)
                {
                    case "id":
                        Query.OrderByDescending(x => x.Id);
                        break;
                    case "county":
                        Query.OrderByDescending(x => x.County);
                        break;
                    case "tax":
                        Query.OrderByDescending(x => x.Tax);
                        break;
                    default:
                        Query.OrderByDescending(x => x.Name);
                        break;
                }
            }

To:

Query.OrderBy(query.Sort, query.Order);

@davidhenley
Copy link
Contributor

davidhenley commented Sep 17, 2021

@fiseni how would you implement a lambda instead of a fieldname in this extension?

OrderBy(x => x.Address.AddressLine1, OrderByEnum.Ascending);

@fiseni
Copy link
Collaborator

fiseni commented Sep 17, 2021

Hmm, the lambda is supported out of the box. You just use Query.OrderBy or Query.OrderByDescending.
Did I misunderstand your question? The whole point with the fieldname is that you don't know the field beforehand.

@davidhenley
Copy link
Contributor

davidhenley commented Sep 17, 2021

I was meaning just so I can write it with the OrderByEnum as the second parameter, so I didn't have to write 2 queries based on the ordering so I could write it like this:

OrderBy(x => x.Address.AddressLine1, OrderByEnum.Ascending);

Instead of this:

if (query.Order == "asc")
  OrderBy(x => x.Address.AddressLine1);
else
  OrderByDescending(x => x.Address.AddressLine1);

I think this would be good to add to the library as it would get rid of a lot of duplicate code checking on the order.

So I just need to create another extension that passes the OrderByEnum down and orders ascending or descending?

If that's the case I'm sure I can figure it out.

@fiseni
Copy link
Collaborator

fiseni commented Sep 17, 2021

Oh ok. Btw, I have used the newly created enum OrderByEnum just as an example. You don't need that one and you can use the one you have SortOrder. We have decided to go with the OrderBy, OrderByDescending, ThenBy, ThenByDescending approach, since that's the official LINQ syntax. In that way, users can just copy-paste the code from their existing EF queries and create specifications. But, surely having additional extensions is not a deal breaker.

public enum OrderByEnum
{
    Ascending = 1,
    Descending = 2
}

public static class MyExtensions
{
    public static IOrderedSpecificationBuilder<T> OrderBy<T>(
        this ISpecificationBuilder<T> specificationBuilder,
        Expression<Func<T, object?>> orderExpression,
        OrderByEnum orderBy)
    {
        if (orderBy == OrderByEnum.Descending)
        {
            ((List<(Expression<Func<T, object?>> OrderExpression, OrderTypeEnum OrderType)>)specificationBuilder.Specification.OrderExpressions)
                .Add((orderExpression, OrderTypeEnum.OrderByDescending));
        }
        else
        {
            // Ascending is default.
            ((List<(Expression<Func<T, object?>> OrderExpression, OrderTypeEnum OrderType)>)specificationBuilder.Specification.OrderExpressions)
                .Add((orderExpression, OrderTypeEnum.OrderBy));
        }

        var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification);

        return orderedSpecificationBuilder;
    }
}

@davidhenley
Copy link
Contributor

davidhenley commented Sep 17, 2021

Awesome. Thank you!

That brings it down to:

var orderBy = new Dictionary<string, Expression<Func<Customer, object?>>>
{
   { "type", x => x.Type.Name },
   { "id", x => x.Id },
   { "alphaId", x => x.AlphaId },
   { "city", x => x.PhysicalAddress.City },
   { "name", x => x.Name },
};

Query.OrderBy(orderBy[query.Sort], query.Order);

@lankymart
Copy link

@davidhenley You are kind of re-hashing what myself and @fiseni already did, but each to their own.

@davidhenley
Copy link
Contributor

@lankymart I know I am really just learning and seeing what's available. Did you implement a lambda version that takes in a sort order enum as well?

@lankymart
Copy link

lankymart commented Sep 17, 2021

@davidhenley Didn't see the point as that's what the Query.OrderBy() and Query.OrderByDescending() do by default. It was more the dynamic element of the order by that was a concern and ISpecificationBuilder extensions solved that. It meant we could pass values like sort=fieldname and sort=-fieldname to our API to sort fields ascending and descending without extra logic being required.

@davidhenley
Copy link
Contributor

@lankymart thanks! Yeah I can see the use case for several different ways. I'm still learning expressions and your implementation has helped a lot!

@mjanofsky
Copy link

mjanofsky commented Feb 15, 2022

I get the an error on the following line and I could sure use some insight. OvercastUser is the class with a property I'm attempting to sort. I feel like I'm missing something outside of the Extension class.

        ((List<(Expression<Func<T, object?>> OrderExpression, OrderTypeEnum OrderType)>)
                specificationBuilder.Specification.OrderExpressions)
            .Add((expr, OrderTypeEnum.OrderBy));

System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List1[Ardalis.Specification.OrderExpressionInfo1[Overcast.Core.ProjectAggregates.OvercastUser.OvercastUser]]' to type 'System.Collections.Generic.List1[System.ValueTuple2[System.Linq.Expressions.Expression1[System.Func2[Overcast.Core.ProjectAggregates.OvercastUser.OvercastUser,System.Object]],Ardalis.Specification.OrderTypeEnum]]'.
at Overcast.Core.ProjectAggregates.Extensions.SpecificationOrderByColumnNameExtension.OrderBy[T](ISpecificationBuilder`1 specificationBuilder, String orderByFields)

@davidhenley
Copy link
Contributor

davidhenley commented Mar 2, 2022

I'm getting the same @mjanofsky on update from 5 to 6

@fiseni @ardalis Any idea why version 6 broke this?

Still works on 5.2.0

public static IOrderedSpecificationBuilder<T> OrderBy<T>(
        this ISpecificationBuilder<T> specificationBuilder,
        Expression<Func<T, object>> orderExpression,
        SortOrder orderBy)
    {
        ((List<(Expression<Func<T, object>> OrderExpression, OrderTypeEnum OrderType)>)specificationBuilder
                .Specification.OrderExpressions)
            .Add(orderBy == SortOrder.DESC
                ? (orderExpression, OrderTypeEnum.OrderByDescending)
                : (orderExpression, OrderTypeEnum.OrderBy));

        var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification);
        return orderedSpecificationBuilder;
    }
Unable to cast object of type
'System.Collections.Generic.List`1[Ardalis.Specification.OrderExpressionInfo`1[Entities.Customer]]' 
to type 'System.Collections.Generic.List`1[System.ValueTuple`2[System.Linq.Expressions.Expression`1[System.Func`2[Entities.Customer,System.Object]],Ardalis.Specification.OrderTypeEnum]]'.

@fiseni
Copy link
Collaborator

fiseni commented Mar 2, 2022

Hi @mjanofsky @davidhenley,

In version 6, we have a breaking change regarding the specification state (we have published this info in the release notes. It is on the Readme page as well).

The state for Order, Where, Search is no longer kept as a list of tuples, but there are separate specific types. This is also visible in the ISpecification contract. So, your code now would look something like:

public static IOrderedSpecificationBuilder<T> OrderBy<T>(
this ISpecificationBuilder<T> specificationBuilder,
Expression<Func<T, object?>> orderExpression,
SortOrder orderBy)
{
    ((List<OrderExpressionInfo<T>>)specificationBuilder.Specification.OrderExpressions)
        .Add(orderBy == SortOrder.DESC
            ? new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderByDescending)
            : new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderBy));

    var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification);
    return orderedSpecificationBuilder;
}

@davidhenley
Copy link
Contributor

Ah, I see! Makes sense. Thank you so much! Will close separate issue

@sarazababajani
Copy link

@davidhenley Could you please post the code that you used in your specification file? Thanks in advance...

@josephwambura
Copy link

Add

using Microsoft.EntityFrameworkCore;

in the code:

var sortOrder = "Name";
var descending = true;

if (descending)
     {
         Query.OrderByDescending(e => EF.Property<object>(e, sortOrder));
     }
     else
     {
         Query.OrderBy(e => EF.Property<object>(e, sortOrder));
     }

@josephwambura
Copy link

Add

using Microsoft.EntityFrameworkCore;

in the code:

var sortOrder = "Name";
var descending = true;

if (descending)
     {
         Query.OrderByDescending(e => EF.Property<object>(e, sortOrder));
     }
     else
     {
         Query.OrderBy(e => EF.Property<object>(e, sortOrder));
     }
public static class IQueryableExtensions
{
    public static void ApplyOrder<T>(ISpecificationBuilder<T> query, string propertyName, bool ascendingOrder)
    {
        if (ascendingOrder)
            query.OrderBy(T => EF.Property<object>(T!, propertyName));
        else
            query.OrderByDescending(T => EF.Property<object>(T!, propertyName));
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants