From 47a73acb50edb0ae3e1d646e567f4cc1b1d4ad76 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 29 Mar 2017 21:25:06 -0500 Subject: [PATCH 1/2] test(acceptance): reproduce issue #78 --- .../Data/AppDbContext.cs | 13 +++ .../JsonApiDotNetCoreExample.csproj | 1 + ...330020650_AddAssignedTodoItems.Designer.cs | 100 ++++++++++++++++++ .../20170330020650_AddAssignedTodoItems.cs | 45 ++++++++ .../Migrations/AppDbContextModelSnapshot.cs | 8 ++ src/JsonApiDotNetCoreExample/Models/Person.cs | 3 + .../Models/TodoItem.cs | 4 + .../Acceptance/Spec/DocumentTests/Included.cs | 34 ++++++ 8 files changed, 208 insertions(+) create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs diff --git a/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs index 7406a11065..592513b94d 100644 --- a/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -9,6 +9,19 @@ public AppDbContext(DbContextOptions options) : base(options) { } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasOne(t => t.Assignee) + .WithMany(p => p.AssignedTodoItems) + .HasForeignKey(t => t.AssigneeId); + + modelBuilder.Entity() + .HasOne(t => t.Owner) + .WithMany(p => p.TodoItems) + .HasForeignKey(t => t.OwnerId); + } + public DbSet TodoItems { get; set; } public DbSet People { get; set; } public DbSet TodoItemCollections { get; set; } diff --git a/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj b/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj index 60e351ab00..f49c233595 100755 --- a/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj +++ b/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj @@ -28,6 +28,7 @@ + diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs b/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs new file mode 100755 index 0000000000..52b60adbcb --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs @@ -0,0 +1,100 @@ +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("20170330020650_AddAssignedTodoItems")] + partial class AddAssignedTodoItems + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "1.1.1"); + + 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("AssigneeId"); + + b.Property("CollectionId"); + + b.Property("Description"); + + b.Property("Ordinal"); + + b.Property("OwnerId"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + 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("TodoItemCollections"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => + { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Assignee") + .WithMany("AssignedTodoItems") + .HasForeignKey("AssigneeId"); + + 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/20170330020650_AddAssignedTodoItems.cs b/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs new file mode 100755 index 0000000000..9d41bb041e --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace JsonApiDotNetCoreExample.Migrations +{ + public partial class AddAssignedTodoItems : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AssigneeId", + table: "TodoItems", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_TodoItems_AssigneeId", + table: "TodoItems", + column: "AssigneeId"); + + migrationBuilder.AddForeignKey( + name: "FK_TodoItems_People_AssigneeId", + table: "TodoItems", + column: "AssigneeId", + principalTable: "People", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TodoItems_People_AssigneeId", + table: "TodoItems"); + + migrationBuilder.DropIndex( + name: "IX_TodoItems_AssigneeId", + table: "TodoItems"); + + migrationBuilder.DropColumn( + name: "AssigneeId", + table: "TodoItems"); + } + } +} diff --git a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs index 58722dd4f7..21f5cbc221 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("AssigneeId"); + b.Property("CollectionId"); b.Property("Description"); @@ -45,6 +47,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("AssigneeId"); + b.HasIndex("CollectionId"); b.HasIndex("OwnerId"); @@ -70,6 +74,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Assignee") + .WithMany("AssignedTodoItems") + .HasForeignKey("AssigneeId"); + b.HasOne("JsonApiDotNetCoreExample.Models.TodoItemCollection", "Collection") .WithMany("TodoItems") .HasForeignKey("CollectionId"); diff --git a/src/JsonApiDotNetCoreExample/Models/Person.cs b/src/JsonApiDotNetCoreExample/Models/Person.cs index 3689f39537..52b67347e9 100644 --- a/src/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/JsonApiDotNetCoreExample/Models/Person.cs @@ -15,6 +15,9 @@ public class Person : Identifiable, IHasMeta [HasMany("todo-items")] public virtual List TodoItems { get; set; } + + [HasMany("assigned-todo-items")] + public virtual List AssignedTodoItems { get; set; } [HasMany("todo-item-collections")] public virtual List TodoItemCollections { get; set; } diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs index 1c27c043d6..008c42b1a6 100644 --- a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs +++ b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs @@ -12,11 +12,15 @@ public class TodoItem : Identifiable public long Ordinal { get; set; } public int? OwnerId { get; set; } + public int? AssigneeId { get; set; } public Guid? CollectionId { get; set; } [HasOne("owner")] public virtual Person Owner { get; set; } + [HasOne("assignee")] + public virtual Person Assignee { get; set; } + [HasOne("collection")] public virtual TodoItemCollection Collection { get; set; } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs index 9ddf5519cf..39747fd68e 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -134,6 +134,40 @@ public async Task GET_Included_Contains_SideloadedData_OneToMany() Assert.Equal(documents.Data.Count, documents.Included.Count); } + [Fact] + public async Task GET_Included_DoesNot_Duplicate_Records_ForMultipleRelationshipsOfSameType() + { + // arrange + _context.People.RemoveRange(_context.People); // ensure all people have todo-items + _context.TodoItems.RemoveRange(_context.TodoItems); + var person = _personFaker.Generate(); + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + todoItem.Assignee = 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&include=assignee"; + + 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; + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(documents.Included); + Assert.Equal(1, documents.Included.Count); + } + [Fact] public async Task GET_ById_Included_Contains_SideloadedData_ForOneToMany() { From 0ab51ef6f7e69b6da26085387805acc6e5f8efca Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 29 Mar 2017 21:26:07 -0500 Subject: [PATCH 2/2] fix(document-builder): unique inclusion of entity ensures that an entity will only be added to the included references once --- src/JsonApiDotNetCore/Builders/DocumentBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 92c785c5d0..971fd2af10 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -188,7 +188,8 @@ private List GetIncludedEntities(ContextEntity contextEntity, IIde private void AddIncludedEntity(List entities, IIdentifiable entity) { var includedEntity = GetIncludedEntity(entity); - if(includedEntity != null) + + if(includedEntity != null && !entities.Any(doc => doc.Id == includedEntity.Id && doc.Type == includedEntity.Type)) entities.Add(includedEntity); }