Skip to content

Fixed: sorting on multiple attributes #755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Person.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public string FirstName
[Attr]
public Gender Gender { get; set; }

[Attr]
public string Category { get; set; }

[HasMany]
public ISet<TodoItem> TodoItems { get; set; }

Expand Down
19 changes: 16 additions & 3 deletions src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,24 @@ public virtual IQueryable<TResource> Filter(IQueryable<TResource> entities, Filt
}

/// <inheritdoc />
public virtual IQueryable<TResource> Sort(IQueryable<TResource> entities, SortQueryContext sortQueryContext)
public virtual IQueryable<TResource> Sort(IQueryable<TResource> entities, IReadOnlyCollection<SortQueryContext> 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;
}

/// <inheritdoc />
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/Data/IResourceReadRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public interface IResourceReadRepository<TResource, in TId>
/// <summary>
/// Apply a sort to the provided queryable
/// </summary>
IQueryable<TResource> Sort(IQueryable<TResource> entities, SortQueryContext sortQueries);
IQueryable<TResource> Sort(IQueryable<TResource> entities, IReadOnlyCollection<SortQueryContext> sortQueryContexts);
/// <summary>
/// Paginate the provided queryable
/// </summary>
Expand Down
1 change: 0 additions & 1 deletion src/JsonApiDotNetCore/Extensions/QueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ public static IOrderedQueryable<TSource> Sort<TSource>(this IQueryable<TSource>

public static IOrderedQueryable<TSource> Sort<TSource>(this IOrderedQueryable<TSource> source, SortQueryContext sortQuery)
{

return sortQuery.Direction == SortDirection.Descending
? source.ThenByDescending(sortQuery.GetPropertyPath())
: source.ThenBy(sortQuery.GetPropertyPath());
Expand Down
18 changes: 13 additions & 5 deletions src/JsonApiDotNetCore/QueryParameterServices/SortService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,21 @@ public SortService(IResourceDefinitionProvider resourceDefinitionProvider,
/// <inheritdoc/>
public List<SortQueryContext> 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<SortQueryContext>();
}

/// <inheritdoc/>
Expand Down
5 changes: 1 addition & 4 deletions src/JsonApiDotNetCore/Services/DefaultResourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,7 @@ protected virtual async Task<IEnumerable<TResource>> ApplyPageQueryAsync(IQuerya
protected virtual IQueryable<TResource> ApplySort(IQueryable<TResource> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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"": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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<Document>(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);
}
}
}