diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs index 6c479a5db1..7c6a171854 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs @@ -43,6 +43,9 @@ public string FirstName [Attr] public Gender Gender { get; set; } + [Attr] + public string Category { get; set; } + [HasMany] public ISet TodoItems { get; set; } diff --git a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs index 7f53bd2124..d523e4e23c 100644 --- a/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs @@ -87,11 +87,24 @@ public virtual IQueryable Filter(IQueryable entities, Filt } /// - public virtual IQueryable Sort(IQueryable entities, SortQueryContext sortQueryContext) + public virtual IQueryable Sort(IQueryable entities, IReadOnlyCollection sortQueryContexts) { - _logger.LogTrace($"Entering {nameof(Sort)}({nameof(entities)}, {nameof(sortQueryContext)})."); + _logger.LogTrace($"Entering {nameof(Sort)}({nameof(entities)}, {nameof(sortQueryContexts)})."); - return entities.Sort(sortQueryContext); + if (!sortQueryContexts.Any()) + { + return entities; + } + + var primarySort = sortQueryContexts.First(); + var entitiesSorted = entities.Sort(primarySort); + + foreach (var secondarySort in sortQueryContexts.Skip(1)) + { + entitiesSorted = entitiesSorted.Sort(secondarySort); + } + + return entitiesSorted; } /// diff --git a/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs b/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs index ad0af9ebd8..d77b57e0ae 100644 --- a/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs +++ b/src/JsonApiDotNetCore/Data/IResourceReadRepository.cs @@ -43,7 +43,7 @@ public interface IResourceReadRepository /// /// Apply a sort to the provided queryable /// - IQueryable Sort(IQueryable entities, SortQueryContext sortQueries); + IQueryable Sort(IQueryable entities, IReadOnlyCollection sortQueryContexts); /// /// Paginate the provided queryable /// diff --git a/src/JsonApiDotNetCore/Extensions/QueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/QueryableExtensions.cs index 30fb7d378a..c4c4cee0ba 100644 --- a/src/JsonApiDotNetCore/Extensions/QueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/QueryableExtensions.cs @@ -77,7 +77,6 @@ public static IOrderedQueryable Sort(this IQueryable public static IOrderedQueryable Sort(this IOrderedQueryable source, SortQueryContext sortQuery) { - return sortQuery.Direction == SortDirection.Descending ? source.ThenByDescending(sortQuery.GetPropertyPath()) : source.ThenBy(sortQuery.GetPropertyPath()); diff --git a/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs b/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs index f1df933a70..e5a4a76cf7 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs @@ -29,13 +29,21 @@ public SortService(IResourceDefinitionProvider resourceDefinitionProvider, /// public List Get() { - if (!_queries.Any()) + if (_queries.Any()) { - var requestResourceDefinition = _resourceDefinitionProvider.Get(_requestResource.ResourceType); - if (requestResourceDefinition != null) - return requestResourceDefinition.DefaultSort()?.Select(d => BuildQueryContext(new SortQuery(d.Attribute.PublicAttributeName, d.SortDirection))).ToList(); + return _queries.ToList(); } - return _queries.ToList(); + + var requestResourceDefinition = _resourceDefinitionProvider.Get(_requestResource.ResourceType); + var defaultSort = requestResourceDefinition?.DefaultSort(); + if (defaultSort != null) + { + return defaultSort + .Select(d => BuildQueryContext(new SortQuery(d.Attribute.PublicAttributeName, d.SortDirection))) + .ToList(); + } + + return new List(); } /// diff --git a/src/JsonApiDotNetCore/Services/DefaultResourceService.cs b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs index dac9ee7aff..b6fb6133b5 100644 --- a/src/JsonApiDotNetCore/Services/DefaultResourceService.cs +++ b/src/JsonApiDotNetCore/Services/DefaultResourceService.cs @@ -290,10 +290,7 @@ protected virtual async Task> ApplyPageQueryAsync(IQuerya protected virtual IQueryable ApplySort(IQueryable entities) { var queries = _sortService.Get(); - if (queries != null && queries.Any()) - foreach (var query in queries) - entities = _repository.Sort(entities, query); - + entities = _repository.Sort(entities, queries); return entities; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs index ac692ce4fb..772ae36613 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs @@ -26,7 +26,8 @@ public async Task When_getting_person_it_must_match_JSON_text() FirstName = "John", LastName = "Doe", Age = 57, - Gender = Gender.Male + Gender = Gender.Male, + Category = "Family" }; _dbContext.People.RemoveRange(_dbContext.People); @@ -65,7 +66,8 @@ public async Task When_getting_person_it_must_match_JSON_text() ""initials"": ""J"", ""lastName"": ""Doe"", ""the-Age"": 57, - ""gender"": ""Male"" + ""gender"": ""Male"", + ""category"": ""Family"" }, ""relationships"": { ""todoItems"": { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs index 0400ad6346..ab7e249b50 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeSortTests.cs @@ -1,8 +1,11 @@ +using System; using JsonApiDotNetCoreExample; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCoreExample.Models; using Newtonsoft.Json; using Xunit; @@ -40,5 +43,66 @@ public async Task Cannot_Sort_If_Explicitly_Forbidden() Assert.Equal("Sorting on attribute 'achievedDate' is not allowed.", errorDocument.Errors[0].Detail); Assert.Equal("sort", errorDocument.Errors[0].Source.Parameter); } + + [Fact] + public async Task Can_Sort_On_Multiple_Attributes() + { + // Arrange + var category = Guid.NewGuid().ToString(); + + var persons = new[] + { + new Person + { + Category = category, + FirstName = "Alice", + LastName = "Smith", + Age = 23 + }, + new Person + { + Category = category, + FirstName = "John", + LastName = "Doe", + Age = 49 + }, + new Person + { + Category = category, + FirstName = "John", + LastName = "Doe", + Age = 31 + }, + new Person + { + Category = category, + FirstName = "Jane", + LastName = "Doe", + Age = 19 + } + }; + + _fixture.Context.People.AddRange(persons); + _fixture.Context.SaveChanges(); + + var httpMethod = new HttpMethod("GET"); + var route = "/api/v1/people?filter[category]=" + category + "&sort=lastName,-firstName,the-Age"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var document = JsonConvert.DeserializeObject(body); + Assert.Equal(4, document.ManyData.Count); + + Assert.Equal(document.ManyData[0].Id, persons[2].StringId); + Assert.Equal(document.ManyData[1].Id, persons[1].StringId); + Assert.Equal(document.ManyData[2].Id, persons[3].StringId); + Assert.Equal(document.ManyData[3].Id, persons[0].StringId); + } } }