Skip to content

Commit e6d8c3f

Browse files
committed
feat(root-links): add pagination links
Issue #21
1 parent cce3149 commit e6d8c3f

File tree

8 files changed

+182
-4
lines changed

8 files changed

+182
-4
lines changed

src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public Document Build(IIdentifiable entity)
2626
var document = new Document
2727
{
2828
Data = _getData(contextEntity, entity),
29-
Meta = _getMeta(entity)
29+
Meta = _getMeta(entity),
30+
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
3031
};
3132

3233
document.Included = _appendIncludedObject(document.Included, contextEntity, entity);
@@ -45,7 +46,8 @@ public Documents Build(IEnumerable<IIdentifiable> entities)
4546
var documents = new Documents
4647
{
4748
Data = new List<DocumentData>(),
48-
Meta = _getMeta(entities.FirstOrDefault())
49+
Meta = _getMeta(entities.FirstOrDefault()),
50+
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
4951
};
5052

5153
foreach (var entity in entities)

src/JsonApiDotNetCore/Builders/LinkBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
using JsonApiDotNetCore.Extensions;
32
using JsonApiDotNetCore.Services;
43
using Microsoft.AspNetCore.Http;
@@ -45,5 +44,10 @@ public string GetRelatedRelationLink(string parent, string parentId, string chil
4544
{
4645
return $"{_context.BasePath}/{parent.Dasherize()}/{parentId}/{child.Dasherize()}";
4746
}
47+
48+
public string GetPageLink(int pageOffset, int pageSize)
49+
{
50+
return $"{_context.BasePath}/{_context.RequestEntity.EntityName.Dasherize()}?page[size]={pageSize}&page[number]={pageOffset}";
51+
}
4852
}
4953
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,42 @@
1+
using System;
2+
using JsonApiDotNetCore.Builders;
3+
using JsonApiDotNetCore.Models;
4+
15
namespace JsonApiDotNetCore.Internal
26
{
37
public class PageManager
48
{
59
public int TotalRecords { get; set; }
610
public int PageSize { get; set; }
11+
public int DefaultPageSize { get; set; }
712
public int CurrentPage { get; set; }
813
public bool IsPaginated { get { return PageSize > 0; } }
14+
public int TotalPages {
15+
get { return (TotalRecords == 0) ? -1: (int)Math.Ceiling(decimal.Divide(TotalRecords, PageSize)); }
16+
}
17+
18+
public RootLinks GetPageLinks(LinkBuilder linkBuilder)
19+
{
20+
if(!IsPaginated || (CurrentPage == 1 && TotalPages <= 0))
21+
return null;
22+
23+
var rootLinks = new RootLinks();
24+
25+
var includePageSize = DefaultPageSize != PageSize;
26+
27+
if(CurrentPage > 1)
28+
rootLinks.First = linkBuilder.GetPageLink(1, PageSize);
29+
30+
if(CurrentPage > 1)
31+
rootLinks.Prev = linkBuilder.GetPageLink(CurrentPage - 1, PageSize);
32+
33+
if(CurrentPage < TotalPages)
34+
rootLinks.Next = linkBuilder.GetPageLink(CurrentPage + 1, PageSize);
35+
36+
if(TotalPages > 0)
37+
rootLinks.Last = linkBuilder.GetPageLink(TotalPages, PageSize);
38+
39+
return rootLinks;
40+
}
941
}
1042
}

src/JsonApiDotNetCore/Models/DocumentBase.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ namespace JsonApiDotNetCore.Models
55
{
66
public class DocumentBase
77
{
8+
[JsonProperty("links")]
9+
public RootLinks Links { get; set; }
10+
811
[JsonProperty("included")]
912
public List<DocumentData> Included { get; set; }
1013

@@ -21,5 +24,10 @@ public bool ShouldSerializeMeta()
2124
{
2225
return (Meta != null);
2326
}
27+
28+
public bool ShouldSerializeLinks()
29+
{
30+
return (Links != null);
31+
}
2432
}
2533
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using Newtonsoft.Json;
2+
3+
namespace JsonApiDotNetCore.Models
4+
{
5+
public class RootLinks
6+
{
7+
[JsonProperty("self")]
8+
public string Self { get; set; }
9+
10+
[JsonProperty("next")]
11+
public string Next { get; set; }
12+
13+
[JsonProperty("prev")]
14+
public string Prev { get; set; }
15+
16+
[JsonProperty("first")]
17+
public string First { get; set; }
18+
19+
[JsonProperty("last")]
20+
public string Last { get; set; }
21+
22+
// http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm
23+
public bool ShouldSerializeSelf()
24+
{
25+
return (!string.IsNullOrEmpty(Self));
26+
}
27+
28+
public bool ShouldSerializeFirst()
29+
{
30+
return (!string.IsNullOrEmpty(First));
31+
}
32+
33+
public bool ShouldSerializeNext()
34+
{
35+
return (!string.IsNullOrEmpty(Next));
36+
}
37+
38+
public bool ShouldSerializePrev()
39+
{
40+
return (!string.IsNullOrEmpty(Prev));
41+
}
42+
43+
public bool ShouldSerializeLast()
44+
{
45+
return (!string.IsNullOrEmpty(Last));
46+
}
47+
}
48+
}

src/JsonApiDotNetCore/Services/JsonApiContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System;
21
using System.Collections.Generic;
32
using System.Linq;
43
using JsonApiDotNetCore.Builders;
@@ -58,6 +57,7 @@ private PageManager GetPageManager()
5857
var query = QuerySet?.PageQuery ?? new PageQuery();
5958

6059
return new PageManager {
60+
DefaultPageSize = Options.DefaultPageSize,
6161
CurrentPage = query.PageOffset > 0 ? query.PageOffset : 1,
6262
PageSize = query.PageSize > 0 ? query.PageSize : Options.DefaultPageSize
6363
};

src/JsonApiDotNetCoreExample/Startup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services)
4444
{
4545
opt.Namespace = "api/v1";
4646
opt.DefaultPageSize = 5;
47+
opt.IncludeTotalRecordCount = true;
4748
});
4849

