Implementing Multi-Field Sorting in a Generic Repository Query Method

A generic repository method intended to support multi-field sorting was found to only apply the last specified sort field. The original implementation used a loop to build OrderBy or OrderByDescending expressions, but each iteration overwrote the previous one, failing to create a composite sort order.

The core issue was the misuse of OrderBy within a loop. In LINQ, subsequent OrderBy calls replace the primary sort order instead of adding secondary sorts. The correct approach is to use OrderBy for the first field and ThenBy or ThenByDescending for subesquent fields.

Original Flawed Implemantation:

public async Task<List<T>> QueryItems(Expression<Func<T, bool>> filter, QueryParams parameters)
{
    bool ascending = parameters.sortDirection.ToLower() == "asc";
    string[] sortFields = parameters.sortIndex.Split(',');
    MethodCallExpression sortExpression = null;
    IQueryable<T> querySet = DataSource.Find(filter);

    foreach (string fieldSpec in sortFields)
    {
        string trimmedSpec = Regex.Replace(fieldSpec, @"\s+", " ");
        string[] specParts = trimmedSpec.Split(' ');
        string fieldName = specParts[0];
        bool localAsc = ascending;

        if (specParts.Length == 2)
        {
            localAsc = specParts[1].ToUpper() == "ASC";
        }

        var param = Expression.Parameter(typeof(T), "e");
        var propInfo = typeof(T).GetProperty(fieldName);
        var propAccess = Expression.MakeMemberAccess(param, propInfo);
        var lambda = Expression.Lambda(propAccess, param);

        // This recreates OrderBy each time, overwriting previous sorts
        sortExpression = Expression.Call(typeof(Queryable),
                                         localAsc ? "OrderBy" : "OrderByDescending",
                                         new Type[] { typeof(T), propInfo.PropertyType },
                                         querySet.Expression,
                                         Expression.Quote(lambda));
    }
    // Only the last sortExpression is applied
    querySet = querySet.Provider.CreateQuery<T>(sortExpression);
    parameters.totalCount = querySet.Count();
    querySet = querySet.Skip<T>(parameters.pageSize * (parameters.pageNumber - 1))
                       .Take<T>(parameters.pageSize).AsQueryable();
    return await querySet.ToListAsync();
}

Corrected Implementation with ThenBy: The fix involves tracking whether a primary sort has been aplied and using the appropriate method (OrderBy for the first field, ThenBy for subsequent ones). The CreateQuery call must be inside the loop to progressively build the sorted query.

public async Task<List<T>> QueryItems(Expression<Func<T, bool>> filter, QueryParams parameters)
{
    bool defaultAsc = parameters.sortDirection.ToLower() == "asc";
    string[] sortFields = parameters.sortIndex.Split(',');
    MethodCallExpression currentSortExpression = null;
    IQueryable<T> querySet = DataSource.Find(filter);
    int sortIndex = 0;

    foreach (string fieldSpec in sortFields)
    {
        string cleanSpec = Regex.Replace(fieldSpec, @"\s+", " ");
        string[] specComponents = cleanSpec.Split(' ');
        string targetField = specComponents[0];
        bool sortAscending = defaultAsc;

        if (specComponents.Length == 2)
        {
            sortAscending = specComponents[1].ToUpper() == "ASC";
        }

        var entityParam = Expression.Parameter(typeof(T), "x");
        var property = typeof(T).GetProperty(targetField);
        var memberAccess = Expression.MakeMemberAccess(entityParam, property);
        var selector = Expression.Lambda(memberAccess, entityParam);

        string methodName;
        if (sortIndex == 0)
        {
            // First field uses OrderBy/OrderByDescending
            methodName = sortAscending ? "OrderBy" : "OrderByDescending";
        }
        else
        {
            // Subsequent fields use ThenBy/ThenByDescending
            methodName = sortAscending ? "ThenBy" : "ThenByDescending";
        }

        currentSortExpression = Expression.Call(typeof(Queryable),
                                                methodName,
                                                new Type[] { typeof(T), property.PropertyType },
                                                querySet.Expression,
                                                Expression.Quote(selector));

        // Apply the sort expression to the query before the next iteration
        querySet = querySet.Provider.CreateQuery<T>(currentSortExpression);
        sortIndex++;
    }

    parameters.totalCount = querySet.Count();
    querySet = querySet.Skip<T>(parameters.pageSize * (parameters.pageNumber - 1))
                       .Take<T>(parameters.pageSize).AsQueryable();
    return await querySet.ToListAsync();
}

Placing the CreateQuery call inside the loop is essential. Attempting to call ThenBy or ThenByDescending on an unsorted IQueryable source will throw a runtime exception: No generic method 'ThenByDescending' on type 'System.Linq.Queryable' is compatible with the supplied type arguments. This error indicates the framework cannot find a secondary sort method because a primary OrderBy has not been applied to the current query expression tree.

Tags: C# LINQ Entity Framework Sorting Generic Repository

Posted on Thu, 02 Jul 2026 17:27:59 +0000 by zeh