/// <summary> /// Add is used by various operations (eg: add, copy, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private void Add(string path, object value, T objectToApplyTo, Operation <T> operationToReport) { // add, in this implementation, does not just "add" properties - that's // technically impossible; It can however be used to add items to arrays, // or to replace values. // first up: if the path ends in a numeric value, we're inserting in a list and // that value represents the position; if the path ends in "-", we're appending // to the list. // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( path, objectToApplyTo, operationToReport, true); var appendList = pathResult.ExecuteAtEnd; var positionAsInteger = pathResult.NumericEnd; var actualPathToProperty = pathResult.PathToProperty; var result = new ObjectTreeAnalysisResult(objectToApplyTo, actualPathToProperty, ContractResolver); if (!result.IsValidPathForAdd) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: the provided path is invalid: {0}.", path)), 422); } // If it' an array, add to that array. If it's not, we replace. // is the path an array (but not a string (= char[]))? In this case, // the path must end with "/position" or "/-", which we already determined before. var patchProperty = result.JsonPatchProperty; if (appendList || positionAsInteger > -1) { if (PropertyHelpers.IsNonStringList(patchProperty.Property.PropertyType)) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value); if (!conversionResult.CanBeConverted) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided value is invalid for array property type at location path: {0}", path)), 422); } if (patchProperty.Property.Readable) { var array = (IList)patchProperty.Property.ValueProvider .GetValue(patchProperty.Parent); if (array == null) { array = (IList)Activator.CreateInstance(typeof(List <>).MakeGenericType(genericTypeOfArray)); patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, array); } if (appendList) { SetValueEventArgs eArg = new SetValueEventArgs(operationToReport) { Cancel = false, Index = array.Count + 1, NewValue = conversionResult.ConvertedInstance, ParentObject = patchProperty.Parent, Property = patchProperty.Property }; InvokeWithBeforeSetValue(eArg, () => array.Add(eArg.NewValue)); } else { // specified index must not be greater than the amount of items in the // array if (positionAsInteger <= array.Count) { SetValueEventArgs eArg = new SetValueEventArgs(operationToReport) { Cancel = false, Index = positionAsInteger, NewValue = conversionResult.ConvertedInstance, ParentObject = patchProperty.Parent, Property = patchProperty.Property }; InvokeWithBeforeSetValue(eArg, () => array.Insert(positionAsInteger, eArg.NewValue)); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)), 422); } } } else { // cannot read the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot get property value at path {0}. Possible cause: the property doesn't have an accessible getter.", path)), 422); } } else if (PropertyHelpers.IsNonStringCollection(patchProperty.Property.PropertyType)) { // we need to do a little work with collections here. All generic collections have an "Add" operation. In this case, we should ONLY // support adding to the end. if (!(positionAsInteger == -1 || appendList)) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: can only insert at end of array when target type is ICollection", path)), 422); } var genericTypeOfCollection = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); ConversionResult conversionResult;// = PropertyHelpers.ConvertToActualType(genericTypeOfCollection, value); if (!genericTypeOfCollection.IsValueType && genericTypeOfCollection != typeof(string) && value != null && value.GetType() == typeof(string)) { object newObj; if (CustomDeserializationHandler != null) { newObj = CustomDeserializationHandler(value.ToString(), genericTypeOfCollection); } else { newObj = JsonConvert.DeserializeObject(value.ToString(), genericTypeOfCollection); } conversionResult = new ConversionResult(true, newObj); } else if (value != null && genericTypeOfCollection.IsAssignableFrom(value.GetType())) { conversionResult = new ConversionResult(true, value); } else { conversionResult = PropertyHelpers.ConvertToActualType( genericTypeOfCollection, value); } if (!conversionResult.CanBeConverted) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided value is invalid for array property type at location path: {0}", path)), 422); } if (patchProperty.Property.Readable) { var array = patchProperty.Property.ValueProvider .GetValue(patchProperty.Parent); if (array == null) { // the array was null. This might mean we are dealing with a fresh object. This is okay. Lets initialize it with a basic collection. array = Activator.CreateInstance(typeof(Collection <>).MakeGenericType(genericTypeOfCollection)); patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, array); } var addMethod = patchProperty.Property.PropertyType.GetMethod("Add"); SetValueEventArgs eArg = new SetValueEventArgs(operationToReport) { Cancel = false, Index = -1, NewValue = conversionResult.ConvertedInstance, ParentObject = patchProperty.Parent, Property = patchProperty.Property }; InvokeWithBeforeSetValue(eArg, () => addMethod.Invoke(array, new object[] { eArg.NewValue })); } else { // cannot read the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot get property value at path {0}. Possible cause: the property doesn't have an accessible getter.", path)), 422); } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", path)), 422); } } else { Type propType = patchProperty.Property.PropertyType; ConversionResult conversionResultTuple; if (!propType.IsValueType && propType != typeof(string) && value != null && value.GetType() == typeof(string)) { object newObj; if (CustomDeserializationHandler != null) { newObj = CustomDeserializationHandler(value.ToString(), propType); } else { newObj = JsonConvert.DeserializeObject(value.ToString(), propType); } conversionResultTuple = new ConversionResult(true, newObj); } else if (value != null && patchProperty.Property.PropertyType.IsAssignableFrom(value.GetType())) { conversionResultTuple = new ConversionResult(true, value); } else { conversionResultTuple = PropertyHelpers.ConvertToActualType( patchProperty.Property.PropertyType, value); } if (conversionResultTuple.CanBeConverted) { if (patchProperty.Property.Writable) { SetValueEventArgs eArg = new SetValueEventArgs(operationToReport) { Cancel = false, NewValue = conversionResultTuple.ConvertedInstance, ParentObject = patchProperty.Parent, Property = patchProperty.Property }; InvokeWithBeforeSetValue(eArg, () => patchProperty.Property.ValueProvider.SetValue( eArg.ParentObject, eArg.NewValue)); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property at path location cannot be set: {0}. Possible causes: the property may not have an accessible setter, or the property may be part of an anonymous object (and thus cannot be changed after initialization).", path)), 422); } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property value cannot be converted to type of path location {0}.", path)), 422); } } }
/// <summary> /// Remove is used by various operations (eg: remove, move, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private RemovedPropertyTypeResult Remove(string path, T objectToApplyTo, Operation <T> operationToReport) { // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( path, objectToApplyTo, operationToReport, false); var removeFromList = pathResult.ExecuteAtEnd; var positionAsInteger = pathResult.NumericEnd; var actualPathToProperty = pathResult.PathToProperty; var expressionToUse = pathResult.ExpressionEnd; var result = new ObjectTreeAnalysisResult(objectToApplyTo, actualPathToProperty, ContractResolver); if (!result.IsValidPathForRemove) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: the provided path is invalid: {0}.", path)), 422); } var patchProperty = result.JsonPatchProperty; if (removeFromList || positionAsInteger > -1) { if (expressionToUse != null) { // we have an expression. var elementType = PropertyHelpers.GetCollectionType(patchProperty.Property.PropertyType); object coll = patchProperty.Property.ValueProvider .GetValue(patchProperty.Parent); var remMethod = typeof(ICollection <>).MakeGenericType(elementType).GetMethod("Remove"); var objToRemove = ExpressionHelpers.GetElementAtFromObjectExpression(coll, expressionToUse); var remResult = remMethod.Invoke(coll, new object[] { objToRemove }); if ((bool)remResult != true) { throw new Exception("D'oh"); } return(new RemovedPropertyTypeResult(elementType, false)); } else if (PropertyHelpers.IsNonStringList(patchProperty.Property.PropertyType)) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); if (patchProperty.Property.Readable) { var array = (IList)patchProperty.Property.ValueProvider .GetValue(patchProperty.Parent); if (removeFromList) { if (array.Count == 0) { // if the array is empty, we should throw an error throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)), 422); } SetValueEventArgs eArg = new SetValueEventArgs(operationToReport) { Cancel = false, //NewValue = conversionResultTuple.ConvertedInstance, ParentObject = patchProperty.Parent, Property = patchProperty.Property, Index = array.Count - 1, }; InvokeWithBeforeSetValue(eArg, () => array.RemoveAt(eArg.Index.Value)); // return the type of the value that has been removed return(new RemovedPropertyTypeResult(genericTypeOfArray, false)); } else { if (positionAsInteger >= array.Count) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)), 422); } SetValueEventArgs eArg = new SetValueEventArgs(operationToReport) { Cancel = false, //NewValue = conversionResultTuple.ConvertedInstance, ParentObject = patchProperty.Parent, Property = patchProperty.Property, Index = positionAsInteger }; InvokeWithBeforeSetValue(eArg, () => array.RemoveAt(eArg.Index.Value)); // return the type of the value that has been removed return(new RemovedPropertyTypeResult(genericTypeOfArray, false)); } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot get property value at path {0}. Possible cause: the property doesn't have an accessible getter.", path)), 422); } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array.", path)), 422); } } else { if (!patchProperty.Property.Writable) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property at path location cannot be set: {0}. Possible causes: the property may not have an accessible setter, or the property may be part of an anonymous object (and thus cannot be changed after initialization).", path)), 422); } // set value to null, or for non-nullable value types, to its default value. object value = null; if (patchProperty.Property.PropertyType.GetType().IsValueType && Nullable.GetUnderlyingType(patchProperty.Property.PropertyType) == null) { value = Activator.CreateInstance(patchProperty.Property.PropertyType); } // check if it can be converted. var conversionResultTuple = PropertyHelpers.ConvertToActualType( patchProperty.Property.PropertyType, value); SetValueEventArgs eArg = new SetValueEventArgs(operationToReport) { Cancel = false, //NewValue = conversionResultTuple.ConvertedInstance, ParentObject = patchProperty.Parent, Property = patchProperty.Property, IsRemoveStepOfReplace = operationToReport.OperationType == OperationType.Replace }; if (!conversionResultTuple.CanBeConverted) { // conversion failed, so use reflection (somewhat slower) to // create a new default instance of the property type to set as value eArg.NewValue = Activator.CreateInstance(patchProperty.Property.PropertyType); //patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, // Activator.CreateInstance(patchProperty.Property.PropertyType)); //return new RemovedPropertyTypeResult(patchProperty.Property.PropertyType, false); } else { eArg.NewValue = conversionResultTuple.ConvertedInstance; } InvokeWithBeforeSetValue(eArg, () => patchProperty.Property.ValueProvider.SetValue(eArg.ParentObject, eArg.NewValue)); return(new RemovedPropertyTypeResult(patchProperty.Property.PropertyType, false)); } }
/// <summary> /// The "copy" operation copies the value at a specified location to the /// target location. /// /// The operation object MUST contain a "from" member, which is a string /// containing a JSON Pointer value that references the location in the /// target document to copy the value from. /// /// The "from" location MUST exist for the operation to be successful. /// /// For example: /// /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } /// /// This operation is functionally identical to an "add" operation at the /// target location using the value specified in the "from" member. /// /// Note: even though it's the same functionally, we do not call add with /// the value specified in from for performance reasons (multiple checks of same requirements). /// </summary> /// <param name="operation">The copy operation</param> /// <param name="objectApplyTo">Object to apply the operation to</param> public void Copy(Operation <T> operation, T objectToApplyTo) { // get value at from location object valueAtFromLocation = null; var positionAsInteger = -1; var actualFromProperty = operation.from; positionAsInteger = PropertyHelpers.GetNumericEnd(operation.from); if (positionAsInteger > -1) { actualFromProperty = operation.from.Substring(0, operation.from.IndexOf('/' + positionAsInteger.ToString())); } var patchProperty = PropertyHelpers .FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver); // does property at from exist? CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from); // get the property path // is the path an array (but not a string (= char[]))? In this case, // the path must end with "/position" or "/-", which we already determined before. if (positionAsInteger > -1) { if (IsNonStringArray(patchProperty)) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); // get value (it can be cast, we just checked that) var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); if (array.Count <= positionAsInteger) { throw new JsonPatchException <T>(operation, string.Format("Patch failed: provided from path is invalid for array property type at " + "location from: {0}: invalid position", operation.from), objectToApplyTo, 422); } valueAtFromLocation = array[positionAsInteger]; } else { throw new JsonPatchException <T>(operation, string.Format("Patch failed: provided from path is invalid for array property type at " + "location from: {0}: expected array", operation.from), objectToApplyTo, 422); } } else { // no list, just get the value // set the new value valueAtFromLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); } // add operation to target location with that value. Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); }
/// <summary> /// Add is used by various operations (eg: add, copy, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private void Add(string path, object value, T objectToApplyTo, Operation <T> operationToReport) { // add, in this implementation, does not just "add" properties - that's // technically impossible; It can however be used to add items to arrays, // or to replace values. // first up: if the path ends in a numeric value, we're inserting in a list and // that value represents the position; if the path ends in "-", we're appending // to the list. var appendList = false; var positionAsInteger = -1; var actualPathToProperty = path; if (path.EndsWith("/-")) { appendList = true; actualPathToProperty = path.Substring(0, path.Length - 2); } else { positionAsInteger = PropertyHelpers.GetNumericEnd(path); if (positionAsInteger > -1) { actualPathToProperty = path.Substring(0, path.IndexOf('/' + positionAsInteger.ToString())); } } var patchProperty = PropertyHelpers .FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver); // does property at path exist? CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path); // it exists. If it' an array, add to that array. If it's not, we replace. // is the path an array (but not a string (= char[]))? In this case, // the path must end with "/position" or "/-", which we already determined before. if (appendList || positionAsInteger > -1) { // what if it's an array but there's no position?? if (IsNonStringArray(patchProperty)) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType( patchProperty.Property.PropertyType); var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value); CheckIfPropertyCanBeSet(conversionResult, objectToApplyTo, operationToReport, path); // get value (it can be cast, we just checked that) var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); if (appendList) { array.Add(conversionResult.ConvertedInstance); } else { // specified index must not be greater than the amount of items in the array if (positionAsInteger <= array.Count) { array.Insert(positionAsInteger, conversionResult.ConvertedInstance); } else { throw new JsonPatchException <T>(operationToReport, string.Format("Patch failed: provided path is invalid for array property type at " + "location path: {0}: position larger than array size", path, 422), objectToApplyTo); } } } else { throw new JsonPatchException <T>(operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location " + "path: {0}: expected array", path), objectToApplyTo, 422); } } else { var conversionResultTuple = PropertyHelpers.ConvertToActualType( patchProperty.Property.PropertyType, value); // Is conversion successful CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operationToReport, path); patchProperty.Property.ValueProvider.SetValue( patchProperty.Parent, conversionResultTuple.ConvertedInstance); } }
/// <summary> /// Remove is used by various operations (eg: remove, move, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private void Remove(string path, T objectToApplyTo, Operation <T> operationToReport) { var removeFromList = false; var positionAsInteger = -1; var actualPathToProperty = path; if (path.EndsWith("/-")) { removeFromList = true; actualPathToProperty = path.Substring(0, path.Length - 2); } else { positionAsInteger = PropertyHelpers.GetNumericEnd(path); if (positionAsInteger > -1) { actualPathToProperty = path.Substring(0, path.IndexOf('/' + positionAsInteger.ToString())); } } var patchProperty = PropertyHelpers .FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver); // does the target location exist? CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path); // get the property, and remove it - in this case, for DTO's, that means setting // it to null or its default value; in case of an array, remove at provided index // or at the end. if (removeFromList || positionAsInteger > -1) { // what if it's an array but there's no position?? if (IsNonStringArray(patchProperty)) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); // get value (it can be cast, we just checked that) var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); if (removeFromList) { array.RemoveAt(array.Count - 1); } else { if (positionAsInteger < array.Count) { array.RemoveAt(positionAsInteger); } else { throw new JsonPatchException <T>(operationToReport, string.Format("Patch failed: provided path is invalid for array property type at " + "location path: {0}: position larger than array size", path), objectToApplyTo, 422); } } } else { throw new JsonPatchException <T>(operationToReport, string.Format("Patch failed: provided path is invalid for array property type at " + "location path: {0}: expected array", path), objectToApplyTo, 422); } } else { // setting the value to "null" will use the default value in case of value types, and // null in case of reference types object value = null; if (patchProperty.Property.PropertyType.GetTypeInfo().IsValueType && Nullable.GetUnderlyingType(patchProperty.Property.PropertyType) == null) { value = Activator.CreateInstance(patchProperty.Property.PropertyType); } patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, value); } }
/// <summary> /// The "test" operation tests that a value at the target location is /// equal to a specified value. /// /// The operation object MUST contain a "value" member that conveys the /// value to be compared to the target location's value. /// /// The target location MUST be equal to the "value" value for the /// operation to be considered successful. /// /// Here, "equal" means that the value at the target location and the /// value conveyed by "value" are of the same JSON type, and that they /// are considered equal by the following rules for that type: /// /// o strings: are considered equal if they contain the same number of /// Unicode characters and their code points are byte-by-byte equal. /// /// o numbers: are considered equal if their values are numerically /// equal. /// /// o arrays: are considered equal if they contain the same number of /// values, and if each value can be considered equal to the value at /// the corresponding position in the other array, using this list of /// type-specific rules. /// /// o objects: are considered equal if they contain the same number of /// members, and if each member can be considered equal to a member in /// the other object, by comparing their keys (as strings) and their /// values (using this list of type-specific rules). /// /// o literals (false, true, and null): are considered equal if they are /// the same. /// /// Note that the comparison that is done is a logical comparison; e.g., /// whitespace between the member values of an array is not significant. /// /// Also, note that ordering of the serialization of object members is /// not significant. /// /// Note that we divert from the rules here - we use .NET's comparison, /// not the one above. In a future version, a "strict" setting might /// be added (configurable), that takes into account above rules. /// /// For example: /// /// { "op": "test", "path": "/a/b/c", "value": "foo" } /// </summary> /// <param name="operation">The test operation</param> /// <param name="objectApplyTo">Object to apply the operation to</param> public void Test(Operation <T> operation, T objectToApplyTo) { // get value at path location object valueAtPathLocation = null; var positionInPathAsInteger = -1; var actualPathProperty = operation.path; positionInPathAsInteger = PropertyHelpers.GetNumericEnd(operation.path); if (positionInPathAsInteger > -1) { actualPathProperty = operation.path.Substring(0, operation.path.IndexOf('/' + positionInPathAsInteger.ToString())); } var patchProperty = PropertyHelpers .FindPropertyAndParent(objectToApplyTo, actualPathProperty, ContractResolver); // does property at path exist? CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.path); // get the property path Type typeOfFinalPropertyAtPathLocation; // is the path an array (but not a string (= char[]))? In this case, // the path must end with "/position" or "/-", which we already determined before. if (positionInPathAsInteger > -1) { if (IsNonStringArray(patchProperty)) { // now, get the generic type of the enumerable typeOfFinalPropertyAtPathLocation = PropertyHelpers .GetEnumerableType(patchProperty.Property.PropertyType); // get value (it can be cast, we just checked that) var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); if (array.Count <= positionInPathAsInteger) { throw new JsonPatchException <T>(operation, string.Format("Patch failed: provided from path is invalid for array property type at " + "location path: {0}: invalid position", operation.path), objectToApplyTo, 422); } valueAtPathLocation = array[positionInPathAsInteger]; } else { throw new JsonPatchException <T>(operation, string.Format("Patch failed: provided from path is invalid for array property type at " + "location path: {0}: expected array", operation.path), objectToApplyTo, 422); } } else { // no list, just get the value valueAtPathLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); typeOfFinalPropertyAtPathLocation = patchProperty.Property.PropertyType; } var conversionResultTuple = PropertyHelpers.ConvertToActualType( typeOfFinalPropertyAtPathLocation, operation.value); // Is conversion successful CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operation, operation.path); //Compare }
/// <summary> /// Add is used by various operations (eg: add, copy, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private void Add(string path, object value, T objectToApplyTo, Operation <T> operationToReport) { // add, in this implementation, does not just "add" properties - that's // technically impossible; It can however be used to add items to arrays, // or to replace values. // first up: if the path ends in a numeric value, we're inserting in a list and // that value represents the position; if the path ends in "-", we're appending // to the list. // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( path, objectToApplyTo, operationToReport, true); var appendList = pathResult.ExecuteAtEnd; var positionAsInteger = pathResult.NumericEnd; var actualPathToProperty = pathResult.PathToProperty; var pathProperty = PropertyHelpers .FindProperty(objectToApplyTo, actualPathToProperty); // does property at path exist? if (pathProperty == null) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property at location path: {0} does not exist", path)) , 422); } // it exists. If it' an array, add to that array. If it's not, we replace. // is the path an array (but not a string (= char[]))? In this case, // the path must end with "/position" or "/-", which we already determined before. if (appendList || positionAsInteger > -1) { // what if it's an array but there's no position?? if (pathProperty.PropertyType.IsNonStringList()) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType); var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value); if (!conversionResult.CanBeConverted) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided value is invalid for array property type at location path: {0}", path)) , 422); } if (!pathProperty.CanRead) { // cannot get the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot get property value at location from: {0}. Possible cause: the property doesn't have an accessible getter.", path)), 422); } // get value (it can be cast, we just checked that) var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList; if (appendList) { array.Add(conversionResult.ConvertedInstance); } else { // specified index must not be greater than the amount of items in the // array if (positionAsInteger <= array.Count) { array.Insert(positionAsInteger, conversionResult.ConvertedInstance); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)) , 422); } } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", path)) , 422); } } else { var conversionResultTuple = PropertyHelpers.ConvertToActualType(pathProperty.PropertyType, value); // conversion successful if (conversionResultTuple.CanBeConverted) { if (!pathProperty.CanWrite) { // cannot set the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot set property value at path {0}. Possible cause: the property doesn't have an accessible setter.", path)), 422); } PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty, conversionResultTuple.ConvertedInstance); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided value is invalid for property type at location path: {0}", path)) , 422); } } }
/// <summary> /// The "copy" operation copies the value at a specified location to the /// target location. /// /// The operation object MUST contain a "from" member, which is a string /// containing a JSON Pointer value that references the location in the /// target document to copy the value from. /// /// The "from" location MUST exist for the operation to be successful. /// /// For example: /// /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } /// /// This operation is functionally identical to an "add" operation at the /// target location using the value specified in the "from" member. /// /// Note: even though it's the same functionally, we do not call add with /// the value specified in from for performance reasons (multiple checks of same requirements). /// </summary> /// <param name="operation">The copy operation</param> /// <param name="objectApplyTo">Object to apply the operation to</param> public void Copy(Operation <T> operation, T objectToApplyTo) { object valueAtFromLocation = null; // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( operation.from, objectToApplyTo, operation, true); var positionAsInteger = pathResult.NumericEnd; var actualPathToFromProperty = pathResult.PathToProperty; PropertyInfo fromProperty = PropertyHelpers .FindProperty(objectToApplyTo, actualPathToFromProperty); // does property at from exist? if (fromProperty == null) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: property at location from: {0} does not exist", operation.from)) , 422); } // get the property path // is the path an array (but not a string (= char[]))? In this case, // the path must end with "/position" or "/-", which we already determined before. if (positionAsInteger > -1) { if (fromProperty.PropertyType.IsNonStringList()) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType); if (!fromProperty.CanRead) { // cannot get the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: cannot get property value at location from: {0}. Possible cause: the property doesn't have an accessible getter.", operation.from)), 422); } // get value (it can be cast, we just checked that) var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualPathToFromProperty) as IList; if (array.Count <= positionAsInteger) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: invalid position", operation.from)) , 422); } valueAtFromLocation = array[positionAsInteger]; } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: expected array", operation.from)) , 422); } } else { if (!fromProperty.CanRead) { // cannot get the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operation, string.Format("Patch failed: cannot get property value at location from: {0}. Possible cause: the property doesn't have an accessible getter.", operation.from)), 422); } // no list, just get the value valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualPathToFromProperty); } // add operation to target location with that value. Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); }
/// <summary> /// Remove is used by various operations (eg: remove, move, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private void Remove(string path, T objectToApplyTo, Operation <T> operationToReport) { // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( path, objectToApplyTo, operationToReport, false); var removeFromList = pathResult.ExecuteAtEnd; var positionAsInteger = pathResult.NumericEnd; var actualPathToProperty = pathResult.PathToProperty; var pathProperty = PropertyHelpers .FindProperty(objectToApplyTo, actualPathToProperty); // does the target location exist? if (pathProperty == null) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property at location path: {0} does not exist", path)) , 422); } // get the property, and remove it - in this case, for DTO's, that means setting // it to null or its default value; in case of an array, remove at provided index // or at the end. if (removeFromList || positionAsInteger > -1) { // what if it's an array but there's no position?? if (pathProperty.PropertyType.IsNonStringList()) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType); if (!pathProperty.CanRead) { // cannot get the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot get property value at path: {0}. Possible cause: the property doesn't have an accessible getter.", path)), 422); } // get value (it can be cast, we just checked that) var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList; if (removeFromList) { if (array.Count == 0) { // if the array is empty, we should throw an error throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)) , 422); } array.RemoveAt(array.Count - 1); } else { if (positionAsInteger < array.Count) { array.RemoveAt(positionAsInteger); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)) , 422); } } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", path)) , 422); } } else { if (!pathProperty.CanWrite) { // cannot set the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot set property value at path {0}. Possible cause: the property doesn't have an accessible setter.", path)), 422); } // setting the value to "null" will use the default value in case of value types, and // null in case of reference types PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty, null); } }
/// <summary> /// Remove is used by various operations (eg: remove, move, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private RemovedPropertyTypeResult Remove(string path, T objectToApplyTo, Operation <T> operationToReport) { // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( path, objectToApplyTo, operationToReport, false); var removeFromList = pathResult.ExecuteAtEnd; var positionAsInteger = pathResult.NumericEnd; var actualPathToProperty = pathResult.PathToProperty; var result = new ObjectTreeAnalysisResult(objectToApplyTo, actualPathToProperty, ContractResolver); if (!result.IsValidPathForRemove) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: the provided path is invalid: {0}.", path)), 422); } var patchProperty = result.JsonPatchProperty; if (removeFromList || positionAsInteger > -1) { if (PropertyHelpers.IsNonStringList(patchProperty.Property.PropertyType)) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); if (patchProperty.Property.Readable) { var array = (IList)patchProperty.Property.ValueProvider .GetValue(patchProperty.Parent); if (removeFromList) { if (array.Count == 0) { // if the array is empty, we should throw an error throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)), 422); } array.RemoveAt(array.Count - 1); // return the type of the value that has been removed return(new RemovedPropertyTypeResult(genericTypeOfArray, false)); } else { if (positionAsInteger >= array.Count) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)), 422); } array.RemoveAt(positionAsInteger); // return the type of the value that has been removed return(new RemovedPropertyTypeResult(genericTypeOfArray, false)); } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot get property value at path {0}. Possible cause: the property doesn't have an accessible getter.", path)), 422); } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array.", path)), 422); } } else { if (!patchProperty.Property.Writable) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property at path location cannot be set: {0}. Possible causes: the property may not have an accessible setter, or the property may be part of an anonymous object (and thus cannot be changed after initialization).", path)), 422); } // set value to null, or for non-nullable value types, to its default value. object value = null; if (patchProperty.Property.PropertyType.GetType().IsValueType && Nullable.GetUnderlyingType(patchProperty.Property.PropertyType) == null) { value = Activator.CreateInstance(patchProperty.Property.PropertyType); } // check if it can be converted. var conversionResultTuple = PropertyHelpers.ConvertToActualType( patchProperty.Property.PropertyType, value); if (!conversionResultTuple.CanBeConverted) { // conversion failed, so use reflection (somewhat slower) to // create a new default instance of the property type to set as value patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, Activator.CreateInstance(patchProperty.Property.PropertyType)); return(new RemovedPropertyTypeResult(patchProperty.Property.PropertyType, false)); } patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, conversionResultTuple.ConvertedInstance); return(new RemovedPropertyTypeResult(patchProperty.Property.PropertyType, false)); } }
/// <summary> /// Add is used by various operations (eg: add, copy, ...), yet through different operations; /// This method allows code reuse yet reporting the correct operation on error /// </summary> private void Add(string path, object value, T objectToApplyTo, Operation <T> operationToReport) { // add, in this implementation, does not just "add" properties - that's // technically impossible; It can however be used to add items to arrays, // or to replace values. // first up: if the path ends in a numeric value, we're inserting in a list and // that value represents the position; if the path ends in "-", we're appending // to the list. // get path result var pathResult = PropertyHelpers.GetActualPropertyPath( path, objectToApplyTo, operationToReport, true); var appendList = pathResult.ExecuteAtEnd; var positionAsInteger = pathResult.NumericEnd; var actualPathToProperty = pathResult.PathToProperty; var result = new ObjectTreeAnalysisResult(objectToApplyTo, actualPathToProperty, ContractResolver); if (!result.IsValidPathForAdd) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: the provided path is invalid: {0}.", path)), 422); } // If it' an array, add to that array. If it's not, we replace. // is the path an array (but not a string (= char[]))? In this case, // the path must end with "/position" or "/-", which we already determined before. var patchProperty = result.JsonPatchProperty; if (appendList || positionAsInteger > -1) { if (PropertyHelpers.IsNonStringList(patchProperty.Property.PropertyType)) { // now, get the generic type of the enumerable var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value); if (!conversionResult.CanBeConverted) { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided value is invalid for array property type at location path: {0}", path)), 422); } if (patchProperty.Property.Readable) { var array = (IList)patchProperty.Property.ValueProvider .GetValue(patchProperty.Parent); if (appendList) { array.Add(conversionResult.ConvertedInstance); } else { // specified index must not be greater than the amount of items in the // array if (positionAsInteger <= array.Count) { array.Insert(positionAsInteger, conversionResult.ConvertedInstance); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", path)), 422); } } } else { // cannot read the property throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: cannot get property value at path {0}. Possible cause: the property doesn't have an accessible getter.", path)), 422); } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", path)), 422); } } else { var conversionResultTuple = PropertyHelpers.ConvertToActualType( patchProperty.Property.PropertyType, value); if (conversionResultTuple.CanBeConverted) { if (patchProperty.Property.Writable) { patchProperty.Property.ValueProvider.SetValue( patchProperty.Parent, conversionResultTuple.ConvertedInstance); } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property at path location cannot be set: {0}. Possible causes: the property may not have an accessible setter, or the property may be part of an anonymous object (and thus cannot be changed after initialization).", path)), 422); } } else { throw new JsonPatchException( new JsonPatchError( objectToApplyTo, operationToReport, string.Format("Patch failed: property value cannot be converted to type of path location {0}.", path)), 422); } } }