System.Linq.Dynamic.Core icon indicating copy to clipboard operation
System.Linq.Dynamic.Core copied to clipboard

OrderBy gets overriden when used multiple times

Open puschie286 opened this issue 1 year ago • 3 comments

1. Description

When OrderBy is used multiple times, only the last one is applied

3. Fiddle or Project

Simplified test cases

private class Sub( int mainId, int other )
{
	public int MainId { get; } = mainId;
	public int Other  { get; } = other;
}

// fail
[Fact]
public void OrderBy_multiple()
{
	// Arrange
	List<Sub> subList = [new Sub( 3, 1 ), new Sub( 1, 3 ), new Sub( 2, 2 )];

	// Act
	var query  = subList.AsQueryable().OrderBy( "Other" ).OrderBy( "MainId" );
	var result = query.ToList();

	// Assert
	result.ShouldBeInOrder( Shouldly.SortDirection.Descending, x => x.MainId );
}

// correct
[Fact]
public void OrderBy_single()
{
	// Arrange
	List<Sub> subList = [new Sub( 3, 1 ), new Sub( 1, 3 ), new Sub( 2, 2 )];

	// Act
	var query = subList.AsQueryable().OrderBy( "Other, MainId" );
	var result = query.ToList();

	// Assert
	result.ShouldBeInOrder( Shouldly.SortDirection.Descending, x => x.MainId );
}

would expect both versions to behave identical.

And here is the (simplified) real case

private class Main( int id )
{
	public int Id { get; } = id;
}

private class Sub( int mainId, int other )
{
	public int MainId { get; } = mainId;
	public int Other  { get; } = other;
}

[Fact]
public void OrderBy_outer_and_inner()
{
	// Arrange
	List<Main> mainList = [new Main( 2 ), new Main( 1 ), new Main( 3 )];
	List<Sub>  subList  = [new Sub( 3, 1 ), new Sub( 1, 3 ), new Sub( 2, 2 )];

	// Act
	IQueryable<Main> query = mainList.AsQueryable().Join(
			subList.AsQueryable(),
			outer => new { F1           = outer.Id },
			inner => new { F1           = inner.MainId },
			( outer, inner ) => new { o = outer, i = inner }
		)
		.OrderBy( "i.Other" )
		.Select( x => x.o )
		.OrderBy( "Id" );
	List<Main> result = query.ToList();

	// Assert
	result.ShouldBeInOrder( Shouldly.SortDirection.Descending, x => x.Id );
}

note: the inner part (join+order by) and outer part(order by) are done by different parts of the program. note 2: the ShouldBeInOrder method is a custom extension, but i think you understand what result is expected ( behavior is identical with ef core queries )

puschie286 avatar Aug 05 '24 19:08 puschie286

If you have multiple OrderBy, you need to use ThenBy. See public void ThenBy_Dynamic()

StefH avatar Aug 05 '24 19:08 StefH

@StefH thanks for the fast response this works for the simplified version - but it throws an exception in the real case example

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 System.Linq.Dynamic.Core.DynamicQueryableExtensions.InternalThenBy(IOrderedQueryable source, ParsingConfig config, String ordering, IComparer comparer, Object[] args)
   at System.Linq.Dynamic.Core.DynamicQueryableExtensions.ThenBy(IOrderedQueryable source, ParsingConfig config, String ordering, Object[] args)
   at System.Linq.Dynamic.Core.DynamicQueryableExtensions.ThenBy[TSource](IOrderedQueryable`1 source, ParsingConfig config, String ordering, Object[] args)
   at System.Linq.Dynamic.Core.DynamicQueryableExtensions.ThenBy[TSource](IOrderedQueryable`1 source, String ordering, Object[] args)

updated complex example

[Fact]
public void OrderBy_outer_and_inner()
{
	// Arrange
	List<Main> mainList = [new Main( 2 ), new Main( 1 ), new Main( 3 )];
	List<Sub>  subList  = [new Sub( 3, 1 ), new Sub( 1, 3 ), new Sub( 2, 2 )];

	// Act
	IQueryable<Main> query = mainList.AsQueryable().Join(
			subList.AsQueryable(),
			outer => new { F1           = outer.Id },
			inner => new { F1           = inner.MainId },
			( outer, inner ) => new { o = outer, i = inner }
		)
		.OrderBy( "i.Other" )
		.Select( x => x.o );

	if( query is IOrderedQueryable<Main> orderedByQuery )
	{
		query = orderedByQuery.ThenBy( "Id" );
	}
		
	List<Main> result = query.ToList();

	// Assert
	result.ShouldBeInOrder( Shouldly.SortDirection.Descending, x => x.Id );
}

do you have any suggestions how this can be achieved ? (as mentioned, the inner join+order and the outer order are done in different parts of the application and doesnt know of each other - there might be multiple join parts)

puschie286 avatar Aug 05 '24 19:08 puschie286

it seems that its not possible with the native OrderBy and Thenby either. Im currenlty waiting for an answer from the dotnet team, how this can be achieved and will keep this ticket updated

puschie286 avatar Aug 06 '24 09:08 puschie286

@puschie286 Can this question be closed?

StefH avatar Oct 13 '24 11:10 StefH

yes - the dotnet team said it is by design. "Select" removes all ordering (except if its the last command), so you have to create an object with references to all related entities before applying all ordering and your final select.

puschie286 avatar Oct 14 '24 08:10 puschie286