-
Notifications
You must be signed in to change notification settings - Fork 249
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
Comments
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.
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. |
Are you OK with the answer, should we close the issue here? |
Some options here that might be helpful: |
Indeed, we find the property by reflection, and then dynamically generate the expression. |
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. |
I think users can create their own extensions. Thanks all! |
Do we have any examples of how you implement an extension like this for |
Yea you're right, we have no examples of that. Before answering that, we should clarify few topics first. 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
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 extensionsFor demonstration purposes, let's add support for 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. |
@ardalis probably we might add this to the Readme as well. |
@fiseni This is all really good information, however I’m still unclear how you would use this to pass an
|
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 extensionsFirst 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 extensionsNow 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. |
@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? |
Sure, soon we'll improve the documentation and add information regarding these tasks. |
@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 |
Well, yea you can do it, but in that case, instead of building upon the 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;
}
} |
@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 |
@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;
}
} |
Great, I'm glad it worked out! |
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.
|
@lkroliko How did you fix the error?
Is this the only addition needed?
Would it be easy to add a Dictionary that can map to properties?
Just trying to remove all this code:
|
You need the conversion part to be able to use int (also for decimal, guid, etc.) 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;
}
} |
You are a godsend @fiseni thank you! I have much to learn in writing Expressions You have changed my code from:
To:
|
@fiseni how would you implement a lambda instead of a fieldname in this extension?
|
Hmm, the lambda is supported out of the box. You just use |
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:
Instead of this:
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. |
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 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;
}
} |
Awesome. Thank you! That brings it down to:
|
@davidhenley You are kind of re-hashing what myself and @fiseni already did, but each to their own. |
@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? |
@davidhenley Didn't see the point as that's what the |
@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! |
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.
System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List |
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;
}
|
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 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;
} |
Ah, I see! Makes sense. Thank you so much! Will close separate issue |
@davidhenley Could you please post the code that you used in your specification file? Thanks in advance... |
Add
in the code:
|
|
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.
The text was updated successfully, but these errors were encountered: