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

[Feature] Extend OrderBy functionality #73

Closed
joacar opened this issue Apr 11, 2017 · 6 comments
Closed

[Feature] Extend OrderBy functionality #73

joacar opened this issue Apr 11, 2017 · 6 comments
Assignees
Labels

Comments

@joacar
Copy link

joacar commented Apr 11, 2017

Hi,

Haven't used this package (yet) but it seem to have a lot of functionality that I might use as my project evolves. It also lacks the only functionality I currently need :) Since the reflection calls, expression tree parsing is probably more efficient than my code I thought to create a PR with a new feature.

To the point: I use EF Core (currently in a MVC application) with an extension OrderBy that takes a string Property [ASC | DESC] | ["," ...] (abusing BNF ;). However I need to be able to order by more complex expressions such as p => p.A.Cost + p.B.Cost + ... etc where, in this example, Cost might be nullable. So I also need to sort null values last, if desired.

So I've created a class Orderable

public class Orderable
{
	internal Orderable(string expression)
	{
		var parts = expression.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
		if (parts.Length == 0 || parts.Length > 2)
		{
			throw new ArgumentException("Expression must be of form 'TKey [ASC | DESC]'", nameof(expression));
		}
		Property = parts[0];
		if (parts.Length == 2)
		{
			Descending = "DESC".Equals(parts[1], StringComparison.OrdinalIgnoreCase);
		}
	}

	public Orderable(Expression expression, Type expressionType, bool descending = false, bool nullLast = true)
	{
		Expression = expression;
		ExpressionType = expressionType;
		Descending = descending;
		NullLast = nullLast;
	}

	public static Orderable Create<TModel, TKey>(Expression<Func<TModel, TKey>> expression, bool descending = false, bool nullLast = true)
	{
		return new Orderable(expression, expression.ReturnType, descending, nullLast);
	}

	public Expression Expression { get; }

	public Type ExpressionType { get; set; }

	public string Parameter { get; set; }

	public bool NullLast { get; }

	public string Property { get; }

	public bool Descending { get; }
}

Extension method

