Skip to content

Commit 65a2a0f

Browse files
shuebner-zeissjaredcnance
authored andcommitted
moved Page into an IQueryable extension method
1 parent 09e7fcd commit 65a2a0f

File tree

4 files changed

+123
-38
lines changed

4 files changed

+123
-38
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -146,27 +146,22 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string
146146

147147
public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber)
148148
{
149-
if (pageSize > 0)
149+
if (pageNumber >= 0)
150150
{
151-
if (pageNumber == 0)
152-
pageNumber = 1;
153-
154-
if (pageNumber > 0)
155-
return await entities
156-
.Skip((pageNumber - 1) * pageSize)
157-
.Take(pageSize)
158-
.ToListAsync();
159-
else // page from the end of the set
160-
return (await entities
161-
.OrderByDescending(t => t.Id)
162-
.Skip((Math.Abs(pageNumber) - 1) * pageSize)
163-
.Take(pageSize)
164-
.ToListAsync())
165-
.OrderBy(t => t.Id)
166-
.ToList();
151+
return await entities.PageForward(pageSize, pageNumber).ToListAsync();
167152
}
168153

169-
return await entities.ToListAsync();
154+
// since EntityFramework does not support IQueryable.Reverse(), we need to know the number of queried entities
155+
int numberOfEntities = await this.CountAsync(entities);
156+
157+
// may be negative
158+
int virtualFirstIndex = numberOfEntities - pageSize * Math.Abs(pageNumber);
159+
int numberOfElementsInPage = Math.Min(pageSize, virtualFirstIndex + pageSize);
160+
161+
return await entities
162+
.Skip(virtualFirstIndex)
163+
.Take(numberOfElementsInPage)
164+
.ToListAsync();
170165
}
171166
}
172167
}

src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,5 +216,21 @@ public static IQueryable<TSource> Select<TSource>(this IQueryable<TSource> sourc
216216
Expression.Call(typeof(Queryable), "Select", new[] { sourceType, resultType },
217217
source.Expression, Expression.Quote(selector)));
218218
}
219+
220+
public static IQueryable<T> PageForward<T>(this IQueryable<T> source, int pageSize, int pageNumber)
221+
{
222+
if (pageSize > 0)
223+
{
224+
if (pageNumber == 0)
225+
pageNumber = 1;
226+
227+
if (pageNumber > 0)
228+
return source
229+
.Skip((pageNumber - 1) * pageSize)
230+
.Take(pageSize);
231+
}
232+
233+
return source;
234+
}
219235
}
220236
}

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/PagingTests.cs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
using System.Collections.Generic;
12
using System.Linq;
23
using System.Net;
34
using System.Threading.Tasks;
45
using Bogus;
6+
using JsonApiDotNetCore.Models;
57
using JsonApiDotNetCore.Serialization;
68
using JsonApiDotNetCoreExample;
79
using JsonApiDotNetCoreExample.Models;
@@ -50,7 +52,7 @@ public async Task Can_Paginate_TodoItems_From_Start() {
5052
const int expectedEntitiesPerPage = 2;
5153
var totalCount = expectedEntitiesPerPage * 2;
5254
var person = new Person();
53-
var todoItems = _todoItemFaker.Generate(totalCount);
55+
var todoItems = _todoItemFaker.Generate(totalCount).ToList();
5456

5557
foreach (var todoItem in todoItems)
5658
todoItem.Owner = person;
@@ -70,12 +72,8 @@ public async Task Can_Paginate_TodoItems_From_Start() {
7072
var body = await response.Content.ReadAsStringAsync();
7173
var deserializedBody = GetService<IJsonApiDeSerializer>().DeserializeList<TodoItem>(body);
7274

73-
Assert.NotEmpty(deserializedBody);
74-
Assert.Equal(expectedEntitiesPerPage, deserializedBody.Count);
75-
76-
var expectedTodoItems = Context.TodoItems.Take(2);
77-
foreach (var todoItem in expectedTodoItems)
78-
Assert.NotNull(deserializedBody.SingleOrDefault(t => t.Id == todoItem.Id));
75+
var expectedTodoItems = new[] { todoItems[0], todoItems[1] };
76+
Assert.Equal(expectedTodoItems, deserializedBody, new IdComparer<TodoItem>());
7977
}
8078

8179
[Fact]
@@ -84,7 +82,7 @@ public async Task Can_Paginate_TodoItems_From_End() {
8482
const int expectedEntitiesPerPage = 2;
8583
var totalCount = expectedEntitiesPerPage * 2;
8684
var person = new Person();
87-
var todoItems = _todoItemFaker.Generate(totalCount);
85+
var todoItems = _todoItemFaker.Generate(totalCount).ToList();
8886

8987
foreach (var todoItem in todoItems)
9088
todoItem.Owner = person;
@@ -104,18 +102,16 @@ public async Task Can_Paginate_TodoItems_From_End() {
104102
var body = await response.Content.ReadAsStringAsync();
105103
var deserializedBody = GetService<IJsonApiDeSerializer>().DeserializeList<TodoItem>(body);
106104

107-
Assert.NotEmpty(deserializedBody);
108-
Assert.Equal(expectedEntitiesPerPage, deserializedBody.Count);
105+
var expectedTodoItems = new[] { todoItems[totalCount - 2], todoItems[totalCount - 1] };
106+
Assert.Equal(expectedTodoItems, deserializedBody, new IdComparer<TodoItem>());
107+
}
109108

110-
var expectedTodoItems = Context.TodoItems
111-
.OrderByDescending(t => t.Id)
112-
.Take(2)
113-
.ToList()
114-
.OrderBy(t => t.Id)
115-
.ToList();
109+
private class IdComparer<T> : IEqualityComparer<T>
110+
where T : IIdentifiable
111+
{
112+
public bool Equals(T x, T y) => x?.StringId == y?.StringId;
116113

117-
for (int i = 0; i < expectedEntitiesPerPage; i++)
118-
Assert.Equal(expectedTodoItems[i].Id, deserializedBody[i].Id);
114+
public int GetHashCode(T obj) => obj?.StringId?.GetHashCode() ?? 0;
119115
}
120116
}
121-
}
117+
}

