Skip to content

Commit 9a99630

Browse files
author
Chris Santero
committed
allow materializers to specify a default sort order
1 parent 4dbb4ca commit 9a99630

19 files changed

Lines changed: 213 additions & 60 deletions

JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipDocumentMaterializer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ public StarshipDocumentMaterializer(
2121
TestDbContext dbContext,
2222
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
2323
IBaseUrlService baseUrlService, ISingleResourceDocumentBuilder singleResourceDocumentBuilder,
24+
ISortExpressionExtractor sortExpressionExtractor,
2425
IQueryableEnumerationTransformer queryableEnumerationTransformer, IResourceTypeRegistry resourceTypeRegistry)
2526
: base(
2627
queryableResourceCollectionDocumentBuilder, baseUrlService, singleResourceDocumentBuilder,
27-
queryableEnumerationTransformer, resourceTypeRegistry)
28+
queryableEnumerationTransformer, sortExpressionExtractor, resourceTypeRegistry)
2829
{
2930
_dbContext = dbContext;
3031
}

JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using JSONAPI.Core;
77
using JSONAPI.Documents.Builders;
88
using JSONAPI.EntityFramework.Http;
9+
using JSONAPI.Http;
910

1011
namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers
1112
{
@@ -15,8 +16,9 @@ public class StarshipOfficersRelatedResourceMaterializer : EntityFrameworkToMany
1516

1617
public StarshipOfficersRelatedResourceMaterializer(ResourceTypeRelationship relationship, DbContext dbContext,
1718
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
19+
ISortExpressionExtractor sortExpressionExtractor,
1820
IResourceTypeRegistration primaryTypeRegistration)
19-
: base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, primaryTypeRegistration)
21+
: base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, primaryTypeRegistration)
2022
{
2123
_dbContext = dbContext;
2224
}

JSONAPI.Autofac/JsonApiAutofacModule.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ protected override void Load(ContainerBuilder builder)
157157
builder.RegisterType<JsonApiExceptionFilterAttribute>().SingleInstance();
158158
builder.RegisterType<DefaultQueryableResourceCollectionDocumentBuilder>().As<IQueryableResourceCollectionDocumentBuilder>();
159159

160+
// Misc
161+
builder.RegisterType<DefaultSortExpressionExtractor>().As<ISortExpressionExtractor>().SingleInstance();
160162
}
161163
}
162164
}

JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class EntityFrameworkDocumentMaterializer<T> : IDocumentMaterializer wher
2323
private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder;
2424
private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder;
2525
private readonly IEntityFrameworkResourceObjectMaterializer _entityFrameworkResourceObjectMaterializer;
26+
private readonly ISortExpressionExtractor _sortExpressionExtractor;
2627
private readonly IBaseUrlService _baseUrlService;
2728

2829
/// <summary>
@@ -34,20 +35,23 @@ public EntityFrameworkDocumentMaterializer(
3435
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
3536
ISingleResourceDocumentBuilder singleResourceDocumentBuilder,
3637
IEntityFrameworkResourceObjectMaterializer entityFrameworkResourceObjectMaterializer,
38+
ISortExpressionExtractor sortExpressionExtractor,
3739
IBaseUrlService baseUrlService)
3840
{
3941
_dbContext = dbContext;
4042
_resourceTypeRegistration = resourceTypeRegistration;
4143
_queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder;
4244
_singleResourceDocumentBuilder = singleResourceDocumentBuilder;
4345
_entityFrameworkResourceObjectMaterializer = entityFrameworkResourceObjectMaterializer;
46+
_sortExpressionExtractor = sortExpressionExtractor;
4447
_baseUrlService = baseUrlService;
4548
}
4649

4750
public virtual Task<IResourceCollectionDocument> GetRecords(HttpRequestMessage request, CancellationToken cancellationToken)
4851
{
4952
var query = _dbContext.Set<T>().AsQueryable();
50-
return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken);
53+
var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request);
54+
return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken);
5155
}
5256

5357
public virtual async Task<ISingleResourceDocument> GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken)
@@ -127,8 +131,9 @@ protected async Task<IResourceCollectionDocument> GetRelatedToMany<TRelated>(str
127131
_resourceTypeRegistration.ResourceTypeName, id));
128132

129133
var relatedResourceQuery = primaryEntityQuery.SelectMany(lambda);
134+
var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request);
130135

