From 34da9a7b212541bd3e86ddf0a579cdc91fa042c3 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 22 Feb 2017 07:48:41 -0600 Subject: [PATCH 1/6] feat: add included object to documents --- src/JsonApiDotNetCore/Models/Document.cs | 4 ++++ src/JsonApiDotNetCore/Models/DocumentData.cs | 1 - src/JsonApiDotNetCore/Models/Documents.cs | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Models/Document.cs b/src/JsonApiDotNetCore/Models/Document.cs index 63a8fa84e6..20d058a702 100644 --- a/src/JsonApiDotNetCore/Models/Document.cs +++ b/src/JsonApiDotNetCore/Models/Document.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Newtonsoft.Json; namespace JsonApiDotNetCore.Models @@ -6,5 +7,8 @@ public class Document { [JsonProperty("data")] public DocumentData Data { get; set; } + + [JsonProperty("included")] + public List Included { get; set; } } } diff --git a/src/JsonApiDotNetCore/Models/DocumentData.cs b/src/JsonApiDotNetCore/Models/DocumentData.cs index f6b4ef2792..b2e97dee6c 100644 --- a/src/JsonApiDotNetCore/Models/DocumentData.cs +++ b/src/JsonApiDotNetCore/Models/DocumentData.cs @@ -20,7 +20,6 @@ public string Type [JsonProperty("attributes")] public Dictionary Attributes { get; set; } - [JsonProperty("relationships")] public Dictionary Relationships { get; set; } diff --git a/src/JsonApiDotNetCore/Models/Documents.cs b/src/JsonApiDotNetCore/Models/Documents.cs index b3d3ac8c80..df5bf57c2a 100644 --- a/src/JsonApiDotNetCore/Models/Documents.cs +++ b/src/JsonApiDotNetCore/Models/Documents.cs @@ -7,5 +7,8 @@ public class Documents { [JsonProperty("data")] public List Data { get; set; } + + [JsonProperty("included")] + public List Included { get; set; } } } From e699b866ff77d8f8f275a1a67eb4f11dfa1f0515 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 22 Feb 2017 07:49:56 -0600 Subject: [PATCH 2/6] fix(example/models/person): use hyphenated attrs --- src/JsonApiDotNetCoreExample/Models/Person.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCoreExample/Models/Person.cs b/src/JsonApiDotNetCoreExample/Models/Person.cs index 6cfc8199b7..45c1eb54d0 100644 --- a/src/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/JsonApiDotNetCoreExample/Models/Person.cs @@ -8,10 +8,10 @@ public class Person : Identifiable { public override int Id { get; set; } - [Attr("firstName")] + [Attr("first-name")] public string FirstName { get; set; } - [Attr("lastName")] + [Attr("last-name")] public string LastName { get; set; } public virtual List TodoItems { get; set; } From c9e276639061bf4e2c3056b6570f6c8545dbe979 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 22 Feb 2017 07:55:28 -0600 Subject: [PATCH 3/6] test(included): add failing tests for included objects --- .../Acceptance/Spec/DocumentTests/Included.cs | 168 ++++++++++++++++++ .../Spec/{ => DocumentTests}/Relationships.cs | 3 +- 2 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs rename test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/{ => DocumentTests}/Relationships.cs (98%) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs new file mode 100644 index 0000000000..5b8096577f --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -0,0 +1,168 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using DotNetCoreDocs; +using DotNetCoreDocs.Writers; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json; +using Xunit; +using Person = JsonApiDotNetCoreExample.Models.Person; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCoreExample.Data; +using Bogus; +using JsonApiDotNetCoreExample.Models; +using System; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests +{ + [Collection("WebHostCollection")] + public class Included + { + private DocsFixture _fixture; + private AppDbContext _context; + private Faker _personFaker; + private Faker _todoItemFaker; + + public Included(DocsFixture fixture) + { + _fixture = fixture; + _context = fixture.GetService(); + _personFaker = new Faker() + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()); + + _todoItemFaker = new Faker() + .RuleFor(t => t.Description, f => f.Lorem.Sentence()) + .RuleFor(t => t.Ordinal, f => f.Random.Number()); + } + + [Fact] + public async Task GET_Included_Contains_SideloadedData_ForManyToOne() + { + // arrange + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?include=owner"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var data = documents.Data[0]; + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(documents.Included); + Assert.Equal(documents.Data.Count, documents.Included.Count); + } + + [Fact] + public async Task GET_ById_Included_Contains_SideloadedData_ForManyToOne() + { + // arrange + var person = _personFaker.Generate(); + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + + var route = $"/api/v1/todo-items/{todoItem.Id}?include=owner"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var responseString = await response.Content.ReadAsStringAsync(); + var document = JsonConvert.DeserializeObject(responseString); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(document.Included); + Assert.Equal(person.Id.ToString(), document.Included[0].Id); + Assert.Equal(person.FirstName, document.Included[0].Attributes["first-name"]); + Assert.Equal(person.LastName, document.Included[0].Attributes["last-name"]); + } + + [Fact] + public async Task GET_Included_Contains_SideloadedData_OneToMany() + { + // arrange + _context.People.RemoveRange(_context.People); // ensure all people have todo-items + var person = _personFaker.Generate(); + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/people?include=todo-items"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var data = documents.Data[0]; + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(documents.Included); + Assert.Equal(documents.Data.Count, documents.Included.Count); + } + + [Fact] + public async Task GET_ById_Included_Contains_SideloadedData_ForOneToMany() + { + // arrange + const int numberOfTodoItems = 5; + var person = _personFaker.Generate(); + for (var i = 0; i < numberOfTodoItems; i++) + { + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + } + + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + + var route = $"/api/v1/people/{person.Id}?include=todo-items"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var responseString = await response.Content.ReadAsStringAsync(); + var document = JsonConvert.DeserializeObject(responseString); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(document.Included); + Assert.Equal(numberOfTodoItems, document.Included.Count); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/Relationships.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs similarity index 98% rename from test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/Relationships.cs rename to test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs index fd11cb47b5..32637a64e2 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/Relationships.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs @@ -11,9 +11,8 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample.Data; using System.Linq; -using System; -namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests { [Collection("WebHostCollection")] public class Relationships From 328f413a4949013d7dfc853fa1b27fd857955b8b Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 22 Feb 2017 07:55:48 -0600 Subject: [PATCH 4/6] fix(document-builder): when relationships are included, sideload them --- .../Builders/DocumentBuilder.cs | 85 ++++++++++++++++--- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index bd06f9936d..dd9a6ff2d2 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -27,15 +27,17 @@ public Document Build(IIdentifiable entity) Data = _getData(contextEntity, entity) }; + document.Included = _appendIncludedObject(document.Included, contextEntity, entity); + return document; } public Documents Build(IEnumerable entities) { var entityType = entities - .GetType() - .GenericTypeArguments[0]; - + .GetType() + .GenericTypeArguments[0]; + var contextEntity = _contextGraph.GetContextEntity(entityType); var documents = new Documents @@ -44,9 +46,25 @@ public Documents Build(IEnumerable entities) }; foreach (var entity in entities) + { documents.Data.Add(_getData(contextEntity, entity)); + documents.Included = _appendIncludedObject(documents.Included, contextEntity, entity); + } - return documents; + return documents; + } + + private List _appendIncludedObject(List includedObject, ContextEntity contextEntity, IIdentifiable entity) + { + var includedEntities = _getIncludedEntities(contextEntity, entity); + if (includedEntities.Count > 0) + { + if (includedObject == null) + includedObject = new List(); + includedObject.AddRange(includedEntities); + } + + return includedObject; } private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity) @@ -61,20 +79,21 @@ private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity) return data; data.Attributes = new Dictionary(); - data.Relationships = new Dictionary(); contextEntity.Attributes.ForEach(attr => { data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity)); }); - _addRelationships(data, contextEntity, entity); + if (contextEntity.Relationships.Count > 0) + _addRelationships(data, contextEntity, entity); return data; } private void _addRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity) { + data.Relationships = new Dictionary(); var linkBuilder = new LinkBuilder(_jsonApiContext); contextEntity.Relationships.ForEach(r => @@ -88,12 +107,12 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I } }; - if (_hasRelationship(r.RelationshipName)) + if (_relationshipIsIncluded(r.RelationshipName)) { var navigationEntity = _jsonApiContext.ContextGraph .GetRelationship(entity, r.RelationshipName); - if(navigationEntity is IEnumerable) + if (navigationEntity is IEnumerable) relationshipData.ManyData = _getRelationships((IEnumerable)navigationEntity, r.RelationshipName); else relationshipData.SingleData = _getRelationship(navigationEntity, r.RelationshipName); @@ -103,20 +122,60 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I }); } - private bool _hasRelationship(string relationshipName) + private List _getIncludedEntities(ContextEntity contextEntity, IIdentifiable entity) + { + var included = new List(); + + contextEntity.Relationships.ForEach(r => + { + if (!_relationshipIsIncluded(r.RelationshipName)) return; + + var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.RelationshipName); + + if (navigationEntity is IEnumerable) + foreach (var includedEntity in (IEnumerable)navigationEntity) + included.Add(_getIncludedEntity((IIdentifiable)includedEntity)); + else + included.Add(_getIncludedEntity((IIdentifiable)navigationEntity)); + }); + + return included; + } + + private DocumentData _getIncludedEntity(IIdentifiable entity) { - return _jsonApiContext.IncludedRelationships != null && + var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType()); + + var data = new DocumentData + { + Type = contextEntity.EntityName, + Id = entity.Id.ToString() + }; + + data.Attributes = new Dictionary(); + + contextEntity.Attributes.ForEach(attr => + { + data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity)); + }); + + return data; + } + + private bool _relationshipIsIncluded(string relationshipName) + { + return _jsonApiContext.IncludedRelationships != null && _jsonApiContext.IncludedRelationships.Contains(relationshipName.ToProperCase()); } private List> _getRelationships(IEnumerable entities, string relationshipName) { var objType = entities.GetType().GenericTypeArguments[0]; - + var typeName = _jsonApiContext.ContextGraph.GetContextEntity(objType); var relationships = new List>(); - foreach(var entity in entities) + foreach (var entity in entities) { relationships.Add(new Dictionary { {"type", typeName.EntityName.Dasherize() }, @@ -128,7 +187,7 @@ private List> _getRelationships(IEnumerable e private Dictionary _getRelationship(object entity, string relationshipName) { var objType = entity.GetType(); - + var typeName = _jsonApiContext.ContextGraph.GetContextEntity(objType); return new Dictionary { From 6f95143a1989d8de5260e09ff57d7f40d3a8521f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 22 Feb 2017 07:59:42 -0600 Subject: [PATCH 5/6] chore(project.json): bump package version --- .travis.yml | 5 ++++- src/JsonApiDotNetCore/project.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 14b810ce36..d3a3dfdfb0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,8 @@ before_script: - psql -c 'create database JsonApiDotNetCoreExample;' -U postgres mono: none dotnet: 1.0.0-preview2-1-003177 +branches: + only: + - master script: - - ./build.sh \ No newline at end of file + - ./build.shgit \ No newline at end of file diff --git a/src/JsonApiDotNetCore/project.json b/src/JsonApiDotNetCore/project.json index 1a028b08ae..eb3ef3ac01 100644 --- a/src/JsonApiDotNetCore/project.json +++ b/src/JsonApiDotNetCore/project.json @@ -1,5 +1,5 @@ { - "version": "0.2.10", + "version": "0.2.11", "dependencies": { "Microsoft.NETCore.App": { From 464fc41dc4e1d96a8bbdbc4b7c085ed3e2d64f4d Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 22 Feb 2017 08:01:56 -0600 Subject: [PATCH 6/6] fix(travis): typo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d3a3dfdfb0..d24277b532 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,4 @@ branches: only: - master script: - - ./build.shgit \ No newline at end of file + - ./build.sh \ No newline at end of file