Skip to content

Commit a1d9f7d

Browse files
committed
feature(paging): support negative paging
1 parent 1044c91 commit a1d9f7d

File tree

9 files changed

+220
-59
lines changed

9 files changed

+220
-59
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313

1414
namespace JsonApiDotNetCore.Data
1515
{
16-
public class DefaultEntityRepository<TEntity>
16+
public class DefaultEntityRepository<TEntity>
1717
: DefaultEntityRepository<TEntity, int>,
18-
IEntityRepository<TEntity>
18+
IEntityRepository<TEntity>
1919
where TEntity : class, IIdentifiable<int>
2020
{
2121
public DefaultEntityRepository(
@@ -25,8 +25,8 @@ public DefaultEntityRepository(
2525
{ }
2626
}
2727

28-
public class DefaultEntityRepository<TEntity, TId>
29-
: IEntityRepository<TEntity, TId>
28+
public class DefaultEntityRepository<TEntity, TId>
29+
: IEntityRepository<TEntity, TId>
3030
where TEntity : class, IIdentifiable<TId>
3131
{
3232
private readonly DbContext _context;
@@ -62,33 +62,33 @@ public DefaultEntityRepository(
6262

6363
public virtual IQueryable<TEntity> Get()
6464
{
65-
if(_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Any())
65+
if (_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Any())
6666
return _dbSet.Select(_jsonApiContext.QuerySet?.Fields);
67-
67+
6868
return _dbSet;
6969
}
7070

71-
public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery)
71+
public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery)
7272
{
73-
if(filterQuery == null)
73+
if (filterQuery == null)
7474
return entities;
7575

76-
if(filterQuery.IsAttributeOfRelationship)
76+
if (filterQuery.IsAttributeOfRelationship)
7777
return entities.Filter(new RelatedAttrFilterQuery(_jsonApiContext, filterQuery));
78-
78+
7979
return entities.Filter(new AttrFilterQuery(_jsonApiContext, filterQuery));
8080
}
8181

8282
public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries)
8383
{
84-
if(sortQueries == null || sortQueries.Count == 0)
84+
if (sortQueries == null || sortQueries.Count == 0)
8585
return entities;
8686

8787
var orderedEntities = entities.Sort(sortQueries[0]);
8888

8989
if (sortQueries.Count <= 1) return orderedEntities;
9090

91-
for(var i=1; i < sortQueries.Count; i++)
91+
for (var i = 1; i < sortQueries.Count; i++)
9292
orderedEntities = orderedEntities.Sort(sortQueries[i]);
9393

9494
return orderedEntities;
@@ -124,10 +124,10 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
124124
if (oldEntity == null)
125125
return null;
126126

127-
foreach(var attr in _jsonApiContext.AttributesToUpdate)
127+
foreach (var attr in _jsonApiContext.AttributesToUpdate)
128128
attr.Key.SetValue(oldEntity, attr.Value);
129129

130-
foreach(var relationship in _jsonApiContext.RelationshipsToUpdate)
130+
foreach (var relationship in _jsonApiContext.RelationshipsToUpdate)
131131
relationship.Key.SetValue(oldEntity, relationship.Value);
132132

133133
await _context.SaveChangesAsync();
@@ -159,20 +159,34 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string
159159
{
160160
var entity = _jsonApiContext.RequestEntity;
161161
var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == relationshipName);
162-
if(relationship != null)
162+
if (relationship != null)
163163
return entities.Include(relationship.InternalRelationshipName);
164-
164+
165165
throw new JsonApiException(400, $"Invalid relationship {relationshipName} on {entity.EntityName}",
166166
$"{entity.EntityName} does not have a relationship named {relationshipName}");
167167
}
168168

169169
public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber)
170170
{
171-
if(pageSize > 0)
172-
return await entities
173-
.Skip((pageNumber - 1) * pageSize)
174-
.Take(pageSize)
175-
.ToListAsync();
171+
if (pageSize > 0)
172+
{
173+
if (pageNumber == 0)
174+
pageNumber = 1;
175+
176+
if (pageNumber > 0)
177+
return await entities
178+
.Skip((pageNumber - 1) * pageSize)
179+
.Take(pageSize)
180+
.ToListAsync();
181+
else // page from the end of the set
182+
return (await entities
183+
.OrderByDescending(t => t.Id)
184+
.Skip((Math.Abs(pageNumber) - 1) * pageSize)
185+
.Take(pageSize)
186+
.ToListAsync())
187+
.OrderBy(t => t.Id)
188+
.ToList();
189+
}
176190

177191
return await entities.ToListAsync();
178192
}

src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<VersionPrefix>2.1.6</VersionPrefix>
3+
<VersionPrefix>2.1.7</VersionPrefix>
44
<TargetFrameworks>netstandard1.6</TargetFrameworks>
55
<AssemblyName>JsonApiDotNetCore</AssemblyName>
66
<PackageId>JsonApiDotNetCore</PackageId>

src/JsonApiDotNetCore/Serialization/IJsonApiDeSerializer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace JsonApiDotNetCore.Serialization
55
public interface IJsonApiDeSerializer
66
{
77
object Deserialize(string requestBody);
8+
TEntity Deserialize<TEntity>(string requestBody);
89
object DeserializeRelationship(string requestBody);
910
List<TEntity> DeserializeList<TEntity>(string requestBody);
1011
}

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ public object Deserialize(string requestBody)
3838
}
3939
}
4040