test/UnitTests/Data/DefaultEntityRepository_Tests.cs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
using Microsoft.Extensions.Logging;
1414
using JsonApiDotNetCore.Services;
1515
using System.Threading.Tasks;
16-
16+
using System.Linq;
17+
1718
namespace UnitTests.Data
1819
{
1920
public class DefaultEntityRepository_Tests : JsonApiControllerMixin
@@ -93,6 +94,83 @@ private DefaultEntityRepository<TodoItem> GetRepository()
9394
_loggFactoryMock.Object,
9495
_jsonApiContextMock.Object,
9596
_contextResolverMock.Object);
97+
}
98+
99+
[Theory]
100+
[InlineData(0)]
101+
[InlineData(-1)]
102+
[InlineData(-10)]
103+
public async Task Page_When_PageSize_Is_NonPositive_Does_Nothing(int pageSize)
104+
{
105+
var todoItems = DbSetMock.Create(TodoItems(2, 3, 1)).Object;
106+
var repository = GetRepository();
107+
108+
var result = await repository.PageAsync(todoItems, pageSize, 3);
109+
110+
Assert.Equal(TodoItems(2, 3, 1), result, new IdComparer<TodoItem>());
111+
}
112+
113+
[Fact]
114+
public async Task Page_When_PageNumber_Is_Zero_Pretends_PageNumber_Is_One()
115+
{
116+
var todoItems = DbSetMock.Create(TodoItems(2, 3, 1)).Object;
117+
var repository = GetRepository();
118+
119+
var result = await repository.PageAsync(todoItems, 1, 0);
120+
121+
Assert.Equal(TodoItems(2), result, new IdComparer<TodoItem>());
122+
}
123+
124+
[Fact]
125+
public async Task Page_When_PageNumber_Of_PageSize_Does_Not_Exist_Return_Empty_Queryable()
126+
{
127+
var todoItems = DbSetMock.Create(TodoItems(2, 3, 1)).Object;
128+
var repository = GetRepository();
129+
130+
var result = await repository.PageAsync(todoItems, 2, 3);
131+
132+
Assert.Empty(result);
133+
}
134+
135+
[Theory]
136+
[InlineData(3, 2, new[] { 4, 5, 6 })]
137+
[InlineData(8, 2, new[] { 9 })]
138+
[InlineData(20, 1, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })]
139+
public async Task Page_When_PageNumber_Is_Positive_Returns_PageNumberTh_Page_Of_Size_PageSize(int pageSize, int pageNumber, int[] expectedResult)
140+
{
141+
var todoItems = DbSetMock.Create(TodoItems(1, 2, 3, 4, 5, 6, 7, 8, 9)).Object;
142+
var repository = GetRepository();
143+
144+
var result = await repository.PageAsync(todoItems, pageSize, pageNumber);
145+
146+
Assert.Equal(TodoItems(expectedResult), result, new IdComparer<TodoItem>());
147+
}
148+
149+
[Theory]
150+
[InlineData(6, -1, new[] { 4, 5, 6, 7, 8, 9 })]
151+
[InlineData(6, -2, new[] { 1, 2, 3 })]
152+
[InlineData(20, -1, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })]
153+
public async Task Page_When_PageNumber_Is_Negative_Returns_PageNumberTh_Page_From_End(int pageSize, int pageNumber, int[] expectedIds)
154+
{
155+
var todoItems = DbSetMock.Create(TodoItems(1, 2, 3, 4, 5, 6, 7, 8, 9)).Object;
156+
var repository = GetRepository();
157+
158+
var result = await repository.PageAsync(todoItems, pageSize, pageNumber);
159+
160+
Assert.Equal(TodoItems(expectedIds), result, new IdComparer<TodoItem>());
161+
}
162+
163+
private static TodoItem[] TodoItems(params int[] ids)
164+
{
165+
return ids.Select(id => new TodoItem { Id = id }).ToArray();
166+
}
167+
168+
private class IdComparer<T> : IEqualityComparer<T>
169+
where T : IIdentifiable
170+
{
171+
public bool Equals(T x, T y) => x?.StringId == y?.StringId;
172+
173+
public int GetHashCode(T obj) => obj?.StringId?.GetHashCode() ?? 0;
96174
}
97175
}
98176
}

0 commit comments

Comments
 (0)