131-
return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, cancellationToken);
136+
return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, sortExpressions, cancellationToken);
132137
}
133138

134139
/// <summary>

JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ public EntityFrameworkToManyRelatedResourceDocumentMaterializer(
2828
ResourceTypeRelationship relationship,
2929
DbContext dbContext,
3030
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
31+
ISortExpressionExtractor sortExpressionExtractor,
3132
IResourceTypeRegistration primaryTypeRegistration)
32-
: base(queryableResourceCollectionDocumentBuilder)
33+
: base(queryableResourceCollectionDocumentBuilder, sortExpressionExtractor)
3334
{
3435
_relationship = relationship;
3536
_dbContext = dbContext;

JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Net.Http;
54
using FluentAssertions;
65
using JSONAPI.Core;
76
using JSONAPI.Documents.Builders;
@@ -73,111 +72,115 @@ private DefaultSortingTransformer GetTransformer()
7372
return new DefaultSortingTransformer(registry);
7473
}
7574

76-
private TFixture[] GetArray<TFixture>(string uri, IQueryable<TFixture> fixturesQuery)
75+
private TFixture[] GetArray<TFixture>(string[] sortExpressions, IQueryable<TFixture> fixturesQuery)
7776
{
78-
var request = new HttpRequestMessage(HttpMethod.Get, uri);
79-
return GetTransformer().Sort(fixturesQuery, request).ToArray();
77+
return GetTransformer().Sort(fixturesQuery, sortExpressions).ToArray();
8078
}
8179

82-
private Dummy[] GetDummyArray(string uri)
80+
private Dummy[] GetDummyArray(string[] sortExpressions)
8381
{
84-
return GetArray<Dummy>(uri, _fixturesQuery);
82+
return GetArray(sortExpressions, _fixturesQuery);
8583
}
8684

87-
private Dummy2[] GetDummy2Array(string uri)
85+
private Dummy2[] GetDummy2Array(string[] sortExpressions)
8886
{
89-
return GetArray<Dummy2>(uri, _fixtures2Query);
87+
return GetArray(sortExpressions, _fixtures2Query);
9088
}
9189

92-
private void RunTransformAndExpectFailure(string uri, string expectedMessage)
90+
private void RunTransformAndExpectFailure(string[] sortExpressions, string expectedMessage)
9391
{
9492
Action action = () =>
9593
{
96-
var request = new HttpRequestMessage(HttpMethod.Get, uri);
97-
9894
// ReSharper disable once UnusedVariable
99-
var result = GetTransformer().Sort(_fixturesQuery, request).ToArray();
95+
var result = GetTransformer().Sort(_fixturesQuery, sortExpressions).ToArray();
10096
};
10197
action.ShouldThrow<JsonApiException>().Which.Error.Detail.Should().Be(expectedMessage);
10298
}
10399

104100
[TestMethod]
105101
public void Sorts_by_attribute_ascending()
106102
{
107-
var array = GetDummyArray("http://api.example.com/dummies?sort=first-name");
103+
var array = GetDummyArray(new [] { "first-name" });
108104
array.Should().BeInAscendingOrder(d => d.FirstName);
109105
}
110106

111107
[TestMethod]
112108
public void Sorts_by_attribute_descending()
113109
{
114-
var array = GetDummyArray("http://api.example.com/dummies?sort=-first-name");
110+
var array = GetDummyArray(new [] { "-first-name" });
115111
array.Should().BeInDescendingOrder(d => d.FirstName);
116112
}
117113

118114
[TestMethod]
119115
public void Sorts_by_two_ascending_attributes()
120116
{
121-
var array = GetDummyArray("http://api.example.com/dummies?sort=last-name,first-name");
117+
var array = GetDummyArray(new [] { "last-name", "first-name" });
122118
array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName));
123119
}
124120

125121
[TestMethod]
126122
public void Sorts_by_two_descending_attributes()
127123
{
128-
var array = GetDummyArray("http://api.example.com/dummies?sort=-last-name,-first-name");
124+
var array = GetDummyArray(new [] { "-last-name", "-first-name" });
129125
array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName));
130126
}
131127

