Skip to content

Commit 2bc4776

Browse files
author
Chris Santero
committed
add pagination transformer
1 parent 284c21d commit 2bc4776

6 files changed

Lines changed: 278 additions & 1 deletion

File tree

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Web.Http;
7+
using FluentAssertions;
8+
using JSONAPI.ActionFilters;
9+
using JSONAPI.Core;
10+
using Microsoft.VisualStudio.TestTools.UnitTesting;
11+
12+
namespace JSONAPI.Tests.ActionFilters
13+
{
14+
[TestClass]
15+
public class DefaultPaginationTransformerTests
16+
{
17+
private class Dummy
18+
{
19+
// ReSharper disable UnusedAutoPropertyAccessor.Local
20+
public string Id { get; set; }
21+
// ReSharper restore UnusedAutoPropertyAccessor.Local
22+
23+
public string FirstName { get; set; }
24+
25+
public string LastName { get; set; }
26+
}
27+
28+
private IList<Dummy> _fixtures;
29+
private IQueryable<Dummy> _fixturesQuery;
30+
31+
[TestInitialize]
32+
public void SetupFixtures()
33+
{
34+
_fixtures = new List<Dummy>
35+
{
36+
new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" },
37+
new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" },
38+
new Dummy { Id = "3", FirstName = "George", LastName = "Washington"},
39+
new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" },
40+
new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"},
41+
new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" },
42+
new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" },
43+
new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" },
44+
new Dummy { Id = "9", FirstName = "William", LastName = "Harrison" }
45+
};
46+
_fixturesQuery = _fixtures.AsQueryable();
47+
}
48+
49+
private DefaultPaginationTransformer GetTransformer(int maxPageSize)
50+
{
51+
return new DefaultPaginationTransformer("page.number", "page.size", maxPageSize);
52+
}
53+
54+
private Dummy[] GetArray(string uri, int maxPageSize = 50)
55+
{
56+
var request = new HttpRequestMessage(HttpMethod.Get, uri);
57+
return GetTransformer(maxPageSize).ApplyPagination(_fixturesQuery, request).ToArray();
58+
}
59+
60+
[TestMethod]
61+
public void ApplyPagination_has_no_effect_when_no_paging_parameters_are_supplied()
62+
{
63+
var array = GetArray("http://api.example.com/dummies");
64+
array.Length.Should().Be(9);
65+
}
66+
67+
[TestMethod]
68+
public void ApplyPagination_returns_all_results_when_they_are_within_page()
69+
{
70+
var array = GetArray("http://api.example.com/dummies?page[number]=0&page[size]=10");
71+
array.Length.Should().Be(9);
72+
}
73+
74+
[TestMethod]
75+
public void ApplyPagination_returns_no_results_when_page_size_is_zero()
76+
{
77+
var array = GetArray("http://api.example.com/dummies?page[number]=0&page[size]=0");
78+
array.Length.Should().Be(0);
79+
}
80+
81+
[TestMethod]
82+
public void ApplyPagination_returns_first_page_of_data()
83+
{
84+
var array = GetArray("http://api.example.com/dummies?page[number]=0&page[size]=4");
85+
array.Should().BeEquivalentTo(_fixtures[0], _fixtures[1], _fixtures[2], _fixtures[3]);
86+
}
87+
88+
[TestMethod]
89+
public void ApplyPagination_returns_second_page_of_data()
90+
{
91+
var array = GetArray("http://api.example.com/dummies?page[number]=1&page[size]=4");
92+
array.Should().BeEquivalentTo(_fixtures[4], _fixtures[5], _fixtures[6], _fixtures[7]);
93+
}
94+
95+
[TestMethod]
96+
public void ApplyPagination_returns_page_at_end()
97+
{
98+
var array = GetArray("http://api.example.com/dummies?page[number]=2&page[size]=4");
99+
array.Should().BeEquivalentTo(_fixtures[8]);
100+
}
101+
102+
[TestMethod]
103+
public void ApplyPagination_returns_nothing_for_page_after_end()
104+
{
105+
var array = GetArray("http://api.example.com/dummies?page[number]=3&page[size]=4");
106+
array.Length.Should().Be(0);
107+
}
108+
109+
[TestMethod]
110+
public void ApplyPagination_uses_max_page_size_when_requested_page_size_is_higher()
111+
{
112+
var array = GetArray("http://api.example.com/dummies?page[number]=1&page[size]=8", 3);
113+
array.Should().BeEquivalentTo(_fixtures[3], _fixtures[4], _fixtures[5]);
114+
}
115+
116+
[TestMethod]
117+
public void ApplyPagination_returns_400_if_page_number_is_negative()
118+
{
119+
Action action = () =>
120+
{
121+
GetArray("http://api.example.com/dummies?page[number]=-4&page[size]=4");
122+
};
123+
action.ShouldThrow<HttpResponseException>().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
124+
}
125+
126+
[TestMethod]
127+
public void ApplyPagination_returns_400_if_page_size_is_negative()
128+
{
129+
Action action = () =>
130+
{
131+
GetArray("http://api.example.com/dummies?page[number]=0&page[size]=-4");
132+
};
133+
action.ShouldThrow<HttpResponseException>().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
134+
}
135+
136+
[TestMethod]
137+
public void ApplyPagination_returns_400_if_page_number_specified_but_not_size()
138+
{
139+
Action action = () =>
140+
{
141+
GetArray("http://api.example.com/dummies?page[number]=0");
142+
};
143+
action.ShouldThrow<HttpResponseException>().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
144+
}
145+
146+
[TestMethod]
147+
public void ApplyPagination_returns_400_if_page_size_specified_but_not_number()
148+
{
149+
Action action = () =>
150+
{
151+
GetArray("http://api.example.com/dummies?page[size]=0");
152+
};
153+
action.ShouldThrow<HttpResponseException>().And.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
154+
}
155+
156+
[TestMethod]
157+
public void DefaultPaginationTransformer_cannot_be_instantiated_if_max_page_size_is_zero()
158+
{
159+
Action action = () =>
160+
{
161+
GetTransformer(0);
162+
};
163+
action.ShouldThrow<ArgumentOutOfRangeException>();
164+
}
165+
166+
[TestMethod]
167+
public void DefaultPaginationTransformer_cannot_be_instantiated_if_max_page_size_is_negative()
168+
{
169+
Action action = () =>
170+
{
171+
GetTransformer(-4);
172+
};
173+
action.ShouldThrow<ArgumentOutOfRangeException>();
174+
}
175+
}
176+
}

