Skip to content

fix(Deserializer): deserialize indpendent hasone pointers #348

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/JsonApiDotNetCore/Models/ResourceIdentifierObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
58 changes: 32 additions & 26 deletions src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
/// <summary>
/// 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.
/// </summary>
private void SetHasOneNavigationPropertyValue(object entity, HasOneAttribute hasOneAttr, ResourceIdentifierObject rio, List<DocumentData> 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,
Expand Down
12 changes: 11 additions & 1 deletion test/UnitTests/Serialization/JsonApiDeSerializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AttrAttribute, object>());
jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers());

var jsonApiOptions = new JsonApiOptions();
jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions);
Expand All @@ -255,7 +256,14 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel
Id = "1",
Attributes = new Dictionary<string, object> { { "property", property } },
// a common case for this is deserialization in unit tests
Relationships = new Dictionary<string, RelationshipData> { { "dependent", new RelationshipData { } } }
Relationships = new Dictionary<string, RelationshipData> {
{
"dependent", new RelationshipData
{
SingleData = new ResourceIdentifierObject("dependents", "1")
}
}
}
}
};

Expand All @@ -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]
Expand Down