using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; namespace Persistence.Repository.Extensions; public static class EFExtensionsSortBy { struct TypeAccessor { public LambdaExpression KeySelector { get; set; } public MethodInfo OrderBy { get; set; } public MethodInfo OrderByDescending { get; set; } public MethodInfo ThenBy { get; set; } public MethodInfo ThenByDescending { get; set; } } private static ConcurrentDictionary<Type, Dictionary<string, TypeAccessor>> TypePropSelectors { get; set; } = new(); private static readonly MethodInfo methodOrderBy = GetExtOrderMethod("OrderBy"); private static readonly MethodInfo methodOrderByDescending = GetExtOrderMethod("OrderByDescending"); private static readonly MethodInfo methodThenBy = GetExtOrderMethod("ThenBy"); private static readonly MethodInfo methodThenByDescending = GetExtOrderMethod("ThenByDescending"); private static MethodInfo GetExtOrderMethod(string methodName) => typeof(Queryable) .GetMethods() .Where(m => m.Name == methodName && m.IsGenericMethodDefinition && m.GetParameters().Length == 2 && m.GetParameters()[1].ParameterType.IsAssignableTo(typeof(LambdaExpression))) .Single(); private static Dictionary<string, TypeAccessor> MakeTypeAccessors(Type type) { var propContainer = new Dictionary<string, TypeAccessor>(); var properties = type.GetProperties(); foreach (var propertyInfo in properties) { var name = propertyInfo.Name.ToLower(); ParameterExpression arg = Expression.Parameter(type, "x"); MemberExpression property = Expression.Property(arg, propertyInfo.Name); var selector = Expression.Lambda(property, [arg]); var typeAccessor = new TypeAccessor { KeySelector = selector, OrderBy = methodOrderBy.MakeGenericMethod(type, propertyInfo.PropertyType), OrderByDescending = methodOrderByDescending.MakeGenericMethod(type, propertyInfo.PropertyType), ThenBy = methodThenBy.MakeGenericMethod(type, propertyInfo.PropertyType), ThenByDescending = methodThenByDescending.MakeGenericMethod(type, propertyInfo.PropertyType), }; propContainer.Add(name, typeAccessor); } return propContainer; } /// <summary> /// Добавить в запрос сортировку по возрастанию или убыванию. /// </summary> /// <typeparam name="TSource"></typeparam> /// <param name="query"></param> /// <param name="propertySort"> /// Свойство сортировки. /// Состоит из названия свойства (в любом регистре) /// и опционально указания направления сортировки "asc" или "desc" /// </param> /// <example> /// var query = query("Date desc"); /// </example> /// <returns>Запрос с примененной сортировкой</returns> public static IOrderedQueryable<TSource> SortBy<TSource>( this IQueryable<TSource> query, IEnumerable<string> propertySorts) { if (propertySorts?.Any() != true) return (IOrderedQueryable<TSource>)query; var sortEnum = propertySorts.GetEnumerator(); sortEnum.MoveNext(); var orderedQuery = query.SortBy(sortEnum.Current); while (sortEnum.MoveNext()) orderedQuery = orderedQuery.ThenSortBy(sortEnum.Current); return orderedQuery; } /// <summary> /// Добавить в запрос сортировку по возрастанию или убыванию. /// Этот метод сбросит ранее наложенные сортировки. /// </summary> /// <typeparam name="TSource"></typeparam> /// <param name="query"></param> /// <param name="propertySort"> /// Свойство сортировки. /// Состоит из названия свойства (в любом регистре) /// и опционально указания направления сортировки "asc" или "desc" /// </param> /// <example> /// var query = query("Date desc"); /// </example> /// <returns>Запрос с примененной сортировкой</returns> public static IOrderedQueryable<TSource> SortBy<TSource>( this IQueryable<TSource> query, string propertySort) { var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries); var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc"; var propertyName = parts[0]; var newQuery = query.SortBy(propertyName, isDesc); return newQuery; } /// <summary> /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию. /// </summary> /// <typeparam name="TSource"></typeparam> /// <param name="query"></param> /// <param name="propertySort"> /// Свойство сортировки. /// Состоит из названия свойства (в любом регистре) /// и опционально указания направления сортировки "asc" или "desc" /// </param> /// <example> /// var query = query("Date desc"); /// </example> /// <returns>Запрос с примененной сортировкой</returns> public static IOrderedQueryable<TSource> ThenSortBy<TSource>( this IOrderedQueryable<TSource> query, string propertySort) { var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries); var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc"; var propertyName = parts[0]; var newQuery = query.ThenSortBy(propertyName, isDesc); return newQuery; } /// <summary> /// Добавить в запрос сортировку по возрастанию или убыванию /// </summary> /// <typeparam name="TSource"></typeparam> /// <param name="query"></param> /// <param name="propertyName">Название свойства (в любом регистре)</param> /// <param name="isDesc">Сортировать по убыванию</param> /// <returns>Запрос с примененной сортировкой</returns> public static IOrderedQueryable<TSource> SortBy<TSource>( this IQueryable<TSource> query, string propertyName, bool isDesc) { Type rootType = typeof(TSource); var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors); var propertyNameLower = propertyName.ToLower(); MethodInfo orderByDescending; MethodInfo orderByAscending; LambdaExpression? lambdaExpression = null; if (propertyName.Contains('.')) { Type type = rootType; ParameterExpression rootExpression = Expression.Parameter(rootType, "x"); Expression expr = rootExpression; var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < propertyPath.Length; i++) { PropertyInfo pi = type.GetProperty(propertyPath[i])!; expr = Expression.Property(expr, pi); type = pi.PropertyType; } Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type); lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression); orderByAscending = methodOrderBy.MakeGenericMethod(rootType, type); orderByDescending = methodOrderByDescending.MakeGenericMethod(rootType, type); } else { var rootTypeAccessor = typePropSelector[propertyNameLower]; orderByAscending = rootTypeAccessor.OrderBy; orderByDescending = rootTypeAccessor.OrderByDescending; lambdaExpression = rootTypeAccessor.KeySelector; } var genericMethod = isDesc ? orderByDescending : orderByAscending; var newQuery = (IOrderedQueryable<TSource>)genericMethod .Invoke(genericMethod, [query, lambdaExpression])!; return newQuery; } /// <summary> /// Добавить в запрос дополнительную сортировку по возрастанию или убыванию /// </summary> /// <typeparam name="TSource"></typeparam> /// <param name="query"></param> /// <param name="propertyName">Название свойства (в любом регистре)</param> /// <param name="isDesc">Сортировать по убыванию</param> /// <returns>Запрос с примененной сортировкой</returns> public static IOrderedQueryable<TSource> ThenSortBy<TSource>( this IOrderedQueryable<TSource> query, string propertyName, bool isDesc) { Type rootType = typeof(TSource); var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors); var propertyNameLower = propertyName.ToLower(); MethodInfo orderByDescending; MethodInfo orderByAscending; LambdaExpression? lambdaExpression = null; // TODO: Устранить дублирование кода if (propertyName.Contains('.')) { Type type = rootType; ParameterExpression rootExpression = Expression.Parameter(rootType, "x"); Expression expr = rootExpression; var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < propertyPath.Length; i++) { PropertyInfo pi = type.GetProperty(propertyPath[i])!; expr = Expression.Property(expr, pi); type = pi.PropertyType; } Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type); lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression); orderByAscending = methodThenBy.MakeGenericMethod(rootType, type); orderByDescending = methodThenByDescending.MakeGenericMethod(rootType, type); } else { var rootTypeAccessor = typePropSelector[propertyNameLower]; orderByAscending = rootTypeAccessor.ThenBy; orderByDescending = rootTypeAccessor.ThenByDescending; lambdaExpression = rootTypeAccessor.KeySelector; } var genericMethod = isDesc ? orderByDescending : orderByAscending; var newQuery = (IOrderedQueryable<TSource>)genericMethod .Invoke(genericMethod, [query, lambdaExpression])!; return newQuery; } }