diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs
index e1b5a308..b189e933 100644
--- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/CreatingResourcesTests.cs
@@ -112,5 +112,34 @@ public async Task Post_with_empty_id()
}
}
}
+
+
+ [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 Post_with_empty_id_and_include()
+ {
+ using (var effortConnection = GetEffortConnection())
+ {
+ var response = await SubmitPost(effortConnection, "posts?include=author", @"Fixtures\CreatingResources\Requests\Post_with_empty_id_Request.json");
+
+ await AssertResponseContent(response, @"Fixtures\CreatingResources\Responses\Post_with_empty_id_and_include_author_Response.json", HttpStatusCode.OK);
+
+ using (var dbContext = new TestDbContext(effortConnection, false))
+ {
+ var allPosts = dbContext.Posts.ToArray();
+ allPosts.Length.Should().Be(5);
+ var actualPost = allPosts.First(t => t.Id == "230");
+ actualPost.Id.Should().Be("230");
+ actualPost.Title.Should().Be("New post");
+ actualPost.Content.Should().Be("The server generated my ID");
+ actualPost.Created.Should().Be(new DateTimeOffset(2015, 04, 13, 12, 09, 0, new TimeSpan(0, 3, 0, 0)));
+ actualPost.AuthorId.Should().Be("401");
+ }
+ }
+ }
}
}
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs
index 36321862..18e83042 100644
--- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/FetchingResourcesTests.cs
@@ -99,6 +99,23 @@ public async Task Get_related_to_many()
}
}
+
+ [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 Get_related_to_many_included()
+ {
+ using (var effortConnection = GetEffortConnection())
+ {
+ var response = await SubmitGet(effortConnection, "posts/201/comments?include=author");
+
+ await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_related_to_many_include_response.json", HttpStatusCode.OK);
+ }
+ }
+
[TestMethod]
[DeploymentItem(@"Data\Comment.csv", @"Data")]
[DeploymentItem(@"Data\Post.csv", @"Data")]
@@ -115,6 +132,23 @@ public async Task Get_related_to_one()
}
}
+
+ [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 Get_included_to_one()
+ {
+ using (var effortConnection = GetEffortConnection())
+ {
+ var response = await SubmitGet(effortConnection, "posts/201?include=author");
+
+ await AssertResponseContent(response, @"Fixtures\FetchingResources\Get_included_to_one_response.json", HttpStatusCode.OK);
+ }
+ }
+
[TestMethod]
[DeploymentItem(@"Data\Comment.csv", @"Data")]
[DeploymentItem(@"Data\Post.csv", @"Data")]
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json
new file mode 100644
index 00000000..f9996946
--- /dev/null
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/CreatingResources/Responses/Post_with_empty_id_and_include_author_Response.json
@@ -0,0 +1,65 @@
+{
+ "data": {
+ "type": "posts",
+ "id": "230",
+ "attributes": {
+ "content": "The server generated my ID",
+ "created": "2015-04-13T09:09:00.0000000+00:00",
+ "title": "New post"
+ },
+ "relationships": {
+ "author": {
+ "links": {
+ "self": "https://www.example.com/posts/230/relationships/author",
+ "related": "https://www.example.com/posts/230/author"
+ },
+ "data": {
+ "type": "users",
+ "id": "401"
+ }
+ },
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/posts/230/relationships/comments",
+ "related": "https://www.example.com/posts/230/comments"
+ }
+ },
+ "tags": {
+ "links": {
+ "self": "https://www.example.com/posts/230/relationships/tags",
+ "related": "https://www.example.com/posts/230/tags"
+ }
+ }
+ }
+ },
+ "included": [
+ {
+ "type": "users",
+ "id": "401",
+ "attributes": {
+ "first-name": "Alice",
+ "last-name": "Smith"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/401/relationships/comments",
+ "related": "https://www.example.com/users/401/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/401/relationships/posts",
+ "related": "https://www.example.com/users/401/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/401/relationships/user-groups",
+ "related": "https://www.example.com/users/401/user-groups"
+ }
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json
new file mode 100644
index 00000000..98fcd282
--- /dev/null
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_included_to_one_response.json
@@ -0,0 +1,65 @@
+{
+ "data": {
+ "type": "posts",
+ "id": "201",
+ "attributes": {
+ "content": "Post 1 content",
+ "created": "2015-01-31T14:00:00.0000000+00:00",
+ "title": "Post 1"
+ },
+ "relationships": {
+ "author": {
+ "links": {
+ "self": "https://www.example.com/posts/201/relationships/author",
+ "related": "https://www.example.com/posts/201/author"
+ },
+ "data": {
+ "type": "users",
+ "id": "401"
+ }
+ },
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/posts/201/relationships/comments",
+ "related": "https://www.example.com/posts/201/comments"
+ }
+ },
+ "tags": {
+ "links": {
+ "self": "https://www.example.com/posts/201/relationships/tags",
+ "related": "https://www.example.com/posts/201/tags"
+ }
+ }
+ }
+ },
+ "included": [
+ {
+ "type": "users",
+ "id": "401",
+ "attributes": {
+ "first-name": "Alice",
+ "last-name": "Smith"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/401/relationships/comments",
+ "related": "https://www.example.com/users/401/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/401/relationships/posts",
+ "related": "https://www.example.com/users/401/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/401/relationships/user-groups",
+ "related": "https://www.example.com/users/401/user-groups"
+ }
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json
new file mode 100644
index 00000000..e1a01091
--- /dev/null
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/FetchingResources/Get_related_to_many_include_response.json
@@ -0,0 +1,140 @@
+{
+ "data": [
+ {
+ "type": "comments",
+ "id": "101",
+ "attributes": {
+ "created": "2015-01-31T14:30:00.0000000+00:00",
+ "text": "Comment 1"
+ },
+ "relationships": {
+ "author": {
+ "links": {
+ "self": "https://www.example.com/comments/101/relationships/author",
+ "related": "https://www.example.com/comments/101/author"
+ },
+ "data": {
+ "type": "users",
+ "id": "403"
+ }
+ },
+ "post": {
+ "links": {
+ "self": "https://www.example.com/comments/101/relationships/post",
+ "related": "https://www.example.com/comments/101/post"
+ }
+ }
+ }
+ },
+ {
+ "type": "comments",
+ "id": "102",
+ "attributes": {
+ "created": "2015-01-31T14:35:00.0000000+00:00",
+ "text": "Comment 2"
+ },
+ "relationships": {
+ "author": {
+ "links": {
+ "self": "https://www.example.com/comments/102/relationships/author",
+ "related": "https://www.example.com/comments/102/author"
+ },
+ "data": {
+ "type": "users",
+ "id": "402"
+ }
+ },
+ "post": {
+ "links": {
+ "self": "https://www.example.com/comments/102/relationships/post",
+ "related": "https://www.example.com/comments/102/post"
+ }
+ }
+ }
+ },
+ {
+ "type": "comments",
+ "id": "103",
+ "attributes": {
+ "created": "2015-01-31T14:41:00.0000000+00:00",
+ "text": "Comment 3"
+ },
+ "relationships": {
+ "author": {
+ "links": {
+ "self": "https://www.example.com/comments/103/relationships/author",
+ "related": "https://www.example.com/comments/103/author"
+ },
+ "data": {
+ "type": "users",
+ "id": "403"
+ }
+ },
+ "post": {
+ "links": {
+ "self": "https://www.example.com/comments/103/relationships/post",
+ "related": "https://www.example.com/comments/103/post"
+ }
+ }
+ }
+ }
+ ],
+ "included": [
+ {
+ "type": "users",
+ "id": "403",
+ "attributes": {
+ "first-name": "Charlie",
+ "last-name": "Michaels"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/403/relationships/comments",
+ "related": "https://www.example.com/users/403/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/403/relationships/posts",
+ "related": "https://www.example.com/users/403/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/403/relationships/user-groups",
+ "related": "https://www.example.com/users/403/user-groups"
+ }
+ }
+ }
+ },
+ {
+ "type": "users",
+ "id": "402",
+ "attributes": {
+ "first-name": "Bob",
+ "last-name": "Jones"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/402/relationships/comments",
+ "related": "https://www.example.com/users/402/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/402/relationships/posts",
+ "related": "https://www.example.com/users/402/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/402/relationships/user-groups",
+ "related": "https://www.example.com/users/402/user-groups"
+ }
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json
new file mode 100644
index 00000000..6bff40d9
--- /dev/null
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/Fixtures/UpdatingResources/Responses/PatchWithAttributeUpdateWithIncludeResponse.json
@@ -0,0 +1,65 @@
+{
+ "data": {
+ "type": "posts",
+ "id": "202",
+ "attributes": {
+ "content": "Post 2 content",
+ "created": "2015-02-05T08:10:00.0000000+00:00",
+ "title": "New post title"
+ },
+ "relationships": {
+ "author": {
+ "links": {
+ "self": "https://www.example.com/posts/202/relationships/author",
+ "related": "https://www.example.com/posts/202/author"
+ },
+ "data": {
+ "type": "users",
+ "id": "401"
+ }
+ },
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/posts/202/relationships/comments",
+ "related": "https://www.example.com/posts/202/comments"
+ }
+ },
+ "tags": {
+ "links": {
+ "self": "https://www.example.com/posts/202/relationships/tags",
+ "related": "https://www.example.com/posts/202/tags"
+ }
+ }
+ }
+ },
+ "included": [
+ {
+ "type": "users",
+ "id": "401",
+ "attributes": {
+ "first-name": "Alice",
+ "last-name": "Smith"
+ },
+ "relationships": {
+ "comments": {
+ "links": {
+ "self": "https://www.example.com/users/401/relationships/comments",
+ "related": "https://www.example.com/users/401/comments"
+ }
+ },
+ "posts": {
+ "links": {
+ "self": "https://www.example.com/users/401/relationships/posts",
+ "related": "https://www.example.com/users/401/posts"
+ }
+ },
+ "user-groups": {
+ "links": {
+ "self": "https://www.example.com/users/401/relationships/user-groups",
+ "related": "https://www.example.com/users/401/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 b335e6c5..b8546fbf 100644
--- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests.csproj
@@ -271,6 +271,10 @@
+
+
+
+
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs
index 48df90f9..e24ae49b 100644
--- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.Tests/UpdatingResourcesTests.cs
@@ -41,6 +41,35 @@ public async Task PatchWithAttributeUpdate()
}
}
+ [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 PatchWithAttributeUpdateAndInclude()
+ {
+ using (var effortConnection = GetEffortConnection())
+ {
+ var response = await SubmitPatch(effortConnection, "posts/202?include=author", @"Fixtures\UpdatingResources\Requests\PatchWithAttributeUpdateRequest.json");
+
+ await AssertResponseContent(response, @"Fixtures\UpdatingResources\Responses\PatchWithAttributeUpdateWithIncludeResponse.json", HttpStatusCode.OK);
+
+ using (var dbContext = new TestDbContext(effortConnection, false))
+ {
+ var allPosts = dbContext.Posts.Include(p => p.Tags).ToArray();
+ allPosts.Length.Should().Be(4);
+ var actualPost = allPosts.First(t => t.Id == "202");
+ actualPost.Id.Should().Be("202");
+ actualPost.Title.Should().Be("New post title");
+ actualPost.Content.Should().Be("Post 2 content");
+ actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0)));
+ actualPost.AuthorId.Should().Be("401");
+ actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303");
+ }
+ }
+ }
+
[TestMethod]
[DeploymentItem(@"Data\PostID.csv", @"Data")]
public async Task PatchWithAttributeUpdateID()
diff --git a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs
index 5cb186b3..9fb646b3 100644
--- a/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs
+++ b/JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp/DocumentMaterializers/StarshipOfficersRelatedResourceMaterializer.cs
@@ -17,8 +17,9 @@ public class StarshipOfficersRelatedResourceMaterializer : EntityFrameworkToMany
public StarshipOfficersRelatedResourceMaterializer(ResourceTypeRelationship relationship, DbContext dbContext,
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
ISortExpressionExtractor sortExpressionExtractor,
+ IIncludeExpressionExtractor includeExpressionExtractor,
IResourceTypeRegistration primaryTypeRegistration)
- : base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, primaryTypeRegistration)
+ : base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, includeExpressionExtractor, primaryTypeRegistration)
{
_dbContext = dbContext;
}
diff --git a/JSONAPI.Autofac/JsonApiAutofacModule.cs b/JSONAPI.Autofac/JsonApiAutofacModule.cs
index d388dafc..9c57ca4a 100644
--- a/JSONAPI.Autofac/JsonApiAutofacModule.cs
+++ b/JSONAPI.Autofac/JsonApiAutofacModule.cs
@@ -166,6 +166,7 @@ protected override void Load(ContainerBuilder builder)
// Misc
builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().As().SingleInstance();
}
}
}
diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs
index 0b86166f..114f7877 100644
--- a/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs
+++ b/JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs
@@ -9,6 +9,7 @@
using JSONAPI.Core;
using JSONAPI.Documents;
using JSONAPI.Documents.Builders;
+using JSONAPI.Extensions;
using JSONAPI.Http;
namespace JSONAPI.EntityFramework.Http
@@ -24,6 +25,7 @@ public class EntityFrameworkDocumentMaterializer : IDocumentMaterializer wher
private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder;
private readonly IEntityFrameworkResourceObjectMaterializer _entityFrameworkResourceObjectMaterializer;
private readonly ISortExpressionExtractor _sortExpressionExtractor;
+ private readonly IIncludeExpressionExtractor _includeExpressionExtractor;
private readonly IBaseUrlService _baseUrlService;
///
@@ -36,6 +38,7 @@ public EntityFrameworkDocumentMaterializer(
ISingleResourceDocumentBuilder singleResourceDocumentBuilder,
IEntityFrameworkResourceObjectMaterializer entityFrameworkResourceObjectMaterializer,
ISortExpressionExtractor sortExpressionExtractor,
+ IIncludeExpressionExtractor includeExpressionExtractor,
IBaseUrlService baseUrlService)
{
DbContext = dbContext;
@@ -44,24 +47,27 @@ public EntityFrameworkDocumentMaterializer(
_singleResourceDocumentBuilder = singleResourceDocumentBuilder;
_entityFrameworkResourceObjectMaterializer = entityFrameworkResourceObjectMaterializer;
_sortExpressionExtractor = sortExpressionExtractor;
+ _includeExpressionExtractor = includeExpressionExtractor;
_baseUrlService = baseUrlService;
}
public virtual Task GetRecords(HttpRequestMessage request, CancellationToken cancellationToken)
{
- var query = DbContext.Set().AsQueryable();
var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request);
- return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken);
+ var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request);
+ var query = QueryIncludeNavigationProperties(null, GetNavigationPropertiesIncludes(includes));
+ return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken, includes);
}
public virtual async Task GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken)
{
var apiBaseUrl = GetBaseUrlFromRequest(request);
- var singleResource = await FilterById(id, _resourceTypeRegistration).FirstOrDefaultAsync(cancellationToken);
+ var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request);
+ var singleResource = await FilterById(id, _resourceTypeRegistration, GetNavigationPropertiesIncludes(includes)).FirstOrDefaultAsync(cancellationToken);
if (singleResource == null)
throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.",
_resourceTypeRegistration.ResourceTypeName, id));
- return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, null, null);
+ return _singleResourceDocumentBuilder.BuildDocument(singleResource, apiBaseUrl, includes, null);
}
public virtual async Task CreateRecord(ISingleResourceDocument requestDocument,
@@ -71,7 +77,8 @@ public virtual async Task CreateRecord(ISingleResourceD
var newRecord = MaterializeAsync(requestDocument.PrimaryData, cancellationToken);
await OnCreate(newRecord);
await DbContext.SaveChangesAsync(cancellationToken);
- var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, null, null);
+ var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request);
+ var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, includes, null);
return returnDocument;
}
@@ -83,8 +90,9 @@ public virtual async Task UpdateRecord(string id, ISing
var apiBaseUrl = GetBaseUrlFromRequest(request);
var newRecord = MaterializeAsync(requestDocument.PrimaryData, cancellationToken);
await OnUpdate(newRecord);
- var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, null, null);
await DbContext.SaveChangesAsync(cancellationToken);
+ var includes = _includeExpressionExtractor.ExtractIncludeExpressions(request);
+ var returnDocument = _singleResourceDocumentBuilder.BuildDocument(await newRecord, apiBaseUrl, includes, null);
return returnDocument;
}
@@ -116,50 +124,7 @@ protected virtual async Task MaterializeAsync(IResourceObject resourceObject,
return (T) await _entityFrameworkResourceObjectMaterializer.MaterializeResourceObject(resourceObject, cancellationToken);
}
- ///
- /// Generic method for getting the related resources for a to-many relationship
- ///
- protected async Task GetRelatedToMany(string id,
- ResourceTypeRelationship relationship, HttpRequestMessage request, CancellationToken cancellationToken)
- {
- var param = Expression.Parameter(typeof(T));
- var accessorExpr = Expression.Property(param, relationship.Property);
- var lambda = Expression.Lambda>>(accessorExpr, param);
-
- var primaryEntityQuery = FilterById(id, _resourceTypeRegistration);
-
- // We have to see if the resource even exists, so we can throw a 404 if it doesn't
- var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken);
- if (relatedResource == null)
- throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.",
- _resourceTypeRegistration.ResourceTypeName, id));
-
- var relatedResourceQuery = primaryEntityQuery.SelectMany(lambda);
- var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request);
-
- return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, sortExpressions, cancellationToken);
- }
-
- ///
- /// Generic method for getting the related resources for a to-one relationship
- ///
- protected async Task GetRelatedToOne(string id,
- ResourceTypeRelationship relationship, HttpRequestMessage request, CancellationToken cancellationToken)
- {
- var param = Expression.Parameter(typeof(T));
- var accessorExpr = Expression.Property(param, relationship.Property);
- var lambda = Expression.Lambda>(accessorExpr, param);
-
- var primaryEntityQuery = FilterById(id, _resourceTypeRegistration);
- var primaryEntityExists = await primaryEntityQuery.AnyAsync(cancellationToken);
- if (!primaryEntityExists)
- throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.",
- _resourceTypeRegistration.ResourceTypeName, id));
- var relatedResource = await primaryEntityQuery.Select(lambda).FirstOrDefaultAsync(cancellationToken);
- return _singleResourceDocumentBuilder.BuildDocument(relatedResource, GetBaseUrlFromRequest(request), null, null);
- }
-
-
+
///
/// Manipulate entity before create.
///
@@ -189,11 +154,34 @@ protected virtual async Task OnDelete(Task record)
await record;
}
- private IQueryable Filter(Expression> predicate,
+ ///
+ /// This method allows to include into query.
+ /// This can reduce the number of queries (eager loading)
+ ///
+ ///
+ ///
+ ///
+ protected virtual Expression>[] GetNavigationPropertiesIncludes(string[] includes)
+ {
+ List>> list = new List>>();
+ foreach (var include in includes)
+ {
+ var incl = include.Pascalize();
+ var param = Expression.Parameter(typeof(TResource));
+ var lambda =
+ Expression.Lambda>(
+ Expression.PropertyOrField(param, incl),param);
+ list.Add(lambda);
+ }
+ return list.ToArray();
+ }
+
+
+ private IQueryable QueryIncludeNavigationProperties(Expression> predicate,
params Expression>[] includes) where TResource : class
{
IQueryable query = DbContext.Set();
- if (includes != null && includes.Any())
+ if (includes != null && includes.Any()) // eager loading
query = includes.Aggregate(query, (current, include) => current.Include(include));
if (predicate != null)
@@ -208,7 +196,7 @@ private IQueryable FilterById(string id, IResourceTypeRegi
var param = Expression.Parameter(typeof(TResource));
var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id);
var predicate = Expression.Lambda>(filterByIdExpression, param);
- return Filter(predicate, includes);
+ return QueryIncludeNavigationProperties(predicate, includes);
}
}
}
diff --git a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs
index 2a52338e..50004e16 100644
--- a/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs
+++ b/JSONAPI.EntityFramework/Http/EntityFrameworkToManyRelatedResourceDocumentMaterializer.cs
@@ -7,6 +7,7 @@
using System.Threading.Tasks;
using JSONAPI.Core;
using JSONAPI.Documents.Builders;
+using JSONAPI.Extensions;
using JSONAPI.Http;
namespace JSONAPI.EntityFramework.Http
@@ -29,8 +30,9 @@ public EntityFrameworkToManyRelatedResourceDocumentMaterializer(
DbContext dbContext,
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
ISortExpressionExtractor sortExpressionExtractor,
+ IIncludeExpressionExtractor includeExpressionExtractor,
IResourceTypeRegistration primaryTypeRegistration)
- : base(queryableResourceCollectionDocumentBuilder, sortExpressionExtractor)
+ : base(queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, includeExpressionExtractor)
{
_relationship = relationship;
_dbContext = dbContext;
@@ -43,8 +45,7 @@ protected override async Task> GetRelatedQuery(string prima
var param = Expression.Parameter(typeof (TPrimaryResource));
var accessorExpr = Expression.Property(param, _relationship.Property);
var lambda = Expression.Lambda>>(accessorExpr, param);
-
- var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration);
+ var primaryEntityQuery = FilterById(primaryResourceId, _primaryTypeRegistration, GetNavigationPropertiesIncludes(Includes));
// We have to see if the resource even exists, so we can throw a 404 if it doesn't
var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken);
@@ -56,6 +57,29 @@ protected override async Task> GetRelatedQuery(string prima
return primaryEntityQuery.SelectMany(lambda);
}
+
+ ///
+ /// This method allows to include into query.
+ /// This can reduce the number of queries (eager loading)
+ ///
+ ///
+ ///
+ ///
+ protected virtual Expression>[] GetNavigationPropertiesIncludes(string[] includes)
+ {
+ List>> list = new List>>();
+ foreach (var include in includes)
+ {
+ var incl = include.Pascalize();
+ var param = Expression.Parameter(typeof(TResource));
+ var lambda =
+ Expression.Lambda>(
+ Expression.PropertyOrField(param, incl), param);
+ list.Add(lambda);
+ }
+ return list.ToArray();
+ }
+
private IQueryable FilterById(string id,
IResourceTypeRegistration resourceTypeRegistration,
params Expression>[] includes) where TResource : class
@@ -63,10 +87,10 @@ private IQueryable FilterById(string id,
var param = Expression.Parameter(typeof (TResource));
var filterByIdExpression = resourceTypeRegistration.GetFilterByIdExpression(param, id);
var predicate = Expression.Lambda>(filterByIdExpression, param);
- return Filter(predicate, includes);
+ return QueryIncludeNavigationProperties(predicate, includes);
}
- private IQueryable Filter(Expression> predicate,
+ private IQueryable QueryIncludeNavigationProperties(Expression> predicate,
params Expression>[] includes) where TResource : class
{
IQueryable query = _dbContext.Set();
diff --git a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs
index 24943f4a..a6b9f14d 100644
--- a/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs
+++ b/JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs
@@ -30,9 +30,10 @@ public async Task Creates_single_resource_document_for_registered_non_collection
var objectContent = new Fruit { Id = "984", Name = "Kiwi" };
var mockDocument = new Mock(MockBehavior.Strict);
+ var includePathExpression = new string[] {};
var singleResourceDocumentBuilder = new Mock(MockBehavior.Strict);
- singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), null, null, null)).Returns(mockDocument.Object);
+ singleResourceDocumentBuilder.Setup(b => b.BuildDocument(objectContent, It.IsAny(), includePathExpression, null, null)).Returns(mockDocument.Object);
var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict);
var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict);
@@ -44,11 +45,14 @@ public async Task Creates_single_resource_document_for_registered_non_collection
mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com");
var mockSortExpressionExtractor = new Mock(MockBehavior.Strict);
- mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new [] { "id "});
+ mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new[] { "id " });
+
+ var mockIncludeExpressionExtractor = new Mock(MockBehavior.Strict);
+ mockIncludeExpressionExtractor.Setup(e => e.ExtractIncludeExpressions(request)).Returns(includePathExpression);
// Act
var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object,
- mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object);
+ mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockIncludeExpressionExtractor.Object, mockBaseUrlService.Object);
var resultDocument = await fallbackDocumentBuilder.BuildDocument(objectContent, request, cancellationTokenSource.Token);
// Assert
@@ -75,12 +79,14 @@ public async Task Creates_resource_collection_document_for_queryables()
mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/");
var sortExpressions = new[] { "id" };
+
+ var includeExpressions = new string[] { };
var cancellationTokenSource = new CancellationTokenSource();
var mockQueryableDocumentBuilder = new Mock(MockBehavior.Strict);
mockQueryableDocumentBuilder
- .Setup(b => b.BuildDocument(items, request, sortExpressions, cancellationTokenSource.Token, null))
+ .Setup(b => b.BuildDocument(items, request, sortExpressions, cancellationTokenSource.Token, includeExpressions))
.Returns(Task.FromResult(mockDocument.Object));
var mockResourceCollectionDocumentBuilder = new Mock(MockBehavior.Strict);
@@ -88,9 +94,12 @@ public async Task Creates_resource_collection_document_for_queryables()
var mockSortExpressionExtractor = new Mock(MockBehavior.Strict);
mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(sortExpressions);
+ var mockIncludeExpressionExtractor = new Mock(MockBehavior.Strict);
+ mockIncludeExpressionExtractor.Setup(e => e.ExtractIncludeExpressions(request)).Returns(includeExpressions);
+
// Act
var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object,
- mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object);
+ mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockIncludeExpressionExtractor.Object, mockBaseUrlService.Object);
var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token);
// Assert
@@ -127,9 +136,12 @@ public async Task Creates_resource_collection_document_for_non_queryable_enumera
var mockSortExpressionExtractor = new Mock(MockBehavior.Strict);
mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new[] { "id " });
+ var mockIncludeExpressionExtractor = new Mock(MockBehavior.Strict);
+ mockIncludeExpressionExtractor.Setup(e => e.ExtractIncludeExpressions(request)).Returns(new string[] { });
+
// Act
var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object,
- mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object);
+ mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockIncludeExpressionExtractor.Object, mockBaseUrlService.Object);
var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token);
// Assert
diff --git a/JSONAPI.Tests/Http/DefaultIncludeExpressionExtractorTests.cs b/JSONAPI.Tests/Http/DefaultIncludeExpressionExtractorTests.cs
new file mode 100644
index 00000000..0fb65380
--- /dev/null
+++ b/JSONAPI.Tests/Http/DefaultIncludeExpressionExtractorTests.cs
@@ -0,0 +1,57 @@
+using System.Net.Http;
+using FluentAssertions;
+using JSONAPI.Http;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace JSONAPI.Tests.Http
+{
+ [TestClass]
+ public class DefaultIncludeExpressionExtractorTests
+ {
+ [TestMethod]
+ public void ExtractsSingleIncludeExpressionFromUri()
+ {
+ // Arrange
+ const string uri = "http://api.example.com/dummies?include=boss";
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+
+ // Act
+ var extractor = new DefaultIncludeExpressionExtractor();
+ var inclExpressions = extractor.ExtractIncludeExpressions(request);
+
+ // Assert
+ inclExpressions.Should().BeEquivalentTo("boss");
+ }
+
+
+ [TestMethod]
+ public void ExtractsMultipleIncludeExpressionsFromUri()
+ {
+ // Arrange
+ const string uri = "http://api.example.com/dummies?include=boss,office-address";
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+
+ // Act
+ var extractor = new DefaultIncludeExpressionExtractor();
+ var inclExpressions = extractor.ExtractIncludeExpressions(request);
+
+ // Assert
+ inclExpressions.Should().BeEquivalentTo("boss", "office-address");
+ }
+
+ [TestMethod]
+ public void ExtractsNothingWhenThereIsNoIncludeParam()
+ {
+ // Arrange
+ const string uri = "http://api.example.com/dummies";
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+
+ // Act
+ var extractor = new DefaultIncludeExpressionExtractor();
+ var inclExpression = extractor.ExtractIncludeExpressions(request);
+
+ // Assert
+ inclExpression.Length.Should().Be(0);
+ }
+ }
+}
diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj
index 4a86feae..7f487003 100644
--- a/JSONAPI.Tests/JSONAPI.Tests.csproj
+++ b/JSONAPI.Tests/JSONAPI.Tests.csproj
@@ -91,6 +91,7 @@
+
diff --git a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs
index f6882571..8208f274 100644
--- a/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs
+++ b/JSONAPI/Documents/Builders/FallbackDocumentBuilder.cs
@@ -18,6 +18,7 @@ public class FallbackDocumentBuilder : IFallbackDocumentBuilder
private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder;
private readonly IResourceCollectionDocumentBuilder _resourceCollectionDocumentBuilder;
private readonly ISortExpressionExtractor _sortExpressionExtractor;
+ private readonly IIncludeExpressionExtractor _includeExpressionExtractor;
private readonly IBaseUrlService _baseUrlService;
private readonly Lazy _openBuildDocumentFromQueryableMethod;
private readonly Lazy _openBuildDocumentFromEnumerableMethod;
@@ -29,12 +30,14 @@ public FallbackDocumentBuilder(ISingleResourceDocumentBuilder singleResourceDocu
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
IResourceCollectionDocumentBuilder resourceCollectionDocumentBuilder,
ISortExpressionExtractor sortExpressionExtractor,
+ IIncludeExpressionExtractor includeExpressionExtractor,
IBaseUrlService baseUrlService)
{
_singleResourceDocumentBuilder = singleResourceDocumentBuilder;
_queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder;
_resourceCollectionDocumentBuilder = resourceCollectionDocumentBuilder;
_sortExpressionExtractor = sortExpressionExtractor;
+ _includeExpressionExtractor = includeExpressionExtractor;
_baseUrlService = baseUrlService;
_openBuildDocumentFromQueryableMethod =
@@ -53,6 +56,9 @@ public async Task BuildDocument(object obj, HttpRequestMessage
{
var type = obj.GetType();
+ // TODO: test includes
+ var includeExpressions = _includeExpressionExtractor.ExtractIncludeExpressions(requestMessage);
+
var queryableInterfaces = type.GetInterfaces();
var queryableInterface =
queryableInterfaces.FirstOrDefault(
@@ -66,7 +72,7 @@ public async Task BuildDocument(object obj, HttpRequestMessage
var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(requestMessage);
dynamic materializedQueryTask = buildDocumentMethod.Invoke(_queryableResourceCollectionDocumentBuilder,
- new[] { obj, requestMessage, sortExpressions, cancellationToken, null });
+ new[] { obj, requestMessage, sortExpressions, cancellationToken, includeExpressions });
return await materializedQueryTask;
}
@@ -89,7 +95,7 @@ public async Task BuildDocument(object obj, HttpRequestMessage
}
// Single resource object
- return _singleResourceDocumentBuilder.BuildDocument(obj, linkBaseUrl, null, null);
+ return _singleResourceDocumentBuilder.BuildDocument(obj, linkBaseUrl, includeExpressions, null);
}
private static Type GetEnumerableElementType(Type collectionType)
diff --git a/JSONAPI/Http/DefaultIncludeExpressionExtractor.cs b/JSONAPI/Http/DefaultIncludeExpressionExtractor.cs
new file mode 100644
index 00000000..cb040b19
--- /dev/null
+++ b/JSONAPI/Http/DefaultIncludeExpressionExtractor.cs
@@ -0,0 +1,21 @@
+using System.Linq;
+using System.Net.Http;
+
+namespace JSONAPI.Http
+{
+ ///
+ /// Default implementation of
+ ///
+ public class DefaultIncludeExpressionExtractor: IIncludeExpressionExtractor
+ {
+ private const string IncludeQueryParamKey = "include";
+
+ public string[] ExtractIncludeExpressions(HttpRequestMessage requestMessage)
+ {
+ var queryParams = requestMessage.GetQueryNameValuePairs();
+ var includeParam = queryParams.FirstOrDefault(kvp => kvp.Key == IncludeQueryParamKey);
+ if (includeParam.Key != IncludeQueryParamKey) return new string[] { };
+ return includeParam.Value.Split(',');
+ }
+ }
+}
diff --git a/JSONAPI/Http/IIncludeExpressionExtractor.cs b/JSONAPI/Http/IIncludeExpressionExtractor.cs
new file mode 100644
index 00000000..c8cf1358
--- /dev/null
+++ b/JSONAPI/Http/IIncludeExpressionExtractor.cs
@@ -0,0 +1,17 @@
+using System.Net.Http;
+
+namespace JSONAPI.Http
+{
+ ///
+ /// Service to extract include expressions from an HTTP request
+ ///
+ public interface IIncludeExpressionExtractor
+ {
+ ///
+ /// Extracts include expressions from the request
+ ///
+ ///
+ ///
+ string[] ExtractIncludeExpressions(HttpRequestMessage requestMessage);
+ }
+}
diff --git a/JSONAPI/Http/JsonApiController.cs b/JSONAPI/Http/JsonApiController.cs
index 09deb1b1..8bc9bf22 100644
--- a/JSONAPI/Http/JsonApiController.cs
+++ b/JSONAPI/Http/JsonApiController.cs
@@ -63,7 +63,7 @@ public virtual async Task Post(string resourceType, [FromBody
}
///
- /// Updates the record with the given ID with data from the request payloaad.
+ /// Updates the record with the given ID with data from the request payload.
///
public virtual async Task Patch(string resourceType, string id, [FromBody]ISingleResourceDocument requestDocument, CancellationToken cancellationToken)
{
diff --git a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs
index 7d190960..6aa2286f 100644
--- a/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs
+++ b/JSONAPI/Http/QueryableToManyRelatedResourceDocumentMaterializer.cs
@@ -15,27 +15,36 @@ public abstract class QueryableToManyRelatedResourceDocumentMaterializer
+ /// List of includes given by url.
+ ///
+ protected string[] Includes = {};
///
/// Creates a new QueryableRelatedResourceDocumentMaterializer
///
protected QueryableToManyRelatedResourceDocumentMaterializer(
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
- ISortExpressionExtractor sortExpressionExtractor)
+ ISortExpressionExtractor sortExpressionExtractor,
+ IIncludeExpressionExtractor includeExpressionExtractor)
{
_queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder;
_sortExpressionExtractor = sortExpressionExtractor;
+ _includeExpressionExtractor = includeExpressionExtractor;
}
public async Task GetRelatedResourceDocument(string primaryResourceId, HttpRequestMessage request,
CancellationToken cancellationToken)
{
+ Includes = _includeExpressionExtractor.ExtractIncludeExpressions(request);
var query = await GetRelatedQuery(primaryResourceId, cancellationToken);
- var includes = GetIncludePaths();
var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request);
if (sortExpressions == null || sortExpressions.Length < 1)
sortExpressions = GetDefaultSortExpressions();
- return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken, includes); // TODO: allow implementors to specify metadata
+
+
+ return await _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken, Includes); // TODO: allow implementors to specify metadata
}
///
@@ -43,15 +52,6 @@ public async Task GetRelatedResourceDocument(string primaryRes
///
protected abstract Task> GetRelatedQuery(string primaryResourceId, CancellationToken cancellationToken);
- ///
- /// Gets a list of relationship paths to include
- ///
- ///
- protected virtual string[] GetIncludePaths()
- {
- return null;
- }
-
///
/// If the client doesn't request any sort expressions, these expressions will be used for sorting instead.
///
diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj
index d6ed5c23..4336261f 100644
--- a/JSONAPI/JSONAPI.csproj
+++ b/JSONAPI/JSONAPI.csproj
@@ -90,10 +90,12 @@
+
+