Skip to content

Instantly share code, notes, and snippets.

@josheinstein
Created September 9, 2022 14:50
Show Gist options
  • Save josheinstein/5e99013d31399be8de21a622cbe9e68e to your computer and use it in GitHub Desktop.
Save josheinstein/5e99013d31399be8de21a622cbe9e68e to your computer and use it in GitHub Desktop.
Adds OrderBy[Descending]/ThenBy[Descending] extension methods to IQueryable types that take a string parameter indicating the selector path, enabling parameterized ORDER BY clauses in LINQ queries.
/// <summary>
/// Adds OrderBy[Descending]/ThenBy[Descending] extension methods to IQueryable types
/// that take a string parameter indicating the selector path, enabling parameterized
/// ORDER BY clauses in LINQ queries.
/// </summary>
public static class DynamicSort
{
private static readonly Lazy<MethodInfo> OrderByMethod = new(() => GetQueryableMethod(nameof(Queryable.OrderBy), 2));
private static readonly Lazy<MethodInfo> OrderByDescendingMethod = new(() => GetQueryableMethod(nameof(Queryable.OrderByDescending), 2));
private static readonly Lazy<MethodInfo> ThenByMethod = new(() => GetQueryableMethod(nameof(Queryable.ThenBy), 2));
private static readonly Lazy<MethodInfo> ThenByDescendingMethod = new(() => GetQueryableMethod(nameof(Queryable.ThenByDescending), 2));
/// <summary>
/// Sorts the elements of a sequence in ascending order by using a parsed selector path expression.
/// </summary>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <param name="source">A sequence of values to order.</param>
/// <param name="selectorPath">A string representation of a member to sort by. Nested members can be specified using dot notation.</param>
/// <returns>An IOrderedQueryable[TSource] whose elements are sorted according to a key selector path.</returns>
public static IOrderedQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, string selectorPath)
{
return CallSortMethod(OrderByMethod.Value, source, selectorPath);
}
/// <summary>
/// Sorts the elements of a sequence in descending order by using a parsed selector path expression.
/// </summary>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <param name="source">A sequence of values to order.</param>
/// <param name="selectorPath">A string representation of a member to sort by. Nested members can be specified using dot notation.</param>
/// <returns>An IOrderedQueryable[TSource] whose elements are sorted in descending order according to a key selector path.</returns>
public static IOrderedQueryable<TSource> OrderByDescending<TSource>(this IQueryable<TSource> source, string selectorPath)
{
return CallSortMethod(OrderByDescendingMethod.Value, source, selectorPath);
}
/// <summary>
/// Performs a subsequent ordering of the elements in a sequence in ascending order by using a parsed selector path expression.
/// </summary>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <param name="source">An IOrderedQueryable[TSource] that contains elements to sort.</param>
/// <param name="selectorPath">A string representation of a member to sort by. Nested members can be specified using dot notation.</param>
/// <returns>An IOrderedQueryable[TSource] whose elements are sorted according to a key selector path.</returns>
public static IOrderedQueryable<TSource> ThenBy<TSource>(this IOrderedQueryable<TSource> source, string selectorPath)
{
return CallSortMethod(ThenByMethod.Value, source, selectorPath);
}
/// <summary>
/// Performs a subsequent ordering of the elements in a sequence in descending order by using a parsed selector path expression.
/// </summary>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <param name="source">A sequence of values to order.</param>
/// <param name="selectorPath">A string representation of a member to sort by. Nested members can be specified using dot notation.</param>
/// <returns>An IOrderedQueryable[TSource] whose elements are sorted in descending order according to a key selector path.</returns>
public static IOrderedQueryable<TSource> ThenByDescending<TSource>(this IQueryable<TSource> source, string selectorPath)
{
return CallSortMethod(ThenByDescendingMethod.Value, source, selectorPath);
}
/// <summary>
/// Creates an expression tree that selects a property from an entity or one of
/// its child objects.
/// </summary>
/// <param name="entityType">The entity type.</param>
/// <param name="selectorPath">A string describing the path to the member. Separate nested members with dots.</param>
/// <returns>A lambda expression that can be passed to OrderBy and other LINQ functions.</returns>
private static LambdaExpression CreateSelectorExpression(Type entityType, string selectorPath)
{
Guard.IsNotNull(entityType);
Guard.IsNotNullOrWhiteSpace(selectorPath);
// We start with a lambda variable x that represents the entity itself
ParameterExpression param = Expression.Parameter(entityType, "x");
Expression expr = param;
// In order to support nested member access, we need to split the
// memberPath variable by dots. Each segment will be used to
// chain the member access expression.
foreach (var segment in selectorPath.Split('.')) {
expr = Expression.PropertyOrField(expr, segment);
}
// Finally, we wrap the entire member access expression in a lambda,
// including the original parameter expression we started with.
LambdaExpression selector = Expression.Lambda(expr, param);
return selector;
}
/// <summary>
/// Looks up MethodInfo for an extension method in the Queryable class by name.
/// </summary>
/// <param name="name">The name of the method.</param>
/// <param name="paramCount">The number of parameters expected, to disambiguate overloads.</param>
/// <returns>The MethodInfo of the extension method, without generic arguments.</returns>
/// <exception cref="ArgumentException">The method could not be found.</exception>
private static MethodInfo GetQueryableMethod(string name, int paramCount)
{
Guard.IsNotNullOrWhiteSpace(name);
Guard.IsGreaterThan(paramCount, 0);
// Get System.Linq.Queryable.OrderBy<T> open generic method
var genericMethod = Enumerable.SingleOrDefault(
from mi in typeof(Queryable).GetMethods()
where mi.Name == name && mi.IsGenericMethodDefinition
let mp = mi.GetParameters()
where mp.Length == paramCount
select mi
);
if (genericMethod == null) {
throw new ArgumentException($"Could not find MethodInfo for {name} with type param count = {paramCount}.");
}
return genericMethod;
}
/// <summary>
/// Creates an IOrderedQueryable by calling a sort extension method on an IQueryable using
/// the specified <paramref name="selectorPath"/> as the sort expression.
/// </summary>
/// <remarks>
/// This method contains the common functionality to apply dynamic sort expression using LINQ
/// extension methods such as OrderBy, OrderByDescending, ThenBy, and ThenByDescending.
/// </remarks>
/// <typeparam name="TSource">The type of entity the IQueryable returns.</typeparam>
/// <param name="sortMethodOpen">The MethodInfo for a generic method such as OrderBy.</param>
/// <param name="source">The source IQueryable.</param>
/// <param name="selectorPath">The member access expression as a string, such as AccountManager.FullName.</param>
/// <returns>The queryable with an ordering expression applied.</returns>
private static IOrderedQueryable<TSource> CallSortMethod<TSource>(MethodInfo sortMethodOpen, IQueryable<TSource> source, string selectorPath)
{
Guard.IsNotNull(sortMethodOpen);
Guard.IsNotNull(source);
Guard.IsNotNullOrWhiteSpace(selectorPath);
Type entityType = typeof(TSource);
// Create the selector expression that we will pass to the OrderBy / OrderByDescending methods
// For example: x => x.AccountManager.Name
LambdaExpression selector = CreateSelectorExpression(entityType, selectorPath);
// Use TSource to create a closed generic method that we can invoke
MethodInfo sortMethodClosed = sortMethodOpen.MakeGenericMethod(entityType, selector.ReturnType);
// Call Queryable.OrderBy[Descending]/ThenBy[Descending](x => selectorPath, comparer) dynamically
var orderedQueryable = (IOrderedQueryable<TSource>)sortMethodClosed.Invoke(null, new object[] { source, selector });
return orderedQueryable;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment