Skip to content

Commit 491beb3

Browse files
author
Jacob Hilty
committed
Closes #273
1 parent 015938f commit 491beb3

File tree

5 files changed

+119
-27
lines changed

5 files changed

+119
-27
lines changed

src/JsonApiDotNetCore/Builders/LinkBuilder.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ public string GetRelatedRelationLink(string parent, string parentId, string chil
4848

4949
public string GetPageLink(int pageOffset, int pageSize)
5050
{
51-
return $"{_context.BasePath}/{_context.RequestEntity.EntityName}?page[size]={pageSize}&page[number]={pageOffset}";
51+
var filterQueryComposer = new QueryComposer();
52+
var filters = filterQueryComposer.Compose(_context);
53+
return $"{_context.BasePath}/{_context.RequestEntity.EntityName}?page[size]={pageSize}&page[number]={pageOffset}{filters}";
5254
}
5355
}
5456
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace JsonApiDotNetCore.Internal.Query{
2+
public static class QueryConstants {
3+
public const string FILTER = "filter";
4+
public const string SORT = "sort";
5+
public const string INCLUDE = "include";
6+
public const string PAGE = "page";
7+
public const string FIELDS = "fields";
8+
public const char OPEN_BRACKET = '[';
9+
public const char CLOSE_BRACKET = ']';
10+
public const char COMMA = ',';
11+
public const char COLON = ':';
12+
public const string COLON_STR = ":";
13+
14+
}
15+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Collections.Generic;
2+
using JsonApiDotNetCore.Internal.Query;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace JsonApiDotNetCore.Services
6+
{
7+
public interface IQueryComposer
8+
{
9+
string Compose(IJsonApiContext jsonApiContext);
10+
}
11+
12+
public class QueryComposer : IQueryComposer
13+
{
14+
public string Compose(IJsonApiContext jsonApiContext)
15+
{
16+
string result = "";
17+
if(jsonApiContext != null)
18+
{
19+
List<FilterQuery> filterQueries = jsonApiContext.QuerySet.Filters;
20+
if (filterQueries.Count > 0)
21+
{
22+
foreach (FilterQuery filter in filterQueries)
23+
{
24+
result += ComposeSingleFilter(filter);
25+
}
26+
}
27+
}
28+
return result;
29+
}
30+
31+
private string ComposeSingleFilter(FilterQuery query)
32+
{
33+
var result = "&filter";
34+
result += QueryConstants.OPEN_BRACKET + query.Attribute + QueryConstants.CLOSE_BRACKET + query.Operation + query.Value;
35+
return result;
36+
}
37+
}
38+
}

src/JsonApiDotNetCore/Services/QueryParser.cs

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,6 @@ public class QueryParser : IQueryParser
2020
private readonly IControllerContext _controllerContext;
2121
private readonly JsonApiOptions _options;
2222

23-
private const string FILTER = "filter";
24-
private const string SORT = "sort";
25-
private const string INCLUDE = "include";
26-
private const string PAGE = "page";
27-
private const string FIELDS = "fields";
28-
private const char OPEN_BRACKET = '[';
29-
private const char CLOSE_BRACKET = ']';
30-
private const char COMMA = ',';
31-
private const char COLON = ':';
32-
private const string COLON_STR = ":";
33-
3423
public QueryParser(
3524
IControllerContext controllerContext,
3625
JsonApiOptions options)
@@ -46,35 +35,35 @@ public virtual QuerySet Parse(IQueryCollection query)
4635

4736
foreach (var pair in query)
4837
{
49-
if (pair.Key.StartsWith(FILTER))
38+
if (pair.Key.StartsWith(QueryConstants.FILTER))
5039
{
5140
if (disabledQueries.HasFlag(QueryParams.Filter) == false)
5241
querySet.Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value));
5342
continue;
5443
}
5544

56-
if (pair.Key.StartsWith(SORT))
45+
if (pair.Key.StartsWith(QueryConstants.SORT))
5746
{
5847
if (disabledQueries.HasFlag(QueryParams.Sort) == false)
5948
querySet.SortParameters = ParseSortParameters(pair.Value);
6049
continue;
6150
}
6251

63-
if (pair.Key.StartsWith(INCLUDE))
52+
if (pair.Key.StartsWith(QueryConstants.INCLUDE))
6453
{
6554
if (disabledQueries.HasFlag(QueryParams.Include) == false)
6655
querySet.IncludedRelationships = ParseIncludedRelationships(pair.Value);
6756
continue;
6857
}
6958

70-
if (pair.Key.StartsWith(PAGE))
59+
if (pair.Key.StartsWith(QueryConstants.PAGE))
7160
{
7261
if (disabledQueries.HasFlag(QueryParams.Page) == false)
7362
querySet.PageQuery = ParsePageQuery(querySet.PageQuery, pair.Key, pair.Value);
7463
continue;
7564
}
7665

77-
if (pair.Key.StartsWith(FIELDS))
66+
if (pair.Key.StartsWith(QueryConstants.FIELDS))
7867
{
7968
if (disabledQueries.HasFlag(QueryParams.Fields) == false)
8069
querySet.Fields = ParseFieldsQuery(pair.Key, pair.Value);
@@ -94,9 +83,9 @@ protected virtual List<FilterQuery> ParseFilterQuery(string key, string value)
9483
// expected input = filter[id]=eq:1
9584
var queries = new List<FilterQuery>();
9685

97-
var propertyName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1];
86+
var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1];
9887

99-
var values = value.Split(COMMA);
88+
var values = value.Split(QueryConstants.COMMA);
10089
foreach (var val in values)
10190
{
10291
(var operation, var filterValue) = ParseFilterOperation(val);
@@ -111,7 +100,7 @@ protected virtual (string operation, string value) ParseFilterOperation(string v
111100
if (value.Length < 3)
112101
return (string.Empty, value);
113102

114-
var operation = value.Split(COLON);
103+
var operation = value.Split(QueryConstants.COLON);
115104

116105
if (operation.Length == 1)
117106
return (string.Empty, value);
@@ -121,7 +110,7 @@ protected virtual (string operation, string value) ParseFilterOperation(string v
121110
return (string.Empty, value);
122111

123112
var prefix = operation[0];
124-
value = string.Join(COLON_STR, operation.Skip(1));
113+
value = string.Join(QueryConstants.COLON_STR, operation.Skip(1));
125114

126115
return (prefix, value);
127116
}
@@ -132,7 +121,7 @@ protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, stri
132121
// page[number]=1
133122
pageQuery = pageQuery ?? new PageQuery();
134123

135-
var propertyName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1];
124+
var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1];
136125

137126
const string SIZE = "size";
138127
const string NUMBER = "number";
@@ -157,7 +146,7 @@ protected virtual List<SortQuery> ParseSortParameters(string value)
157146
var sortParameters = new List<SortQuery>();
158147

159148
const char DESCENDING_SORT_OPERATOR = '-';
160-
var sortSegments = value.Split(COMMA);
149+
var sortSegments = value.Split(QueryConstants.COMMA);
161150

162151
foreach (var sortSegment in sortSegments)
163152
{
@@ -189,14 +178,14 @@ protected virtual List<string> ParseIncludedRelationships(string value)
189178
throw new JsonApiException(400, "Deeply nested relationships are not supported");
190179

191180
return value
192-
.Split(COMMA)
181+
.Split(QueryConstants.COMMA)
193182
.ToList();
194183
}
195184

196185
protected virtual List<string> ParseFieldsQuery(string key, string value)
197186
{
198187
// expected: fields[TYPE]=prop1,prop2
199-
var typeName = key.Split(OPEN_BRACKET, CLOSE_BRACKET)[1];
188+
var typeName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1];
200189

