Created
September 9, 2022 14:50
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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