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.