Skip to content

Commit 989b862

Browse files
authoredFeb 25, 2017
Merge pull request #38 from Research-Institute/meta-objects
Meta objects
2 parents 605e039 + 456d65c commit 989b862

File tree

15 files changed

+247
-22
lines changed

15 files changed

+247
-22
lines changed
 

‎README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or
2424
- [Pagination](#pagination)
2525
- [Filtering](#filtering)
2626
- [Sorting](#sorting)
27+
- [Meta](#meta)
2728
- [Tests](#tests)
2829

2930
## Installation
@@ -240,6 +241,18 @@ when setting up the services:
240241
opt => opt.DefaultPageSize = 10);
241242
```
242243

244+
**Total Record Count**
245+
246+
The total number of records can be added to the document meta by setting it in the options:
247+
248+
```
249+
services.AddJsonApi<AppDbContext>(opt =>
250+
{
251+
opt.DefaultPageSize = 5;
252+
opt.IncludeTotalRecordCount = true;
253+
});
254+
```
255+
243256
### Filtering
244257

245258
You can filter resources by attributes using the `filter` query parameter.
@@ -270,6 +283,25 @@ Resources can be sorted by an attribute:
270283
?sort=-attribute // descending
271284
```
272285

286+
### Meta
287+
288+
Resource meta can be defined by implementing `IHasMeta` on the model class:
289+
290+
```
291+
public class Person : Identifiable<int>, IHasMeta
292+
{
293+
// ...
294+
295+
public Dictionary<string, object> GetMeta(IJsonApiContext context)
296+
{
297+
return new Dictionary<string, object> {
298+
{ "copyright", "Copyright 2015 Example Corp." },
299+
{ "authors", new string[] { "Jared Nance" } }
300+
};
301+
}
302+
}
303+
```
304+
273305
## Tests
274306

275307
I am using DotNetCoreDocs to generate sample requests and documentation.

‎src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using JsonApiDotNetCore.Extensions;
45
using JsonApiDotNetCore.Internal;
56
using JsonApiDotNetCore.Models;
@@ -24,7 +25,8 @@ public Document Build(IIdentifiable entity)
2425

2526
var document = new Document
2627
{
27-
Data = _getData(contextEntity, entity)
28+
Data = _getData(contextEntity, entity),
29+
Meta = _getMeta(entity)
2830
};
2931

3032
document.Included = _appendIncludedObject(document.Included, contextEntity, entity);
@@ -42,7 +44,8 @@ public Documents Build(IEnumerable<IIdentifiable> entities)
4244

4345
var documents = new Documents
4446
{
45-
Data = new List<DocumentData>()
47+
Data = new List<DocumentData>(),
48+
Meta = _getMeta(entities.FirstOrDefault())
4649
};
4750

4851
foreach (var entity in entities)
@@ -54,6 +57,23 @@ public Documents Build(IEnumerable<IIdentifiable> entities)
5457
return documents;
5558
}
5659

60+
private Dictionary<string, object> _getMeta(IIdentifiable entity)
61+
{
62+
if (entity == null) return null;
63+
64+
var meta = new Dictionary<string, object>();
65+
var metaEntity = entity as IHasMeta;
66+
67+
if(metaEntity != null)
68+
meta = metaEntity.GetMeta(_jsonApiContext);
69+
70+
if(_jsonApiContext.Options.IncludeTotalRecordCount)
71+
meta["total-records"] = _jsonApiContext.TotalRecords;
72+
73+
if(meta.Count > 0) return meta;
74+
return null;
75+
}
76+
5777
private List<DocumentData> _appendIncludedObject(List<DocumentData> includedObject, ContextEntity contextEntity, IIdentifiable entity)
5878
{
5979
var includedEntities = _getIncludedEntities(contextEntity, entity);

‎src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ public class JsonApiOptions
44
{
55
public string Namespace { get; set; }
66
public int DefaultPageSize { get; set; }
7+
public bool IncludeTotalRecordCount { get; set; }
78
}
89
}

‎src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public virtual async Task<IActionResult> GetAsync()
6464
if (_jsonApiContext.QuerySet != null && _jsonApiContext.QuerySet.IncludedRelationships != null && _jsonApiContext.QuerySet.IncludedRelationships.Count > 0)
6565
entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships);
6666

67+
if (_jsonApiContext.Options.IncludeTotalRecordCount)
68+
_jsonApiContext.TotalRecords = await entities.CountAsync();
69+
6770
// pagination should be done last since it will execute the query
6871
var pagedEntities = await ApplyPageQueryAsync(entities);
6972

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
using System.Collections.Generic;
21
using Newtonsoft.Json;
32

43
namespace JsonApiDotNetCore.Models
54
{
6-
public class Document
5+
public class Document : DocumentBase
76
{
87
[JsonProperty("data")]
98
public DocumentData Data { get; set; }
10-
11-
[JsonProperty("included")]
12-
public List<DocumentData> Included { get; set; }
139
}
1410
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Collections.Generic;
2+
using Newtonsoft.Json;
3+
4+
namespace JsonApiDotNetCore.Models
5+
{
6+
public class DocumentBase
7+
{
8+
[JsonProperty("included")]
9+
public List<DocumentData> Included { get; set; }
10+
public Dictionary<string, object> Meta { get; set; }
11+
}
12+
}

‎src/JsonApiDotNetCore/Models/Documents.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33

44
namespace JsonApiDotNetCore.Models
55
{
6-
public class Documents
6+
public class Documents : DocumentBase
77
{
88
[JsonProperty("data")]
99
public List<DocumentData> Data { get; set; }
10-
11-
[JsonProperty("included")]
12-
public List<DocumentData> Included { get; set; }
1310
}
1411
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Collections.Generic;
2+
using JsonApiDotNetCore.Services;
3+
4+
namespace JsonApiDotNetCore.Models
5+
{
6+
public interface IHasMeta
7+
{
8+
Dictionary<string, object> GetMeta(IJsonApiContext context);
9+
}
10+
}

‎src/JsonApiDotNetCore/Services/IJsonApiContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ public interface IJsonApiContext
1515
QuerySet QuerySet { get; set; }
1616
bool IsRelationshipData { get; set; }
1717
List<string> IncludedRelationships { get; set; }
18+
int TotalRecords { get; set; }
1819
}
1920
}

‎src/JsonApiDotNetCore/Services/JsonApiContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ public JsonApiContext(
2020
_httpContextAccessor = httpContextAccessor;
2121
Options = options;
2222
}
23+
2324
public JsonApiOptions Options { get; set; }
2425
public IContextGraph ContextGraph { get; set; }
2526
public ContextEntity RequestEntity { get; set; }
2627
public string BasePath { get; set; }
2728
public QuerySet QuerySet { get; set; }
2829
public bool IsRelationshipData { get; set; }
2930
public List<string> IncludedRelationships { get; set; }
31+
public int TotalRecords { get; set; }
3032

3133
public IJsonApiContext ApplyContext<T>()
3234
{

‎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.11",
2+
"version": "0.2.12",
33

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

‎src/JsonApiDotNetCoreExample/Models/Person.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System.Collections.Generic;
22
using JsonApiDotNetCore.Internal;
33
using JsonApiDotNetCore.Models;
4+
using JsonApiDotNetCore.Services;
45

56
namespace JsonApiDotNetCoreExample.Models
67
{
7-
public class Person : Identifiable<int>
8+
public class Person : Identifiable<int>, IHasMeta
89
{
910
public override int Id { get; set; }
1011

@@ -15,5 +16,13 @@ public class Person : Identifiable<int>
1516
public string LastName { get; set; }
1617

1718
public virtual List<TodoItem> TodoItems { get; set; }
19+
20+
public Dictionary<string, object> GetMeta(IJsonApiContext context)
21+
{
22+
return new Dictionary<string, object> {
23+
{ "copyright", "Copyright 2015 Example Corp." },
24+
{ "authors", new string[] { "Jared Nance" } }
25+
};
26+
}
1827
}
1928
}

‎src/JsonApiDotNetCoreExample/Startup.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace JsonApiDotNetCoreExample
1515
{
1616
public class Startup
1717
{
18-
private readonly IConfiguration _config;
18+
public readonly IConfiguration Config;
1919

2020
public Startup(IHostingEnvironment env)
2121
{
@@ -25,10 +25,10 @@ public Startup(IHostingEnvironment env)
2525
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
2626
.AddEnvironmentVariables();
2727

28-
_config = builder.Build();
28+
Config = builder.Build();
2929
}
3030

31-
public IServiceProvider ConfigureServices(IServiceCollection services)
31+
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
3232
{
3333
var loggerFactory = new LoggerFactory();
3434
loggerFactory
@@ -37,7 +37,7 @@ public IServiceProvider ConfigureServices(IServiceCollection services)
3737

3838
services.AddDbContext<AppDbContext>(options =>
3939
{
40-
options.UseNpgsql(_getDbConnectionString());
40+
options.UseNpgsql(GetDbConnectionString());
4141
}, ServiceLifetime.Transient);
4242

4343
services.AddJsonApi<AppDbContext>(opt =>
@@ -46,7 +46,7 @@ public IServiceProvider ConfigureServices(IServiceCollection services)
4646
opt.DefaultPageSize = 5;
4747
});
4848

49-
services.AddDocumentationConfiguration(_config);
49+
services.AddDocumentationConfiguration(Config);
5050

5151
var provider = services.BuildServiceProvider();
5252
var appContext = provider.GetRequiredService<AppDbContext>();
@@ -55,25 +55,25 @@ public IServiceProvider ConfigureServices(IServiceCollection services)
5555
return provider;
5656
}
5757

58-
public void Configure(
58+
public virtual void Configure(
5959
IApplicationBuilder app,
6060
IHostingEnvironment env,
6161
ILoggerFactory loggerFactory,
6262
AppDbContext context)
6363
{
6464
context.Database.Migrate();
6565

66-
loggerFactory.AddConsole(_config.GetSection("Logging"));
66+
loggerFactory.AddConsole(Config.GetSection("Logging"));
6767
loggerFactory.AddDebug();
6868

6969
app.UseDocs();
7070

7171
app.UseJsonApi();
7272
}
7373

74-
private string _getDbConnectionString()
74+
public string GetDbConnectionString()
7575
{
76-
return _config["Data:DefaultConnection"];
76+
return Config["Data:DefaultConnection"];
7777
}
7878
}
7979
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 JsonApiDotNetCore.Models;
12+
using JsonApiDotNetCoreExample.Data;
13+
using System.Linq;
14+
using JsonApiDotNetCoreExampleTests.Startups;
15+
using JsonApiDotNetCoreExample.Models;
16+
using System.Collections;
17+
18+
namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests
19+
{
20+
[Collection("WebHostCollection")]
21+
public class Meta
22+
{
23+
private DocsFixture<Startup, JsonDocWriter> _fixture;
24+
private AppDbContext _context;
25+
public Meta(DocsFixture<Startup, JsonDocWriter> fixture)
26+
{
27+
_fixture = fixture;
28+
_context = fixture.GetService<AppDbContext>();
29+
}
30+
31+
[Fact]
32+
public async Task Total_Record_Count_Included()
33+
{
34+
// arrange
35+
var expectedCount = _context.TodoItems.Count();
36+
var builder = new WebHostBuilder()
37+
.UseStartup<MetaStartup>();
38+
39+
var httpMethod = new HttpMethod("GET");
40+
var route = $"/api/v1/todo-items";
41+
42+
var server = new TestServer(builder);
43+
var client = server.CreateClient();
44+
var request = new HttpRequestMessage(httpMethod, route);
45+
46+
// act
47+
var response = await client.SendAsync(request);
48+
var documents = JsonConvert.DeserializeObject<Documents>(await response.Content.ReadAsStringAsync());
49+
50+
// assert
51+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
52+
Assert.NotNull(documents.Meta);
53+
Assert.Equal((long)expectedCount, (long)documents.Meta["total-records"]);
54+
}
55+
56+
[Fact]
57+
public async Task EntityThatImplements_IHasMeta_Contains_MetaData()
58+
{
59+
// arrange
60+
var person = new Person();
61+
var expectedMeta = person.GetMeta(null);
62+
var builder = new WebHostBuilder()
63+
.UseStartup<Startup>();
64+
65+
var httpMethod = new HttpMethod("GET");
66+
var route = $"/api/v1/people";
67+
68+
var server = new TestServer(builder);
69+
var client = server.CreateClient();
70+
var request = new HttpRequestMessage(httpMethod, route);
71+
72+
// act
73+
var response = await client.SendAsync(request);
74+
var documents = JsonConvert.DeserializeObject<Documents>(await response.Content.ReadAsStringAsync());
75+
76+
// assert
77+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
78+
Assert.NotNull(documents.Meta);
79+
Assert.NotNull(expectedMeta);
80+
Assert.NotEmpty(expectedMeta);
81+
82+
foreach(var hash in expectedMeta)
83+
{
84+
if(hash.Value is IList)
85+
{
86+
var listValue = (IList)hash.Value;
87+
for(var i=0; i < listValue.Count; i++)
88+
Assert.Equal(listValue[i].ToString(), ((IList)documents.Meta[hash.Key])[i].ToString());
89+
}
90+
else
91+
{
92+
Assert.Equal(hash.Value, documents.Meta[hash.Key]);
93+
}
94+
}
95+
}
96+
}
97+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using JsonApiDotNetCoreExample.Data;
5+
using Microsoft.EntityFrameworkCore;
6+
using JsonApiDotNetCore.Extensions;
7+
using DotNetCoreDocs.Configuration;
8+
using System;
9+
using JsonApiDotNetCoreExample;
10+
11+
namespace JsonApiDotNetCoreExampleTests.Startups
12+
{
13+
public class MetaStartup : Startup
14+
{
15+
public MetaStartup(IHostingEnvironment env)
16+
: base (env)
17+
{ }
18+
19+
public override IServiceProvider ConfigureServices(IServiceCollection services)
20+
{
21+
var loggerFactory = new LoggerFactory();
22+
23+
loggerFactory
24+
.AddConsole(LogLevel.Trace);
25+
26+
services.AddSingleton<ILoggerFactory>(loggerFactory);
27+
28+
services.AddDbContext<AppDbContext>(options =>
29+
{
30+
options.UseNpgsql(GetDbConnectionString());
31+
}, ServiceLifetime.Transient);
32+
33+
services.AddJsonApi<AppDbContext>(opt =>
34+
{
35+
opt.Namespace = "api/v1";
36+
opt.DefaultPageSize = 5;
37+
opt.IncludeTotalRecordCount = true;
38+
});
39+
40+
services.AddDocumentationConfiguration(Config);
41+
42+
return services.BuildServiceProvider();
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)
Please sign in to comment.