From d1777cf87bf8b4c28e19b3d92b8e54a9d5fd5e56 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 13 Aug 2018 20:32:16 -0700 Subject: [PATCH 01/17] add acceptance test for deeply nested inclusions --- .../Spec/DeeplyNestedInclusionTests.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs new file mode 100644 index 0000000000..7af5c3b594 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Bogus; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.AspNetCore.Hosting; +using Newtonsoft.Json; +using Xunit; +using Person = JsonApiDotNetCoreExample.Models.Person; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +{ + [Collection("WebHostCollection")] + public class DeeplyNestedInclusionTests + { + private TestFixture _fixture; + + public DeeplyNestedInclusionTests(TestFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task Can_Include_Nested_Relationships() + { + // arrange + const string route = "/api/v1/todo-items?include=collection.owner"; + + var todoItem = new TodoItem { + Collection = new TodoItemCollection { + Owner = new Person() + } + }; + + var context = _fixture.GetService(); + context.TodoItems.RemoveRange(context.TodoItems); + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var todoItems = _fixture.DeSerializer.DeserializeList(body); + + var responseTodoItem = Assert.Single(todoItems); + Assert.NotNull(responseTodoItem); + Assert.NotNull(responseTodoItem.Collection); + Assert.NotNull(responseTodoItem.Collection.Owner); + } + } +} \ No newline at end of file From 3528e1f4ca9c86c563eebcb1daa363e68c541a1e Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 13 Aug 2018 22:08:09 -0700 Subject: [PATCH 02/17] first pass at deeply nested inclusion implementation --- .../Builders/DocumentBuilder.cs | 45 +++++++++++-------- .../Data/DefaultEntityRepository.cs | 36 +++++++++++---- .../Models/RelationshipAttribute.cs | 5 +++ .../Serialization/JsonApiDeSerializer.cs | 9 ++++ src/JsonApiDotNetCore/Services/QueryParser.cs | 4 -- 5 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index ce060f57b7..420fa7c36a 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -192,20 +192,35 @@ private RelationshipData GetRelationshipData(RelationshipAttribute attr, Context return relationshipData; } - private List GetIncludedEntities(List included, ContextEntity contextEntity, IIdentifiable entity) + private List GetIncludedEntities(List included, ContextEntity rootContextEntity, IIdentifiable rootResource) { - contextEntity.Relationships.ForEach(r => + if(_jsonApiContext.IncludedRelationships != null) { - if (!RelationshipIsIncluded(r.PublicRelationshipName)) return; - - var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName); - - if (navigationEntity is IEnumerable hasManyNavigationEntity) - foreach (IIdentifiable includedEntity in hasManyNavigationEntity) - included = AddIncludedEntity(included, includedEntity); - else - included = AddIncludedEntity(included, (IIdentifiable)navigationEntity); - }); + foreach(var relationshipName in _jsonApiContext.IncludedRelationships) + { + var relationshipChain = relationshipName.Split('.'); + + var contextEntity = rootContextEntity; + var entity = rootResource; + + for(var i = 0; i < relationshipChain.Length; i++) + { + var requestedRelationship = relationshipChain[i]; + var relationship = contextEntity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship); + var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, relationship.InternalRelationshipName); + if (navigationEntity is IEnumerable hasManyNavigationEntity) + foreach (IIdentifiable includedEntity in hasManyNavigationEntity) + included = AddIncludedEntity(included, includedEntity); + else + included = AddIncludedEntity(included, (IIdentifiable)navigationEntity); + + if(i < relationshipChain.Length) { + contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); + entity = (IIdentifiable)navigationEntity; // HACK: only handles HasOne case ... + } + } + } + } return included; } @@ -245,12 +260,6 @@ private DocumentData GetIncludedEntity(IIdentifiable entity) return data; } - private bool RelationshipIsIncluded(string relationshipName) - { - return _jsonApiContext.IncludedRelationships != null && - _jsonApiContext.IncludedRelationships.Contains(relationshipName); - } - private List GetRelationships(IEnumerable entities) { var objType = entities.GetElementType(); diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 862079aeee..512f45fe3c 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -203,20 +203,38 @@ public virtual async Task DeleteAsync(TId id) /// public virtual IQueryable Include(IQueryable entities, string relationshipName) { + if(string.IsNullOrWhiteSpace(relationshipName)) throw new JsonApiException(400, "Include parameter must not be empty if provided"); + + var relationshipChain = relationshipName.Split('.'); + + // variables mutated in recursive loop + // TODO: make recursive method + string internalRelationshipPath = null; var entity = _jsonApiContext.RequestEntity; - var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == relationshipName); - if (relationship == null) + for(var i = 0; i < relationshipChain.Length; i++) { - throw new JsonApiException(400, $"Invalid relationship {relationshipName} on {entity.EntityName}", - $"{entity.EntityName} does not have a relationship named {relationshipName}"); - } + var requestedRelationship = relationshipChain[i]; + var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship); + if (relationship == null) + { + throw new JsonApiException(400, $"Invalid relationship {requestedRelationship} on {entity.EntityName}", + $"{entity.EntityName} does not have a relationship named {requestedRelationship}"); + } - if (!relationship.CanInclude) - { - throw new JsonApiException(400, $"Including the relationship {relationshipName} on {entity.EntityName} is not allowed"); + if (relationship.CanInclude == false) + { + throw new JsonApiException(400, $"Including the relationship {requestedRelationship} on {entity.EntityName} is not allowed"); + } + + internalRelationshipPath = (internalRelationshipPath == null) + ? relationship.InternalRelationshipName + : $"{internalRelationshipPath}.{relationship.InternalRelationshipName}"; + + if(i < relationshipChain.Length) + entity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); } - return entities.Include(relationship.InternalRelationshipName); + return entities.Include(internalRelationshipPath); } /// diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index f6ca5b5d2a..7408df0998 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -54,6 +54,11 @@ public bool TryGetHasMany(out HasManyAttribute result) public abstract void SetValue(object entity, object newValue); + public object GetValue(object entity) => entity + ?.GetType() + .GetProperty(InternalRelationshipName) + .GetValue(entity); + public override string ToString() { return base.ToString() + ":" + PublicRelationshipName; diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 29569f43e9..f71e028626 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -214,6 +214,15 @@ private object SetHasOneRelationship(object entity, SetHasOneForeignKeyValue(entity, attr, foreignKeyProperty, rio); SetHasOneNavigationPropertyValue(entity, attr, rio, included); + // recursive call ... + var navigationPropertyValue = attr.GetValue(entity); + var contextGraphEntity = _jsonApiContext.ContextGraph.GetContextEntity(attr.Type); + if(navigationPropertyValue != null && contextGraphEntity != null) + { + var includedResource = included.Single(r => r.Type == rio.Type && r.Id == rio.Id); + SetRelationships(navigationPropertyValue, contextGraphEntity, includedResource.Relationships, included); + } + return entity; } diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index 0a6d4f2c16..b42d616a0c 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -179,10 +179,6 @@ protected virtual List ParseSortParameters(string value) protected virtual List ParseIncludedRelationships(string value) { - const string NESTED_DELIMITER = "."; - if (value.Contains(NESTED_DELIMITER)) - throw new JsonApiException(400, "Deeply nested relationships are not supported"); - return value .Split(QueryConstants.COMMA) .ToList(); From 1b80f05ce23f939c28626748d821fdf2c6e76e7e Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 14 Aug 2018 20:53:45 -0700 Subject: [PATCH 03/17] fix(JsonApiDeserializer): NullReferenceException don't do unnecessary work if no relationships were included --- .../Serialization/JsonApiDeSerializer.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index f71e028626..6dc5fe0b69 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -215,12 +215,15 @@ private object SetHasOneRelationship(object entity, SetHasOneNavigationPropertyValue(entity, attr, rio, included); // recursive call ... - var navigationPropertyValue = attr.GetValue(entity); - var contextGraphEntity = _jsonApiContext.ContextGraph.GetContextEntity(attr.Type); - if(navigationPropertyValue != null && contextGraphEntity != null) + if(included != null) { - var includedResource = included.Single(r => r.Type == rio.Type && r.Id == rio.Id); - SetRelationships(navigationPropertyValue, contextGraphEntity, includedResource.Relationships, included); + var navigationPropertyValue = attr.GetValue(entity); + var contextGraphEntity = _jsonApiContext.ContextGraph.GetContextEntity(attr.Type); + if(navigationPropertyValue != null && contextGraphEntity != null) + { + var includedResource = included.Single(r => r.Type == rio.Type && r.Id == rio.Id); + SetRelationships(navigationPropertyValue, contextGraphEntity, includedResource.Relationships, included); + } } return entity; From d930c5b4eadcc341f12702856a27367bd4976f62 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 14 Aug 2018 21:21:04 -0700 Subject: [PATCH 04/17] fix(DocumentBuilder): handle HasMany relationships --- .../Builders/DocumentBuilder.cs | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 420fa7c36a..15e46ffd6f 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -202,29 +202,52 @@ private List GetIncludedEntities(List included, Cont var contextEntity = rootContextEntity; var entity = rootResource; - - for(var i = 0; i < relationshipChain.Length; i++) - { - var requestedRelationship = relationshipChain[i]; - var relationship = contextEntity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship); - var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, relationship.InternalRelationshipName); - if (navigationEntity is IEnumerable hasManyNavigationEntity) - foreach (IIdentifiable includedEntity in hasManyNavigationEntity) - included = AddIncludedEntity(included, includedEntity); - else - included = AddIncludedEntity(included, (IIdentifiable)navigationEntity); - - if(i < relationshipChain.Length) { - contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); - entity = (IIdentifiable)navigationEntity; // HACK: only handles HasOne case ... - } - } + included = IncludeRelationshipChain(included, rootContextEntity, rootResource, relationshipChain, 0); } } return included; } + private List IncludeRelationshipChain( + List included, ContextEntity parentEntity, IIdentifiable parentResource, string[] relationshipChain, int relationshipChainIndex) + { + var requestedRelationship = relationshipChain[relationshipChainIndex]; + var relationship = parentEntity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship); + var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(parentResource, relationship.InternalRelationshipName); + if (navigationEntity is IEnumerable hasManyNavigationEntity) + { + foreach (IIdentifiable includedEntity in hasManyNavigationEntity) + { + included = AddIncludedEntity(included, includedEntity); + included = IncludeSingleResourceRelationships(included, includedEntity, relationship, relationshipChain, relationshipChainIndex); + } + } + else + { + included = AddIncludedEntity(included, (IIdentifiable)navigationEntity); + included = IncludeSingleResourceRelationships(included, (IIdentifiable)navigationEntity, relationship, relationshipChain, relationshipChainIndex); + } + + return included; + } + + private List IncludeSingleResourceRelationships( + List included, IIdentifiable navigationEntity, RelationshipAttribute relationship, string[] relationshipChain, int relationshipChainIndex) + { + if(relationshipChainIndex < relationshipChain.Length) + { + var nextContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); + var resource = (IIdentifiable)navigationEntity; + // recursive call + if(relationshipChainIndex < relationshipChain.Length - 1) + included = IncludeRelationshipChain(included, nextContextEntity, resource, relationshipChain, relationshipChainIndex + 1); + } + + return included; + } + + private List AddIncludedEntity(List entities, IIdentifiable entity) { var includedEntity = GetIncludedEntity(entity); From 88474039731510f2310fd015c1f077f6fd0038ae Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 14 Aug 2018 21:35:04 -0700 Subject: [PATCH 05/17] fix(JsonApiDeSerializer): allow empty collection Sequence contains no matching element --- src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 6dc5fe0b69..bd2a25b0ad 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -221,8 +221,9 @@ private object SetHasOneRelationship(object entity, var contextGraphEntity = _jsonApiContext.ContextGraph.GetContextEntity(attr.Type); if(navigationPropertyValue != null && contextGraphEntity != null) { - var includedResource = included.Single(r => r.Type == rio.Type && r.Id == rio.Id); - SetRelationships(navigationPropertyValue, contextGraphEntity, includedResource.Relationships, included); + var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id); + if(includedResource != null) + SetRelationships(navigationPropertyValue, contextGraphEntity, includedResource.Relationships, included); } } From bea642d45183268873571f70cd7eef3758fba6ca Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 11:40:45 -0400 Subject: [PATCH 06/17] work on adding tests for different scenarios --- .../Spec/DeeplyNestedInclusionTests.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 7af5c3b594..63fa767328 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -53,5 +54,85 @@ public async Task Can_Include_Nested_Relationships() Assert.NotNull(responseTodoItem.Collection); Assert.NotNull(responseTodoItem.Collection.Owner); } + + [Fact] + public async Task Can_Include_Nested_HasMany_Relationships() + { + // arrange + const string route = "/api/v1/todo-items?include=collection.todo-items"; + + var todoItem = new TodoItem { + Collection = new TodoItemCollection { + Owner = new Person(), + TodoItems = new List { + new TodoItem(), + new TodoItem() + } + } + }; + + var context = _fixture.GetService(); + context.TodoItems.RemoveRange(context.TodoItems); + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var todoItems = _fixture.DeSerializer.DeserializeList(body); + + var responseTodoItem = todoItems[0]; + Assert.NotNull(responseTodoItem); + Assert.NotNull(responseTodoItem.Collection); + Assert.NotNull(responseTodoItem.Collection.TodoItems); + Assert.Equal(2, responseTodoItem.Collection.TodoItems.Count); + + // TODO: assert number of things in included + } + + [Fact] + public async Task Can_Include_Nested_HasMany_Relationships_BelongsTo() + { + // arrange + const string route = "/api/v1/todo-items?include=collection.todo-items.owner"; + + var todoItem = new TodoItem { + Collection = new TodoItemCollection { + Owner = new Person(), + TodoItems = new List { + new TodoItem { + Owner = new Person() + }, + new TodoItem() + } + } + }; + + var context = _fixture.GetService(); + context.TodoItems.RemoveRange(context.TodoItems); + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var todoItems = _fixture.DeSerializer.DeserializeList(body); + + var responseTodoItem = todoItems[0]; + Assert.NotNull(responseTodoItem); + Assert.NotNull(responseTodoItem.Collection); + Assert.NotNull(responseTodoItem.Collection.TodoItems); + Assert.Equal(2, responseTodoItem.Collection.TodoItems.Count); + + // TODO: assert number of things in included + } } } \ No newline at end of file From 4af95d117c8db22175c3d1e803743b2f4a485ed2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 12:41:31 -0400 Subject: [PATCH 07/17] assert included counts --- .../Spec/DeeplyNestedInclusionTests.cs | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 63fa767328..d89bd6739b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -3,6 +3,7 @@ using System.Net.Http; using System.Threading.Tasks; using Bogus; +using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -83,15 +84,10 @@ public async Task Can_Include_Nested_HasMany_Relationships() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var todoItems = _fixture.DeSerializer.DeserializeList(body); - - var responseTodoItem = todoItems[0]; - Assert.NotNull(responseTodoItem); - Assert.NotNull(responseTodoItem.Collection); - Assert.NotNull(responseTodoItem.Collection.TodoItems); - Assert.Equal(2, responseTodoItem.Collection.TodoItems.Count); - - // TODO: assert number of things in included + var documents = JsonConvert.DeserializeObject(body); + var included = documents.Included; + + Assert.Equal(3, included.Count); // 1 collection, 2 todos } [Fact] @@ -124,15 +120,10 @@ public async Task Can_Include_Nested_HasMany_Relationships_BelongsTo() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var todoItems = _fixture.DeSerializer.DeserializeList(body); - - var responseTodoItem = todoItems[0]; - Assert.NotNull(responseTodoItem); - Assert.NotNull(responseTodoItem.Collection); - Assert.NotNull(responseTodoItem.Collection.TodoItems); - Assert.Equal(2, responseTodoItem.Collection.TodoItems.Count); - - // TODO: assert number of things in included + var documents = JsonConvert.DeserializeObject(body); + var included = documents.Included; + + Assert.Equal(4, included.Count); // 1 collection, 2 todos, 1 owner } } } \ No newline at end of file From 36c5eb6ebb4167324dfae07865428dc0c41998c0 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 12:59:35 -0400 Subject: [PATCH 08/17] these tests pass -- should probably add more specific tests though --- .../Spec/DeeplyNestedInclusionTests.cs | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index d89bd6739b..9e663f45bc 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -87,7 +87,7 @@ public async Task Can_Include_Nested_HasMany_Relationships() var documents = JsonConvert.DeserializeObject(body); var included = documents.Included; - Assert.Equal(3, included.Count); // 1 collection, 2 todos + Assert.Equal(4, included.Count); // 1 collection, 3 todos } [Fact] @@ -123,7 +123,45 @@ public async Task Can_Include_Nested_HasMany_Relationships_BelongsTo() var documents = JsonConvert.DeserializeObject(body); var included = documents.Included; - Assert.Equal(4, included.Count); // 1 collection, 2 todos, 1 owner + Assert.Equal(5, included.Count); // 1 collection, 3 todos, 1 owner + } + + [Fact] + public async Task Can_Include_Nested_Relationships_With_Multiple_Paths() + { + // arrange + const string route = "/api/v1/todo-items?include=collection.owner.role,collection.todo-items.owner"; + + var todoItem = new TodoItem { + Collection = new TodoItemCollection { + Owner = new Person { + Role = new PersonRole() + }, + TodoItems = new List { + new TodoItem { + Owner = new Person() + }, + new TodoItem() + } + } + }; + + var context = _fixture.GetService(); + context.TodoItems.RemoveRange(context.TodoItems); + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject(body); + var included = documents.Included; + + Assert.Equal(7, included.Count); // 1 collection, 3 todos, 2 owners, 1 role } } } \ No newline at end of file From c7887b8d98ddc096d7a2519d446bacc51bc21625 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 13:41:19 -0400 Subject: [PATCH 09/17] try to assert some more complicated stuff, and check specific included records --- .../Spec/DeeplyNestedInclusionTests.cs | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 9e663f45bc..2b03ddc002 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -24,6 +24,14 @@ public DeeplyNestedInclusionTests(TestFixture fixture) _fixture = fixture; } + private void ResetContext(AppDbContext context) + { + context.TodoItems.RemoveRange(context.TodoItems); + context.TodoItemCollections.RemoveRange(context.TodoItemCollections); + context.People.RemoveRange(context.People); + context.PersonRoles.RemoveRange(context.PersonRoles); + } + [Fact] public async Task Can_Include_Nested_Relationships() { @@ -72,8 +80,10 @@ public async Task Can_Include_Nested_HasMany_Relationships() } }; + var context = _fixture.GetService(); - context.TodoItems.RemoveRange(context.TodoItems); + ResetContext(context); + context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -109,7 +119,8 @@ public async Task Can_Include_Nested_HasMany_Relationships_BelongsTo() }; var context = _fixture.GetService(); - context.TodoItems.RemoveRange(context.TodoItems); + ResetContext(context); + context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -147,7 +158,8 @@ public async Task Can_Include_Nested_Relationships_With_Multiple_Paths() }; var context = _fixture.GetService(); - context.TodoItems.RemoveRange(context.TodoItems); + ResetContext(context); + context.TodoItems.Add(todoItem); await context.SaveChangesAsync(); @@ -163,5 +175,59 @@ public async Task Can_Include_Nested_Relationships_With_Multiple_Paths() Assert.Equal(7, included.Count); // 1 collection, 3 todos, 2 owners, 1 role } + + [Fact] + public async Task Included_Resources_Are_Correct() + { + // arrange + var role = new PersonRole(); + var asignee = new Person { Role = role }; + var collectionOwner = new Person(); + var someOtherOwner = new Person(); + var collection = new TodoItemCollection { Owner = collectionOwner }; + var todoItem1 = new TodoItem { Collection = collection, Assignee = asignee }; + var todoItem2 = new TodoItem { Collection = collection, Assignee = asignee }; + var todoItem3 = new TodoItem { Collection = collection, Owner = someOtherOwner }; + var todoItem4 = new TodoItem { Collection = collection, Owner = asignee }; + + + string route = + "/api/v1/todo-items/" + todoItem1.Id + "?include=" + + "collection.owner," + + "asignee.role," + + "asignee.assigned-todo-items"; + + + var context = _fixture.GetService(); + ResetContext(context); + + context.TodoItems.Add(todoItem1); + context.TodoItems.Add(todoItem2); + context.TodoItems.Add(todoItem3); + context.TodoItems.Add(todoItem4); + context.PersonRoles.Add(role); + context.People.Add(asignee); + context.People.Add(collectionOwner); + context.People.Add(someOtherOwner); + context.TodoItemCollections.Add(collection); + + + await context.SaveChangesAsync(); + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject(body); + var included = documents.Included; + + // 1 collection, 1 owner, + // 1 asignee, 1 asignee role, + // 2 assigned todo items + Assert.Equal(6, included.Count); + } } } \ No newline at end of file From b02b3ecdd7f4713a8ff34b7a8c21ffb7ffdde87f Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 14:41:59 -0400 Subject: [PATCH 10/17] I think I found a bug in the current implementation? --- .../Spec/DeeplyNestedInclusionTests.cs | 55 ++++++++++++++----- .../Helpers/Extensions/DocumentExtensions.cs | 26 +++++++++ 2 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 2b03ddc002..c2540f91c0 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -7,6 +7,7 @@ using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCoreExampleTests.Helpers.Extensions; using Microsoft.AspNetCore.Hosting; using Newtonsoft.Json; using Xunit; @@ -181,22 +182,14 @@ public async Task Included_Resources_Are_Correct() { // arrange var role = new PersonRole(); - var asignee = new Person { Role = role }; + var assignee = new Person { Role = role }; var collectionOwner = new Person(); var someOtherOwner = new Person(); var collection = new TodoItemCollection { Owner = collectionOwner }; - var todoItem1 = new TodoItem { Collection = collection, Assignee = asignee }; - var todoItem2 = new TodoItem { Collection = collection, Assignee = asignee }; + var todoItem1 = new TodoItem { Collection = collection, Assignee = assignee }; + var todoItem2 = new TodoItem { Collection = collection, Assignee = assignee }; var todoItem3 = new TodoItem { Collection = collection, Owner = someOtherOwner }; - var todoItem4 = new TodoItem { Collection = collection, Owner = asignee }; - - - string route = - "/api/v1/todo-items/" + todoItem1.Id + "?include=" + - "collection.owner," + - "asignee.role," + - "asignee.assigned-todo-items"; - + var todoItem4 = new TodoItem { Collection = collection, Owner = assignee }; var context = _fixture.GetService(); ResetContext(context); @@ -206,7 +199,7 @@ public async Task Included_Resources_Are_Correct() context.TodoItems.Add(todoItem3); context.TodoItems.Add(todoItem4); context.PersonRoles.Add(role); - context.People.Add(asignee); + context.People.Add(assignee); context.People.Add(collectionOwner); context.People.Add(someOtherOwner); context.TodoItemCollections.Add(collection); @@ -214,6 +207,12 @@ public async Task Included_Resources_Are_Correct() await context.SaveChangesAsync(); + string route = + "/api/v1/todo-items/" + todoItem1.Id + "?include=" + + "collection.owner," + + "assignee.role," + + "assignee.assigned-todo-items"; + // act var response = await _fixture.Client.GetAsync(route); @@ -225,9 +224,35 @@ public async Task Included_Resources_Are_Correct() var included = documents.Included; // 1 collection, 1 owner, - // 1 asignee, 1 asignee role, - // 2 assigned todo items + // 1 assignee, 1 assignee role, + // 2 assigned todo items (including the primary resource) Assert.Equal(6, included.Count); + + + var collectionDocument = included.FindResource("todo-item-collections", collection.Id); + var ownerDocument = included.FindResource("people", collectionOwner.Id); + var assigneeDocument = included.FindResource("people", assignee.Id); + var roleDocument = included.FindResource("person-roles", role.Id); + var assignedTodo1 = included.FindResource("todo-items", todoItem1.Id); + var assignedTodo2 = included.FindResource("todo-items", todoItem2.Id); + + Assert.NotNull(assignedTodo1); + Assert.Equal(todoItem1.Id.ToString(), assignedTodo1.Id); + + Assert.NotNull(assignedTodo2); + Assert.Equal(todoItem2.Id.ToString(), assignedTodo2.Id); + + Assert.NotNull(collectionDocument); + Assert.Equal(collection.Id.ToString(), collectionDocument.Id); + + Assert.NotNull(ownerDocument); + Assert.Equal(collectionOwner.Id.ToString(), ownerDocument.Id); + + Assert.NotNull(assigneeDocument); + Assert.Equal(assignee.Id.ToString(), assigneeDocument.Id); + + Assert.NotNull(roleDocument); + Assert.Equal(role.Id.ToString(), roleDocument.Id); } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs new file mode 100644 index 0000000000..4d7130def6 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.Models; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using Database = Microsoft.EntityFrameworkCore.Storage.Database; + +namespace JsonApiDotNetCoreExampleTests.Helpers.Extensions +{ + public static class DocumentExtensions + { + public static DocumentData FindResource(this List included, string type, TId id) + { + var document = included.Where(documentData => ( + documentData.Type == type + && documentData.Id == id.ToString() + )).FirstOrDefault(); + + return document; + } + } +} \ No newline at end of file From 41ba2ed3d2b513a890079d29315eec542d6d5bbd Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 16:51:43 -0400 Subject: [PATCH 11/17] omg. im dumb --- .../Acceptance/Spec/DeeplyNestedInclusionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index c2540f91c0..7689376b49 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -229,7 +229,7 @@ public async Task Included_Resources_Are_Correct() Assert.Equal(6, included.Count); - var collectionDocument = included.FindResource("todo-item-collections", collection.Id); + var collectionDocument = included.FindResource("todo-collections", collection.Id); var ownerDocument = included.FindResource("people", collectionOwner.Id); var assigneeDocument = included.FindResource("people", assignee.Id); var roleDocument = included.FindResource("person-roles", role.Id); From bda10b854994aaac551982c456057b87f81460ad Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 17:09:18 -0400 Subject: [PATCH 12/17] hasMany.hasMany --- .../Spec/DeeplyNestedInclusionTests.cs | 48 ++++++++++++++++++- .../Helpers/Extensions/DocumentExtensions.cs | 4 ++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 7689376b49..f5e35292ba 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -228,7 +228,6 @@ public async Task Included_Resources_Are_Correct() // 2 assigned todo items (including the primary resource) Assert.Equal(6, included.Count); - var collectionDocument = included.FindResource("todo-collections", collection.Id); var ownerDocument = included.FindResource("people", collectionOwner.Id); var assigneeDocument = included.FindResource("people", assignee.Id); @@ -254,5 +253,52 @@ public async Task Included_Resources_Are_Correct() Assert.NotNull(roleDocument); Assert.Equal(role.Id.ToString(), roleDocument.Id); } + + [Fact] + public async Task Can_Include_Doubly_HasMany_Relationships() + { + // arrange + var person = new Person { + TodoItemCollections = new List { + new TodoItemCollection { + TodoItems = new List { + new TodoItem(), + new TodoItem() + } + }, + new TodoItemCollection { + TodoItems = new List { + new TodoItem(), + new TodoItem(), + new TodoItem() + } + } + } + }; + + var context = _fixture.GetService(); + ResetContext(context); + + context.People.Add(person); + + await context.SaveChangesAsync(); + + string route = "/api/v1/people/" + person.Id + "?include=todo-collections.todo-items"; + + // act + var response = await _fixture.Client.GetAsync(route); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var documents = JsonConvert.DeserializeObject(body); + var included = documents.Included; + + Assert.Equal(7, included.Count); + + Assert.Equal(5, included.CountOfType("todo-items")); + Assert.Equal(2, included.CountOfType("todo-collections")); + } } } \ No newline at end of file diff --git a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs index 4d7130def6..f467e17f5b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs +++ b/test/JsonApiDotNetCoreExampleTests/Helpers/Extensions/DocumentExtensions.cs @@ -22,5 +22,9 @@ public static DocumentData FindResource(this List included, s return document; } + + public static int CountOfType(this List included, string type) { + return included.Where(documentData => documentData.Type == type).Count(); + } } } \ No newline at end of file From daa61abaad7f4e3168a0b2bd503650b330d632eb Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 17:19:37 -0400 Subject: [PATCH 13/17] some more assertions --- .../Spec/DeeplyNestedInclusionTests.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index f5e35292ba..5e4754c7c5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -98,7 +98,10 @@ public async Task Can_Include_Nested_HasMany_Relationships() var documents = JsonConvert.DeserializeObject(body); var included = documents.Included; - Assert.Equal(4, included.Count); // 1 collection, 3 todos + Assert.Equal(4, included.Count); + + Assert.Equal(3, included.CountOfType("todo-items")); + Assert.Equal(1, included.CountOfType("todo-collections")); } [Fact] @@ -135,7 +138,11 @@ public async Task Can_Include_Nested_HasMany_Relationships_BelongsTo() var documents = JsonConvert.DeserializeObject(body); var included = documents.Included; - Assert.Equal(5, included.Count); // 1 collection, 3 todos, 1 owner + Assert.Equal(5, included.Count); + + Assert.Equal(3, included.CountOfType("todo-items")); + Assert.Equal(1, included.CountOfType("people")); + Assert.Equal(1, included.CountOfType("todo-collections")); } [Fact] @@ -174,7 +181,12 @@ public async Task Can_Include_Nested_Relationships_With_Multiple_Paths() var documents = JsonConvert.DeserializeObject(body); var included = documents.Included; - Assert.Equal(7, included.Count); // 1 collection, 3 todos, 2 owners, 1 role + Assert.Equal(7, included.Count); + + Assert.Equal(3, included.CountOfType("todo-items")); + Assert.Equal(2, included.CountOfType("people")); + Assert.Equal(1, included.CountOfType("person-roles")); + Assert.Equal(1, included.CountOfType("todo-collections")); } [Fact] From 1a367fa0f71a090519e5fe714d116e1fff187168 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 18:23:40 -0400 Subject: [PATCH 14/17] this is a big one --- .../Serialization/JsonApiDeSerializerTests.cs | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index 18d4b0ee6b..501576a1b2 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -532,17 +532,176 @@ public void Sets_Attribute_Values_On_Included_HasOne_Relationships() Assert.Equal(expectedName, result.Independent.Name); } + + [Fact] + public void Can_Deserialize_Nested_Included_HasMany_Relationships() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("independents"); + contextGraphBuilder.AddResource("dependents"); + contextGraphBuilder.AddResource("many-to-manys"); + + var deserializer = GetDeserializer(contextGraphBuilder); + + var contentString = + @"{ + ""data"": { + ""type"": ""independents"", + ""id"": ""1"", + ""attributes"": { }, + ""relationships"": { + ""many-to-manys"": { + ""data"": [{ + ""type"": ""many-to-manys"", + ""id"": ""2"" + }, { + ""type"": ""many-to-manys"", + ""id"": ""3"" + }] + } + } + }, + ""included"": [ + { + ""type"": ""many-to-manys"", + ""id"": ""2"", + ""attributes"": {}, + ""relationships"": { + ""dependent"": { + ""data"": { + ""type"": ""dependents"", + ""id"": ""4"" + } + }, + ""independent"": { + ""data"": { + ""type"": ""independents"", + ""id"": ""5"" + } + } + } + }, + { + ""type"": ""many-to-manys"", + ""id"": ""3"", + ""attributes"": {}, + ""relationships"": { + ""dependent"": { + ""data"": { + ""type"": ""dependents"", + ""id"": ""4"" + } + }, + ""independent"": { + ""data"": { + ""type"": ""independents"", + ""id"": ""6"" + } + } + } + }, + { + ""type"": ""dependents"", + ""id"": ""4"", + ""attributes"": {}, + ""relationships"": { + ""many-to-manys"": { + ""data"": [{ + ""type"": ""many-to-manys"", + ""id"": ""2"" + }, { + ""type"": ""many-to-manys"", + ""id"": ""3"" + }] + } + } + } + , + { + ""type"": ""independents"", + ""id"": ""5"", + ""attributes"": {}, + ""relationships"": { + ""many-to-manys"": { + ""data"": [{ + ""type"": ""many-to-manys"", + ""id"": ""2"" + }] + } + } + } + , + { + ""type"": ""independents"", + ""id"": ""6"", + ""attributes"": {}, + ""relationships"": { + ""many-to-manys"": { + ""data"": [{ + ""type"": ""many-to-manys"", + ""id"": ""3"" + }] + } + } + } + ] + }"; + + // act + var result = deserializer.Deserialize(contentString); + + // assert + Assert.NotNull(result); + Assert.Equal(1, result.Id); + Assert.NotNull(result.ManyToManys); + Assert.Equal(2, result.ManyToManys.Count); + } + + private JsonApiDeSerializer GetDeserializer(ContextGraphBuilder contextGraphBuilder) + { + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); + jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); + + var jsonApiOptions = new JsonApiOptions(); + jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object); + + return deserializer; + } + + private class ManyToManyNested : Identifiable + { + [Attr("name")] public string Name { get; set; } + [HasOne("dependent")] public OneToManyDependent Dependents { get; set; } + public int DependentId { get; set; } + [HasOne("independent")] public OneToManyIndependent Independents { get; set; } + public int InependentId { get; set; } + } + private class OneToManyDependent : Identifiable { [Attr("name")] public string Name { get; set; } [HasOne("independent")] public OneToManyIndependent Independent { get; set; } public int IndependentId { get; set; } + + [HasMany("many-to-manys")] public List ManyToManys { get; set; } } private class OneToManyIndependent : Identifiable { [Attr("name")] public string Name { get; set; } [HasMany("dependents")] public List Dependents { get; set; } + + [HasMany("many-to-manys")] public List ManyToManys { get; set; } } } } From 82cfa719e2c3b5aef1b81c71a9c10f63ec1c0ba3 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Wed, 29 Aug 2018 18:36:01 -0400 Subject: [PATCH 15/17] more assertions... and then commented out, because I don't know what desired deserializer behavior is --- .../Serialization/JsonApiDeSerializerTests.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index 501576a1b2..bf003bcecf 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -649,13 +649,23 @@ public void Can_Deserialize_Nested_Included_HasMany_Relationships() }"; // act - var result = deserializer.Deserialize(contentString); + var result = deserializer.Deserialize(contentString); // assert Assert.NotNull(result); Assert.Equal(1, result.Id); Assert.NotNull(result.ManyToManys); Assert.Equal(2, result.ManyToManys.Count); + + // TODO: not sure if this should be a thing that works? + // could this cause cycles in the graph? + // Assert.NotNull(result.ManyToManys[0].Dependent); + // Assert.NotNull(result.ManyToManys[0].Independent); + // Assert.NotNull(result.ManyToManys[1].Dependent); + // Assert.NotNull(result.ManyToManys[1].Independent); + + // Assert.Equal(result.ManyToManys[0].Dependent, result.ManyToManys[1].Dependent); + // Assert.NotEqual(result.ManyToManys[0].Independent, result.ManyToManys[1].Independent); } private JsonApiDeSerializer GetDeserializer(ContextGraphBuilder contextGraphBuilder) @@ -681,9 +691,9 @@ private JsonApiDeSerializer GetDeserializer(ContextGraphBuilder contextGraphBuil private class ManyToManyNested : Identifiable { [Attr("name")] public string Name { get; set; } - [HasOne("dependent")] public OneToManyDependent Dependents { get; set; } + [HasOne("dependent")] public OneToManyDependent Dependent { get; set; } public int DependentId { get; set; } - [HasOne("independent")] public OneToManyIndependent Independents { get; set; } + [HasOne("independent")] public OneToManyIndependent Independent { get; set; } public int InependentId { get; set; } } From a0b60805f498b82b848c6e49dc3aac5c8355a379 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Thu, 30 Aug 2018 14:37:42 -0400 Subject: [PATCH 16/17] this one is tricky --- .../Serialization/JsonApiSerializerTests.cs | 169 ++++++++++++++++-- 1 file changed, 155 insertions(+), 14 deletions(-) diff --git a/test/UnitTests/Serialization/JsonApiSerializerTests.cs b/test/UnitTests/Serialization/JsonApiSerializerTests.cs index 8a67b80434..005400d870 100644 --- a/test/UnitTests/Serialization/JsonApiSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiSerializerTests.cs @@ -1,7 +1,11 @@ -using JsonApiDotNetCore.Builders; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Request; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using Moq; @@ -17,19 +21,9 @@ public void Can_Serialize_Complex_Types() // arrange var contextGraphBuilder = new ContextGraphBuilder(); contextGraphBuilder.AddResource("test-resource"); - var contextGraph = contextGraphBuilder.Build(); - - var jsonApiContextMock = new Mock(); - jsonApiContextMock.SetupAllProperties(); - jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); - jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions()); - jsonApiContextMock.Setup(m => m.RequestEntity) - .Returns(contextGraph.GetContextEntity("test-resource")); - jsonApiContextMock.Setup(m => m.MetaBuilder).Returns(new MetaBuilder()); - jsonApiContextMock.Setup(m => m.PageManager).Returns(new PageManager()); + + var serializer = GetSerializer(contextGraphBuilder); - var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object); - var serializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); var resource = new TestResource { ComplexMember = new ComplexType @@ -43,18 +37,165 @@ public void Can_Serialize_Complex_Types() // assert Assert.NotNull(result); - Assert.Equal("{\"data\":{\"attributes\":{\"complex-member\":{\"compound-name\":\"testname\"}},\"type\":\"test-resource\",\"id\":\"\"}}", result); + + var expectedFormatted = + @"{ + ""data"": { + ""attributes"": { + ""complex-member"": { + ""compound-name"": ""testname"" + } + }, + ""relationships"": { + ""children"": { + ""links"": { + ""self"": ""/test-resource//relationships/children"", + ""related"": ""/test-resource//children"" + } + } + }, + ""type"": ""test-resource"", + ""id"": """" + } + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + + Assert.Equal(expected, result); + } + + [Fact] + public void Can_Serialize_Deeply_Nested_Relationships() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("test-resources"); + contextGraphBuilder.AddResource("children"); + contextGraphBuilder.AddResource("infections"); + + var serializer = GetSerializer( + contextGraphBuilder, + included: new List { "children.infections" } + ); + + var resource = new TestResource + { + Children = new List { + new ChildResource { + Infections = new List { + new InfectionResource(), + new InfectionResource(), + } + }, + new ChildResource() + } + }; + + // act + var result = serializer.Serialize(resource); + + // assert + Assert.NotNull(result); + + var expectedFormatted = + @"{ + ""data"": { + ""attributes"": { + ""complex-member"": { + ""compound-name"": ""testname"" + } + }, + ""relationships"": { + ""children"": { + ""links"": { + ""self"": ""/test-resource//relationships/children"", + ""related"": ""/test-resource//children"" + } + } + }, + ""type"": ""test-resource"", + ""id"": """" + }, + ""included"": { + { + ""attributes"": {}, + ""relationships"": {}, + ""type"": ""children"" + }, + { + ""attributes"": {}, + ""relationships"": {}, + ""type"": ""children"" + }, + { + ""attributes"": {}, + ""relationships"": {}, + ""type"": ""infections"" + }, + { + ""attributes"": {}, + ""relationships"": {}, + ""type"": ""infections"" + } + } + }"; + var expected = Regex.Replace(expectedFormatted, @"\s+", ""); + + Assert.Equal(expected, result); + } + + private JsonApiSerializer GetSerializer( + ContextGraphBuilder contextGraphBuilder, + List included = null) + { + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.Options).Returns(new JsonApiOptions()); + jsonApiContextMock.Setup(m => m.RequestEntity).Returns(contextGraph.GetContextEntity("test-resource")); + // jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + // jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary()); + // jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers()); + // jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); + jsonApiContextMock.Setup(m => m.MetaBuilder).Returns(new MetaBuilder()); + jsonApiContextMock.Setup(m => m.PageManager).Returns(new PageManager()); + + if (included != null) + jsonApiContextMock.Setup(m => m.IncludedRelationships).Returns(included); + + var jsonApiOptions = new JsonApiOptions(); + jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + + var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object); + var serializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); + + return serializer; } private class TestResource : Identifiable { [Attr("complex-member")] public ComplexType ComplexMember { get; set; } + + [HasMany("children")] public List Children { get; set; } } private class ComplexType { public string CompoundName { get; set; } } + + private class ChildResource : Identifiable + { + [HasMany("infections")] public List Infections { get; set;} + + [HasOne("parent")] public TestResource Parent { get; set; } + } + + private class InfectionResource : Identifiable + { + [HasOne("infected")] public ChildResource Infected { get; set; } + } } } From 909c4c6f093dea5aca6a83c2f999786162e11c0b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli Date: Thu, 30 Aug 2018 16:32:20 -0400 Subject: [PATCH 17/17] it passes! --- .../Serialization/JsonApiSerializerTests.cs | 116 ++++++++++++++---- 1 file changed, 94 insertions(+), 22 deletions(-) diff --git a/test/UnitTests/Serialization/JsonApiSerializerTests.cs b/test/UnitTests/Serialization/JsonApiSerializerTests.cs index 005400d870..c6b4a09128 100644 --- a/test/UnitTests/Serialization/JsonApiSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiSerializerTests.cs @@ -3,11 +3,13 @@ using System.Text.RegularExpressions; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Request; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; +using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; @@ -68,7 +70,7 @@ public void Can_Serialize_Deeply_Nested_Relationships() { // arrange var contextGraphBuilder = new ContextGraphBuilder(); - contextGraphBuilder.AddResource("test-resources"); + contextGraphBuilder.AddResource("test-resource"); contextGraphBuilder.AddResource("children"); contextGraphBuilder.AddResource("infections"); @@ -79,14 +81,18 @@ public void Can_Serialize_Deeply_Nested_Relationships() var resource = new TestResource { + Id = 1, Children = new List { new ChildResource { + Id = 2, Infections = new List { - new InfectionResource(), - new InfectionResource(), + new InfectionResource { Id = 4 }, + new InfectionResource { Id = 5 }, } }, - new ChildResource() + new ChildResource { + Id = 3 + } } }; @@ -100,43 +106,99 @@ public void Can_Serialize_Deeply_Nested_Relationships() @"{ ""data"": { ""attributes"": { - ""complex-member"": { - ""compound-name"": ""testname"" - } + ""complex-member"": null }, ""relationships"": { ""children"": { ""links"": { - ""self"": ""/test-resource//relationships/children"", - ""related"": ""/test-resource//children"" - } + ""self"": ""/test-resource/1/relationships/children"", + ""related"": ""/test-resource/1/children"" + }, + ""data"": [{ + ""type"": ""children"", + ""id"": ""2"" + }, { + ""type"": ""children"", + ""id"": ""3"" + }] } }, ""type"": ""test-resource"", - ""id"": """" + ""id"": ""1"" }, - ""included"": { + ""included"": [ { ""attributes"": {}, - ""relationships"": {}, - ""type"": ""children"" + ""relationships"": { + ""infections"": { + ""links"": { + ""self"": ""/children/2/relationships/infections"", + ""related"": ""/children/2/infections"" + }, + ""data"": [{ + ""type"": ""infections"", + ""id"": ""4"" + }, { + ""type"": ""infections"", + ""id"": ""5"" + }] + }, + ""parent"": { + ""links"": { + ""self"": ""/children/2/relationships/parent"", + ""related"": ""/children/2/parent"" + } + } + }, + ""type"": ""children"", + ""id"": ""2"" }, { ""attributes"": {}, - ""relationships"": {}, - ""type"": ""children"" + ""relationships"": { + ""infected"": { + ""links"": { + ""self"": ""/infections/4/relationships/infected"", + ""related"": ""/infections/4/infected"" + } + } + }, + ""type"": ""infections"", + ""id"": ""4"" }, { ""attributes"": {}, - ""relationships"": {}, - ""type"": ""infections"" + ""relationships"": { + ""infected"": { + ""links"": { + ""self"": ""/infections/5/relationships/infected"", + ""related"": ""/infections/5/infected"" + } + } + }, + ""type"": ""infections"", + ""id"": ""5"" }, { ""attributes"": {}, - ""relationships"": {}, - ""type"": ""infections"" + ""relationships"": { + ""infections"": { + ""links"": { + ""self"": ""/children/3/relationships/infections"", + ""related"": ""/children/3/infections"" + } + }, + ""parent"": { + ""links"": { + ""self"": ""/children/3/relationships/parent"", + ""related"": ""/children/3/parent"" + } + } + }, + ""type"": ""children"", + ""id"": ""3"" } - } + ] }"; var expected = Regex.Replace(expectedFormatted, @"\s+", ""); @@ -167,7 +229,17 @@ private JsonApiSerializer GetSerializer( var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); - var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object); + var services = new ServiceCollection(); + + var mvcBuilder = services.AddMvcCore(); + + services + .AddJsonApiInternals(jsonApiOptions); + + var provider = services.BuildServiceProvider(); + var scoped = new TestScopedServiceProvider(provider); + + var documentBuilder = new DocumentBuilder(jsonApiContextMock.Object, scopedServiceProvider: scoped); var serializer = new JsonApiSerializer(jsonApiContextMock.Object, documentBuilder); return serializer;