128+
[TestMethod]
129+
public void Sorts_by_id_when_expressions_are_empty()
130+
{
131+
var array = GetDummyArray(new string[] { });
132+
array.Should().ContainInOrder(_fixtures.OrderBy(d => d.Id));
133+
}
134+
132135
[TestMethod]
133136
public void Returns_400_if_sort_argument_is_empty()
134137
{
135-
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=", "One of the sort expressions is empty.");
138+
RunTransformAndExpectFailure(new[] { "" }, "One of the sort expressions is empty.");
136139
}
137140

138141
[TestMethod]
139142
public void Returns_400_if_sort_argument_is_whitespace()
140143
{
141-
RunTransformAndExpectFailure("http://api.example.com/dummies?sort= ", "One of the sort expressions is empty.");
144+
RunTransformAndExpectFailure(new [] { " " }, "One of the sort expressions is empty.");
142145
}
143146

144147
[TestMethod]
145148
public void Returns_400_if_sort_argument_is_empty_descending()
146149
{
147-
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=-", "One of the sort expressions is empty.");
150+
RunTransformAndExpectFailure(new [] { "-" }, "One of the sort expressions is empty.");
148151
}
149152

150153
[TestMethod]
151154
public void Returns_400_if_sort_argument_is_whitespace_descending()
152155
{
153-
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=- ", "One of the sort expressions is empty.");
156+
RunTransformAndExpectFailure(new[] { "- " }, "One of the sort expressions is empty.");
154157
}
155158

156159
[TestMethod]
157160
public void Returns_400_if_no_property_exists()
158161
{
159-
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=foobar",
162+
RunTransformAndExpectFailure(new[] { "foobar" },
160163
"The attribute \"foobar\" does not exist on type \"dummies\".");
161164
}
162165

163166
[TestMethod]
164167
public void Returns_400_if_the_same_property_is_specified_more_than_once()
165168
{
166-
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=last-name,last-name",
169+
RunTransformAndExpectFailure(new[] { "last-name", "last-name" },
167170
"The attribute \"last-name\" was specified more than once.");
168171
}
169172

170173
[TestMethod]
171174
public void Can_sort_by_DateTimeOffset()
172175
{
173-
var array = GetDummyArray("http://api.example.com/dummies?sort=birth-date");
176+
var array = GetDummyArray(new [] { "birth-date" });
174177
array.Should().BeInAscendingOrder(d => d.BirthDate);
175178
}
176179

177180
[TestMethod]
178181
public void Can_sort_by_resource_with_integer_key()
179182
{
180-
var array = GetDummy2Array("http://api.example.com/dummy2s?sort=name");
183+
var array = GetDummy2Array(new [] { "name" });
181184
array.Should().BeInAscendingOrder(d => d.Name);
182185
}
183186
}

JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,12 @@ public async Task Creates_single_resource_document_for_registered_non_collection
4343
var mockBaseUrlService = new Mock<IBaseUrlService>(MockBehavior.Strict);
4444
mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com");
4545

46+
var mockSortExpressionExtractor = new Mock<ISortExpressionExtractor>(MockBehavior.Strict);
47+
mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new [] { "id "});
48+
4649
// Act
4750
var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object,
48-
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object);
51+
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object);
4952
var resultDocument = await fallbackDocumentBuilder.BuildDocument(objectContent, request, cancellationTokenSource.Token);
5053

5154
// Assert
@@ -70,19 +73,24 @@ public async Task Creates_resource_collection_document_for_queryables()
7073

7174
var mockBaseUrlService = new Mock<IBaseUrlService>(MockBehavior.Strict);
7275
mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/");
76+
77+
var sortExpressions = new[] { "id" };
7378

7479
var cancellationTokenSource = new CancellationTokenSource();
7580

7681
var mockQueryableDocumentBuilder = new Mock<IQueryableResourceCollectionDocumentBuilder>(MockBehavior.Strict);
7782
mockQueryableDocumentBuilder
78-
.Setup(b => b.BuildDocument(items, request, cancellationTokenSource.Token, null))
83+
.Setup(b => b.BuildDocument(items, request, sortExpressions, cancellationTokenSource.Token, null))
7984
.Returns(Task.FromResult(mockDocument.Object));
8085

8186
var mockResourceCollectionDocumentBuilder = new Mock<IResourceCollectionDocumentBuilder>(MockBehavior.Strict);
8287

88+
var mockSortExpressionExtractor = new Mock<ISortExpressionExtractor>(MockBehavior.Strict);
89+
mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(sortExpressions);
90+
8391
// Act
8492
var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object,
85-
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object);
93+
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object);
8694
var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token);
8795

