[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class SortableAttribute : Attribute { public string EntityName { get; set; } public bool Default { get; set; } }
public class SortTerm { public string Name { get; set; } public string EntityName { get; set; } public bool Descending { get; set; } public bool Default { get; set; } }
public class SortOptionsProcessor<Model, Entity> { private readonly string[] _sort; public SortOptionsProcessor(string[] sortArray) { _sort = sortArray; } public IEnumerable<SortTerm> GetTerms() { if (_sort == null) { yield break; } foreach (var s in _sort) { if (string.IsNullOrEmpty(s)) { continue; } var arr = s.Split(' '); if (arr.Length > 1) { yield return new SortTerm { Name = arr[0], EntityName = arr[0], Descending = arr[1].Equals("desc", StringComparison.OrdinalIgnoreCase), Default = false }; } else { yield return new SortTerm { Name = s, EntityName = s, Descending = false, Default = false }; } } } public IEnumerable<SortTerm> GetValidTerms() { var terms = GetTerms().ToArray(); if (terms.Any()) { var termsFromModel = GetModelTerms(); foreach (var t in terms) { var term = termsFromModel.SingleOrDefault(v => v.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase)); if (term == null) { continue; } yield return new SortTerm { Name = term.Name, EntityName = term.EntityName, Descending = t.Descending, Default = term.Default }; } } } public IEnumerable<SortTerm> GetModelTerms() { return typeof(Model).GetTypeInfo().DeclaredProperties .Where(v => v.GetCustomAttributes<SortableAttribute>().Any()) .Select(v => new SortTerm { Name = v.Name, EntityName = v.GetCustomAttribute<SortableAttribute>().EntityName, Descending = false, Default = v.GetCustomAttribute<SortableAttribute>().Default }); } public IQueryable<Entity> Process(IQueryable<Entity> queryable) { var terms = GetValidTerms().ToArray(); if (terms.Any() == false) { terms = GetModelTerms().Where(v => v.Default).ToArray(); } if (terms.Any() == false) { return queryable; } var useThenBy = false; foreach (var t in terms) { var propertyInfo = ExpressionHelper.GetPropertyInfo<Entity>(t.EntityName ?? t.Name); var parameterExpr = ExpressionHelper.Parameter<Entity>(); var memberExpr = ExpressionHelper.GetPropertyExpression(parameterExpr, propertyInfo); var lambdaExpr = ExpressionHelper.GetLambda(typeof(Entity), propertyInfo.PropertyType, parameterExpr, memberExpr); queryable = ExpressionHelper.CallOrderByOrThenBy(queryable, useThenBy, t.Descending, propertyInfo.PropertyType, lambdaExpr); useThenBy = true; } return queryable; } }
public class SortOptions<Model, Entity> : IValidatableObject { public string[] Sort { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { var processor = new SortOptionsProcessor<Model, Entity>(Sort); var terms = processor.GetTerms() .Select(v => v.Name) .Except(processor.GetValidTerms().Select(v => v.Name), StringComparer.OrdinalIgnoreCase); foreach (var t in terms) { yield return new ValidationResult($"Invalid sort term '{t}'.", new[] { nameof(Sort) }); } } public IQueryable<Entity> Process(IQueryable<Entity> queryable) { return new SortOptionsProcessor<Model, Entity>(Sort).Process(queryable); } }
public class MemberModel { [Sortable(EntityName = nameof(Id), Default = true)] public int Id { get; set; } [Sortable(EntityName = nameof(Name))] public string Name { get; set; } }
public class Member { public int Id { get; set; } public string Name { get; set; } }
[ApiController] [Route("/members")] public class MemberController : ControllerBase { [HttpGet] public IActionResult Get([FromQuery]SortOptions<MemberModel, Member> sortOptions) { var members = new List<Member> { new Member { Id = 1, Name = "A" }, new Member { Id = 3, Name = "B" }, new Member { Id = 2, Name = "B" } }; var queryable = members.AsQueryable(); queryable = sortOptions.Process(queryable); return Ok(queryable.ToList()); } }