From b93b9f7580a8c813ed451b38e4796fb540bbd56b Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 11:33:10 -0600 Subject: [PATCH 01/17] test(fetching): Request for an empty set should return an empty set Issue #21 - 1 --- .../Acceptance/Spec/FetchingDataTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs new file mode 100644 index 0000000000..af73eb83f1 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using DotNetCoreDocs; +using DotNetCoreDocs.Writers; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +{ + [Collection("WebHostCollection")] + public class FetchingDataTests + { + private DocsFixture _fixture; + private IJsonApiContext _jsonApiContext; + + public FetchingDataTests(DocsFixture fixture) + { + _fixture = fixture; + _jsonApiContext = fixture.GetService(); + } + + [Fact] + public async Task Request_ForEmptyCollection_Returns_EmptyDataCollection() + { + // arrange + var context = _fixture.GetService(); + context.TodoItems.RemoveRange(context.TodoItems); + await context.SaveChangesAsync(); + + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("GET"); + var route = "/api/v1/todo-items"; + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var expectedBody = JsonConvert.SerializeObject(new { + data = new List() + }); + + // act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = JsonApiDeSerializer.DeserializeList(body, _jsonApiContext); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/vnd.api+json", response.Content.Headers.ContentType.ToString()); + Assert.Empty(deserializedBody); + Assert.Equal(expectedBody, body); + + context.Dispose(); + } + } +} From 72a95ee86f23d227dfcc40a29d889ba232bd77f5 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:02:55 -0600 Subject: [PATCH 02/17] test(fetching): relationship request should ret null if unset Issue #21 - 2 Relationship requests should return null if the relationship has not been set --- .../Acceptance/Spec/FetchingDataTests.cs | 1 - .../Spec/FetchingRelationshipsTests.cs | 70 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs index af73eb83f1..6a11cb459d 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Net; using System.Net.Http; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs new file mode 100644 index 0000000000..5c73c55811 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Bogus; +using DotNetCoreDocs; +using DotNetCoreDocs.Writers; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json; +using Xunit; +using Person = JsonApiDotNetCoreExample.Models.Person; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +{ + [Collection("WebHostCollection")] + public class FetchingRelationshipsTests + { + private DocsFixture _fixture; + private IJsonApiContext _jsonApiContext; + private Faker _todoItemFaker; + + public FetchingRelationshipsTests(DocsFixture fixture) + { + _fixture = fixture; + _jsonApiContext = fixture.GetService(); + _todoItemFaker = new Faker() + .RuleFor(t => t.Description, f => f.Lorem.Sentence()) + .RuleFor(t => t.Ordinal, f => f.Random.Number()); + } + + [Fact] + public async Task Request_UnsetRelationship_Returns_Null_DataObject() + { + // arrange + var context = _fixture.GetService(); + var todoItem = _todoItemFaker.Generate(); + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items/{todoItem.Id}/owner"; + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + var expectedBody = "{\"data\":null}"; + + // act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/vnd.api+json", response.Content.Headers.ContentType.ToString()); + Assert.Equal(expectedBody, body); + + context.Dispose(); + } + } +} From 96b2efcf5f52dc08f4015cf9ed4153ea72cf76d2 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:03:33 -0600 Subject: [PATCH 03/17] feat(example-models): make owner optional on todo item --- ...0_MakeOwnerOptionalOnTodoItems.Designer.cs | 60 +++++++++++++++++++ ...0228175630_MakeOwnerOptionalOnTodoItems.cs | 49 +++++++++++++++ 2 files changed, 109 insertions(+) create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170228175630_MakeOwnerOptionalOnTodoItems.Designer.cs create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170228175630_MakeOwnerOptionalOnTodoItems.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170228175630_MakeOwnerOptionalOnTodoItems.Designer.cs b/src/JsonApiDotNetCoreExample/Migrations/20170228175630_MakeOwnerOptionalOnTodoItems.Designer.cs new file mode 100755 index 0000000000..504ec8f1d3 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170228175630_MakeOwnerOptionalOnTodoItems.Designer.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using JsonApiDotNetCoreExample.Data; + +namespace JsonApiDotNetCoreExample.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20170228175630_MakeOwnerOptionalOnTodoItems")] + partial class MakeOwnerOptionalOnTodoItems + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "1.1.0-rtm-22752"); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FirstName"); + + b.Property("LastName"); + + b.HasKey("Id"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description"); + + b.Property("Ordinal"); + + b.Property("OwnerId"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("TodoItems"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => + { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") + .WithMany("TodoItems") + .HasForeignKey("OwnerId"); + }); + } + } +} diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170228175630_MakeOwnerOptionalOnTodoItems.cs b/src/JsonApiDotNetCoreExample/Migrations/20170228175630_MakeOwnerOptionalOnTodoItems.cs new file mode 100755 index 0000000000..96f199d56b --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170228175630_MakeOwnerOptionalOnTodoItems.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace JsonApiDotNetCoreExample.Migrations +{ + public partial class MakeOwnerOptionalOnTodoItems : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TodoItems_People_OwnerId", + table: "TodoItems"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "TodoItems", + nullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_TodoItems_People_OwnerId", + table: "TodoItems", + column: "OwnerId", + principalTable: "People", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TodoItems_People_OwnerId", + table: "TodoItems"); + + migrationBuilder.AlterColumn( + name: "OwnerId", + table: "TodoItems", + nullable: false); + + migrationBuilder.AddForeignKey( + name: "FK_TodoItems_People_OwnerId", + table: "TodoItems", + column: "OwnerId", + principalTable: "People", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} From e966f66035e217642470d95cef3e36e27abe3fb1 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:04:00 -0600 Subject: [PATCH 04/17] feat(example-models): db snapshot --- .../Migrations/AppDbContextModelSnapshot.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs index 181ffc1fa8..e52cf05149 100755 --- a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs +++ b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs @@ -39,7 +39,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Ordinal"); - b.Property("OwnerId"); + b.Property("OwnerId"); b.HasKey("Id"); @@ -52,8 +52,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") .WithMany("TodoItems") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("OwnerId"); }); } } From 2b3aecb96d8aed717e6fb2d3165f78730ff77c49 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:07:59 -0600 Subject: [PATCH 05/17] fix(*): allow null data objects Issue #21 - 2 --- .../Builders/DocumentBuilder.cs | 6 ++- .../Controllers/JsonApiController.cs | 4 +- .../Formatters/JsonApiOutputFormatter.cs | 37 +++++++++++++------ .../Internal/ContextGraph.cs | 2 +- src/JsonApiDotNetCore/Models/DocumentBase.cs | 15 +++++++- .../Serialization/JsonApiDeSerializer.cs | 35 +++++++++++++----- .../Models/TodoItem.cs | 2 +- 7 files changed, 72 insertions(+), 29 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index eda58f66a3..c36816c3e5 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -132,7 +132,9 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I var navigationEntity = _jsonApiContext.ContextGraph .GetRelationship(entity, r.RelationshipName); - if (navigationEntity is IEnumerable) + if(navigationEntity == null) + relationshipData.SingleData = null; + else if (navigationEntity is IEnumerable) relationshipData.ManyData = _getRelationships((IEnumerable)navigationEntity, r.RelationshipName); else relationshipData.SingleData = _getRelationship(navigationEntity, r.RelationshipName); @@ -164,6 +166,8 @@ private List _getIncludedEntities(ContextEntity contextEntity, IId private DocumentData _getIncludedEntity(IIdentifiable entity) { + if(entity == null) return null; + var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType()); var data = new DocumentData diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index 1378a373c5..31bc6b1546 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -126,9 +127,6 @@ public virtual async Task GetRelationshipAsync(TId id, string rel var relationship = _jsonApiContext.ContextGraph .GetRelationship(entity, relationshipName); - if (relationship == null) - return NotFound(); - return Ok(relationship); } diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs index 05f9c57c4a..95e607fca5 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs @@ -2,6 +2,7 @@ using System.Text; using System.Threading.Tasks; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Mvc.Formatters; @@ -65,23 +66,35 @@ private T GetService(OutputFormatterWriteContext context) private string GetResponseBody(object responseObject, IJsonApiContext jsonApiContext, ILogger logger) { + if (responseObject == null) + return GetNullDataResponse(); + if (responseObject.GetType() == typeof(Error) || jsonApiContext.RequestEntity == null) + return GetErrorJson(responseObject, logger); + + return JsonApiSerializer.Serialize(responseObject, jsonApiContext); + } + + private string GetNullDataResponse() + { + return JsonConvert.SerializeObject(new Document { - if (responseObject.GetType() == typeof(Error)) - { - var errors = new ErrorCollection(); - errors.Add((Error)responseObject); - return errors.GetJson(); - } - else - { - logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON."); - return JsonConvert.SerializeObject(responseObject); - } + Data = null + }); + } + + private string GetErrorJson(object responseObject, ILogger logger) + { + if (responseObject.GetType() == typeof(Error)) + { + var errors = new ErrorCollection(); + errors.Add((Error)responseObject); + return errors.GetJson(); } else { - return JsonApiSerializer.Serialize(responseObject, jsonApiContext); + logger?.LogInformation("Response was not a JSONAPI entity. Serializing as plain JSON."); + return JsonConvert.SerializeObject(responseObject); } } } diff --git a/src/JsonApiDotNetCore/Internal/ContextGraph.cs b/src/JsonApiDotNetCore/Internal/ContextGraph.cs index 07f4629df4..7d1493f2c1 100644 --- a/src/JsonApiDotNetCore/Internal/ContextGraph.cs +++ b/src/JsonApiDotNetCore/Internal/ContextGraph.cs @@ -33,7 +33,7 @@ public object GetRelationship(TParent entity, string relationshipName) .FirstOrDefault(p => p.Name.ToLower() == relationshipName.ToLower()); if(navigationProperty == null) - return null; + throw new JsonApiException("400", $"{parentEntityType} does not contain a relationship named {relationshipName}"); return navigationProperty.GetValue(entity); } diff --git a/src/JsonApiDotNetCore/Models/DocumentBase.cs b/src/JsonApiDotNetCore/Models/DocumentBase.cs index 7864c72d13..36ce795a43 100644 --- a/src/JsonApiDotNetCore/Models/DocumentBase.cs +++ b/src/JsonApiDotNetCore/Models/DocumentBase.cs @@ -4,9 +4,22 @@ namespace JsonApiDotNetCore.Models { public class DocumentBase - { + { [JsonProperty("included")] public List Included { get; set; } + + [JsonProperty("meta")] public Dictionary Meta { get; set; } + + // http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm + public bool ShouldSerializeIncluded() + { + return (Included != null); + } + + public bool ShouldSerializeMeta() + { + return (Meta != null); + } } } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 9a3fd6436c..c86fe70322 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -41,16 +41,16 @@ private static object DataToObject(DocumentData data, IJsonApiContext context) var contextEntity = context.ContextGraph.GetContextEntity(entityTypeName); context.RequestEntity = contextEntity; - + var entity = Activator.CreateInstance(contextEntity.EntityType); - + entity = _setEntityAttributes(entity, contextEntity, data.Attributes); entity = _setRelationships(entity, contextEntity, data.Relationships); var identifiableEntity = (IIdentifiable)entity; - if(data.Id != null) - identifiableEntity.Id = Convert.ChangeType(data.Id, identifiableEntity.Id.GetType()); + if (data.Id != null) + identifiableEntity.Id = ChangeType(data.Id, identifiableEntity.Id.GetType()); return identifiableEntity; } @@ -70,7 +70,7 @@ private static object _setEntityAttributes( object newValue; if (attributeValues.TryGetValue(attr.PublicAttributeName.Dasherize(), out newValue)) { - var convertedValue = Convert.ChangeType(newValue, entityProperty.PropertyType); + var convertedValue = ChangeType(newValue, entityProperty.PropertyType); entityProperty.SetValue(entity, convertedValue); } } @@ -92,22 +92,37 @@ private static object _setRelationships( if (entityProperty == null) throw new JsonApiException("400", $"{contextEntity.EntityType.Name} does not contain an relationsip named {attr.RelationshipName}"); - + var relationshipName = attr.RelationshipName.Dasherize(); RelationshipData relationshipData; if (relationships.TryGetValue(relationshipName, out relationshipData)) { var data = (Dictionary)relationshipData.ExposedData; - - if(data == null) continue; - + + if (data == null) continue; + var newValue = data["id"]; - var convertedValue = Convert.ChangeType(newValue, entityProperty.PropertyType); + var convertedValue = ChangeType(newValue, entityProperty.PropertyType); entityProperty.SetValue(entity, convertedValue); } } return entity; } + + private static object ChangeType(object value, Type conversion) + { + var t = conversion; + + if (t.GetTypeInfo().IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) + { + if (value == null) + return null; + + t = Nullable.GetUnderlyingType(t); + } + + return Convert.ChangeType(value, t); + } } } diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs index cba9e45c5d..d700e55c84 100644 --- a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs +++ b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs @@ -13,7 +13,7 @@ public class TodoItem : Identifiable [Attr("ordinal")] public long Ordinal { get; set; } - public int OwnerId { get; set; } + public int? OwnerId { get; set; } public virtual Person Owner { get; set; } } } From e0a958300e15672082007a7c0eee5dbaea28c3da Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:13:55 -0600 Subject: [PATCH 06/17] test(fetching): request for non-existant relationship ret 404 Issue #21 - 3 --- .../Spec/FetchingRelationshipsTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs index 5c73c55811..6522e79b62 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -66,5 +67,33 @@ public async Task Request_UnsetRelationship_Returns_Null_DataObject() context.Dispose(); } + + [Fact] + public async Task Request_ForRelationshipLink_ThatDoesNotExist_Returns_404() + { + // arrange + var context = _fixture.GetService(); + var todoItem = context.TodoItems.First(); + var todoItemId = todoItem.Id; + context.TodoItems.Remove(todoItem); + await context.SaveChangesAsync(); + + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items/{todoItemId}/owner"; + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + context.Dispose(); + } } } From 18f6bac299f9fc3fdec49f54c477f32930bc3fcb Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:25:56 -0600 Subject: [PATCH 07/17] refactor(Identifiable): there is no need to make Id abstract --- README.md | 10 +--------- src/JsonApiDotNetCore/Models/Identifiable.cs | 2 +- src/JsonApiDotNetCoreExample/Models/Person.cs | 2 -- src/JsonApiDotNetCoreExample/Models/TodoItem.cs | 1 - .../Acceptance/Spec/DocumentTests/Included.cs | 1 - 5 files changed, 2 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e12a416cb7..1bcea71739 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,7 @@ Your models should inherit `Identifiable` where `TId` is the type of the pr ```csharp public class Person : Identifiable -{ - public override Guid Id { get; set; } -} +{ } ``` #### Specifying Public Attributes @@ -89,8 +87,6 @@ add the `AttrAttribute` and provide the outbound name. ```csharp public class Person : Identifiable { - public override int Id { get; set; } - [Attr("first-name")] public string FirstName { get; set; } } @@ -104,8 +100,6 @@ they should be labeled as virtual. ```csharp public class Person : Identifiable { - public override int Id { get; set; } - [Attr("first-name")] public string FirstName { get; set; } @@ -119,8 +113,6 @@ For example, a `TodoItem` may have an `Owner` and so the Id attribute should be ```csharp public class TodoItem : Identifiable { - public override int Id { get; set; } - [Attr("description")] public string Description { get; set; } diff --git a/src/JsonApiDotNetCore/Models/Identifiable.cs b/src/JsonApiDotNetCore/Models/Identifiable.cs index 9912c7ad4d..90fc27eb31 100644 --- a/src/JsonApiDotNetCore/Models/Identifiable.cs +++ b/src/JsonApiDotNetCore/Models/Identifiable.cs @@ -2,7 +2,7 @@ namespace JsonApiDotNetCore.Models { public abstract class Identifiable : IIdentifiable, IIdentifiable { - public abstract T Id { get; set; } + public T Id { get; set; } object IIdentifiable.Id { diff --git a/src/JsonApiDotNetCoreExample/Models/Person.cs b/src/JsonApiDotNetCoreExample/Models/Person.cs index 4c09bd01f5..f893556097 100644 --- a/src/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/JsonApiDotNetCoreExample/Models/Person.cs @@ -7,8 +7,6 @@ namespace JsonApiDotNetCoreExample.Models { public class Person : Identifiable, IHasMeta { - public override int Id { get; set; } - [Attr("first-name")] public string FirstName { get; set; } diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs index d700e55c84..cf41adc88e 100644 --- a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs +++ b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs @@ -5,7 +5,6 @@ namespace JsonApiDotNetCoreExample.Models { public class TodoItem : Identifiable { - public override int Id { get; set; } [Attr("description")] public string Description { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs index 5b8096577f..5f8aab6a07 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -13,7 +13,6 @@ using JsonApiDotNetCoreExample.Data; using Bogus; using JsonApiDotNetCoreExample.Models; -using System; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests { From bbd2ccbd1de5985aed47d3c3df79fb7137d2ae0f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:35:28 -0600 Subject: [PATCH 08/17] example(*): add todo-item-collection --- .../TodoItemCollectionsController.cs | 18 ++++ .../Data/AppDbContext.cs | 1 + ...28193414_AddTodoItemCollection.Designer.cs | 92 +++++++++++++++++++ .../20170228193414_AddTodoItemCollection.cs | 74 +++++++++++++++ .../Migrations/AppDbContextModelSnapshot.cs | 32 +++++++ src/JsonApiDotNetCoreExample/Models/Person.cs | 3 +- .../Models/TodoItem.cs | 6 +- .../Models/TodoItemCollection.cs | 13 +++ 8 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 src/JsonApiDotNetCoreExample/Controllers/TodoItemCollectionsController.cs create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170228193414_AddTodoItemCollection.Designer.cs create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170228193414_AddTodoItemCollection.cs create mode 100644 src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs diff --git a/src/JsonApiDotNetCoreExample/Controllers/TodoItemCollectionsController.cs b/src/JsonApiDotNetCoreExample/Controllers/TodoItemCollectionsController.cs new file mode 100644 index 0000000000..c2dbb48a51 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Controllers/TodoItemCollectionsController.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Models; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreExample.Controllers +{ + public class TodoItemCollectionsController : JsonApiController + { + public TodoItemCollectionsController( + IJsonApiContext jsonApiContext, + IEntityRepository entityRepository, + ILoggerFactory loggerFactory) + : base(jsonApiContext, entityRepository, loggerFactory) + { } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs index 1a1090f617..46b29924f8 100644 --- a/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -11,5 +11,6 @@ public AppDbContext(DbContextOptions options) public DbSet TodoItems { get; set; } public DbSet People { get; set; } + public DbSet TodoItemCollection { get; set; } } } diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170228193414_AddTodoItemCollection.Designer.cs b/src/JsonApiDotNetCoreExample/Migrations/20170228193414_AddTodoItemCollection.Designer.cs new file mode 100755 index 0000000000..eb84318106 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170228193414_AddTodoItemCollection.Designer.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using JsonApiDotNetCoreExample.Data; + +namespace JsonApiDotNetCoreExample.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20170228193414_AddTodoItemCollection")] + partial class AddTodoItemCollection + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "1.1.0-rtm-22752"); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FirstName"); + + b.Property("LastName"); + + b.HasKey("Id"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CollectionId"); + + b.Property("Description"); + + b.Property("Ordinal"); + + b.Property("OwnerId"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("OwnerId"); + + b.ToTable("TodoItems"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItemCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name"); + + b.Property("OwnerId"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("TodoItemCollection"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => + { + b.HasOne("JsonApiDotNetCoreExample.Models.TodoItemCollection", "Collection") + .WithMany("TodoItems") + .HasForeignKey("CollectionId"); + + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") + .WithMany("TodoItems") + .HasForeignKey("OwnerId"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItemCollection", b => + { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") + .WithMany("TodoItemCollections") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170228193414_AddTodoItemCollection.cs b/src/JsonApiDotNetCoreExample/Migrations/20170228193414_AddTodoItemCollection.cs new file mode 100755 index 0000000000..3262a40f28 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170228193414_AddTodoItemCollection.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace JsonApiDotNetCoreExample.Migrations +{ + public partial class AddTodoItemCollection : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CollectionId", + table: "TodoItems", + nullable: true); + + migrationBuilder.CreateTable( + name: "TodoItemCollection", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn), + Name = table.Column(nullable: true), + OwnerId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItemCollection", x => x.Id); + table.ForeignKey( + name: "FK_TodoItemCollection_People_OwnerId", + column: x => x.OwnerId, + principalTable: "People", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TodoItems_CollectionId", + table: "TodoItems", + column: "CollectionId"); + + migrationBuilder.CreateIndex( + name: "IX_TodoItemCollection_OwnerId", + table: "TodoItemCollection", + column: "OwnerId"); + + migrationBuilder.AddForeignKey( + name: "FK_TodoItems_TodoItemCollection_CollectionId", + table: "TodoItems", + column: "CollectionId", + principalTable: "TodoItemCollection", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TodoItems_TodoItemCollection_CollectionId", + table: "TodoItems"); + + migrationBuilder.DropTable( + name: "TodoItemCollection"); + + migrationBuilder.DropIndex( + name: "IX_TodoItems_CollectionId", + table: "TodoItems"); + + migrationBuilder.DropColumn( + name: "CollectionId", + table: "TodoItems"); + } + } +} diff --git a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs index e52cf05149..9fe2c5e9c2 100755 --- a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs +++ b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs @@ -35,6 +35,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd(); + b.Property("CollectionId"); + b.Property("Description"); b.Property("Ordinal"); @@ -43,17 +45,47 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CollectionId"); + b.HasIndex("OwnerId"); b.ToTable("TodoItems"); }); + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItemCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name"); + + b.Property("OwnerId"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("TodoItemCollection"); + }); + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => { + b.HasOne("JsonApiDotNetCoreExample.Models.TodoItemCollection", "Collection") + .WithMany("TodoItems") + .HasForeignKey("CollectionId"); + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") .WithMany("TodoItems") .HasForeignKey("OwnerId"); }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItemCollection", b => + { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") + .WithMany("TodoItemCollections") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); } } } diff --git a/src/JsonApiDotNetCoreExample/Models/Person.cs b/src/JsonApiDotNetCoreExample/Models/Person.cs index f893556097..900971951d 100644 --- a/src/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/JsonApiDotNetCoreExample/Models/Person.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCoreExample.Models { - public class Person : Identifiable, IHasMeta + public class Person : Identifiable, IHasMeta { [Attr("first-name")] public string FirstName { get; set; } @@ -14,6 +14,7 @@ public class Person : Identifiable, IHasMeta public string LastName { get; set; } public virtual List TodoItems { get; set; } + public virtual List TodoItemCollections { get; set; } public Dictionary GetMeta(IJsonApiContext context) { diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs index cf41adc88e..fbf4fe8ba6 100644 --- a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs +++ b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs @@ -3,9 +3,8 @@ namespace JsonApiDotNetCoreExample.Models { - public class TodoItem : Identifiable + public class TodoItem : Identifiable { - [Attr("description")] public string Description { get; set; } @@ -14,5 +13,8 @@ public class TodoItem : Identifiable public int? OwnerId { get; set; } public virtual Person Owner { get; set; } + + public int? CollectionId { get; set; } + public virtual TodoItemCollection Collection { get; set; } } } diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs b/src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs new file mode 100644 index 0000000000..2c4e42c3f0 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCoreExample.Models +{ + public class TodoItemCollection : Identifiable + { + public string Name { get; set; } + public virtual List TodoItems { get; set; } + public int OwnerId { get; set; } + public virtual Person Owner { get; set; } + } +} \ No newline at end of file From 8b366d9aff76fc3599262eb0940a22a7adc362c7 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:35:46 -0600 Subject: [PATCH 09/17] feat(identifiable): add default for generic --- src/JsonApiDotNetCore/Models/Identifiable.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Models/Identifiable.cs b/src/JsonApiDotNetCore/Models/Identifiable.cs index 90fc27eb31..bf4565fa10 100644 --- a/src/JsonApiDotNetCore/Models/Identifiable.cs +++ b/src/JsonApiDotNetCore/Models/Identifiable.cs @@ -1,6 +1,9 @@ namespace JsonApiDotNetCore.Models { - public abstract class Identifiable : IIdentifiable, IIdentifiable + public class Identifiable : Identifiable + {} + + public class Identifiable : IIdentifiable, IIdentifiable { public T Id { get; set; } From 8d28c8060d5341ec0f05cb1571cca4ae37835b9c Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:43:26 -0600 Subject: [PATCH 10/17] test(inclusion): multiple relationships can be included --- .../Acceptance/Spec/DocumentTests/Included.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs index 5f8aab6a07..2cb21651e2 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -23,6 +23,7 @@ public class Included private AppDbContext _context; private Faker _personFaker; private Faker _todoItemFaker; + private Faker _todoItemCollectionFaker; public Included(DocsFixture fixture) { @@ -35,6 +36,9 @@ public Included(DocsFixture fixture) _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()); + + _todoItemCollectionFaker = new Faker() + .RuleFor(t => t.Name, f => f.Company.CatchPhrase()); } [Fact] @@ -163,5 +167,45 @@ public async Task GET_ById_Included_Contains_SideloadedData_ForOneToMany() Assert.NotEmpty(document.Included); Assert.Equal(numberOfTodoItems, document.Included.Count); } + + [Fact] + public async Task Can_Include_MultipleRelationships() + { + // arrange + var person = _personFaker.Generate(); + var todoItemCollection = _todoItemCollectionFaker.Generate(); + todoItemCollection.Owner = person; + + const int numberOfTodoItems = 5; + for (var i = 0; i < numberOfTodoItems; i++) + { + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + todoItem.Collection = todoItemCollection; + _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,todo-item-collections"; + + 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 + 1, document.Included.Count); + } } } From 1dd78459b1ee22ea7034b8ff1f2e7e780a5e45f3 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 13:47:27 -0600 Subject: [PATCH 11/17] test(inclusion): 400 response if relationship does not exist Issue #21 - 5 --- .../Acceptance/Spec/DocumentTests/Included.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs index 2cb21651e2..77dd542cb8 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -13,6 +13,7 @@ using JsonApiDotNetCoreExample.Data; using Bogus; using JsonApiDotNetCoreExample.Models; +using System.Linq; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests { @@ -207,5 +208,53 @@ public async Task Can_Include_MultipleRelationships() Assert.NotEmpty(document.Included); Assert.Equal(numberOfTodoItems + 1, document.Included.Count); } + + [Fact] + public async Task Request_ToIncludeUnknownRelationship_Returns_400() + { + // arrange + var person = _context.People.First(); + + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + + var route = $"/api/v1/people/{person.Id}?include=non-existent-relationship"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Request_ToIncludeRelationshipPath_Returns_400() + { + // arrange + var person = _context.People.First(); + + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + + var route = $"/api/v1/people/{person.Id}?include=owner.name"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } } } From c7299134030416049df3e50a683ee289b0aba268 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 14:31:56 -0600 Subject: [PATCH 12/17] refactor(query-set): error details for nested rel --- src/JsonApiDotNetCore/Internal/Query/QuerySet.cs | 3 +++ .../Acceptance/Spec/DocumentTests/Included.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs index 25bb9f1417..2dd9e56b97 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs @@ -147,6 +147,9 @@ private List ParseSortParameters(string value) private List ParseIncludedRelationships(string value) { + if(value.Contains(".")) + throw new JsonApiException("400", "Deeply nested relationships are not supported"); + return value .Split(',') .Select(s => s.ToProperCase()) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs index 77dd542cb8..e6883b68b4 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -234,7 +234,7 @@ public async Task Request_ToIncludeUnknownRelationship_Returns_400() } [Fact] - public async Task Request_ToIncludeRelationshipPath_Returns_400() + public async Task Request_ToIncludeDeeplyNestedRelationships_Returns_400() { // arrange var person = _context.People.First(); From cce3149dc318caa322f7f2fecc4ffd399b3ab48d Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Tue, 28 Feb 2017 15:40:19 -0600 Subject: [PATCH 13/17] feat(*): add page manager to hold pagination details --- .../Builders/DocumentBuilder.cs | 2 +- .../Controllers/JsonApiController.cs | 12 +++++------- src/JsonApiDotNetCore/Internal/PageManager.cs | 10 ++++++++++ .../Services/IJsonApiContext.cs | 2 +- .../Services/JsonApiContext.cs | 19 +++++++++++++++++-- 5 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/PageManager.cs diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index c36816c3e5..20327020ed 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -68,7 +68,7 @@ private Dictionary _getMeta(IIdentifiable entity) meta = metaEntity.GetMeta(_jsonApiContext); if(_jsonApiContext.Options.IncludeTotalRecordCount) - meta["total-records"] = _jsonApiContext.TotalRecords; + meta["total-records"] = _jsonApiContext.PageManager.TotalRecords; if(meta.Count > 0) return meta; return null; diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index 31bc6b1546..073c6aa846 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -66,7 +66,7 @@ public virtual async Task GetAsync() entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships); if (_jsonApiContext.Options.IncludeTotalRecordCount) - _jsonApiContext.TotalRecords = await entities.CountAsync(); + _jsonApiContext.PageManager.TotalRecords = await entities.CountAsync(); // pagination should be done last since it will execute the query var pagedEntities = await ApplyPageQueryAsync(entities); @@ -188,17 +188,15 @@ private IQueryable ApplySortAndFilterQuery(IQueryable entities) private async Task> ApplyPageQueryAsync(IQueryable entities) { - if(_jsonApiContext.Options.DefaultPageSize == 0 && (_jsonApiContext.QuerySet == null || _jsonApiContext.QuerySet.PageQuery.PageSize == 0)) + var pageManager = _jsonApiContext.PageManager; + if(!pageManager.IsPaginated) return entities; var query = _jsonApiContext.QuerySet?.PageQuery ?? new PageQuery(); - - var pageNumber = query.PageOffset > 0 ? query.PageOffset : 1; - var pageSize = query.PageSize > 0 ? query.PageSize : _jsonApiContext.Options.DefaultPageSize; - _logger?.LogInformation($"Applying paging query. Fetching page {pageNumber} with {pageSize} entities"); + _logger?.LogInformation($"Applying paging query. Fetching page {pageManager.CurrentPage} with {pageManager.PageSize} entities"); - return await _entities.PageAsync(entities, pageSize, pageNumber); + return await _entities.PageAsync(entities, pageManager.PageSize, pageManager.CurrentPage); } private IQueryable IncludeRelationships(IQueryable entities, List relationships) diff --git a/src/JsonApiDotNetCore/Internal/PageManager.cs b/src/JsonApiDotNetCore/Internal/PageManager.cs new file mode 100644 index 0000000000..fd8ad6b4f4 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/PageManager.cs @@ -0,0 +1,10 @@ +namespace JsonApiDotNetCore.Internal +{ + public class PageManager + { + public int TotalRecords { get; set; } + public int PageSize { get; set; } + public int CurrentPage { get; set; } + public bool IsPaginated { get { return PageSize > 0; } } + } +} diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index 30faf32fea..5818530338 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -15,6 +15,6 @@ public interface IJsonApiContext QuerySet QuerySet { get; set; } bool IsRelationshipData { get; set; } List IncludedRelationships { get; set; } - int TotalRecords { get; set; } + PageManager PageManager { get; set; } } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 8423eb4450..cfd0bff144 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Builders; @@ -28,7 +29,7 @@ public JsonApiContext( public QuerySet QuerySet { get; set; } public bool IsRelationshipData { get; set; } public List IncludedRelationships { get; set; } - public int TotalRecords { get; set; } + public PageManager PageManager { get; set; } public IJsonApiContext ApplyContext() { @@ -40,12 +41,26 @@ public IJsonApiContext ApplyContext() { QuerySet = new QuerySet(this, context.Request.Query); IncludedRelationships = QuerySet.IncludedRelationships; - } + } var linkBuilder = new LinkBuilder(this); BasePath = linkBuilder.GetBasePath(context, RequestEntity.EntityName); + PageManager = GetPageManager(); return this; } + + private PageManager GetPageManager() + { + if(Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0)) + return new PageManager(); + + var query = QuerySet?.PageQuery ?? new PageQuery(); + + return new PageManager { + CurrentPage = query.PageOffset > 0 ? query.PageOffset : 1, + PageSize = query.PageSize > 0 ? query.PageSize : Options.DefaultPageSize + }; + } } } From e6d8c3f860ddb3ade2ae60cbc7d573f3958da1bf Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 1 Mar 2017 07:31:59 -0600 Subject: [PATCH 14/17] feat(root-links): add pagination links Issue #21 --- .../Builders/DocumentBuilder.cs | 6 +- src/JsonApiDotNetCore/Builders/LinkBuilder.cs | 6 +- src/JsonApiDotNetCore/Internal/PageManager.cs | 32 ++++++++ src/JsonApiDotNetCore/Models/DocumentBase.cs | 8 ++ src/JsonApiDotNetCore/Models/RootLinks.cs | 48 +++++++++++ .../Services/JsonApiContext.cs | 2 +- src/JsonApiDotNetCoreExample/Startup.cs | 2 + .../Spec/DocumentTests/PagingTests.cs | 82 +++++++++++++++++++ 8 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 src/JsonApiDotNetCore/Models/RootLinks.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 20327020ed..9dc64e06b4 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -26,7 +26,8 @@ public Document Build(IIdentifiable entity) var document = new Document { Data = _getData(contextEntity, entity), - Meta = _getMeta(entity) + Meta = _getMeta(entity), + Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)) }; document.Included = _appendIncludedObject(document.Included, contextEntity, entity); @@ -45,7 +46,8 @@ public Documents Build(IEnumerable entities) var documents = new Documents { Data = new List(), - Meta = _getMeta(entities.FirstOrDefault()) + Meta = _getMeta(entities.FirstOrDefault()), + Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext)) }; foreach (var entity in entities) diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs index d819aab253..5fd25793d7 100644 --- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs @@ -1,4 +1,3 @@ - using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -45,5 +44,10 @@ public string GetRelatedRelationLink(string parent, string parentId, string chil { return $"{_context.BasePath}/{parent.Dasherize()}/{parentId}/{child.Dasherize()}"; } + + public string GetPageLink(int pageOffset, int pageSize) + { + return $"{_context.BasePath}/{_context.RequestEntity.EntityName.Dasherize()}?page[size]={pageSize}&page[number]={pageOffset}"; + } } } diff --git a/src/JsonApiDotNetCore/Internal/PageManager.cs b/src/JsonApiDotNetCore/Internal/PageManager.cs index fd8ad6b4f4..c85d81b1e9 100644 --- a/src/JsonApiDotNetCore/Internal/PageManager.cs +++ b/src/JsonApiDotNetCore/Internal/PageManager.cs @@ -1,10 +1,42 @@ +using System; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Models; + namespace JsonApiDotNetCore.Internal { public class PageManager { public int TotalRecords { get; set; } public int PageSize { get; set; } + public int DefaultPageSize { get; set; } public int CurrentPage { get; set; } public bool IsPaginated { get { return PageSize > 0; } } + public int TotalPages { + get { return (TotalRecords == 0) ? -1: (int)Math.Ceiling(decimal.Divide(TotalRecords, PageSize)); } + } + + public RootLinks GetPageLinks(LinkBuilder linkBuilder) + { + if(!IsPaginated || (CurrentPage == 1 && TotalPages <= 0)) + return null; + + var rootLinks = new RootLinks(); + + var includePageSize = DefaultPageSize != PageSize; + + if(CurrentPage > 1) + rootLinks.First = linkBuilder.GetPageLink(1, PageSize); + + if(CurrentPage > 1) + rootLinks.Prev = linkBuilder.GetPageLink(CurrentPage - 1, PageSize); + + if(CurrentPage < TotalPages) + rootLinks.Next = linkBuilder.GetPageLink(CurrentPage + 1, PageSize); + + if(TotalPages > 0) + rootLinks.Last = linkBuilder.GetPageLink(TotalPages, PageSize); + + return rootLinks; + } } } diff --git a/src/JsonApiDotNetCore/Models/DocumentBase.cs b/src/JsonApiDotNetCore/Models/DocumentBase.cs index 36ce795a43..1cb31595ec 100644 --- a/src/JsonApiDotNetCore/Models/DocumentBase.cs +++ b/src/JsonApiDotNetCore/Models/DocumentBase.cs @@ -5,6 +5,9 @@ namespace JsonApiDotNetCore.Models { public class DocumentBase { + [JsonProperty("links")] + public RootLinks Links { get; set; } + [JsonProperty("included")] public List Included { get; set; } @@ -21,5 +24,10 @@ public bool ShouldSerializeMeta() { return (Meta != null); } + + public bool ShouldSerializeLinks() + { + return (Links != null); + } } } diff --git a/src/JsonApiDotNetCore/Models/RootLinks.cs b/src/JsonApiDotNetCore/Models/RootLinks.cs new file mode 100644 index 0000000000..42b0a7863f --- /dev/null +++ b/src/JsonApiDotNetCore/Models/RootLinks.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Models +{ + public class RootLinks + { + [JsonProperty("self")] + public string Self { get; set; } + + [JsonProperty("next")] + public string Next { get; set; } + + [JsonProperty("prev")] + public string Prev { get; set; } + + [JsonProperty("first")] + public string First { get; set; } + + [JsonProperty("last")] + public string Last { get; set; } + + // http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm + public bool ShouldSerializeSelf() + { + return (!string.IsNullOrEmpty(Self)); + } + + public bool ShouldSerializeFirst() + { + return (!string.IsNullOrEmpty(First)); + } + + public bool ShouldSerializeNext() + { + return (!string.IsNullOrEmpty(Next)); + } + + public bool ShouldSerializePrev() + { + return (!string.IsNullOrEmpty(Prev)); + } + + public bool ShouldSerializeLast() + { + return (!string.IsNullOrEmpty(Last)); + } + } +} diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index cfd0bff144..88cd045ea1 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using JsonApiDotNetCore.Builders; @@ -58,6 +57,7 @@ private PageManager GetPageManager() var query = QuerySet?.PageQuery ?? new PageQuery(); return new PageManager { + DefaultPageSize = Options.DefaultPageSize, CurrentPage = query.PageOffset > 0 ? query.PageOffset : 1, PageSize = query.PageSize > 0 ? query.PageSize : Options.DefaultPageSize }; diff --git a/src/JsonApiDotNetCoreExample/Startup.cs b/src/JsonApiDotNetCoreExample/Startup.cs index 6ae05f93d3..2abea4baad 100644 --- a/src/JsonApiDotNetCoreExample/Startup.cs +++ b/src/JsonApiDotNetCoreExample/Startup.cs @@ -44,6 +44,7 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services) { opt.Namespace = "api/v1"; opt.DefaultPageSize = 5; + opt.IncludeTotalRecordCount = true; }); services.AddDocumentationConfiguration(Config); @@ -52,6 +53,7 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services) var appContext = provider.GetRequiredService(); if(appContext == null) throw new ArgumentException(); + return provider; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs new file mode 100644 index 0000000000..d62d5120f2 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs @@ -0,0 +1,82 @@ +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.Linq; +using System; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests +{ + [Collection("WebHostCollection")] + public class PagingTests + { + private DocsFixture _fixture; + private AppDbContext _context; + private Faker _personFaker; + private Faker _todoItemFaker; + private Faker _todoItemCollectionFaker; + + public PagingTests(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()); + + _todoItemCollectionFaker = new Faker() + .RuleFor(t => t.Name, f => f.Company.CatchPhrase()); + } + + [Fact] + public async Task Server_IncludesPagination_Links() + { + // arrange + var pageSize = 5; + var numberOfTodoItems = _context.TodoItems.Count(); + var numberOfPages = (int)Math.Ceiling(decimal.Divide(numberOfTodoItems, pageSize)); + var startPageNumber = 2; + + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?page[number]=2"; + + 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 links = documents.Links; + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(links.First); + Assert.NotEmpty(links.Next); + Assert.NotEmpty(links.Last); + + Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={startPageNumber+1}", links.Next); + Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={startPageNumber-1}", links.Prev); + Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]={numberOfPages}", links.Last); + Assert.Equal($"http://localhost/api/v1/todo-items?page[size]={pageSize}&page[number]=1", links.First); + } + } +} From 1ffd68ec6042deb20fef31116d69fe927b7a9649 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 1 Mar 2017 07:41:23 -0600 Subject: [PATCH 15/17] publish to MyGet on staging branch --- appveyor.yml | 2 +- src/JsonApiDotNetCore/project.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 45405958fe..dc0da18919 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,7 +19,7 @@ deploy: secure: 6CeYcZ4Ze+57gxfeuHzqP6ldbUkPtF6pfpVM1Gw/K2jExFrAz763gNAQ++tiacq3 skip_symbols: true on: - branch: master + branch: staging - provider: NuGet name: production api_key: diff --git a/src/JsonApiDotNetCore/project.json b/src/JsonApiDotNetCore/project.json index d56b00e58f..8785c130a7 100644 --- a/src/JsonApiDotNetCore/project.json +++ b/src/JsonApiDotNetCore/project.json @@ -1,5 +1,5 @@ { - "version": "0.2.12", + "version": "1.0.0-beta1-*", "dependencies": { "Microsoft.NETCore.App": { From 7be572e796ae8ddcc4ba8e54b6d6440a3eea7c13 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 1 Mar 2017 07:44:00 -0600 Subject: [PATCH 16/17] feat(ci): run builds on staging --- .travis.yml | 3 ++- appveyor.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d24277b532..274f9e6228 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,5 +10,6 @@ dotnet: 1.0.0-preview2-1-003177 branches: only: - master + - staging script: - - ./build.sh \ No newline at end of file + - ./build.sh diff --git a/appveyor.yml b/appveyor.yml index dc0da18919..65646e6b38 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,6 +4,7 @@ pull_requests: branches: only: - master + - staging nuget: disable_publish_on_pr: true build_script: From e490683163271904c4eaf24292d4b920a2bf0fbf Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 1 Mar 2017 08:05:52 -0600 Subject: [PATCH 17/17] test(*): fix test that are dependent on some items being defined No tests should be dependent upon any items existing in the context previously --- .../Spec/DocumentTests/PagingTests.cs | 11 +++++-- .../Spec/DocumentTests/Relationships.cs | 29 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs index d62d5120f2..8a9a33f14d 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/PagingTests.cs @@ -48,8 +48,15 @@ public async Task Server_IncludesPagination_Links() { // arrange var pageSize = 5; - var numberOfTodoItems = _context.TodoItems.Count(); - var numberOfPages = (int)Math.Ceiling(decimal.Divide(numberOfTodoItems, pageSize)); + const int minimumNumberOfRecords = 11; + _context.TodoItems.RemoveRange(_context.TodoItems); + + for(var i=0; i < minimumNumberOfRecords; i++) + _context.TodoItems.Add(_todoItemFaker.Generate()); + + await _context.SaveChangesAsync(); + + var numberOfPages = (int)Math.Ceiling(decimal.Divide(minimumNumberOfRecords, pageSize)); var startPageNumber = 2; var builder = new WebHostBuilder() diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs index 32637a64e2..744a395ce5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Relationships.cs @@ -11,6 +11,8 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample.Data; using System.Linq; +using Bogus; +using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec.DocumentTests { @@ -19,10 +21,15 @@ public class Relationships { private DocsFixture _fixture; private AppDbContext _context; + private Faker _todoItemFaker; + public Relationships(DocsFixture fixture) { _fixture = fixture; _context = fixture.GetService(); + _todoItemFaker = new Faker() + .RuleFor(t => t.Description, f => f.Lorem.Sentence()) + .RuleFor(t => t.Ordinal, f => f.Random.Number()); } [Fact] @@ -31,9 +38,13 @@ public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships() // arrange var builder = new WebHostBuilder() .UseStartup(); + + var todoItem = _todoItemFaker.Generate(); + _context.TodoItems.Add(todoItem); + await _context.SaveChangesAsync(); var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todo-items"; + var route = $"/api/v1/todo-items/{todoItem.Id}"; var server = new TestServer(builder); var client = server.CreateClient(); @@ -41,8 +52,8 @@ public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships() // act var response = await client.SendAsync(request); - var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - var data = documents.Data[0]; + var document = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var data = document.Data; var expectedOwnerSelfLink = $"http://localhost/api/v1/todo-items/{data.Id}/relationships/owner"; var expectedOwnerRelatedLink = $"http://localhost/api/v1/todo-items/{data.Id}/owner"; @@ -56,13 +67,15 @@ public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships() public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships_ById() { // arrange - var todoItemId = _context.TodoItems.Last().Id; - var builder = new WebHostBuilder() .UseStartup(); + + var todoItem = _todoItemFaker.Generate(); + _context.TodoItems.Add(todoItem); + await _context.SaveChangesAsync(); var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todo-items/{todoItemId}"; + var route = $"/api/v1/todo-items/{todoItem.Id}"; var server = new TestServer(builder); var client = server.CreateClient(); @@ -72,8 +85,8 @@ public async Task Correct_RelationshipObjects_For_ManyToOne_Relationships_ById() var response = await client.SendAsync(request); var responseString = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject(responseString).Data; - var expectedOwnerSelfLink = $"http://localhost/api/v1/todo-items/{todoItemId}/relationships/owner"; - var expectedOwnerRelatedLink = $"http://localhost/api/v1/todo-items/{todoItemId}/owner"; + var expectedOwnerSelfLink = $"http://localhost/api/v1/todo-items/{todoItem.Id}/relationships/owner"; + var expectedOwnerRelatedLink = $"http://localhost/api/v1/todo-items/{todoItem.Id}/owner"; // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode);