diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json
new file mode 100644
index 00000000..7bf2f5ba
--- /dev/null
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetAllResponsePaged-2-2.json
@@ -0,0 +1,66 @@
+{
+ "meta": {
+ "total-pages": 2,
+ "total-count": 4
+ },
+ "data": [
+ {
+ "type": "posts",
+ "id": "203",
+ "attributes": {
+ "content": "Post 3 content",
+ "created": "2015-02-07T11:11:00.0000000+00:00",
+ "title": "Post 3"
+ },
+ "relationships": {
+ "author": {
+ "links": {
+ "self": "https://www.example.com/posts/203/relationships/author",
+ "related": "https://www.example.com/posts/203/author"
+ }
+ },
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/posts/203/relationships/comments",
+ "related": "https://www.example.com/posts/203/comments"
+ }
+ },
+ "tags": {
+ "links": {
+ "self": "https://www.example.com/posts/203/relationships/tags",
+ "related": "https://www.example.com/posts/203/tags"
+ }
+ }
+ }
+ },
+ {
+ "type": "posts",
+ "id": "204",
+ "attributes": {
+ "content": "Post 4 content",
+ "created": "2015-02-08T06:59:00.0000000+00:00",
+ "title": "Post 4"
+ },
+ "relationships": {
+ "author": {
+ "links": {
+ "self": "https://www.example.com/posts/204/relationships/author",
+ "related": "https://www.example.com/posts/204/author"
+ }
+ },
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/posts/204/relationships/comments",
+ "related": "https://www.example.com/posts/204/comments"
+ }
+ },
+ "tags": {
+ "links": {
+ "self": "https://www.example.com/posts/204/relationships/tags",
+ "related": "https://www.example.com/posts/204/tags"
+ }
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json
new file mode 100644
index 00000000..b65cea48
--- /dev/null
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted-desc.json
@@ -0,0 +1,64 @@
+{
+ "meta": {
+ "total-pages": 2,
+ "total-count": 3
+ },
+ "data": [
+ {
+ "type": "users",
+ "id": "410",
+ "attributes": {
+ "first-name": "Sally",
+ "last-name": "Burns"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/410/relationships/comments",
+ "related": "https://www.example.com/users/410/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/410/relationships/posts",
+ "related": "https://www.example.com/users/410/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/410/relationships/user-groups",
+ "related": "https://www.example.com/users/410/user-groups"
+ }
+ }
+ }
+ },
+ {
+ "type": "users",
+ "id": "406",
+ "attributes": {
+ "first-name": "Ed",
+ "last-name": "Burns"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/406/relationships/comments",
+ "related": "https://www.example.com/users/406/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/406/relationships/posts",
+ "related": "https://www.example.com/users/406/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/406/relationships/user-groups",
+ "related": "https://www.example.com/users/406/user-groups"
+ }
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json
new file mode 100644
index 00000000..ee562655
--- /dev/null
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-1-2-sorted.json
@@ -0,0 +1,64 @@
+{
+ "meta": {
+ "total-pages": 2,
+ "total-count": 3
+ },
+ "data": [
+ {
+ "type": "users",
+ "id": "409",
+ "attributes": {
+ "first-name": "Charlie",
+ "last-name": "Burns"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/409/relationships/comments",
+ "related": "https://www.example.com/users/409/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/409/relationships/posts",
+ "related": "https://www.example.com/users/409/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/409/relationships/user-groups",
+ "related": "https://www.example.com/users/409/user-groups"
+ }
+ }
+ }
+ },
+ {
+ "type": "users",
+ "id": "406",
+ "attributes": {
+ "first-name": "Ed",
+ "last-name": "Burns"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/406/relationships/comments",
+ "related": "https://www.example.com/users/406/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/406/relationships/posts",
+ "related": "https://www.example.com/users/406/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/406/relationships/user-groups",
+ "related": "https://www.example.com/users/406/user-groups"
+ }
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json
new file mode 100644
index 00000000..16e92908
--- /dev/null
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/Pagination/GetFilterPaged-2-1.json
@@ -0,0 +1,36 @@
+{
+ "meta": {
+ "total-pages": 3,
+ "total-count": 3
+ },
+ "data": [
+ {
+ "type": "users",
+ "id": "409",
+ "attributes": {
+ "first-name": "Charlie",
+ "last-name": "Burns"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/409/relationships/comments",
+ "related": "https://www.example.com/users/409/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/409/relationships/posts",
+ "related": "https://www.example.com/users/409/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/409/relationships/user-groups",
+ "related": "https://www.example.com/users/409/user-groups"
+ }
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj
index b8546fbf..e9cb8a2c 100644
--- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj
@@ -106,6 +106,7 @@
+
@@ -275,8 +276,13 @@
+
+
+
+
+
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs
new file mode 100644
index 00000000..0db20f57
--- /dev/null
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/PaginationTests.cs
@@ -0,0 +1,77 @@
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests
+{
+ [TestClass]
+ public class PaginationTests : AcceptanceTestsBase
+ {
+ [TestMethod]
+ [DeploymentItem(@"Data\Comment.csv", @"Data")]
+ [DeploymentItem(@"Data\Post.csv", @"Data")]
+ [DeploymentItem(@"Data\PostTagLink.csv", @"Data")]
+ [DeploymentItem(@"Data\Tag.csv", @"Data")]
+ [DeploymentItem(@"Data\User.csv", @"Data")]
+ public async Task GetPage2Post2()
+ {
+ using (var effortConnection = GetEffortConnection())
+ {
+ var response = await SubmitGet(effortConnection, "posts?page[number]=1&page[size]=2");
+
+ await AssertResponseContent(response, @"Fixtures\Pagination\GetAllResponsePaged-2-2.json", HttpStatusCode.OK);
+ }
+ }
+
+ [TestMethod]
+ [DeploymentItem(@"Data\Comment.csv", @"Data")]
+ [DeploymentItem(@"Data\Post.csv", @"Data")]
+ [DeploymentItem(@"Data\PostTagLink.csv", @"Data")]
+ [DeploymentItem(@"Data\Tag.csv", @"Data")]
+ [DeploymentItem(@"Data\User.csv", @"Data")]
+ public async Task GetWithFilter()
+ {
+ using (var effortConnection = GetEffortConnection())
+ {
+ var response = await SubmitGet(effortConnection, "users?filter[last-name]=Burns&page[number]=1&page[size]=1");
+
+ await AssertResponseContent(response, @"Fixtures\Pagination\GetFilterPaged-2-1.json", HttpStatusCode.OK);
+ }
+ }
+
+
+ [TestMethod]
+ [DeploymentItem(@"Data\Comment.csv", @"Data")]
+ [DeploymentItem(@"Data\Post.csv", @"Data")]
+ [DeploymentItem(@"Data\PostTagLink.csv", @"Data")]
+ [DeploymentItem(@"Data\Tag.csv", @"Data")]
+ [DeploymentItem(@"Data\User.csv", @"Data")]
+ public async Task GetWithFilterSortedAscTest()
+ {
+ using (var effortConnection = GetEffortConnection())
+ {
+ var response = await SubmitGet(effortConnection, "users?filter[last-name]=Burns&page[number]=0&page[size]=2&sort=first-name");
+
+ await AssertResponseContent(response, @"Fixtures\Pagination\GetFilterPaged-1-2-sorted.json", HttpStatusCode.OK);
+ }
+ }
+
+
+ [TestMethod]
+ [DeploymentItem(@"Data\Comment.csv", @"Data")]
+ [DeploymentItem(@"Data\Post.csv", @"Data")]
+ [DeploymentItem(@"Data\PostTagLink.csv", @"Data")]
+ [DeploymentItem(@"Data\Tag.csv", @"Data")]
+ [DeploymentItem(@"Data\User.csv", @"Data")]
+ public async Task GetWithFilterSortedDescTest()
+ {
+ using (var effortConnection = GetEffortConnection())
+ {
+ var response = await SubmitGet(effortConnection, "users?filter[last-name]=Burns&page[number]=0&page[size]=2&sort=-first-name");
+
+ await AssertResponseContent(response, @"Fixtures\Pagination\GetFilterPaged-1-2-sorted-desc.json", HttpStatusCode.OK);
+ }
+ }
+
+ }
+}
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs
index aec594d2..678f8532 100644
--- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/Startup.cs
@@ -14,6 +14,8 @@
using JSONAPI.EntityFramework.Configuration;
using Owin;
using System.Collections.Generic;
+using JSONAPI.Documents.Builders;
+using JSONAPI.EntityFramework.Documents.Builders;
namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp
{
@@ -112,6 +114,8 @@ internal JsonApiHttpAutofacConfigurator BuildAutofacConfigurator(IAppBuilder app
builder.RegisterType()
.As();
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
+ builder.RegisterType().As();
+
});
configurator.OnApplicationLifetimeScopeBegun(applicationLifetimeScope =>
{
diff --git a/JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs
new file mode 100644
index 00000000..1bd9e2c8
--- /dev/null
+++ b/JSONAPI.EntityFramework/Documents/Builders/EntityFrameworkQueryableResourceCollectionDocumentBuilder.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Data.Entity;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JSONAPI.Documents;
+using JSONAPI.Documents.Builders;
+using JSONAPI.Http;
+using JSONAPI.QueryableTransformers;
+
+namespace JSONAPI.EntityFramework.Documents.Builders
+{
+ ///
+ /// Provides a entity framework implementation of an IQueryableResourceCollectionDocumentBuilder
+ ///
+ public class EntityFrameworkQueryableResourceCollectionDocumentBuilder: DefaultQueryableResourceCollectionDocumentBuilder
+ {
+ ///
+ /// Creates a new EntityFrameworkQueryableResourceCollectionDocumentBuilder
+ ///
+ public EntityFrameworkQueryableResourceCollectionDocumentBuilder(
+ IResourceCollectionDocumentBuilder resourceCollectionDocumentBuilder,
+ IQueryableEnumerationTransformer enumerationTransformer,
+ IQueryableFilteringTransformer filteringTransformer,
+ IQueryableSortingTransformer sortingTransformer,
+ IQueryablePaginationTransformer paginationTransformer,
+ IBaseUrlService baseUrlService) :
+ base(resourceCollectionDocumentBuilder,
+ enumerationTransformer,
+ filteringTransformer,
+ sortingTransformer,
+ paginationTransformer,
+ baseUrlService)
+ {
+ }
+
+ ///
+ /// Returns the metadata that should be sent with this document.
+ ///
+ protected override async Task GetDocumentMetadata(IQueryable originalQuery, IQueryable filteredQuery, IOrderedQueryable sortedQuery,
+ IPaginationTransformResult paginationResult, CancellationToken cancellationToken)
+ {
+ var metadata = new Metadata();
+ if (paginationResult.PaginationWasApplied)
+ {
+ var count = await filteredQuery.CountAsync(cancellationToken);
+ metadata.MetaObject.Add("total-pages", (int)Math.Ceiling((decimal) count / paginationResult.PageSize));
+ metadata.MetaObject.Add("total-count", count);
+ }
+ if (metadata.MetaObject.HasValues)
+ return metadata;
+ return null;
+ }
+ }
+}
diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj
index 0f1dd5c8..2045b33d 100644
--- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj
+++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj
@@ -72,6 +72,7 @@
+
diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json
index 9eb2c810..fad9ad33 100644
--- a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json
+++ b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_all_possible_members.json
@@ -1,12 +1,12 @@
{
- "data": [
- "Primary data 1",
- "Primary data 2"
- ],
- "included": [
- "Related data object 1",
- "Related data object 2",
- "Related data object 3"
- ],
- "meta": "Placeholder metadata object"
+ "meta": "Placeholder metadata object",
+ "data": [
+ "Primary data 1",
+ "Primary data 2"
+ ],
+ "included": [
+ "Related data object 1",
+ "Related data object 2",
+ "Related data object 3"
+ ]
}
\ No newline at end of file
diff --git a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json
index ef98daf7..6d770373 100644
--- a/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json
+++ b/JSONAPI.Tests/Json/Fixtures/ResourceCollectionDocumentFormatter/Serialize_ResourceCollectionDocument_for_primary_data_only_and_metadata.json
@@ -1,7 +1,7 @@
{
- "data": [
- "Primary data 1",
- "Primary data 2"
- ],
- "meta": "Placeholder metadata object"
+ "meta": "Placeholder metadata object",
+ "data": [
+ "Primary data 1",
+ "Primary data 2"
+ ]
}
\ No newline at end of file
diff --git a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs
index d47e93c6..d0488b1c 100644
--- a/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs
+++ b/JSONAPI/Documents/Builders/DefaultQueryableResourceCollectionDocumentBuilder.cs
@@ -60,7 +60,7 @@ public async Task BuildDocument(IQueryable qu
protected virtual Task GetDocumentMetadata(IQueryable originalQuery, IQueryable filteredQuery, IOrderedQueryable sortedQuery,
IPaginationTransformResult paginationResult, CancellationToken cancellationToken)
{
- return Task.FromResult((IMetadata)null);
+ return Task.FromResult((IMetadata) null);
}
}
}
diff --git a/JSONAPI/Documents/Metadata.cs b/JSONAPI/Documents/Metadata.cs
new file mode 100644
index 00000000..004de8c7
--- /dev/null
+++ b/JSONAPI/Documents/Metadata.cs
@@ -0,0 +1,16 @@
+using Newtonsoft.Json.Linq;
+
+namespace JSONAPI.Documents
+{
+ ///
+ /// Default implementation of
+ ///
+ public class Metadata : IMetadata
+ {
+ public Metadata()
+ {
+ MetaObject = new JObject();
+ }
+ public JObject MetaObject { get; }
+ }
+}
diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj
index 4336261f..9bd3d2e2 100644
--- a/JSONAPI/JSONAPI.csproj
+++ b/JSONAPI/JSONAPI.csproj
@@ -91,6 +91,7 @@
+
diff --git a/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs b/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs
index 79cd75d8..b7a0c999 100644
--- a/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs
+++ b/JSONAPI/Json/ResourceCollectionDocumentFormatter.cs
@@ -75,6 +75,12 @@ public Task Serialize(IResourceCollectionDocument document, JsonWriter writer)
{
writer.WriteStartObject();
+ if (document.Metadata != null)
+ {
+ writer.WritePropertyName(MetaKeyName);
+ MetadataFormatter.Serialize(document.Metadata, writer);
+ }
+
writer.WritePropertyName(PrimaryDataKeyName);
writer.WriteStartArray();
@@ -95,11 +101,7 @@ public Task Serialize(IResourceCollectionDocument document, JsonWriter writer)
writer.WriteEndArray();
}
- if (document.Metadata != null)
- {
- writer.WritePropertyName(MetaKeyName);
- MetadataFormatter.Serialize(document.Metadata, writer);
- }
+
writer.WriteEndObject();
diff --git a/README.md b/README.md
index 129060b7..1fb411ca 100644
--- a/README.md
+++ b/README.md
@@ -139,3 +139,13 @@ configuration.CustomBaseUrlService = new BaseUrlService(new Uri("https://api.exa
```
+# Metadata
+
+
+
+## Pagination
+
+### total-pages / total-count
+When using Entity Framework you can register type `EntityFrameworkQueryableResourceCollectionDocumentBuilder` to enable the `total-pages` and `total-count` meta properties.
+When pagination is used the `total-pages` and `total-count` meta properties are provided to indicate the number of pages and records to the client.
+