JSONAPI.Tests/JSONAPI.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
</CodeAnalysisDependentAssemblyPaths>
8080
</ItemGroup>
8181
<ItemGroup>
82+
<Compile Include="ActionFilters\DefaultPaginationTransformerTests.cs" />
8283
<Compile Include="ActionFilters\QueryableTransformerTestsBase.cs" />
8384
<Compile Include="ActionFilters\DefaultSortingTransformerTests.cs" />
8485
<Compile Include="ActionFilters\DefaultFilteringTransformerTests.cs" />
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Linq;
3+
using System.Net;
4+
using System.Net.Http;
5+
using System.Web.Http;
6+
7+
namespace JSONAPI.ActionFilters
8+
{
9+
/// <summary>
10+
/// Performs pagination a
11+
/// </summary>
12+
public class DefaultPaginationTransformer : IQueryablePaginationTransformer
13+
{
14+
private const int DefaultPageSize = 100;
15+
16+
private readonly string _pageNumberQueryParam;
17+
private readonly string _pageSizeQueryParam;
18+
private readonly int? _maxPageSize;
19+
20+
/// <summary>
21+
/// Creates a DefaultPaginationTransformer
22+
/// </summary>
23+
/// <param name="pageNumberQueryParam">The query parameter to use to indicate the page number</param>
24+
/// <param name="pageSizeQueryParam">The query parameter to use to indicate the page size</param>
25+
/// <param name="maxPageSize">The maximum page size to allow clients to request. Leave null for no restriction.</param>
26+
public DefaultPaginationTransformer(string pageNumberQueryParam, string pageSizeQueryParam, int? maxPageSize = null)
27+
{
28+
if (maxPageSize <= 0) throw new ArgumentOutOfRangeException("maxPageSize", "The maximum page size must be 1 or greater.");
29+
30+
_pageNumberQueryParam = pageNumberQueryParam;
31+
_pageSizeQueryParam = pageSizeQueryParam;
32+
_maxPageSize = maxPageSize;
33+
}
34+
35+
public IQueryable<T> ApplyPagination<T>(IQueryable<T> query, HttpRequestMessage request)
36+
{
37+
var hasPageNumberParam = false;
38+
var hasPageSizeParam = false;
39+
var pageNumber = 0;
40+
var pageSize = _maxPageSize ?? DefaultPageSize;
41+
foreach (var kvp in request.GetQueryNameValuePairs())
42+
{
43+
if (kvp.Key == _pageNumberQueryParam)
44+
{
45+
hasPageNumberParam = true;
46+
if (!int.TryParse(kvp.Value, out pageNumber)) throw new HttpResponseException(HttpStatusCode.BadRequest);
47+
}
48+
else if (kvp.Key == _pageSizeQueryParam)
49+
{
50+
hasPageSizeParam = true;
51+
if (!int.TryParse(kvp.Value, out pageSize)) throw new HttpResponseException(HttpStatusCode.BadRequest);
52+
}
53+
}
54+
55+
if (!hasPageNumberParam && !hasPageSizeParam)
56+
return query;
57+
58+
if ((hasPageNumberParam && !hasPageSizeParam) || (!hasPageNumberParam && hasPageSizeParam))
59+
throw new HttpResponseException(HttpStatusCode.BadRequest);
60+
61+
if (pageNumber < 0 || pageSize < 0)
62+
throw new HttpResponseException(HttpStatusCode.BadRequest);
63+
64+
if (_maxPageSize != null && pageSize > _maxPageSize.Value)
65+
pageSize = _maxPageSize.Value;
66+
67+
var skip = pageNumber * pageSize;
68+
return query.Skip(skip).Take(pageSize);
69+
}
70+
}
71+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Linq;
2+
using System.Net.Http;
3+
4+
namespace JSONAPI.ActionFilters
5+
{
6+
/// <summary>
7+
/// Provides a service to provide a page of data based on information from the request.
8+
/// </summary>
9+
public interface IQueryablePaginationTransformer
10+
{
11+
/// <summary>
12+
/// Pages the query according to information from the request.
13+
/// </summary>
14+
/// <param name="query">The query to page</param>
15+
/// <param name="request">The request message</param>
16+
/// <typeparam name="T">The queryable element type</typeparam>
17+
/// <returns>An IQueryable configured to return a page of data</returns>
18+
IQueryable<T> ApplyPagination<T>(IQueryable<T> query, HttpRequestMessage request);
19+
}
20+
}

JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,23 @@ public class JsonApiQueryableAttribute : ActionFilterAttribute
1919
private readonly IQueryableEnumerationTransformer _enumerationTransformer;
2020
private readonly IQueryableFilteringTransformer _filteringTransformer;
2121
private readonly IQueryableSortingTransformer _sortingTransformer;
22+
private readonly IQueryablePaginationTransformer _paginationTransformer;
2223

2324
/// <summary>
2425
/// Creates a new JsonApiQueryableAttribute.
2526
/// </summary>
2627
/// <param name="enumerationTransformer">The transform to be used for enumerating IQueryable payloads.</param>
2728
/// <param name="filteringTransformer">The transform to be used for filtering IQueryable payloads</param>
2829
/// <param name="sortingTransformer">The transform to be used for sorting IQueryable payloads.</param>
30+
/// <param name="paginationTransformer">The transform to be used for pagination of IQueryable payloads.</param>
2931
public JsonApiQueryableAttribute(
3032
IQueryableEnumerationTransformer enumerationTransformer,
3133
IQueryableFilteringTransformer filteringTransformer = null,
32-
IQueryableSortingTransformer sortingTransformer = null)
34+
IQueryableSortingTransformer sortingTransformer = null,
35+
IQueryablePaginationTransformer paginationTransformer = null)
3336
{
3437
_sortingTransformer = sortingTransformer;
38+
_paginationTransformer = paginationTransformer;
3539
_enumerationTransformer = enumerationTransformer;
3640
_filteringTransformer = filteringTransformer;
3741
}
@@ -98,6 +102,9 @@ private async Task<T[]> ApplyTransforms<T>(IQueryable<T> query, HttpRequestMessa
98102
if (_sortingTransformer != null)
99103
query = _sortingTransformer.Sort(query, request);
100104

105+
if (_paginationTransformer != null)
106+
query = _paginationTransformer.ApplyPagination(query, request);
107+
101108
return await _enumerationTransformer.Enumerate(query, cancellationToken);
102109
}
103110
}

JSONAPI/JSONAPI.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@
6767
</ItemGroup>
6868
<ItemGroup>
6969
<Compile Include="ActionFilters\DefaultFilteringTransformer.cs" />
70+
<Compile Include="ActionFilters\DefaultPaginationTransformer.cs" />
7071
<Compile Include="ActionFilters\IQueryableEnumerationTransformer.cs" />
7172
<Compile Include="ActionFilters\IQueryableFilteringTransformer.cs" />
73+
<Compile Include="ActionFilters\IQueryablePaginationTransformer.cs" />
7274
<Compile Include="ActionFilters\IQueryableSortingTransformer.cs" />
7375
<Compile Include="ActionFilters\JsonApiQueryableAttribute.cs" />
7476
<Compile Include="ActionFilters\QueryableTransformException.cs" />

0 commit comments

Comments
 (0)