From 588b98036c79d266f32f6dda0dc876123dbd7a4f Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 10:50:02 -0500 Subject: [PATCH 1/6] test(patch): failing test for patching entity has one relationship --- .../Acceptance/Spec/UpdatingDataTests.cs | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index c87683fa2f..5bfed72684 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; @@ -7,8 +6,6 @@ using Bogus; using DotNetCoreDocs; using DotNetCoreDocs.Writers; -using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; @@ -16,6 +13,7 @@ using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using Xunit; +using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -25,14 +23,18 @@ public class UpdatingDataTests private DocsFixture _fixture; private AppDbContext _context; private Faker _todoItemFaker; + private Faker _personFaker; public UpdatingDataTests(DocsFixture fixture) { _fixture = fixture; _context = fixture.GetService(); - _todoItemFaker = new Faker() + _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()); + _personFaker = new Faker() + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()); } [Fact] @@ -73,5 +75,61 @@ public async Task Respond_404_If_EntityDoesNotExist() // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + + [Fact] + public async Task Can_Patch_Entity_And_HasOne_Relationships() + { + // arrange + var todoItem = _todoItemFaker.Generate(); + var person = _personFaker.Generate(); + _context.TodoItems.Add(todoItem); + _context.People.Add(person); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup(); + var server = new TestServer(builder); + var client = server.CreateClient(); + + var content = new + { + data = new + { + type = "todo-items", + attributes = new + { + description = todoItem.Description, + ordinal = todoItem.Ordinal + }, + relationships = new + { + owner = new + { + data = new + { + type = "people", + id = person.Id.ToString() + } + } + } + } + }; + + var httpMethod = new HttpMethod("PATCH"); + var route = $"/api/v1/todo-items/{todoItem.Id}"; + var request = new HttpRequestMessage(httpMethod, route); + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json"); + + // Act + var response = await client.SendAsync(request); + var updatedTodoItem = _context.TodoItems + .SingleOrDefault(t => t.Id == todoItem.Id); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(person.Id, updatedTodoItem.OwnerId); + } } } From 402d6ccbe5137338d63a7872e8265052dffe5931 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:36:10 -0500 Subject: [PATCH 2/6] feat(relationship-attr): make setValue abstract allows setting of has-one relationship by Id --- src/JsonApiDotNetCore/Models/HasManyAttribute.cs | 11 ++++++++++- src/JsonApiDotNetCore/Models/HasOneAttribute.cs | 15 +++++++++++++++ .../Models/RelationshipAttribute.cs | 12 ++---------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs index 13e4a9efad..445b82c22a 100644 --- a/src/JsonApiDotNetCore/Models/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasManyAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System.Reflection; namespace JsonApiDotNetCore.Models { @@ -9,5 +9,14 @@ public HasManyAttribute(string publicName) { PublicRelationshipName = publicName; } + + public override void SetValue(object entity, object newValue) + { + var propertyInfo = entity + .GetType() + .GetProperty(InternalRelationshipName); + + propertyInfo.SetValue(entity, newValue); + } } } diff --git a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs index e5670eae29..29661de485 100644 --- a/src/JsonApiDotNetCore/Models/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore/Models/HasOneAttribute.cs @@ -1,3 +1,5 @@ +using System.Reflection; + namespace JsonApiDotNetCore.Models { public class HasOneAttribute : RelationshipAttribute @@ -7,5 +9,18 @@ public HasOneAttribute(string publicName) { PublicRelationshipName = publicName; } + + public override void SetValue(object entity, object newValue) + { + var propertyName = (newValue.GetType() == Type) + ? InternalRelationshipName + : $"{InternalRelationshipName}Id"; + + var propertyInfo = entity + .GetType() + .GetProperty(propertyName); + + propertyInfo.SetValue(entity, newValue); + } } } diff --git a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs index 45b3565592..5e02eaf1ef 100644 --- a/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Models/RelationshipAttribute.cs @@ -1,9 +1,8 @@ using System; -using System.Reflection; namespace JsonApiDotNetCore.Models { - public class RelationshipAttribute : Attribute + public abstract class RelationshipAttribute : Attribute { protected RelationshipAttribute(string publicName) { @@ -16,13 +15,6 @@ protected RelationshipAttribute(string publicName) public bool IsHasMany { get { return this.GetType() == typeof(HasManyAttribute); } } public bool IsHasOne { get { return this.GetType() == typeof(HasOneAttribute); } } - public void SetValue(object entity, object newValue) - { - var propertyInfo = entity - .GetType() - .GetProperty(InternalRelationshipName); - - propertyInfo.SetValue(entity, newValue); - } + public abstract void SetValue(object entity, object newValue); } } From 0f2ba282edeb626fe345f18d25028ba675fbf141 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:37:21 -0500 Subject: [PATCH 3/6] feat(json-api-context): store a list of relationships to update when PATCHing entities, we need to know which relationships to also PATCH --- src/JsonApiDotNetCore/Services/IJsonApiContext.cs | 2 ++ src/JsonApiDotNetCore/Services/JsonApiContext.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index f7d05dc4b3..2860c3eb74 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Services { @@ -20,5 +21,6 @@ public interface IJsonApiContext PageManager PageManager { get; set; } IMetaBuilder MetaBuilder { get; set; } IGenericProcessorFactory GenericProcessorFactory { get; set; } + Dictionary RelationshipsToUpdate { get; set; } } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index f9bd3f0b0f..9cc244677d 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Services @@ -23,6 +24,7 @@ public JsonApiContext( Options = options; MetaBuilder = metaBuilder; GenericProcessorFactory = genericProcessorFactory; + RelationshipsToUpdate = new Dictionary(); } public JsonApiOptions Options { get; set; } @@ -36,6 +38,7 @@ public JsonApiContext( public PageManager PageManager { get; set; } public IMetaBuilder MetaBuilder { get; set; } public IGenericProcessorFactory GenericProcessorFactory { get; set; } + public Dictionary RelationshipsToUpdate { get; set; } public IJsonApiContext ApplyContext() { From 7f49ef914722013662b9079c18599a5aa27d54ea Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:38:22 -0500 Subject: [PATCH 4/6] feat(de-serializer): store which relationships should be updated --- src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 63187099ea..367904f8dd 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -141,12 +141,18 @@ private object _setHasOneRelationship(object entity, if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData)) { + var relationshipAttr = _jsonApiContext.RequestEntity.Relationships + .SingleOrDefault(r => r.PublicRelationshipName == relationshipName); + var data = (Dictionary)relationshipData.ExposedData; if (data == null) return entity; var newValue = data["id"]; var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType); + + _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + entityProperty.SetValue(entity, convertedValue); } From 3143e4ad61efa90e997ef1701518ddbdc0fb9089 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:38:54 -0500 Subject: [PATCH 5/6] feat(entity-repository): update relationship values --- src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 4c98f8cec8..6e18aebc96 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -105,9 +105,12 @@ public virtual async Task UpdateAsync(TId id, TEntity entity) attr.SetValue(oldEntity, attr.GetValue(entity)); }); + foreach(var relationship in _jsonApiContext.RelationshipsToUpdate) + relationship.Key.SetValue(oldEntity, relationship.Value); + await _context.SaveChangesAsync(); - return oldEntity; + return oldEntity; } public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds) From f6d5c34e649ffe7297d66cd6762af9b65d518586 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Mon, 20 Mar 2017 12:39:58 -0500 Subject: [PATCH 6/6] test(patch): use non-cached context otherwise the todoItem.OwnerId will be null from the cached version --- .../Acceptance/Spec/UpdatingDataTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index 5bfed72684..a5ad8f6a7a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -11,6 +11,7 @@ using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; @@ -124,7 +125,8 @@ public async Task Can_Patch_Entity_And_HasOne_Relationships() // Act var response = await client.SendAsync(request); - var updatedTodoItem = _context.TodoItems + var updatedTodoItem = _context.TodoItems.AsNoTracking() + .Include(t => t.Owner) .SingleOrDefault(t => t.Id == todoItem.Id); // Assert