| | 1 | | using Microsoft.AspNetCore.Mvc.ModelBinding; |
| | 2 | | using Microsoft.Extensions.DependencyInjection; |
| | 3 | | using Microsoft.Extensions.Options; |
| | 4 | | using Plainquire.Filter.Abstractions; |
| | 5 | | using Plainquire.Sort.Abstractions; |
| | 6 | | using System; |
| | 7 | | using System.Collections.Generic; |
| | 8 | | using System.Diagnostics.CodeAnalysis; |
| | 9 | | using System.Linq; |
| | 10 | | using System.Reflection; |
| | 11 | | using System.Text.RegularExpressions; |
| | 12 | | using System.Threading.Tasks; |
| | 13 | |
|
| | 14 | | namespace Plainquire.Sort.Mvc.ModelBinders; |
| | 15 | |
|
| | 16 | | /// <summary> |
| | 17 | | /// ModelBinder for <see cref="EntitySort{TEntity}"/> |
| | 18 | | /// Implements <see cref="IModelBinder" /> |
| | 19 | | /// </summary> |
| | 20 | | /// <seealso cref="IModelBinder" /> |
| | 21 | | public class EntitySortModelBinder : IModelBinder |
| | 22 | | { |
| | 23 | | /// <inheritdoc /> |
| | 24 | | public Task BindModelAsync(ModelBindingContext bindingContext) |
| | 25 | | { |
| 38 | 26 | | if (bindingContext == null) |
| 0 | 27 | | throw new ArgumentNullException(nameof(bindingContext)); |
| | 28 | |
|
| 38 | 29 | | var serviceProvider = bindingContext.ActionContext.HttpContext.RequestServices; |
| | 30 | |
|
| 38 | 31 | | var sortedType = bindingContext.ModelType.GetGenericArguments()[0]; |
| 38 | 32 | | var entitySort = CreateEntitySort(sortedType, serviceProvider); |
| | 33 | |
|
| 38 | 34 | | var entitySortConfiguration = entitySort.Configuration; |
| 38 | 35 | | var configuration = entitySortConfiguration ?? SortConfiguration.Default ?? new SortConfiguration(); |
| | 36 | |
|
| 38 | 37 | | var sortByParameterName = bindingContext.OriginalModelName; |
| 38 | 38 | | var sortByParameterValues = bindingContext.HttpContext.Request.Query.Keys |
| 38 | 39 | | .Where(queryParameter => IsSortByParameter(queryParameter, sortByParameterName)) |
| 38 | 40 | | .SelectMany(queryParameter => GetParameterValues(queryParameter, bindingContext)) |
| 38 | 41 | | .SelectMany(value => value.SplitCommaSeparatedValues()) |
| 38 | 42 | | .ToList(); |
| | 43 | |
|
| 38 | 44 | | var entityEntitySort = entitySort.Apply(sortByParameterValues, configuration); |
| 38 | 45 | | bindingContext.Result = ModelBindingResult.Success(entityEntitySort); |
| 38 | 46 | | return Task.CompletedTask; |
| | 47 | | } |
| | 48 | |
|
| | 49 | | private static bool IsSortByParameter(string queryParameterName, string sortByParameterName) |
| 58 | 50 | | => Regex.IsMatch(queryParameterName, @$"{sortByParameterName}(\[\d*\])?", RegexOptions.IgnoreCase, RegexDefaults |
| | 51 | |
|
| | 52 | | private static ValueProviderResult GetParameterValues(string queryParameter, ModelBindingContext bindingContext) |
| 38 | 53 | | => bindingContext.ValueProvider.GetValue(queryParameter); |
| | 54 | |
|
| | 55 | | private static EntitySort CreateEntitySort(Type sortedType, IServiceProvider serviceProvider) |
| | 56 | | { |
| 38 | 57 | | var entitySortType = typeof(EntitySort<>).MakeGenericType(sortedType); |
| 38 | 58 | | var entitySortInstance = Activator.CreateInstance(entitySortType) |
| 38 | 59 | | ?? throw new InvalidOperationException($"Unable to create instance of type {entitySortType.Name}"); |
| | 60 | |
|
| 38 | 61 | | var entitySort = (EntitySort)entitySortInstance; |
| | 62 | |
|
| 38 | 63 | | var prototypeConfiguration = ((EntitySort?)serviceProvider.GetService(entitySortType))?.Configuration; |
| 38 | 64 | | var injectedConfiguration = serviceProvider.GetService<IOptions<SortConfiguration>>()?.Value; |
| 38 | 65 | | entitySort.Configuration = prototypeConfiguration ?? injectedConfiguration; |
| | 66 | |
|
| 38 | 67 | | return entitySort; |
| | 68 | | } |
| | 69 | | } |
| | 70 | |
|
| | 71 | | file static class Extensions |
| | 72 | | { |
| | 73 | | public static EntitySort Apply(this EntitySort entitySort, IEnumerable<string> sortParameters, SortConfiguration con |
| | 74 | | { |
| | 75 | | var sortedType = entitySort.GetType().GenericTypeArguments[0]; |
| | 76 | | var entityFilterAttribute = sortedType.GetCustomAttribute<EntityFilterAttribute>(); |
| | 77 | |
|
| | 78 | | var sortablePropertyNameToParameterMap = sortedType |
| | 79 | | .GetSortableProperties() |
| | 80 | | .Select(property => new PropertyNameToParameterMap( |
| | 81 | | PropertyName: property.Name, |
| | 82 | | ParameterName: property.GetSortParameterName(entityFilterAttribute?.Prefix) |
| | 83 | | )) |
| | 84 | | .ToList(); |
| | 85 | |
|
| | 86 | | var propertySorts = sortParameters |
| | 87 | | .Select(parameter => MapToPropertyPath(parameter, sortablePropertyNameToParameterMap, configuration)) |
| | 88 | | .Select((propertyPath, index) => propertyPath != null ? PropertySort.Create(propertyPath, index, configurati |
| | 89 | | .WhereNotNull() |
| | 90 | | .ToList(); |
| | 91 | |
|
| | 92 | | foreach (var propertySort in propertySorts) |
| | 93 | | entitySort.PropertySorts.Add(propertySort); |
| | 94 | |
|
| | 95 | | return entitySort; |
| | 96 | | } |
| | 97 | |
|
| | 98 | | private static string? MapToPropertyPath(string parameter, IReadOnlyCollection<PropertyNameToParameterMap> sortableP |
| | 99 | | { |
| | 100 | | var sortSyntaxMatch = Regex.Match(parameter, configuration.SortDirectionPattern, RegexOptions.IgnoreCase, RegexD |
| | 101 | | if (!sortSyntaxMatch.Success) |
| | 102 | | return null; |
| | 103 | |
|
| | 104 | | var prefix = sortSyntaxMatch.Groups["prefix"].Value; |
| | 105 | | var propertyPath = sortSyntaxMatch.Groups["propertyPath"].Value; |
| | 106 | | var postfix = sortSyntaxMatch.Groups["postfix"].Value; |
| | 107 | |
|
| | 108 | | var propertyPathSegments = propertyPath |
| | 109 | | .Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); |
| | 110 | |
|
| | 111 | | var primaryParameterName = propertyPathSegments.FirstOrDefault(); |
| | 112 | |
|
| | 113 | | var property = sortableProperties.FirstOrDefault(x => x.ParameterName.EqualsOrdinal(primaryParameterName)) ?? |
| | 114 | | sortableProperties.FirstOrDefault(x => x.ParameterName.Equals(primaryParameterName, StringCompari |
| | 115 | |
|
| | 116 | | if (property == null) |
| | 117 | | return null; |
| | 118 | |
|
| | 119 | | propertyPathSegments[0] = property.PropertyName; |
| | 120 | |
|
| | 121 | | return prefix + string.Join('.', propertyPathSegments) + postfix; |
| | 122 | | } |
| | 123 | |
|
| | 124 | | [ExcludeFromCodeCoverage] |
| | 125 | | private record PropertyNameToParameterMap(string PropertyName, string ParameterName); |
| | 126 | | } |