From fdeef8e94b49707a0364faa9464009c60741038a Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 19 Jul 2018 21:47:28 -0700 Subject: [PATCH 1/3] fix(Deserializer): deserialize indpendent hasone pointers --- .../Serialization/JsonApiDeSerializer.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 5ad0a2b63d..acb20885a5 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -218,21 +218,21 @@ private object SetHasOneRelationship(object entity, if (foreignKeyProperty == null && rio == null) return entity; - if (foreignKeyProperty == null && rio != null) - throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'"); - - // e.g. PATCH /articles - // {... { "relationships":{ "Owner": { "data": null } } } } - if (rio == null && Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) == null) - throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type."); - - var newValue = rio?.Id ?? null; - var convertedValue = TypeHelper.ConvertType(newValue, foreignKeyProperty.PropertyType); - - _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + var foreignKeyPropertyValue = rio?.Id ?? null; + if (foreignKeyProperty != null) + { + // in the case of the HasOne independent side of the relationship, we should still create the shell entity on the other side + // we should not actually require the resource to have a foreign key (be the dependent side of the relationship) - foreignKeyProperty.SetValue(entity, convertedValue); + // e.g. PATCH /articles + // {... { "relationships":{ "Owner": { "data": null } } } } + if (rio == null && Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) == null) + throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type."); + var convertedValue = TypeHelper.ConvertType(foreignKeyPropertyValue, foreignKeyProperty.PropertyType); + foreignKeyProperty.SetValue(entity, convertedValue); + _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + } if (rio != null // if the resource identifier is null, there should be no reason to instantiate an instance From 2f5d684ec25a20f9469ec446c8ae1f9d5374d7a7 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 19 Jul 2018 21:59:25 -0700 Subject: [PATCH 2/3] chore(Deserializer): clean up --- .../Serialization/JsonApiDeSerializer.cs | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index acb20885a5..fc9f11d7e0 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -204,20 +204,21 @@ private object SetHasOneRelationship(object entity, if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData) == false) return entity; - var relationshipAttr = _jsonApiContext.RequestEntity.Relationships - .SingleOrDefault(r => r.PublicRelationshipName == relationshipName); - - if (relationshipAttr == null) - throw new JsonApiException(400, $"{_jsonApiContext.RequestEntity.EntityName} does not contain a relationship '{relationshipName}'"); - var rio = (ResourceIdentifierObject)relationshipData.ExposedData; var foreignKey = attr.IdentifiablePropertyName; var foreignKeyProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey); - if (foreignKeyProperty == null && rio == null) return entity; + SetHasOneForeignKeyValue(entity, attr, foreignKeyProperty, rio); + SetHasOneNavigationPropertyValue(entity, attr, rio, included); + + return entity; + } + + private void SetHasOneForeignKeyValue(object entity, HasOneAttribute hasOneAttr, PropertyInfo foreignKeyProperty, ResourceIdentifierObject rio) + { var foreignKeyPropertyValue = rio?.Id ?? null; if (foreignKeyProperty != null) { @@ -227,30 +228,35 @@ private object SetHasOneRelationship(object entity, // e.g. PATCH /articles // {... { "relationships":{ "Owner": { "data": null } } } } if (rio == null && Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) == null) - throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null because it is a non-nullable type."); + throw new JsonApiException(400, $"Cannot set required relationship identifier '{hasOneAttr.IdentifiablePropertyName}' to null because it is a non-nullable type."); var convertedValue = TypeHelper.ConvertType(foreignKeyPropertyValue, foreignKeyProperty.PropertyType); foreignKeyProperty.SetValue(entity, convertedValue); - _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + _jsonApiContext.RelationshipsToUpdate[hasOneAttr] = convertedValue; } + } - if (rio != null - // if the resource identifier is null, there should be no reason to instantiate an instance - && rio.Id != null) + /// + /// Sets the value of the navigation property for the related resource. + /// If the resource has been included, all attributes will be set. + /// If the resource has not been included, only the id will be set. + /// + private void SetHasOneNavigationPropertyValue(object entity, HasOneAttribute hasOneAttr, ResourceIdentifierObject rio, List included) + { + // if the resource identifier is null, there should be no reason to instantiate an instance + if (rio != null && rio.Id != null) { // we have now set the FK property on the resource, now we need to check to see if the // related entity was included in the payload and update its attributes - var includedRelationshipObject = GetIncludedRelationship(rio, included, relationshipAttr); + var includedRelationshipObject = GetIncludedRelationship(rio, included, hasOneAttr); if (includedRelationshipObject != null) - relationshipAttr.SetValue(entity, includedRelationshipObject); + hasOneAttr.SetValue(entity, includedRelationshipObject); // we need to store the fact that this relationship was included in the payload // for EF, the repository will use these pointers to make ensure we don't try to // create resources if they already exist, we just need to create the relationship - _jsonApiContext.HasOneRelationshipPointers.Add(attr, includedRelationshipObject); + _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, includedRelationshipObject); } - - return entity; } private object SetHasManyRelationship(object entity, From 8d6ef80bbe0f61d9e293a62329df850e13266815 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 19 Jul 2018 22:14:19 -0700 Subject: [PATCH 3/3] test(Deserializer): add test for independent id setting --- .../Models/ResourceIdentifierObject.cs | 9 ++++++++- .../Serialization/JsonApiDeSerializerTests.cs | 12 +++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs index 1ebab6c474..2cf4ef401e 100644 --- a/src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs +++ b/src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs @@ -4,9 +4,16 @@ namespace JsonApiDotNetCore.Models { public class ResourceIdentifierObject { + public ResourceIdentifierObject() { } + public ResourceIdentifierObject(string type, string id) + { + Type = type; + Id = id; + } + [JsonProperty("type")] public string Type { get; set; } - + [JsonProperty("id")] public string Id { get; set; } diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index 2da434c765..18d4b0ee6b 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -240,6 +240,7 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel jsonApiContextMock.SetupAllProperties(); jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers()); var jsonApiOptions = new JsonApiOptions(); jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); @@ -255,7 +256,14 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel Id = "1", Attributes = new Dictionary { { "property", property } }, // a common case for this is deserialization in unit tests - Relationships = new Dictionary { { "dependent", new RelationshipData { } } } + Relationships = new Dictionary { + { + "dependent", new RelationshipData + { + SingleData = new ResourceIdentifierObject("dependents", "1") + } + } + } } }; @@ -267,6 +275,8 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel // assert Assert.NotNull(result); Assert.Equal(property, result.Property); + Assert.NotNull(result.Dependent); + Assert.Equal(1, result.Dependent.Id); } [Fact]