Skip to content

Commit f7626a8

Browse files
committed
Merged feature/pagination into develop
2 parents a4f927a + 32383e7 commit f7626a8

File tree

15 files changed

+124
-48
lines changed

15 files changed

+124
-48
lines changed

.gitlab-ci.yml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@ before_script:
88
build:
99
stage: build
1010
script:
11-
- 'dotnet pack ./src/JsonApiDotNetCore -c Release -o ./artifacts'
11+
- dotnet pack ./src/JsonApiDotNetCore -c Release -o ./artifacts
12+
- mono /bin/NuGet.exe push artifacts/**.nupkg -ApiKey $NUGET_KEY
1213
only:
1314
- master
14-
15-
publish:
16-
stage: deploy
17-
script:
18-
- 'mono /bin/NuGet.exe push ./artifacts/*.nupkg $NUGET_KEY -Source $NUGET_SOURCE'
19-
only:
20-
- master

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
- [ ] Ability to disable dasherization of names
2828
- [ ] Rename ContextEntity ??
2929

30+
# Generators
31+
32+
- TODO: Document usage of the yeoman jadn generator
33+
3034
## Usage
3135

3236
- Add Middleware
@@ -147,3 +151,13 @@ You can add a namespace to the URL by specifying it in `ConfigureServices`:
147151
services.AddJsonApi<AppDbContext>(
148152
opt => opt.Namespace = "api/v1");
149153
```
154+
155+
## Pagination
156+
157+
If you would like pagination implemented by default, you can specify the page size
158+
when setting up the services:
159+
160+
```
161+
services.AddJsonApi<AppDbContext>(
162+
opt => opt.DefaultPageSize = 10);
163+
```

src/JsonApiDotNetCore/Builders/LinkBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ private string GetNamespaceFromPath(string path, string entityName)
3636

3737
public string GetSelfRelationLink(string parent, string parentId, string child)
3838
{
39-
return $"{_context.BasePath}/{parent.Dasherize()}/{parentId}/relationships/{child.Dasherize()}";
39+
return $"{_context.BasePath}/relationships/{child.Dasherize()}";
4040
}
4141

4242
public string GetRelatedRelationLink(string parent, string parentId, string child)
4343
{
44-
return $"{_context.BasePath}/{parent.Dasherize()}/{parentId}/{child.Dasherize()}";
44+
return $"{_context.BasePath}/{child.Dasherize()}";
4545
}
4646
}
4747
}

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ namespace JsonApiDotNetCore.Configuration
33
public class JsonApiOptions
44
{
55
public string Namespace { get; set; }
6+
public int DefaultPageSize { get; set; }
67
}
78
}

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using System.Threading.Tasks;
45
using JsonApiDotNetCore.Data;
56
using JsonApiDotNetCore.Extensions;
7+
using JsonApiDotNetCore.Internal.Query;
68
using JsonApiDotNetCore.Models;
79
using JsonApiDotNetCore.Services;
810
using Microsoft.AspNetCore.Mvc;
@@ -36,6 +38,7 @@ public JsonApiController(
3638
ILoggerFactory loggerFactory)
3739
{
3840
_jsonApiContext = jsonApiContext.ApplyContext<T>();
41+
3942
_entities = entityRepository;
4043

4144
_logger = loggerFactory.CreateLogger<JsonApiController<T, TId>>();
@@ -47,21 +50,25 @@ public JsonApiController(
4750
IJsonApiContext jsonApiContext,
4851
IEntityRepository<T, TId> entityRepository)
4952
{
53+
_jsonApiContext = jsonApiContext.ApplyContext<T>();
5054
_jsonApiContext = jsonApiContext;
5155
_entities = entityRepository;
5256
}
5357

5458
[HttpGet]
55-
public virtual IActionResult Get()
59+
public virtual async Task<IActionResult> GetAsync()
5660
{
5761
var entities = _entities.Get();
5862

5963
entities = ApplySortAndFilterQuery(entities);
6064

61-
if(_jsonApiContext.QuerySet != null)
65+
if (_jsonApiContext.QuerySet != null && _jsonApiContext.QuerySet.IncludedRelationships != null && _jsonApiContext.QuerySet.IncludedRelationships.Count > 0)
6266
entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships);
6367

64-
return Ok(entities);
68+
// pagination should be done last since it will execute the query
69+
var pagedEntities = await ApplyPageQueryAsync(entities);
70+
71+
return Ok(pagedEntities);
6572
}
6673

6774
[HttpGet("{id}")]
@@ -142,12 +149,6 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
142149
return Ok(updatedEntity);
143150
}
144151

145-
// [HttpPatch("{id}/{relationship}")]
146-
// public virtual IActionResult PatchRelationship(int id, string relation)
147-
// {
148-
// return Ok("Patch Id/relationship");
149-
// }
150-
151152
[HttpDelete("{id}")]
152153
public virtual async Task<IActionResult> DeleteAsync(TId id)
153154
{
@@ -159,26 +160,37 @@ public virtual async Task<IActionResult> DeleteAsync(TId id)
159160
return Ok();
160161
}
161162

162-
// [HttpDelete("{id}/{relationship}")]
163-
// public virtual IActionResult Delete(int id, string relation)
164-
// {
165-
// return Ok("Delete Id/relationship");
166-
// }
167-
168163
private IQueryable<T> ApplySortAndFilterQuery(IQueryable<T> entities)
169164
{
170165
var query = _jsonApiContext.QuerySet;
171166

172167
if(_jsonApiContext.QuerySet == null)
173168
return entities;
174169

175-
entities = _entities.Filter(entities, query.Filter);
170+
if(query.Filter != null)
171+
entities = _entities.Filter(entities, query.Filter);
176172

177-
entities = _entities.Sort(entities, query.SortParameters);
173+
if(query.SortParameters != null && query.SortParameters.Count > 0)
174+
entities = _entities.Sort(entities, query.SortParameters);
178175

179176
return entities;
180177
}
181178

179+
private async Task<IEnumerable<T>> ApplyPageQueryAsync(IQueryable<T> entities)
180+
{
181+
if(_jsonApiContext.Options.DefaultPageSize == 0 && (_jsonApiContext.QuerySet == null || _jsonApiContext.QuerySet.PageQuery.PageSize == 0))
182+
return entities;
183+
184+
var query = _jsonApiContext.QuerySet?.PageQuery ?? new PageQuery();
185+
186+
var pageNumber = query.PageOffset > 0 ? query.PageOffset : 1;
187+
var pageSize = query.PageSize > 0 ? query.PageSize : _jsonApiContext.Options.DefaultPageSize;
188+
189+
_logger?.LogInformation($"Applying paging query. Fetching page {pageNumber} with {pageSize} entities");
190+
191+
return await _entities.PageAsync(entities, pageSize, pageNumber);
192+
}
193+
182194
private IQueryable<T> IncludeRelationships(IQueryable<T> entities, List<string> relationships)
183195
{
184196
_jsonApiContext.IncludedRelationships = relationships;

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,16 @@ public IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relation
132132
throw new JsonApiException("400", "Invalid relationship",
133133
$"{entity.EntityName} does not have a relationship named {relationshipName}");
134134
}
135+
136+
public async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber)
137+
{
138+
if(pageSize > 0)
139+
return await entities
140+
.Skip((pageNumber - 1) * pageSize)
141+
.Take(pageSize)
142+
.ToListAsync();
143+
144+
return await entities.ToListAsync();
145+
}
135146
}
136147
}

src/JsonApiDotNetCore/Data/IEntityRepository.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public interface IEntityRepository<TEntity, in TId>
2323

2424
IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries);
2525

26+
Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber);
27+
2628
Task<TEntity> GetAsync(TId id);
2729

2830
Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName);

src/JsonApiDotNetCore/Extensions/ServiceProviderExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,25 @@ public static void AddJsonApi<TContext>(this IServiceCollection services, Action
3030
private static void _addInternals<TContext>(IServiceCollection services, JsonApiOptions jsonApiOptions)
3131
where TContext : DbContext
3232
{
33-
services.AddJsonApiInternals<TContext>();
33+
services.AddJsonApiInternals<TContext>(jsonApiOptions);
3434
services.AddMvc()
3535
.AddMvcOptions(opt => {
3636
opt.Filters.Add(typeof(JsonApiExceptionFilter));
3737
opt.SerializeAsJsonApi(jsonApiOptions);
3838
});
3939
}
4040

41-
public static void AddJsonApiInternals<TContext>(this IServiceCollection services)
41+
public static void AddJsonApiInternals<TContext>(this IServiceCollection services, JsonApiOptions jsonApiOptions)
4242
where TContext : DbContext
4343
{
4444
var contextGraphBuilder = new ContextGraphBuilder<TContext>();
4545
var contextGraph = contextGraphBuilder.Build();
4646

4747
services.AddScoped(typeof(DbContext), typeof(TContext));
48-
4948
services.AddScoped(typeof(IEntityRepository<>), typeof(DefaultEntityRepository<>));
5049
services.AddScoped(typeof(IEntityRepository<,>), typeof(DefaultEntityRepository<,>));
5150

51+
services.AddSingleton<JsonApiOptions>(jsonApiOptions);
5252
services.AddSingleton<IContextGraph>(contextGraph);
5353
services.AddScoped<IJsonApiContext,JsonApiContext>();
5454
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace JsonApiDotNetCore.Internal.Query
2+
{
3+
public class PageQuery
4+
{
5+
public int PageSize { get; set; }
6+
public int PageOffset { get; set; }
7+
}
8+
}

src/JsonApiDotNetCore/Internal/Query/QuerySet.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using JsonApiDotNetCore.Extensions;
@@ -10,13 +11,16 @@ public class QuerySet
1011
{
1112
IJsonApiContext _jsonApiContext;
1213

13-
public QuerySet(IJsonApiContext jsonApiContext, IQueryCollection query)
14+
public QuerySet(
15+
IJsonApiContext jsonApiContext,
16+
IQueryCollection query)
1417
{
1518
_jsonApiContext = jsonApiContext;
1619
BuildQuerySet(query);
1720
}
1821

1922
public FilterQuery Filter { get; set; }
23+
public PageQuery PageQuery { get; set; }
2024
public List<SortQuery> SortParameters { get; set; }
2125
public List<string> IncludedRelationships { get; set; }
2226

@@ -40,6 +44,11 @@ private void BuildQuerySet(IQueryCollection query)
4044
{
4145
IncludedRelationships = ParseIncludedRelationships(pair.Value);
4246
}
47+
48+
if (pair.Key.StartsWith("page"))
49+
{
50+
PageQuery = ParsePageQuery(pair.Key, pair.Value);
51+
}
4352
}
4453
}
4554

@@ -49,12 +58,28 @@ private FilterQuery ParseFilterQuery(string key, string value)
4958
var propertyName = key.Split('[', ']')[1];
5059
var attribute = GetAttribute(propertyName);
5160

52-
if(attribute == null)
61+
if (attribute == null)
5362
return null;
54-
63+
5564
return new FilterQuery(attribute, value);
5665
}
5766

67+
private PageQuery ParsePageQuery(string key, string value)
68+
{
69+
// expected input = page[size]=10
70+
// page[number]=1
71+
PageQuery = PageQuery ?? new PageQuery();
72+
73+
var propertyName = key.Split('[', ']')[1];
74+
75+
if (propertyName == "size")
76+
PageQuery.PageSize = Convert.ToInt32(value);
77+
else if (propertyName == "number")
78+
PageQuery.PageOffset = Convert.ToInt32(value);
79+
80+
return PageQuery;
81+
}
82+
5883
// sort=id,name
5984
// sort=-id
6085
private List<SortQuery> ParseSortParameters(string value)
@@ -88,7 +113,7 @@ private List<string> ParseIncludedRelationships(string value)
88113
private AttrAttribute GetAttribute(string propertyName)
89114
{
90115
return _jsonApiContext.RequestEntity.Attributes
91-
.FirstOrDefault(attr =>
116+
.FirstOrDefault(attr =>
92117
attr.InternalAttributeName.ToLower() == propertyName.ToLower()
93118
);
94119
}

src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@ public JsonApiExceptionFilter(ILoggerFactory loggerFactory)
1717

1818
public void OnException(ExceptionContext context)
1919
{
20-
_logger.LogError(context.Exception.Message);
20+
_logger.LogError(new EventId(), context.Exception, "An unhandled exception occurred during the request");
2121

22-
var jsonApiException = (JsonApiException)context.Exception;
23-
if(jsonApiException != null)
24-
{
25-
var error = jsonApiException.GetError();
26-
var result = new ObjectResult(error);
27-
result.StatusCode = Convert.ToInt16(error.Status);
28-
context.Result = result;
29-
}
22+
var jsonApiException = context.Exception as JsonApiException;
23+
24+
if(jsonApiException == null)
25+
jsonApiException = new JsonApiException("500", context.Exception.Message);
26+
27+
var error = jsonApiException.GetError();
28+
var result = new ObjectResult(error);
29+
result.StatusCode = Convert.ToInt16(error.Status);
30+
context.Result = result;
3031
}
3132
}
3233
}

src/JsonApiDotNetCore/Services/IJsonApiContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using System.Collections.Generic;
2+
using JsonApiDotNetCore.Configuration;
23
using JsonApiDotNetCore.Internal;
34
using JsonApiDotNetCore.Internal.Query;
45

56
namespace JsonApiDotNetCore.Services
67
{
78
public interface IJsonApiContext
89
{
10+
JsonApiOptions Options { get; set; }
911
IJsonApiContext ApplyContext<T>();
1012
IContextGraph ContextGraph { get; set; }
1113
ContextEntity RequestEntity { get; set; }

src/JsonApiDotNetCore/Services/JsonApiContext.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Generic;
22
using System.Linq;
33
using JsonApiDotNetCore.Builders;
4+
using JsonApiDotNetCore.Configuration;
45
using JsonApiDotNetCore.Internal;
56
using JsonApiDotNetCore.Internal.Query;
67
using Microsoft.AspNetCore.Http;
@@ -12,12 +13,14 @@ public class JsonApiContext : IJsonApiContext
1213
private IHttpContextAccessor _httpContextAccessor;
1314
public JsonApiContext(
1415
IContextGraph contextGraph,
15-
IHttpContextAccessor httpContextAccessor)
16+
IHttpContextAccessor httpContextAccessor,
17+
JsonApiOptions options)
1618
{
1719
ContextGraph = contextGraph;
1820
_httpContextAccessor = httpContextAccessor;
21+
Options = options;
1922
}
20-
23+
public JsonApiOptions Options { get; set; }
2124
public IContextGraph ContextGraph { get; set; }
2225
public ContextEntity RequestEntity { get; set; }
2326
public string BasePath { get; set; }

src/JsonApiDotNetCore/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.2.0",
2+
"version": "0.2.2",
33

44
"dependencies": {
55
"Microsoft.NETCore.App": {

0 commit comments

Comments
 (0)