41-
public TEntity Deserialize<TEntity>(string requestBody)
42-
=> (TEntity)Deserialize(requestBody);
41+
public TEntity Deserialize<TEntity>(string requestBody) => (TEntity)Deserialize(requestBody);
4342

4443
public object DeserializeRelationship(string requestBody)
4544
{
@@ -117,7 +116,7 @@ private object SetEntityAttributes(
117116
var convertedValue = ConvertAttrValue(newValue, entityProperty.PropertyType);
118117
entityProperty.SetValue(entity, convertedValue);
119118

120-
if(attr.IsImmutable == false)
119+
if (attr.IsImmutable == false)
121120
_jsonApiContext.AttributesToUpdate[attr] = convertedValue;
122121
}
123122
}

src/JsonApiDotNetCore/Services/JsonApiContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ private PageManager GetPageManager()
8686
return new PageManager
8787
{
8888
DefaultPageSize = Options.DefaultPageSize,
89-
CurrentPage = query.PageOffset > 0 ? query.PageOffset : 1,
89+
CurrentPage = query.PageOffset,
9090
PageSize = query.PageSize > 0 ? query.PageSize : Options.DefaultPageSize
9191
};
9292
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System.Collections.Generic;
2+
using System;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Threading.Tasks;
7+
using Bogus;
8+
using DotNetCoreDocs;
9+
using DotNetCoreDocs.Models;
10+
using DotNetCoreDocs.Writers;
11+
using JsonApiDotNetCore.Serialization;
12+
using JsonApiDotNetCore.Services;
13+
using JsonApiDotNetCoreExample;
14+
using JsonApiDotNetCoreExample.Data;
15+
using JsonApiDotNetCoreExample.Models;
16+
using Xunit;
17+
using Person = JsonApiDotNetCoreExample.Models.Person;
18+
19+
namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec
20+
{
21+
public class PagingTests : TestFixture<Startup>
22+
{
23+
private readonly Faker<TodoItem> _todoItemFaker = new Faker<TodoItem>()
24+
.RuleFor(t => t.Description, f => f.Lorem.Sentence())
25+
.RuleFor(t => t.Ordinal, f => f.Random.Number())
26+
.RuleFor(t => t.CreatedDate, f => f.Date.Past());
27+
28+
[Fact]
29+
public async Task Can_Paginate_TodoItems()
30+
{
31+
// Arrange
32+
const int expectedEntitiesPerPage = 2;
33+
var totalCount = expectedEntitiesPerPage * 2;
34+
var person = new Person();
35+
var todoItems = _todoItemFaker.Generate(totalCount);
36+
37+
foreach (var todoItem in todoItems)
38+
todoItem.Owner = person;
39+
40+
Context.TodoItems.AddRange(todoItems);
41+
Context.SaveChanges();
42+
43+
var route = $"/api/v1/todo-items?page[size]={expectedEntitiesPerPage}";
44+
45+
// Act
46+
var response = await Client.GetAsync(route);
47+
48+
// Assert
49+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
50+
51+
var body = await response.Content.ReadAsStringAsync();
52+
var deserializedBody = GetService<IJsonApiDeSerializer>().DeserializeList<TodoItem>(body);
53+
54+
Assert.NotEmpty(deserializedBody);
55+
Assert.Equal(expectedEntitiesPerPage, deserializedBody.Count);
56+
}
57+
58+
[Fact]
59+
public async Task Can_Paginate_TodoItems_From_Start()
60+
{
61+
// Arrange
62+
const int expectedEntitiesPerPage = 2;
63+
var totalCount = expectedEntitiesPerPage * 2;
64+
var person = new Person();
65+
var todoItems = _todoItemFaker.Generate(totalCount);
66+
67+
foreach (var todoItem in todoItems)
68+
todoItem.Owner = person;
69+
70+
Context.TodoItems.RemoveRange(Context.TodoItems);
71+
Context.TodoItems.AddRange(todoItems);
72+
Context.SaveChanges();
73+
74+
var route = $"/api/v1/todo-items?page[size]={expectedEntitiesPerPage}&page[number]=1";
75+
76+
// Act
77+
var response = await Client.GetAsync(route);
78+
79+
// Assert
80+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
81+
82+
var body = await response.Content.ReadAsStringAsync();
83+
var deserializedBody = GetService<IJsonApiDeSerializer>().DeserializeList<TodoItem>(body);
84+
85+
Assert.NotEmpty(deserializedBody);
86+
Assert.Equal(expectedEntitiesPerPage, deserializedBody.Count);
87+
88+
var expectedTodoItems = Context.TodoItems.Take(2);
89+
foreach (var todoItem in expectedTodoItems)
90+
Assert.NotNull(deserializedBody.SingleOrDefault(t => t.Id == todoItem.Id));
91+
}
92+
93+
[Fact]
94+
public async Task Can_Paginate_TodoItems_From_End()
95+
{
96+
// Arrange
97+
const int expectedEntitiesPerPage = 2;
98+
var totalCount = expectedEntitiesPerPage * 2;
99+
var person = new Person();
100+
var todoItems = _todoItemFaker.Generate(totalCount);
101+
102+
foreach (var todoItem in todoItems)
103+
todoItem.Owner = person;
104+
105+
Context.TodoItems.RemoveRange(Context.TodoItems);
106+
Context.TodoItems.AddRange(todoItems);
107+
Context.SaveChanges();
108+
109+
var route = $"/api/v1/todo-items?page[size]={expectedEntitiesPerPage}&page[number]=-1";
110+
111+
// Act
112+
var response = await Client.GetAsync(route);
113+
114+
// Assert
115+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
116+
117+
var body = await response.Content.ReadAsStringAsync();
118+
var deserializedBody = GetService<IJsonApiDeSerializer>().DeserializeList<TodoItem>(body);
119+
120+
Assert.NotEmpty(deserializedBody);
121+
Assert.Equal(expectedEntitiesPerPage, deserializedBody.Count);
122+
123+
var expectedTodoItems = Context.TodoItems
124+
.OrderByDescending(t => t.Id)
125+
.Take(2)
126+
.ToList()
127+
.OrderBy(t => t.Id)
128+
.ToList();
129+
130+
for (int i = 0; i < expectedEntitiesPerPage; i++)
131+
Assert.Equal(expectedTodoItems[i].Id, deserializedBody[i].Id);
132+
}
133+
}
134+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Net.Http;
3+
using JsonApiDotNetCore.Serialization;
4+
using JsonApiDotNetCoreExample.Data;
5+
using Microsoft.AspNetCore.Hosting;
6+
using Microsoft.AspNetCore.TestHost;
7+
using JsonApiDotNetCore.Services;
8+
using Newtonsoft.Json;
9+
10+
namespace JsonApiDotNetCoreExampleTests.Acceptance
11+
{
12+
public class TestFixture<TStartup> where TStartup : class
13+
{
14+
private readonly TestServer _server;
15+
private IServiceProvider _services;
16+
17+
public TestFixture()
18+
{
19+
var builder = new WebHostBuilder()
20+
.UseStartup<TStartup>();
21+
22+
_server = new TestServer(builder);
23+
_services = _server.Host.Services;
24+
25+
Client = _server.CreateClient();
26+
Context = GetService<AppDbContext>();
27+
DeSerializer = GetService<IJsonApiDeSerializer>();
28+
JsonApiContext = GetService<IJsonApiContext>();
29+
}
30+
31+
public HttpClient Client { get; set; }
32+
public AppDbContext Context { get; private set; }
33+
public IJsonApiDeSerializer DeSerializer { get; private set; }
34+
public IJsonApiContext JsonApiContext { get; private set; }
35+
public T GetService<T>() => (T)_services.GetService(typeof(T));
36+
}
37+
}

0 commit comments

Comments
 (0)