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/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 5ad0a2b63d..fc9f11d7e0 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -204,53 +204,59 @@ 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; - 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."); + SetHasOneForeignKeyValue(entity, attr, foreignKeyProperty, rio); + SetHasOneNavigationPropertyValue(entity, attr, rio, included); - var newValue = rio?.Id ?? null; - var convertedValue = TypeHelper.ConvertType(newValue, foreignKeyProperty.PropertyType); + return entity; + } - _jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue; + private void SetHasOneForeignKeyValue(object entity, HasOneAttribute hasOneAttr, PropertyInfo foreignKeyProperty, ResourceIdentifierObject rio) + { + 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 '{hasOneAttr.IdentifiablePropertyName}' to null because it is a non-nullable type."); + var convertedValue = TypeHelper.ConvertType(foreignKeyPropertyValue, foreignKeyProperty.PropertyType); + foreignKeyProperty.SetValue(entity, 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, 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]