4950
services.AddDocumentationConfiguration(Config);
@@ -52,6 +53,7 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services)
5253
var appContext = provider.GetRequiredService<AppDbContext>();
5354
if(appContext == null)
5455
throw new ArgumentException();
56+
5557
return provider;
5658
}
5759

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using DotNetCoreDocs;
5+
using DotNetCoreDocs.Writers;
6+
using JsonApiDotNetCoreExample;
7+
using Microsoft.AspNetCore.Hosting;
8+
using Microsoft.AspNetCore.TestHost;
9+
using Newtonsoft.Json;
10+
using Xunit;
11+
using Person = JsonApiDotNetCoreExample.Models.Person;
12+
using JsonApiDotNetCore.Models;
13+
using JsonApiDotNetCoreExample.Data;
14+
using Bogus;
15+
using JsonApiDotNetCoreExample.Models;
16+
using System.Linq;
17+
using System;
18+
19+
namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests
20+
{
21+
[Collection("WebHostCollection")]
22+
public class PagingTests
23+
{
24+
private DocsFixture<Startup, JsonDocWriter> _fixture;
25+
private AppDbContext _context;
26+
private Faker<Person> _personFaker;
27+
private Faker<TodoItem> _todoItemFaker;
28+
private Faker<TodoItemCollection> _todoItemCollectionFaker;
29+
30+
public PagingTests(DocsFixture<Startup, JsonDocWriter> fixture)
31+
{
32+
_fixture = fixture;
33+
_context = fixture.GetService<AppDbContext>();
34+
_personFaker = new Faker<Person>()
35+
.RuleFor(p => p.FirstName, f => f.Name.FirstName())
36+
.RuleFor(p => p.LastName, f => f.Name.LastName());
37+
38+
_todoItemFaker = new Faker<TodoItem>()
39+
.RuleFor(t => t.Description, f => f.Lorem.Sentence())
40+
.RuleFor(t => t.Ordinal, f => f.Random.Number());
41+
42+
_todoItemCollectionFaker = new Faker<TodoItemCollection>()
43+
.RuleFor(t => t.Name, f => f.Company.CatchPhrase());
44+
}
45+
46+
[Fact]
47+
public async Task Server_IncludesPagination_Links()
48+
{
49+
// arrange
50+
var pageSize = 5;
51+
var numberOfTodoItems = _context.TodoItems.Count();
52+
var numberOfPages = (int)Math.Ceiling(decimal.Divide(numberOfTodoItems, pageSize));
53+
var startPageNumber = 2;
54+
55+
var builder = new WebHostBuilder()
56+
.UseStartup<Startup>();
57+
58+
var httpMethod = new HttpMethod("GET");
59+
var route = $"/api/v1/todo-items?page[number]=2";
60+
61+
var server = new TestServer(builder);
62+
var client = server.CreateClient();
63+
var request = new HttpRequestMessage(httpMethod, route);
64+
65+
// act
66+
var response = await client.SendAsync(request);
67+
var documents = JsonConvert.DeserializeObject<Documents>(await response.Content.ReadAsStringAsync());
68+
var links = documents.Links;
69+
70+
// assert
71+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
72+
Assert.NotEmpty(links.First);
73+
Assert.NotEmpty(links.Next);
74+
Assert.NotEmpty(links.Last);
75+
76+
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={startPageNumber+1}", links.Next);
77+
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={startPageNumber-1}", links.Prev);
78+
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={numberOfPages}", links.Last);
79+
Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]=1", links.First);
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)