private void AssertSameIdentityInRefData(AtomicOperationObject operation, ResourceIdentifierObject resourceIdentifierObject) { if (operation.Ref.Id != null && resourceIdentifierObject.Id != null && resourceIdentifierObject.Id != operation.Ref.Id) { throw new JsonApiSerializationException("Resource ID mismatch between 'ref.id' and 'data.id' element.", $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentifierObject.Id}'.", atomicOperationIndex: AtomicOperationIndex); } if (operation.Ref.Lid != null && resourceIdentifierObject.Lid != null && resourceIdentifierObject.Lid != operation.Ref.Lid) { throw new JsonApiSerializationException("Resource local ID mismatch between 'ref.lid' and 'data.lid' element.", $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentifierObject.Lid}'.", atomicOperationIndex: AtomicOperationIndex); } if (operation.Ref.Id != null && resourceIdentifierObject.Lid != null) { throw new JsonApiSerializationException("Resource identity mismatch between 'ref.id' and 'data.lid' element.", $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentifierObject.Lid}' in 'data.lid'.", atomicOperationIndex: AtomicOperationIndex); } if (operation.Ref.Lid != null && resourceIdentifierObject.Id != null) { throw new JsonApiSerializationException("Resource identity mismatch between 'ref.lid' and 'data.id' element.", $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentifierObject.Id}' in 'data.id'.", atomicOperationIndex: AtomicOperationIndex); } }
private void AssertElementHasType(ResourceIdentifierObject resourceIdentifierObject, string elementPath) { if (resourceIdentifierObject.Type == null) { throw new JsonApiSerializationException($"The '{elementPath}.type' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } }
private void AssertHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, RelationshipAttribute relationship) { if (AtomicOperationIndex != null) { bool hasNone = resourceIdentifierObject.Id == null && resourceIdentifierObject.Lid == null; bool hasBoth = resourceIdentifierObject.Id != null && resourceIdentifierObject.Lid != null; if (hasNone || hasBoth) { throw new JsonApiSerializationException("Request body must include 'id' or 'lid' element.", $"Expected 'id' or 'lid' element in '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } } else { if (resourceIdentifierObject.Id == null) { throw new JsonApiSerializationException("Request body must include 'id' element.", $"Expected 'id' element in '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } AssertHasNoLid(resourceIdentifierObject); } }
private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List <DocumentData> includedResources, RelationshipAttribute relationshipAttr) { // at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ContextGraph var relatedInstance = relationshipAttr.Type.New <IIdentifiable>(); relatedInstance.StringId = relatedResourceIdentifier.Id; // can't provide any more data other than the rio since it is not contained in the included section if (includedResources == null || includedResources.Count == 0) { return(relatedInstance); } var includedResource = GetLinkedResource(relatedResourceIdentifier, includedResources); if (includedResource == null) { return(relatedInstance); } var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationshipAttr.Type); if (contextEntity == null) { throw new JsonApiException(400, $"Included type '{relationshipAttr.Type}' is not a registered json:api resource."); } SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes); return(relatedInstance); }
private void AssertHasNoLid(ResourceIdentifierObject resourceIdentifierObject) { if (resourceIdentifierObject.Lid != null) { throw new JsonApiSerializationException("Local IDs cannot be used at this endpoint.", null, atomicOperationIndex: AtomicOperationIndex); } }
/// <summary> /// Additional processing required for client deserialization, responsible for parsing the <see cref="Document.Included" /> property. When a relationship /// value is parsed, it goes through the included list to set its attributes and relationships. /// </summary> /// <param name="resource"> /// The resource that was constructed from the document's body. /// </param> /// <param name="field"> /// The metadata for the exposed field. /// </param> /// <param name="data"> /// Relationship data for <paramref name="resource" />. Is null when <paramref name="field" /> is not a <see cref="RelationshipAttribute" />. /// </param> protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(field, nameof(field)); // Client deserializers do not need additional processing for attributes. if (field is AttrAttribute) { return; } // if the included property is empty or absent, there is no additional data to be parsed. if (Document.Included.IsNullOrEmpty()) { return; } if (data != null) { if (field is HasOneAttribute hasOneAttr) { // add attributes and relationships of a parsed HasOne relationship ResourceIdentifierObject rio = data.SingleData; hasOneAttr.SetValue(resource, rio == null ? null : ParseIncludedRelationship(rio)); } else if (field is HasManyAttribute hasManyAttr) { // add attributes and relationships of a parsed HasMany relationship IEnumerable <IIdentifiable> items = data.ManyData.Select(ParseIncludedRelationship); IEnumerable values = CollectionConverter.CopyToTypedCollection(items, hasManyAttr.Property.PropertyType); hasManyAttr.SetValue(resource, values); } } }
private void AssertHasId(ResourceIdentifierObject resourceIdentifierObject, RelationshipAttribute relationship) { if (resourceIdentifierObject.Id == null) { throw new JsonApiSerializationException("Request body must include 'id' element.", $"Expected 'id' element in '{relationship.PublicName}' relationship."); } }
private void AssertHasType(ResourceIdentifierObject resourceIdentifierObject, RelationshipAttribute relationship) { if (resourceIdentifierObject.Type == null) { var details = relationship != null ? $"Expected 'type' element in '{relationship.PublicName}' relationship." : "Expected 'type' element in 'data' element."; throw new JsonApiSerializationException("Request body must include 'type' element.", details); } }
private void AssertElementHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, string elementPath, bool isRequired) { bool hasNone = resourceIdentifierObject.Id == null && resourceIdentifierObject.Lid == null; bool hasBoth = resourceIdentifierObject.Id != null && resourceIdentifierObject.Lid != null; if (isRequired ? hasNone || hasBoth : hasBoth) { throw new JsonApiSerializationException($"The '{elementPath}.id' or '{elementPath}.lid' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } }
private DocumentData GetLinkedResource(ResourceIdentifierObject relatedResourceIdentifier, List <DocumentData> includedResources) { try { return(includedResources.SingleOrDefault(r => r.Type == relatedResourceIdentifier.Type && r.Id == relatedResourceIdentifier.Id)); } catch (InvalidOperationException e) { throw new JsonApiException(400, $"A compound document MUST NOT include more than one resource object for each type and id pair." + $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e); } }
private ResourceObject GetLinkedResource(ResourceIdentifierObject relatedResourceIdentifier) { try { return(_document.Included.SingleOrDefault(r => r.Type == relatedResourceIdentifier.Type && r.Id == relatedResourceIdentifier.Id)); } catch (InvalidOperationException e) { throw new InvalidOperationException("A compound document MUST NOT include more than one resource object for each type and id pair." + $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e); } }
private void AssertCompatibleId(ResourceIdentifierObject resourceIdentifierObject, Type idType) { if (resourceIdentifierObject.Id != null) { try { RuntimeTypeConverter.ConvertType(resourceIdentifierObject.Id, idType); } catch (FormatException exception) { throw new JsonApiSerializationException(null, exception.Message, null, AtomicOperationIndex); } } }
public void Setting_ExposeData_To_RIO_Sets_SingleData() { // Arrange var relationshipData = new RelationshipEntry(); var relationship = new ResourceIdentifierObject { Id = "9", Type = "authors" }; // Act relationshipData.Data = relationship; // Assert Assert.NotNull(relationshipData.SingleData); Assert.Equal("authors", relationshipData.SingleData.Type); Assert.Equal("9", relationshipData.SingleData.Id); Assert.False(relationshipData.IsManyData); }
private IIdentifiable CreateRightResource(RelationshipAttribute relationship, ResourceIdentifierObject resourceIdentifierObject) { if (resourceIdentifierObject != null) { AssertHasType(resourceIdentifierObject, relationship); AssertHasId(resourceIdentifierObject, relationship); var rightResourceContext = GetExistingResourceContext(resourceIdentifierObject.Type); AssertRightTypeIsCompatible(rightResourceContext, relationship); var rightInstance = ResourceFactory.CreateInstance(rightResourceContext.ResourceType); rightInstance.StringId = resourceIdentifierObject.Id; return(rightInstance); } return(null); }
private IIdentifiable GetIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier, List <ResourceObject> includedResources, RelationshipAttribute relationshipAttr, List <string> includePaths = null) { // at this point we can be sure the relationshipAttr.Type is IIdentifiable because we were able to successfully build the ResourceGraph var relatedInstance = relationshipAttr.DependentType.New <IIdentifiable>(); relatedInstance.StringId = relatedResourceIdentifier.Id; // can't provide any more data other than the rio since it is not contained in the included section if (includedResources == null || includedResources.Count == 0) { return(relatedInstance); } var includedResource = GetLinkedResource(relatedResourceIdentifier, includedResources); if (includedResource == null) { return(relatedInstance); } var contextEntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationshipAttr.DependentType); if (contextEntity == null) { throw new JsonApiException(400, $"Included type '{relationshipAttr.DependentType}' is not a registered json:api resource."); } SetEntityAttributes(relatedInstance, contextEntity, includedResource.Attributes); var tmp = includedResources.FirstOrDefault(x => x.Id.Equals(relatedResourceIdentifier.Id) && x.Type.Equals(relatedResourceIdentifier.Type)); if (tmp != null) { relatedInstance = SetRelationships(relatedInstance, contextEntity, tmp.Relationships, includedResources, includePaths) as IIdentifiable; } return(relatedInstance); }
/// <summary> /// Searches for and parses the included relationship. /// </summary> private IIdentifiable ParseIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier) { var relatedResourceContext = ResourceContextProvider.GetResourceContext(relatedResourceIdentifier.Type); if (relatedResourceContext == null) { throw new InvalidOperationException($"Included type '{relatedResourceIdentifier.Type}' is not a registered JSON:API resource."); } var relatedInstance = ResourceFactory.CreateInstance(relatedResourceContext.ResourceType); relatedInstance.StringId = relatedResourceIdentifier.Id; var includedResource = GetLinkedResource(relatedResourceIdentifier); if (includedResource != null) { SetAttributes(relatedInstance, includedResource.Attributes, relatedResourceContext.Attributes); SetRelationships(relatedInstance, includedResource.Relationships, relatedResourceContext.Relationships); } return(relatedInstance); }
protected RelationshipEntry CreateRelationshipData(string relatedType = null, bool isToManyData = false, string id = "10") { var entry = new RelationshipEntry(); ResourceIdentifierObject rio = relatedType == null ? null : new ResourceIdentifierObject { Id = id, Type = relatedType }; if (isToManyData) { entry.Data = relatedType != null?rio.AsList() : new List <ResourceIdentifierObject>(); } else { entry.Data = rio; } return(entry); }
public void BuildIncluded_DeeplyNestedCircularChainOfSingleData_CanBuild() { // Arrange (Article article, Person author, _, Person reviewer, _) = GetAuthorChainInstances(); IReadOnlyCollection <RelationshipAttribute> authorChain = GetIncludedRelationshipsChain("author.blogs.reviewer.favoriteFood"); IncludedResourceObjectBuilder builder = GetBuilder(); // Act builder.IncludeRelationshipChain(authorChain, article); IList <ResourceObject> result = builder.Build(); // Assert Assert.Equal(6, result.Count); ResourceObject authorResourceObject = result.Single(ro => ro.Type == "people" && ro.Id == author.StringId); ResourceIdentifierObject authorFoodRelation = authorResourceObject.Relationships["favoriteFood"].SingleData; Assert.Equal(author.FavoriteFood.StringId, authorFoodRelation.Id); ResourceObject reviewerResourceObject = result.Single(ro => ro.Type == "people" && ro.Id == reviewer.StringId); ResourceIdentifierObject reviewerFoodRelation = reviewerResourceObject.Relationships["favoriteFood"].SingleData; Assert.Equal(reviewer.FavoriteFood.StringId, reviewerFoodRelation.Id); }
private bool HasLocalId(ResourceIdentifierObject rio) => string.IsNullOrEmpty(rio.LocalId) == false;
/// <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, hasOneAttr); if (includedRelationshipObject != null) { 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(hasOneAttr, includedRelationshipObject); } }
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) // 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; } }
/// <summary> /// Searches for and parses the included relationship /// </summary> private IIdentifiable ParseIncludedRelationship(RelationshipAttribute relationshipAttr, ResourceIdentifierObject relatedResourceIdentifier) { var relatedInstance = relationshipAttr.RightType.New <IIdentifiable>(); relatedInstance.StringId = relatedResourceIdentifier.Id; var includedResource = GetLinkedResource(relatedResourceIdentifier); if (includedResource == null) { return(relatedInstance); } var resourceContext = _provider.GetResourceContext(relatedResourceIdentifier.Type); if (resourceContext == null) { throw new InvalidOperationException($"Included type '{relationshipAttr.RightType}' is not a registered json:api resource."); } SetAttributes(relatedInstance, includedResource.Attributes, resourceContext.Attributes); SetRelationships(relatedInstance, includedResource.Relationships, resourceContext.Relationships); return(relatedInstance); }
/// <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 <ResourceObject> 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, hasOneAttr); if (includedRelationshipObject != null) { hasOneAttr.SetValue(entity, includedRelationshipObject); } /// todo: as a part of the process of decoupling JADNC (specifically /// through the decoupling IJsonApiContext), we now no longer need to /// store the updated relationship values in this property. For now /// just assigning null as value, will remove this property later as a whole. /// see #512 _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null); } }
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) // e.g. PATCH /articles // {... { "relationships":{ "Owner": { "data": null } } } } bool foreignKeyPropertyIsNullableType = Nullable.GetUnderlyingType(foreignKeyProperty.PropertyType) != null || foreignKeyProperty.PropertyType == typeof(string); if (rio == null && !foreignKeyPropertyIsNullableType) { 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); /// todo: as a part of the process of decoupling JADNC (specifically /// through the decoupling IJsonApiContext), we now no longer need to /// store the updated relationship values in this property. For now /// just assigning null as value, will remove this property later as a whole. /// see #512 if (convertedValue == null) { _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null); } } }