public static IQueryable<T> SortBy<T>(this IQueryable<T> source, params Ordering[] orderings) where T : class
{
	// Argument checks ommitted
	string methodName = "";
	IQueryable<T> query = source;
	foreach (var ordering in orderings)
	{
		if (string.IsNullOrEmpty(methodName))
		{
			methodName = ordering.Descending ? "OrderByDescending" : "OrderBy";
		}
		else
		{
			methodName = ordering.Descending ? "ThenByDescending" : "ThenBy";
		}
		ParameterExpression parameter = Expression.Parameter(source.ElementType, string.Empty);
		LambdaExpression lambda;
		Expression propertyExpression;
		Type propertyType;
		if (string.IsNullOrEmpty(ordering.Property))
		{
			// Build directly from Expression
			propertyType = ordering.ExpressionType;
			lambda = (LambdaExpression)ordering.Expression;
			if (ordering.NullLast)
			{
				// TODO: Check type is nullable
				try
				{
					var op = lambda.Body;
					var equal = Expression.Equal(op, Expression.Constant(null));
					var condition = Expression.Condition(equal, Expression.Constant(1), Expression.Constant(0));
					Expression mce = Expression.Call(
						typeof(Queryable),
			   methodName,
			   new[] { source.ElementType, typeof(int) },
			   query.Expression,
				Expression.Quote(condition));
					query = (IQueryable<T>)query.Provider.CreateQuery(mce);

				}
				catch (Exception)
				{
				}
			}
		}
		else
		{
			lambda = CreateLambdaExpression(parameter, ordering, out propertyExpression);
			propertyType = propertyExpression.Type;
		}

		Expression methodCallExpression = Expression.Call(
			   typeof(Queryable),
			   methodName,
			   new[] { source.ElementType, propertyType },
			   query.Expression,
				Expression.Quote(lambda));
		query = (IQueryable<T>)query.Provider.CreateQuery(methodCallExpression);
	}

So, is this something that is align with the project? If so I'm happy to create a PR.

Regards

@StefH StefH self-assigned this Apr 15, 2017
@StefH StefH added the feature label Apr 15, 2017
@StefH
Copy link
Collaborator

StefH commented Apr 15, 2017

This library already supports ordering like:

var orderByComplex1 = qry.OrderBy("Profile.Age, Id");

See https://github.com/StefH/System.Linq.Dynamic.Core/blob/40d3b73ffadcb2a6e927767a9b9f7cdd49641663/test/System.Linq.Dynamic.Core.Tests/QueryableTests.OrderBy.cs#L19

So what else do you need?

@joacar
Copy link
Author

joacar commented Apr 24, 2017

Something like

var orderByComplex = query.OrderBy("(Profile.Instagram.Cost ?? 0) + (Profile.Facebook.Cost ?? 0) == 0 ? 1 : 0").ThenBy("(Profile.Instagram.Cost ?? 0) + (Profile.Facebook.Cost ?? 0)")

@StefH
Copy link
Collaborator

StefH commented Apr 24, 2017

This looks like not valid Linq.

Can you rewrite you question into a normal Linq query ?

@joacar
Copy link
Author

joacar commented Apr 24, 2017

Might not be a valid Linq expression.

The query I'm after is to sort those that doesn't have any cost (null) last and then those that have cost in ascending/descending order depending on option choosen in web gui.

Currently I do this by return an Expression (like the one above) to my an extension method SortBy that performs that ordering either with Expression or string values for valid properties

@joacar
Copy link
Author

joacar commented Apr 26, 2017

The solution ended up as

Infrastructure

public interface ISortableExpressionFactory<TModel>
{
	bool CanResolve(string key);

	ISortableExpressionAction<TModel> Resolve(string key);
}

public interface ISortableExpressionAction<TModel>
{
	IOrderedQueryable<TModel> Execute(IQueryable<TModel> query, bool ascending);
}

Sorting

public static IQueryable<TModel> SortBy<TModel>(this IQueryable<TModel> query, string sortExpressions, ISortableExpressionFactory<TModel> factory) where TModel : class
{
	var sortables = sortExpressions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
		.Select(expression => new Sortable(expression))
		.ToList();
	foreach (var sortable in sortables)
	{
		if (factory.CanResolve(sortable.Property))
		{
			var sorter = factory.Resolve(sortable.Property);
			query = sorter.Execute(query, !sortable.Descending);
		}
		else
		{
			query = query.SortBy(sortable); // Same is issue body
		}
	}
	return query;
}

Model

public class Sortable
{
	internal Sortable(string expression)
	{
		var parts = expression.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
		if (parts.Length == 0 || parts.Length > 2)
		{
			throw new ArgumentException("Expression must be of form 'TKey [ASC | DESC]'", nameof(expression));
		}
		Property = parts[0];
		if (parts.Length == 2)
		{
			Descending = "DESC".Equals(parts[1], StringComparison.OrdinalIgnoreCase);
		}
	}

	public Sortable(LambdaExpression expression, Type expressionType, bool descending = false, bool nullLast = true)
	{
		Expression = expression;
		ExpressionType = expressionType;
		Descending = descending;
		NullLast = nullLast;
	}

	public static Sortable Create<TModel, TKey>(Expression<Func<TModel, TKey>> expression, bool descending = false, bool nullLast = true)
	{
		return new Sortable(expression, expression.ReturnType, descending, nullLast);
	}

	public LambdaExpression Expression { get; }

	public Type ExpressionType { get; set; }

	public string Parameter { get; set; }

	public bool NullLast { get; }

	public string Property { get; }

	public bool Descending { get; }
}

Then a can implement reusable components of type ISortableExpressionAction such as

class CostSorter : ISortableExpressionAction<Profile>
{
	public IOrderedQueryable<Profile> Execute(IQueryable<Profile> query, bool ascending)
	{
		var orderable = query
				.OrderBy(Profile.FilterExpressions.OrderByCostNull());
		if (ascending)
		{
			orderable = orderable.ThenBy(Profile.FilterExpressions.OrderByCost());
		}
		else
		{
			orderable = orderable.ThenByDescending(Profile.FilterExpressions.OrderByCost());
		}
		return orderable;
	}
}

and simply execute query.SortBy("Property1,Property2 DESC, ...", new SortableExpressionFactory())

@StefH
Copy link
Collaborator

StefH commented Apr 26, 2017

I'm glad you could make it work.

@StefH StefH closed this as completed Apr 26, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

2 participants