Dynamic Sorting Techniques for Entity Framework Queries

Implementing dynamic sorting in Entity Framework requires runtime expression construction. Here are two distinct patterns to handle property-based ordering without compile-time type knowledge.

Pattern 1: Generic Expression Builder with Type Constraints

This factory method creates strongly-typed ordering expressions but demands prior knowledge of the target property's type.

public static class SortExpressionBuilder
{
    public static Expression<Func<TEntity, TOrderKey>> ConstructSortExpression<TEntity, TOrderKey>(
        string propertyName, string fallbackProperty = null)
    {
        var entityParameter = Expression.Parameter(typeof(TEntity), "entity");
        
        var selectedProperty = string.IsNullOrWhiteSpace(propertyName) ? fallbackProperty : propertyName;
        
        var propertyReference = Expression.Property(entityParameter, selectedProperty);
        
        var typeConvertedExpr = Expression.Convert(propertyReference, typeof(TOrderKey));
        
        return Expression.Lambda<Func<TEntity, TOrderKey>>(typeConvertedExpr, entityParameter);
    }
}

Usage in a paging method:

public IQueryable<T> RetrievePagedResults<TKey>(
    int pageSize, int pageNumber, bool sortAscending,
    Expression<Func<T, TKey>> sortExpression,
    Expression<Func<T, bool>> filterCriteria,
    out int totalRows)
{
    var baseQuery = dbContext.Set<T>().Where(filterCriteria);
    totalRows = baseQuery.Count();
    
    var orderedResult = sortAscending 
        ? baseQuery.OrderBy(sortExpression) 
        : baseQuery.OrderByDescending(sortExpression);
    
    return orderedResult.Skip((pageNumber - 1) * pageSize).Take(pageSize);
}

Limitation: The TKey generic parameter must precisely match the property's runtime type. Type mismatches trigger conversion exceptions, and you cannot default to a universal type like object without breaking query translation.

Pattern 2: Runtime Type Discovery via Extension Methods

This approach removes compile-time type requirements by deferring type resolusion until execution.

public static class QueryableDynamicSort
{
    public static IOrderedQueryable<T> OrderByProperty<T>(this IQueryable<T> source, string propertyPath)
    {
        return ImplementDynamicSort(source, propertyPath, descending: false);
    }

    public static IOrderedQueryable<T> OrderByPropertyDescending<T>(this IQueryable<T> source, string propertyPath)
    {
        return ImplementDynamicSort(source, propertyPath, descending: true);
    }

    private static IOrderedQueryable<T> ImplementDynamicSort<T>(
        IQueryable<T> source, string propertyPath, bool descending)
    {
        var propertyMetadata = typeof(T).GetProperty(
            propertyPath, 
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
        
        if (propertyMetadata == null)
            throw new ArgumentException($"No property '{propertyPath}' found on {typeof(T).Name}");

        var operationMethod = descending ? nameof(GenerateDescendingQuery) : nameof(GenerateAscendingQuery);
        
        var typedMethod = typeof(QueryableDynamicSort)
            .GetMethod(operationMethod, BindingFlags.Static | BindingFlags.NonPublic)
            .MakeGenericMethod(typeof(T), propertyMetadata.PropertyType);
        
        return (IOrderedQueryable<T>)typedMethod.Invoke(null, new object[] { source, propertyMetadata });
    }

    private static IOrderedQueryable<T> GenerateAscendingQuery<T, TProperty>(
        IQueryable<T> source, PropertyInfo propertyMetadata)
    {
        var orderingExpr = CreatePropertyAccessor<T, TProperty>(propertyMetadata);
        return source.OrderBy(orderingExpr);
    }

    private static IOrderedQueryable<T> GenerateDescendingQuery<T, TProperty>(
        IQueryable<T> source, PropertyInfo propertyMetadata)
    {
        var orderingExpr = CreatePropertyAccessor<T, TProperty>(propertyMetadata);
        return source.OrderByDescending(orderingExpr);
    }

    private static Expression<Func<T, TProperty>> CreatePropertyAccessor<T, TProperty>(PropertyInfo propertyMetadata)
    {
        var itemParam = Expression.Parameter(typeof(T), "item");
        var propertyAccess = Expression.Property(itemParam, propertyMetadata);
        return Expression.Lambda<Func<T, TProperty>>(propertyAccess, itemParam);
    }
}

Prcatical implementation:

public IQueryable<T> FetchPageSortedByProperty(
    int pageSize, int pageIndex, bool ascendingOrder,
    string sortProperty, Expression<Func<T, bool>> whereClause,
    out int totalMatches)
{
    var filteredSet = dbContext.Set<T>().Where(whereClause);
    totalMatches = filteredSet.Count();
    
    return ascendingOrder
        ? filteredSet.OrderByProperty(sortProperty).Skip((pageIndex - 1) * pageSize).Take(pageSize)
        : filteredSet.OrderByPropertyDescending(sortProperty).Skip((pageIndex - 1) * pageSize).Take(pageSize);
}

Trade-off: While this eliminates type constraints, the reflection-based dispatch adds complexity and minor performance overhead. The layered generic method calls can obscure the underlying mechanism, making troubleshooting more difficult for teams unfamiliar with dynamic expression compilation.

Tags: Entity Framework C# Lambda Expressions reflection Dynamic Queries

Posted on Tue, 19 May 2026 20:09:15 +0000 by chrisuk