201190
const string ID = "Id";
202191
var includedFields = new List<string> { ID };
@@ -205,7 +194,7 @@ protected virtual List<string> ParseFieldsQuery(string key, string value)
205194
if (string.Equals(typeName, _controllerContext.RequestEntity.EntityName, StringComparison.OrdinalIgnoreCase) == false)
206195
return includedFields;
207196

208-
var fields = value.Split(COMMA);
197+
var fields = value.Split(QueryConstants.COMMA);
209198
foreach (var field in fields)
210199
{
211200
var attr = _controllerContext.RequestEntity

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ public class PagingTests
2323
private Faker<Person> _personFaker;
2424
private Faker<TodoItem> _todoItemFaker;
2525
private Faker<TodoItemCollection> _todoItemCollectionFaker;
26+
private DateTime CurrentTime;
2627

2728
public PagingTests(TestFixture<TestStartup> fixture)
2829
{
30+
CurrentTime = DateTime.Now;
2931
_fixture = fixture;
3032
_context = fixture.GetService<AppDbContext>();
3133
_personFaker = new Faker<Person>()
@@ -35,7 +37,7 @@ public PagingTests(TestFixture<TestStartup> fixture)
3537
_todoItemFaker = new Faker<TodoItem>()
3638
.RuleFor(t => t.Description, f => f.Lorem.Sentence())
3739
.RuleFor(t => t.Ordinal, f => f.Random.Number())
38-
.RuleFor(t => t.CreatedDate, f => f.Date.Past());
40+
.RuleFor(t => t.CreatedDate, f => CurrentTime);
3941

4042
_todoItemCollectionFaker = new Faker<TodoItemCollection>()
4143
.RuleFor(t => t.Name, f => f.Company.CatchPhrase());
@@ -83,5 +85,51 @@ public async Task Server_IncludesPagination_Links()
8385
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={numberOfPages}", links.Last);
8486
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]=1", links.First);
8587
}
88+
89+
[Fact]
90+
public async Task Server_IncludesPaginationAndFilter_LinksContainFilter()
91+
{
92+
//arrange
93+
// arrange
94+
var pageSize = 5;
95+
const int minimumNumberOfRecords = 11;
96+
_context.TodoItems.RemoveRange(_context.TodoItems);
97+
98+
for(var i=0; i < minimumNumberOfRecords; i++)
99+
_context.TodoItems.Add(_todoItemFaker.Generate());
100+
101+
await _context.SaveChangesAsync();
102+
103+
var numberOfPages = (int)Math.Ceiling(decimal.Divide(minimumNumberOfRecords, pageSize));
104+
var startPageNumber = 2;
105+
106+
var builder = new WebHostBuilder()
107+
.UseStartup<Startup>();
108+
109+
var httpMethod = new HttpMethod("GET");
110+
var route = $"/api/v1/todo-items?page[number]=2&filter[created-date]=eq:{CurrentTime}";
111+
112+
var server = new TestServer(builder);
113+
var client = server.CreateClient();
114+
var request = new HttpRequestMessage(httpMethod, route);
115+
116+
// act
117+
var response = await client.SendAsync(request);
118+
var documents = JsonConvert.DeserializeObject<Documents>(await response.Content.ReadAsStringAsync());
119+
var links = documents.Links;
120+
121+
// assert
122+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
123+
Assert.NotEmpty(links.First);
124+
Assert.NotEmpty(links.Next);
125+
Assert.NotEmpty(links.Last);
126+
127+
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={startPageNumber+1}&filter[created-date]=eq:{CurrentTime}", links.Next);
128+
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={startPageNumber-1}&filter[created-date]=eq:{CurrentTime}", links.Prev);
129+
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={numberOfPages}&filter[created-date]=eq:{CurrentTime}", links.Last);
130+
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]=1&filter[created-date]=eq:{CurrentTime}", links.First);
131+
132+
133+
}
86134
}
87135
}

0 commit comments

Comments
 (0)