zzzprojects / System.Linq.Dynamic.Core

The .NET Standard / .NET Core version from the System Linq Dynamic functionality.
https://dynamic-linq.net/
Apache License 2.0
1.55k stars 228 forks source link

OrderBy gets overriden when used multiple times #834

Open puschie286 opened 1 month ago

puschie286 commented 1 month ago

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 )

StefH commented 1 month ago

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

puschie286 commented 1 month ago

@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 commented 1 month ago

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