8896
// Assert
@@ -116,9 +124,12 @@ public async Task Creates_resource_collection_document_for_non_queryable_enumera
116124
.Setup(b => b.BuildDocument(items, "https://www.example.com/", It.IsAny<string[]>(), It.IsAny<IMetadata>(), null))
117125
.Returns(() => (mockDocument.Object));
118126

127+
var mockSortExpressionExtractor = new Mock<ISortExpressionExtractor>(MockBehavior.Strict);
128+
mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new[] { "id " });
129+
119130
// Act
120131
var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object,
121-
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object);
132+
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object);
122133
var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token);
123134

124135
// Assert
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Net.Http;
2+
using FluentAssertions;
3+
using JSONAPI.Http;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
6+
namespace JSONAPI.Tests.Http
7+
{
8+
[TestClass]
9+
public class DefaultSortExpressionExtractorTests
10+
{
11+
[TestMethod]
12+
public void ExtractsSingleSortExpressionFromUri()
13+
{
14+
// Arrange
15+
const string uri = "http://api.example.com/dummies?sort=first-name";
16+
var request = new HttpRequestMessage(HttpMethod.Get, uri);
17+
18+
// Act
19+
var extractor = new DefaultSortExpressionExtractor();
20+
var sortExpressions = extractor.ExtractSortExpressions(request);
21+
22+
// Assert
23+
sortExpressions.Should().BeEquivalentTo("first-name");
24+
}
25+
26+
[TestMethod]
27+
public void ExtractsSingleDescendingSortExpressionFromUri()
28+
{
29+
// Arrange
30+
const string uri = "http://api.example.com/dummies?sort=-first-name";
31+
var request = new HttpRequestMessage(HttpMethod.Get, uri);
32+
33+
// Act
34+
var extractor = new DefaultSortExpressionExtractor();
35+
var sortExpressions = extractor.ExtractSortExpressions(request);
36+
37+
// Assert
38+
sortExpressions.Should().BeEquivalentTo("-first-name");
39+
}
40+
41+
[TestMethod]
42+
public void ExtractsMultipleSortExpressionsFromUri()
43+
{
44+
// Arrange
45+
const string uri = "http://api.example.com/dummies?sort=last-name,first-name";
46+
var request = new HttpRequestMessage(HttpMethod.Get, uri);
47+
48+
// Act
49+
var extractor = new DefaultSortExpressionExtractor();
50+
var sortExpressions = extractor.ExtractSortExpressions(request);
51+
52+
// Assert
53+
sortExpressions.Should().BeEquivalentTo("last-name", "first-name");
54+
}
55+
56+
[TestMethod]
57+
public void ExtractsMultipleSortExpressionsFromUriWithDifferentDirections()
58+
{
59+
// Arrange
60+
const string uri = "http://api.example.com/dummies?sort=last-name,-first-name";
61+
var request = new HttpRequestMessage(HttpMethod.Get, uri);
62+
63+
// Act
64+
var extractor = new DefaultSortExpressionExtractor();
65+
var sortExpressions = extractor.ExtractSortExpressions(request);
66+
67+
// Assert
68+
sortExpressions.Should().BeEquivalentTo("last-name", "-first-name");
69+
}
70+
71+
[TestMethod]
72+
public void ExtractsNothingWhenThereIsNoSortParam()
73+
{
74+
// Arrange
75+
const string uri = "http://api.example.com/dummies";
76+
var request = new HttpRequestMessage(HttpMethod.Get, uri);
77+
78+
// Act
79+
var extractor = new DefaultSortExpressionExtractor();
80+
var sortExpressions = extractor.ExtractSortExpressions(request);
81+
82+
// Assert
83+
sortExpressions.Length.Should().Be(0);
84+
}
85+
}
86+
}

JSONAPI.Tests/JSONAPI.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
<Compile Include="Core\ResourceTypeRegistrarTests.cs" />
9191
<Compile Include="Core\ResourceTypeRegistryTests.cs" />
9292
<Compile Include="Extensions\TypeExtensionsTests.cs" />
93+
<Compile Include="Http\DefaultSortExpressionExtractorTests.cs" />
9394
<Compile Include="Json\JsonApiFormatterTests.cs" />
9495
<Compile Include="Models\Author.cs" />
9596
<Compile Include="Models\Comment.cs" />

0 commit comments

Comments
 (0)