| | 1 | | using Plainquire.Filter.Abstractions; |
| | 2 | | using Plainquire.Filter.Abstractions.Exceptions; |
| | 3 | | using Plainquire.Sort.Abstractions; |
| | 4 | | using System; |
| | 5 | | using System.Collections.Generic; |
| | 6 | | using System.Diagnostics.CodeAnalysis; |
| | 7 | | using System.Linq; |
| | 8 | | using System.Linq.Expressions; |
| | 9 | | using System.Reflection; |
| | 10 | |
|
| | 11 | | namespace Plainquire.Sort; |
| | 12 | |
|
| | 13 | | /// <summary> |
| | 14 | | /// Extension methods for <see cref="IQueryable{TEntity}"/> |
| | 15 | | /// </summary> |
| | 16 | | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global", Justification = "Provided as library, can be used from outsid |
| | 17 | | public static class QueryableExtensions |
| | 18 | | { |
| | 19 | | /// <inheritdoc cref="OrderBy{TEntity}(IQueryable{TEntity}, EntitySort{TEntity}, ISortInterceptor?)"/> |
| | 20 | | public static IOrderedQueryable<TEntity> OrderBy<TEntity>(this IEnumerable<TEntity> source, EntitySort<TEntity> sort |
| 162 | 21 | | => source.AsQueryable().OrderBy(sort, interceptor); |
| | 22 | |
|
| | 23 | | /// <summary> |
| | 24 | | /// Sorts the elements of a sequence according to the given <paramref name="sort"/>. |
| | 25 | | /// </summary> |
| | 26 | | /// <typeparam name="TEntity"></typeparam> |
| | 27 | | /// <param name="source">The elements to sort.</param> |
| | 28 | | /// <param name="sort">The <see cref="EntitySort{TEntity}"/> used to sort the elements.</param> |
| | 29 | | /// <param name="interceptor">An interceptor to manipulate the generated sort order.</param> |
| | 30 | | public static IOrderedQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, EntitySort<TEntity> sort, |
| | 31 | | { |
| 317 | 32 | | var propertySorts = sort.PropertySorts.OrderBy(x => x.Position).ToList(); |
| 317 | 33 | | if (!propertySorts.Any()) |
| 0 | 34 | | return (IOrderedQueryable<TEntity>)source.Provider.CreateQuery<TEntity>(source.Expression); |
| | 35 | |
|
| 317 | 36 | | var configuration = sort.Configuration ?? SortConfiguration.Default ?? new SortConfiguration(); |
| 317 | 37 | | interceptor ??= ISortInterceptor.Default; |
| | 38 | |
|
| 317 | 39 | | var first = propertySorts[0]; |
| 317 | 40 | | var result = interceptor?.OrderBy(source, first); |
| 317 | 41 | | if (result == null) |
| | 42 | | { |
| 311 | 43 | | var ascending = first.Direction == SortDirection.Ascending; |
| 311 | 44 | | result ??= ascending |
| 311 | 45 | | ? source.OrderBy(first.PropertyPath, configuration) |
| 311 | 46 | | : source.OrderByDescending(first.PropertyPath, configuration); |
| | 47 | | } |
| | 48 | |
|
| 692 | 49 | | foreach (var sortedProperty in propertySorts.Skip(1)) |
| | 50 | | { |
| 36 | 51 | | var interceptedResult = interceptor?.ThenBy(result, sortedProperty); |
| 36 | 52 | | if (interceptedResult != null) |
| | 53 | | { |
| 6 | 54 | | result = interceptedResult; |
| 6 | 55 | | continue; |
| | 56 | | } |
| | 57 | |
|
| 30 | 58 | | var ascending = sortedProperty.Direction == SortDirection.Ascending; |
| 30 | 59 | | result = ascending |
| 30 | 60 | | ? result.ThenBy(sortedProperty.PropertyPath, configuration) |
| 30 | 61 | | : result.ThenByDescending(sortedProperty.PropertyPath, configuration); |
| | 62 | | } |
| | 63 | |
|
| 310 | 64 | | return result; |
| | 65 | | } |
| | 66 | |
|
| | 67 | | /// <summary> |
| | 68 | | /// Sorts the elements of a sequence in ascending order according to a property path. |
| | 69 | | /// </summary> |
| | 70 | | /// <typeparam name="TEntity">The type of the sorted entity.</typeparam> |
| | 71 | | /// <param name="source">A sequence of values to sort.</param> |
| | 72 | | /// <param name="propertyPath">Path to the property to sort by.</param> |
| | 73 | | /// <param name="configuration">Sort order configuration.</param> |
| | 74 | | public static IOrderedQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string propertyPath, Sort |
| 139 | 75 | | => source.OrderBy(nameof(Queryable.OrderBy), propertyPath, configuration); |
| | 76 | |
|
| | 77 | | /// <summary> |
| | 78 | | /// Sorts the elements of a sequence in descending order according to a property path. |
| | 79 | | /// </summary> |
| | 80 | | /// <typeparam name="TEntity">The type of the sorted entity.</typeparam> |
| | 81 | | /// <param name="source">A sequence of values to sort.</param> |
| | 82 | | /// <param name="propertyPath">Path to the property to sort by.</param> |
| | 83 | | /// <param name="configuration">Sort order configuration.</param> |
| | 84 | | public static IOrderedQueryable<TEntity> OrderByDescending<TEntity>(this IQueryable<TEntity> source, string property |
| 172 | 85 | | => source.OrderBy(nameof(Queryable.OrderByDescending), propertyPath, configuration); |
| | 86 | |
|
| | 87 | | /// <summary> |
| | 88 | | /// Performs a subsequent ordering of the elements in a sequence in ascending order according to a property path. |
| | 89 | | /// </summary> |
| | 90 | | /// <typeparam name="TEntity">The type of the sorted entity.</typeparam> |
| | 91 | | /// <param name="source">A sequence of values to sort.</param> |
| | 92 | | /// <param name="propertyPath">Path to the property to sort by.</param> |
| | 93 | | /// <param name="configuration">Sort order configuration.</param> |
| | 94 | | public static IOrderedQueryable<TEntity> ThenBy<TEntity>(this IOrderedQueryable<TEntity> source, string propertyPath |
| 18 | 95 | | => source.OrderBy(nameof(Queryable.ThenBy), propertyPath, configuration); |
| | 96 | |
|
| | 97 | | /// <summary> |
| | 98 | | /// Performs a subsequent ordering of the elements in a sequence in descending order according to a property path. |
| | 99 | | /// </summary> |
| | 100 | | /// <typeparam name="TEntity">The type of the sorted entity.</typeparam> |
| | 101 | | /// <param name="source">A sequence of values to sort.</param> |
| | 102 | | /// <param name="propertyPath">Path to the property to sort by.</param> |
| | 103 | | /// <param name="configuration">Sort order configuration.</param> |
| | 104 | | public static IOrderedQueryable<TEntity> ThenByDescending<TEntity>(this IOrderedQueryable<TEntity> source, string pr |
| 12 | 105 | | => source.OrderBy(nameof(Queryable.ThenByDescending), propertyPath, configuration); |
| | 106 | |
|
| | 107 | | private static IOrderedQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string methodName, strin |
| | 108 | | { |
| 341 | 109 | | var propertyPathParts = propertyPath.Split('.'); |
| 341 | 110 | | if (!propertyPathParts.Any()) |
| 0 | 111 | | throw new ArgumentException("Property path must not be empty.", nameof(propertyPath)); |
| | 112 | |
|
| 341 | 113 | | var parameter = Expression.Parameter(typeof(TEntity), "x"); |
| 341 | 114 | | var useConditionalAccess = source.Provider.ConditionalAccessRequested(configuration); |
| 341 | 115 | | var caseInsensitive = configuration.CaseInsensitivePropertyMatching; |
| 341 | 116 | | var propertyAccess = (Expression)parameter; |
| 1746 | 117 | | foreach (var part in propertyPathParts) |
| | 118 | | { |
| | 119 | | try |
| | 120 | | { |
| 540 | 121 | | propertyAccess = Add(propertyAccess, part, caseInsensitive, useConditionalAccess); |
| 524 | 122 | | } |
| 16 | 123 | | catch (ArgumentException ex) when (ex.Message.Contains("not found on type")) |
| | 124 | | { |
| 16 | 125 | | if (configuration.IgnoreParseExceptions) |
| 9 | 126 | | return source.OrderBy(x => 0); |
| 7 | 127 | | throw; |
| | 128 | | } |
| | 129 | | } |
| | 130 | |
|
| 325 | 131 | | var propertyAccessLambda = Expression |
| 325 | 132 | | .Lambda(propertyAccess, parameter); |
| | 133 | |
|
| 325 | 134 | | var orderByExpression = Expression |
| 325 | 135 | | .Call( |
| 325 | 136 | | type: typeof(Queryable), |
| 325 | 137 | | methodName: methodName, |
| 325 | 138 | | typeArguments: [typeof(TEntity), propertyAccess.Type], |
| 325 | 139 | | arguments: [source.Expression, Expression.Quote(propertyAccessLambda)] |
| 325 | 140 | | ); |
| | 141 | |
|
| 325 | 142 | | return (IOrderedQueryable<TEntity>)source.Provider.CreateQuery<TEntity>(orderByExpression); |
| 9 | 143 | | } |
| | 144 | |
|
| | 145 | | private static bool ConditionalAccessRequested(this IQueryProvider provider, SortConfiguration configuration) |
| | 146 | | { |
| 341 | 147 | | switch (configuration.UseConditionalAccess) |
| | 148 | | { |
| | 149 | | case SortConditionalAccess.Never: |
| 1 | 150 | | return false; |
| | 151 | | case SortConditionalAccess.Always: |
| 1 | 152 | | return true; |
| | 153 | | case SortConditionalAccess.WhenEnumerableQuery: |
| 339 | 154 | | var providerType = provider.GetType(); |
| 339 | 155 | | var isEnumerableQueryProvider = providerType.IsGenericType && providerType.GetGenericTypeDefinition() == |
| 166 | 156 | | return isEnumerableQueryProvider; |
| | 157 | | default: |
| 0 | 158 | | throw new UnreachableException(); |
| | 159 | | } |
| | 160 | | } |
| | 161 | |
|
| | 162 | | private static Expression Add(Expression memberAccess, string propertyName, bool caseInsensitive, bool useConditiona |
| | 163 | | { |
| 540 | 164 | | if (propertyName.EqualsOrdinal(PropertySort.PATH_TO_SELF)) |
| 0 | 165 | | return memberAccess; |
| | 166 | |
|
| 540 | 167 | | var memberType = memberAccess.Type; |
| 540 | 168 | | var property = memberType.GetProperty(propertyName); |
| 540 | 169 | | if (property == null && caseInsensitive) |
| 28 | 170 | | property = memberType.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags |
| 540 | 171 | | if (property == null) |
| 16 | 172 | | throw new ArgumentException($"Property '{propertyName}' not found on type '{memberType.Name}'.", nameof(prop |
| | 173 | |
|
| 524 | 174 | | var propertyAccess = Expression.MakeMemberAccess(memberAccess, property); |
| | 175 | |
|
| 524 | 176 | | var conditionalAccessRequired = useConditionalAccess && property.PropertyType.IsNullable(); |
| 524 | 177 | | if (!conditionalAccessRequired) |
| 268 | 178 | | return propertyAccess; |
| | 179 | |
|
| 256 | 180 | | var memberNull = Expression.Constant(null, memberAccess.Type); |
| 256 | 181 | | var memberIsNull = Expression.Equal(memberAccess, memberNull); |
| 256 | 182 | | var propertyNull = Expression.Constant(null, property.PropertyType); |
| 256 | 183 | | var conditionalPropertyAccess = Expression.Condition(memberIsNull, propertyNull, propertyAccess); |
| 256 | 184 | | return conditionalPropertyAccess; |
| | 185 | | } |
| | 186 | | } |