diff --git a/samples/MvcSample.Web/Controllers/JsonPatchController.cs b/samples/MvcSample.Web/Controllers/JsonPatchController.cs new file mode 100644 index 0000000000..2e6c964c19 --- /dev/null +++ b/samples/MvcSample.Web/Controllers/JsonPatchController.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.JsonPatch; +using Microsoft.AspNet.Mvc; + +namespace MvcSample.Web.Controllers +{ + [Route("api/[controller]")] + public class JsonPatchController : Controller + { + public ActionResult Index() + { + return View(); + } + + [HttpPatch] + public IActionResult Patch([FromBody] JsonPatchDocument patchDoc) + { + var customer = new Customer + { + Name = "John", + Orders = new List() + { + new Order + { + OrderName = "Order1" + }, + new Order + { + OrderName = "Order2" + } + } + }; + + patchDoc.ApplyTo(customer); + + return new ObjectResult(customer); + } + + public class Customer + { + public string Name { get; set; } + + public List Orders { get; set; } + } + + public class Order + { + public string OrderName { get; set; } + } + } +} diff --git a/samples/MvcSample.Web/Views/JsonPatch/Index.cshtml b/samples/MvcSample.Web/Views/JsonPatch/Index.cshtml new file mode 100644 index 0000000000..4e86235cf9 --- /dev/null +++ b/samples/MvcSample.Web/Views/JsonPatch/Index.cshtml @@ -0,0 +1,114 @@ + + + + + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+
+
+ +
+ +
+ diff --git a/src/Microsoft.AspNet.JsonPatch/Adapters/SimpleObjectAdapter.cs b/src/Microsoft.AspNet.JsonPatch/Adapters/SimpleObjectAdapter.cs index d2ae4cc840..69aa482a5a 100644 --- a/src/Microsoft.AspNet.JsonPatch/Adapters/SimpleObjectAdapter.cs +++ b/src/Microsoft.AspNet.JsonPatch/Adapters/SimpleObjectAdapter.cs @@ -1,9 +1,12 @@ -using System; -using Microsoft.AspNet.JsonPatch.Operations; -using Microsoft.AspNet.JsonPatch.Helpers; -using Microsoft.AspNet.JsonPatch.Exceptions; -using System.Reflection; +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; using System.Collections; +using System.Reflection; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Microsoft.AspNet.JsonPatch.Helpers; +using Microsoft.AspNet.JsonPatch.Operations; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -11,6 +14,13 @@ namespace Microsoft.AspNet.JsonPatch.Adapters { public class SimpleObjectAdapter : IObjectAdapter where T : class { + public IContractResolver ContractResolver { get; set; } + + public SimpleObjectAdapter(IContractResolver contractResolver) + { + ContractResolver = contractResolver; + } + /// /// The "add" operation performs one of the following functions, /// depending upon what the target location references: @@ -89,7 +99,6 @@ namespace Microsoft.AspNet.JsonPatch.Adapters // 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; @@ -110,47 +119,30 @@ namespace Microsoft.AspNet.JsonPatch.Adapters } } - var pathProperty = PropertyHelpers - .FindProperty(objectToApplyTo, actualPathToProperty); - + var patchProperty = PropertyHelpers + .FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver); // does property at path exist? - if (pathProperty == null) - { - throw new JsonPatchException(operationToReport, - string.Format("Patch failed: property at location path: {0} does not exist", path), - objectToApplyTo); - } + 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) { - - var isNonStringArray = !(pathProperty.PropertyType == typeof(string)) - && typeof(IList).IsAssignableFrom(pathProperty.PropertyType); - // what if it's an array but there's no position?? - if (isNonStringArray) + if (IsNonStringArray(patchProperty)) { // now, get the generic type of the enumerable - var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType); + var genericTypeOfArray = PropertyHelpers.GetEnumerableType( + patchProperty.Property.PropertyType); var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value); - if (!conversionResult.CanBeConverted) - { - throw new JsonPatchException(operationToReport, - string.Format("Patch failed: provided value is invalid for array property type at location path: {0}", - path), - objectToApplyTo); - } + CheckIfPropertyCanBeSet(conversionResult, objectToApplyTo, operationToReport, path); // get value (it can be cast, we just checked that) - var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList; + var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); if (appendList) { @@ -167,40 +159,33 @@ namespace Microsoft.AspNet.JsonPatch.Adapters { throw new JsonPatchException(operationToReport, string.Format("Patch failed: provided path is invalid for array property type at " + - "location path: {0}: position doesn't exist in array", + "location path: {0}: position larger than array size", path), objectToApplyTo); } } - - } else { throw new JsonPatchException(operationToReport, - string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", - path), + string.Format("Patch failed: provided path is invalid for array property type at location " + + "path: {0}: expected array", + path), objectToApplyTo); } } else { - var conversionResultTuple = PropertyHelpers.ConvertToActualType(pathProperty.PropertyType, value); + var conversionResultTuple = PropertyHelpers.ConvertToActualType( + patchProperty.Property.PropertyType, + value); - // conversion successful - if (conversionResultTuple.CanBeConverted) - { - PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty, + // Is conversion successful + CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operationToReport, path); + + patchProperty.Property.ValueProvider.SetValue( + patchProperty.Parent, conversionResultTuple.ConvertedInstance); - } - else - { - throw new JsonPatchException(operationToReport, - string.Format("Patch failed: provided value is invalid for property type at location path: {0}", - path), - objectToApplyTo); - } - } } @@ -229,13 +214,11 @@ namespace Microsoft.AspNet.JsonPatch.Adapters /// Object to apply the operation to public void Move(Operation 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) @@ -244,74 +227,56 @@ namespace Microsoft.AspNet.JsonPatch.Adapters operation.from.IndexOf('/' + positionAsInteger.ToString())); } - var fromProperty = PropertyHelpers - .FindProperty(objectToApplyTo, actualFromProperty); - + var patchProperty = PropertyHelpers + .FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver); // does property at from exist? - if (fromProperty == null) - { - throw new JsonPatchException(operation, - string.Format("Patch failed: property at location from: {0} does not exist", operation.from), - objectToApplyTo); - } - + CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from); // 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) { - - var isNonStringArray = !(fromProperty.PropertyType == typeof(string)) - && typeof(IList).IsAssignableFrom(fromProperty.PropertyType); - - if (isNonStringArray) + if (IsNonStringArray(patchProperty)) { // now, get the generic type of the enumerable - var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType); + var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); // get value (it can be cast, we just checked that) - var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty) as IList; + var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); if (array.Count <= positionAsInteger) { throw new JsonPatchException(operation, - string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: invalid position", - operation.from), - objectToApplyTo); + string.Format("Patch failed: provided from path is invalid for array property type at " + + "location from: {0}: invalid position", + operation.from), + objectToApplyTo); } valueAtFromLocation = array[positionAsInteger]; - } else { throw new JsonPatchException(operation, - string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: expected array", - operation.from), + string.Format("Patch failed: provided from path is invalid for array property type at " + + "location from: {0}: expected array", + operation.from), objectToApplyTo); } } else { // no list, just get the value - // set the new value - - valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty); - + valueAtFromLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); } - // remove that value - Remove(operation.from, objectToApplyTo, operation); // add that value to the path location - Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); - } /// @@ -359,35 +324,25 @@ namespace Microsoft.AspNet.JsonPatch.Adapters } } - var pathProperty = PropertyHelpers - .FindProperty(objectToApplyTo, actualPathToProperty); + var patchProperty = PropertyHelpers + .FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver); // does the target location exist? - if (pathProperty == null) - { - throw new JsonPatchException(operationToReport, - string.Format("Patch failed: property at location path: {0} does not exist", path), - objectToApplyTo); - } + 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) { - - var isNonStringArray = !(pathProperty.PropertyType == typeof(string)) - && typeof(IList).IsAssignableFrom(pathProperty.PropertyType); - // what if it's an array but there's no position?? - if (isNonStringArray) + if (IsNonStringArray(patchProperty)) { // now, get the generic type of the enumerable - var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType); + var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); - // TODO: nested! // get value (it can be cast, we just checked that) - var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList; + var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); if (removeFromList) { @@ -402,54 +357,58 @@ namespace Microsoft.AspNet.JsonPatch.Adapters else { throw new JsonPatchException(operationToReport, - string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size", - path), - objectToApplyTo); + string.Format("Patch failed: provided path is invalid for array property type at " + + "location path: {0}: position larger than array size", + path), + objectToApplyTo); } } - } else { throw new JsonPatchException(operationToReport, - string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", - path), + string.Format("Patch failed: provided path is invalid for array property type at " + + "location path: {0}: expected array", + path), objectToApplyTo); } } else { - // 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); + 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); } - } - - - /// /// 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 @@ -468,7 +427,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters /// /// 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. @@ -482,12 +441,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters public void Test(Operation 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) @@ -496,146 +453,120 @@ namespace Microsoft.AspNet.JsonPatch.Adapters operation.path.IndexOf('/' + positionInPathAsInteger.ToString())); } - var pathProperty = PropertyHelpers - .FindProperty(objectToApplyTo, actualPathProperty); + var patchProperty = PropertyHelpers + .FindPropertyAndParent(objectToApplyTo, actualPathProperty, ContractResolver); // does property at path exist? - if (pathProperty == null) - { - throw new JsonPatchException(operation, - string.Format("Patch failed: property at location path: {0} does not exist", operation.path), - objectToApplyTo); - } + 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) { - - var isNonStringArray = !(pathProperty.PropertyType == typeof(string)) - && typeof(IList).IsAssignableFrom(pathProperty.PropertyType); - - if (isNonStringArray) + if (IsNonStringArray(patchProperty)) { // now, get the generic type of the enumerable - typeOfFinalPropertyAtPathLocation = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType); + typeOfFinalPropertyAtPathLocation = PropertyHelpers + .GetEnumerableType(patchProperty.Property.PropertyType); // get value (it can be cast, we just checked that) - var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathProperty) as IList; + var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); if (array.Count <= positionInPathAsInteger) { throw new JsonPatchException(operation, - string.Format("Patch failed: provided from path is invalid for array property type at location path: {0}: invalid position", - operation.path), - objectToApplyTo); + string.Format("Patch failed: provided from path is invalid for array property type at " + + "location path: {0}: invalid position", + operation.path), + objectToApplyTo); } valueAtPathLocation = array[positionInPathAsInteger]; - } else { throw new JsonPatchException(operation, - string.Format("Patch failed: provided from path is invalid for array property type at location path: {0}: expected array", - operation.path), + string.Format("Patch failed: provided from path is invalid for array property type at " + + "location path: {0}: expected array", + operation.path), objectToApplyTo); } } else { // no list, just get the value - valueAtPathLocation = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathProperty); - typeOfFinalPropertyAtPathLocation = pathProperty.PropertyType; - } - - - - var conversionResultTuple = PropertyHelpers.ConvertToActualType(typeOfFinalPropertyAtPathLocation, operation.value); - - // conversion successful - if (conversionResultTuple.CanBeConverted) - { - // COMPARE - TODO - } - else - { - throw new JsonPatchException(operation, - string.Format("Patch failed: provided value is invalid for property type at location path: {0}", - operation.path), - objectToApplyTo); + 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 } - /// /// The "replace" operation replaces the value at the target location /// with a new value. The operation object MUST contain a "value" member /// whose content specifies the replacement value. - /// + /// /// The target location MUST exist for the operation to be successful. - /// + /// /// For example: - /// + /// /// { "op": "replace", "path": "/a/b/c", "value": 42 } - /// + /// /// This operation is functionally identical to a "remove" operation for /// a value, followed immediately by an "add" operation at the same /// location with the replacement value. - /// - /// Note: even though it's the same functionally, we do not call remove + add + /// + /// Note: even though it's the same functionally, we do not call remove + add /// for performance reasons (multiple checks of same requirements). /// /// The replace operation /// Object to apply the operation to public void Replace(Operation operation, T objectToApplyTo) { - Remove(operation.path, objectToApplyTo, operation); Add(operation.path, operation.value, objectToApplyTo, operation); - } - /// /// 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 + /// + /// 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). /// /// The copy operation /// Object to apply the operation to public void Copy(Operation 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) @@ -644,70 +575,96 @@ namespace Microsoft.AspNet.JsonPatch.Adapters operation.from.IndexOf('/' + positionAsInteger.ToString())); } - - var fromProperty = PropertyHelpers - .FindProperty(objectToApplyTo, actualFromProperty); + var patchProperty = PropertyHelpers + .FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver); // does property at from exist? - if (fromProperty == null) - { - throw new JsonPatchException(operation, - string.Format("Patch failed: property at location from: {0} does not exist", operation.from), - objectToApplyTo); - } + 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) { - - var isNonStringArray = !(fromProperty.PropertyType == typeof(string)) - && typeof(IList).IsAssignableFrom(fromProperty.PropertyType); - - if (isNonStringArray) + if (IsNonStringArray(patchProperty)) { // now, get the generic type of the enumerable - var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType); + var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType); // get value (it can be cast, we just checked that) - var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty) as IList; + var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); if (array.Count <= positionAsInteger) { throw new JsonPatchException(operation, - string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: invalid position", - operation.from), - objectToApplyTo); + string.Format("Patch failed: provided from path is invalid for array property type at " + + "location from: {0}: invalid position", + operation.from), + objectToApplyTo); } valueAtFromLocation = array[positionAsInteger]; - } else { throw new JsonPatchException(operation, - string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: expected array", - operation.from), + string.Format("Patch failed: provided from path is invalid for array property type at " + + "location from: {0}: expected array", + operation.from), objectToApplyTo); } } else { // no list, just get the value - // set the new value - - valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty); - + valueAtFromLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); } // add operation to target location with that value. - Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); + } + private void CheckIfPropertyExists( + JsonPatchProperty patchProperty, + T objectToApplyTo, + Operation operation, + string propertyPath) + { + if (patchProperty == null) + { + throw new JsonPatchException( + operation, + string.Format("Patch failed: property at location {0} does not exist", propertyPath), + objectToApplyTo); + } + if (patchProperty.Property.Ignored) + { + throw new JsonPatchException( + operation, + string.Format("Patch failed: cannot update property at location {0}", propertyPath), + objectToApplyTo); + } + } + + private bool IsNonStringArray(JsonPatchProperty patchProperty) + { + return !(patchProperty.Property.PropertyType == typeof(string)) + && typeof(IList).GetTypeInfo().IsAssignableFrom( + patchProperty.Property.PropertyType.GetTypeInfo()); + } + + private void CheckIfPropertyCanBeSet( + ConversionResult result, + T objectToApplyTo, + Operation operation, + string path) + { + var errorMessage = "Patch failed: provided value is invalid for property type at location path: "; + if (!result.CanBeConverted) + { + throw new JsonPatchException(operation, string.Format(errorMessage + "{0}", path), objectToApplyTo); + } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs b/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs index 53474cf3a7..b4f8f1ef40 100644 --- a/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs +++ b/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs @@ -1,79 +1,79 @@ -using Microsoft.AspNet.JsonPatch.Exceptions; -using Microsoft.AspNet.JsonPatch.Operations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.Collections.Generic; using System.Reflection; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Microsoft.AspNet.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; namespace Microsoft.AspNet.JsonPatch.Converters { - public class TypedJsonPatchDocumentConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { + public class TypedJsonPatchDocumentConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return true; + } - return true; - } + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + try + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, - JsonSerializer serializer) - { - try - { - if (reader.TokenType == JsonToken.Null) - return null; + var genericType = objectType.GetGenericArguments()[0]; - var genericType = objectType.GetGenericArguments()[0]; + // load jObject + var jObject = JArray.Load(reader); - // load jObject - var jObject = JArray.Load(reader); + // Create target object for Json => list of operations, typed to genericType + var genericOperation = typeof(Operation<>); + var concreteOperationType = genericOperation.MakeGenericType(genericType); - // Create target object for Json => list of operations, typed to genericType + var genericList = typeof(List<>); + var concreteList = genericList.MakeGenericType(concreteOperationType); - var genericOperation = typeof(Operation<>); - var concreteOperationType = genericOperation.MakeGenericType(genericType); + var targetOperations = Activator.CreateInstance(concreteList); - var genericList = typeof(List<>); - var concreteList = genericList.MakeGenericType(concreteOperationType); - - var targetOperations = Activator.CreateInstance(concreteList); - - - //Create a new reader for this jObject, and set all properties to match the original reader. - JsonReader jObjectReader = jObject.CreateReader(); - jObjectReader.Culture = reader.Culture; - jObjectReader.DateParseHandling = reader.DateParseHandling; - jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling; - jObjectReader.FloatParseHandling = reader.FloatParseHandling; + //Create a new reader for this jObject, and set all properties to match the original reader. + JsonReader jObjectReader = jObject.CreateReader(); + jObjectReader.Culture = reader.Culture; + jObjectReader.DateParseHandling = reader.DateParseHandling; + jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling; + jObjectReader.FloatParseHandling = reader.FloatParseHandling; // Populate the object properties - serializer.Populate(jObjectReader, targetOperations); + serializer.Populate(jObjectReader, targetOperations); - // container target: the typed JsonPatchDocument. - var container = Activator.CreateInstance(objectType, targetOperations); + // container target: the typed JsonPatchDocument. + var container = Activator.CreateInstance(objectType, targetOperations, new DefaultContractResolver()); - return container; + return container; + } + catch (Exception ex) + { + throw new JsonPatchException("The JsonPatchDocument was malformed and could not be parsed.", ex); + } + } - } - catch (Exception ex) - { - throw new JsonPatchException("The JsonPatchDocument was malformed and could not be parsed.", ex); - } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is IJsonPatchDocument) + { + var jsonPatchDoc = (IJsonPatchDocument)value; + var lst = jsonPatchDoc.GetOperations(); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value is IJsonPatchDocument) - { - var jsonPatchDoc = (IJsonPatchDocument)value; - var lst = jsonPatchDoc.GetOperations(); - - // write out the operations, no envelope - serializer.Serialize(writer, lst); - - } - } - } + // write out the operations, no envelope + serializer.Serialize(writer, lst); + } + } + } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/JsonPatchProperty.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/JsonPatchProperty.cs new file mode 100644 index 0000000000..0b94a50eb5 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/JsonPatchProperty.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch +{ + /// + /// Metadata for JsonProperty. + /// + public class JsonPatchProperty + { + /// + /// Initializes a new instance. + /// + public JsonPatchProperty(JsonProperty property, object parent) + { + Property = property; + Parent = parent; + } + + /// + /// Gets or sets JsonProperty. + /// + public JsonProperty Property { get; set; } + + /// + /// Gets or sets Parent. + /// + public object Parent { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/PropertyHelpers.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/PropertyHelpers.cs index 17c2b9e7ef..35e1247b72 100644 --- a/src/Microsoft.AspNet.JsonPatch/Helpers/PropertyHelpers.cs +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/PropertyHelpers.cs @@ -1,59 +1,21 @@ -using Newtonsoft.Json; +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; +using System.Collections; using System.Collections.Generic; -using System.Dynamic; -using System.Linq; -using System.Linq.Expressions; using System.Reflection; +using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNet.JsonPatch.Helpers { internal static class PropertyHelpers { - public static object GetValue(PropertyInfo propertyToGet, object targetObject, string pathToProperty) - { - // it is possible the path refers to a nested property. In that case, we need to - // get from a different target object: the nested object. - - var splitPath = pathToProperty.Split('/'); - - // skip the first one if it's empty - var startIndex = (string.IsNullOrWhiteSpace(splitPath[0]) ? 1 : 0); - - for (int i = startIndex; i < splitPath.Length - 1; i++) - { - var propertyInfoToGet = GetPropertyInfo(targetObject, splitPath[i] - , BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - targetObject = propertyInfoToGet.GetValue(targetObject, null); - } - - return propertyToGet.GetValue(targetObject, null); - } - - public static bool SetValue(PropertyInfo propertyToSet, object targetObject, string pathToProperty, object value) - { - // it is possible the path refers to a nested property. In that case, we need to - // set on a different target object: the nested object. - var splitPath = pathToProperty.Split('/'); - - // skip the first one if it's empty - var startIndex = (string.IsNullOrWhiteSpace(splitPath[0]) ? 1 : 0); - - for (int i = startIndex; i < splitPath.Length - 1; i++) - { - var propertyInfoToGet = GetPropertyInfo(targetObject, splitPath[i] - , BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - targetObject = propertyInfoToGet.GetValue(targetObject, null); - } - - propertyToSet.SetValue(targetObject, value, null); - - return true; - } - - - public static PropertyInfo FindProperty(object targetObject, string propertyPath) + public static JsonPatchProperty FindPropertyAndParent( + object targetObject, + string propertyPath, + IContractResolver contractResolver) { try { @@ -62,17 +24,37 @@ namespace Microsoft.AspNet.JsonPatch.Helpers // skip the first one if it's empty var startIndex = (string.IsNullOrWhiteSpace(splitPath[0]) ? 1 : 0); - for (int i = startIndex; i < splitPath.Length - 1; i++) + for (int i = startIndex; i < splitPath.Length; i++) { - var propertyInfoToGet = GetPropertyInfo(targetObject, splitPath[i] - , BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - targetObject = propertyInfoToGet.GetValue(targetObject, null); + var jsonContract = (JsonObjectContract)contractResolver.ResolveContract(targetObject.GetType()); + + foreach (var property in jsonContract.Properties) + { + if (string.Equals(property.PropertyName, splitPath[i], StringComparison.OrdinalIgnoreCase)) + { + if (i == (splitPath.Length - 1)) + { + return new JsonPatchProperty(property, targetObject); + } + else + { + targetObject = property.ValueProvider.GetValue(targetObject); + + // if property is of IList type then get the array index from splitPath and get the + // object at the indexed position from the list. + if (typeof(IList).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo())) + { + var index = int.Parse(splitPath[++i]); + targetObject = ((IList)targetObject)[index]; + } + } + + break; + } + } } - var propertyToFind = targetObject.GetType().GetProperty(splitPath.Last(), - BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - - return propertyToFind; + return null; } catch (Exception) { @@ -100,20 +82,11 @@ namespace Microsoft.AspNet.JsonPatch.Helpers if (type == null) throw new ArgumentNullException(); foreach (Type interfaceType in type.GetInterfaces()) { -#if NETFX_CORE || ASPNETCORE50 - if (interfaceType.GetTypeInfo().IsGenericType && - interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - { - return interfaceType.GetGenericArguments()[0]; - } -#else if (interfaceType.GetTypeInfo().IsGenericType && - interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { return interfaceType.GetGenericArguments()[0]; } -#endif - } return null; } @@ -130,11 +103,5 @@ namespace Microsoft.AspNet.JsonPatch.Helpers return -1; } - - private static PropertyInfo GetPropertyInfo(object targetObject, string propertyName, - BindingFlags bindingFlags) - { - return targetObject.GetType().GetProperty(propertyName, bindingFlags); - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs b/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs index a0fc3d14d3..82f8e935b6 100644 --- a/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs +++ b/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs @@ -3,11 +3,14 @@ using Microsoft.AspNet.JsonPatch.Operations; using System.Collections.Generic; +using Newtonsoft.Json.Serialization; namespace Microsoft.AspNet.JsonPatch { public interface IJsonPatchDocument { + IContractResolver ContractResolver { get; set; } + List GetOperations(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs b/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs index 7ada538c33..812735691a 100644 --- a/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs +++ b/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs @@ -1,377 +1,392 @@ -using Microsoft.AspNet.JsonPatch.Adapters; +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Microsoft.AspNet.JsonPatch.Adapters; using Microsoft.AspNet.JsonPatch.Converters; using Microsoft.AspNet.JsonPatch.Helpers; using Microsoft.AspNet.JsonPatch.Operations; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq.Expressions; +using Newtonsoft.Json.Serialization; namespace Microsoft.AspNet.JsonPatch { - // Implementation details: the purpose of this type of patch document is to ensure we can do type-checking - // when producing a JsonPatchDocument. However, we cannot send this "typed" over the wire, as that would require - // including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's - // not according to RFC 6902, and would thus break cross-platform compatibility. - [JsonConverter(typeof(TypedJsonPatchDocumentConverter))] - public class JsonPatchDocument : IJsonPatchDocument where T : class - { - public List> Operations { get; private set; } + // Implementation details: the purpose of this type of patch document is to ensure we can do type-checking + // when producing a JsonPatchDocument. However, we cannot send this "typed" over the wire, as that would require + // including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's + // not according to RFC 6902, and would thus break cross-platform compatibility. + [JsonConverter(typeof(TypedJsonPatchDocumentConverter))] + public class JsonPatchDocument : IJsonPatchDocument where T : class + { + public List> Operations { get; private set; } - public JsonPatchDocument() - { - Operations = new List>(); - } + [JsonIgnore] + public IContractResolver ContractResolver { get; set; } - // Create from list of operations - public JsonPatchDocument(List> operations) - { - Operations = operations; - } + public JsonPatchDocument() + { + Operations = new List>(); + ContractResolver = new DefaultContractResolver(); + } - /// - /// Add operation. Will result in, for example, - /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } - /// - /// value type - /// path - /// value - /// - public JsonPatchDocument Add(Expression> path, TProp value) - { - Operations.Add(new Operation("add", ExpressionHelpers.GetPath(path).ToLower(), null, value)); - return this; - } + // Create from list of operations + public JsonPatchDocument(List> operations, IContractResolver contractResolver) + { + Operations = operations; + ContractResolver = contractResolver; + } - /// - /// Add value to list at given position - /// - /// value type - /// path - /// value - /// position - /// - public JsonPatchDocument Add(Expression>> path, TProp value, int position) - { - Operations.Add(new Operation("add", ExpressionHelpers.GetPath>(path).ToLower() + "/" + position, null, value)); - return this; - } + /// + /// Add operation. Will result in, for example, + /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } + /// + /// value type + /// path + /// value + /// + public JsonPatchDocument Add(Expression> path, TProp value) + { + Operations.Add(new Operation( + "add", + ExpressionHelpers.GetPath(path).ToLower(), + from: null, + value: value)); + return this; + } - /// - /// At value at end of list - /// - /// value type - /// path - /// value - /// - public JsonPatchDocument Add(Expression>> path, TProp value) - { - Operations.Add(new Operation("add", ExpressionHelpers.GetPath>(path).ToLower() + "/-", null, value)); - return this; - } + /// + /// Add value to list at given position + /// + /// value type + /// path + /// value + /// position + /// + public JsonPatchDocument Add(Expression>> path, TProp value, int position) + { + Operations.Add(new Operation( + "add", + ExpressionHelpers.GetPath>(path).ToLower() + "/" + position, + null, value)); + return this; + } - /// - /// Remove value at target location. Will result in, for example, - /// { "op": "remove", "path": "/a/b/c" } - /// - /// - /// - /// - public JsonPatchDocument Remove(Expression> path) - { - Operations.Add(new Operation("remove", ExpressionHelpers.GetPath(path).ToLower(), null)); - return this; - } + /// + /// At value at end of list + /// + /// value type + /// path + /// value + /// + public JsonPatchDocument Add(Expression>> path, TProp value) + { + Operations.Add(new Operation("add", ExpressionHelpers.GetPath>(path).ToLower() + "/-", null, value)); + return this; + } - /// - /// Remove value from list at given position - /// - /// value type - /// target location - /// position - /// - public JsonPatchDocument Remove(Expression>> path, int position) - { - Operations.Add(new Operation("remove", ExpressionHelpers.GetPath>(path).ToLower() + "/" + position, null)); - return this; - } + /// + /// Remove value at target location. Will result in, for example, + /// { "op": "remove", "path": "/a/b/c" } + /// + /// + /// + /// + public JsonPatchDocument Remove(Expression> path) + { + Operations.Add(new Operation("remove", ExpressionHelpers.GetPath(path).ToLower(), null)); + return this; + } - /// - /// Remove value from end of list - /// - /// value type - /// target location - /// - public JsonPatchDocument Remove(Expression>> path) - { - Operations.Add(new Operation("remove", ExpressionHelpers.GetPath>(path).ToLower() + "/-", null)); - return this; - } + /// + /// Remove value from list at given position + /// + /// value type + /// target location + /// position + /// + public JsonPatchDocument Remove(Expression>> path, int position) + { + Operations.Add(new Operation("remove", ExpressionHelpers.GetPath>(path).ToLower() + "/" + position, null)); + return this; + } - /// - /// Replace value. Will result in, for example, - /// { "op": "replace", "path": "/a/b/c", "value": 42 } - /// - /// - /// - /// - public JsonPatchDocument Replace(Expression> path, TProp value) - { - Operations.Add(new Operation("replace", ExpressionHelpers.GetPath(path).ToLower(), null, value)); - return this; - } + /// + /// Remove value from end of list + /// + /// value type + /// target location + /// + public JsonPatchDocument Remove(Expression>> path) + { + Operations.Add(new Operation("remove", ExpressionHelpers.GetPath>(path).ToLower() + "/-", null)); + return this; + } - /// - /// Replace value in a list at given position - /// - /// value type - /// target location - /// position - /// - public JsonPatchDocument Replace(Expression>> path, TProp value, int position) - { - Operations.Add(new Operation("replace", ExpressionHelpers.GetPath>(path).ToLower() + "/" + position, null, value)); - return this; - } + /// + /// Replace value. Will result in, for example, + /// { "op": "replace", "path": "/a/b/c", "value": 42 } + /// + /// + /// + /// + public JsonPatchDocument Replace(Expression> path, TProp value) + { + Operations.Add(new Operation("replace", ExpressionHelpers.GetPath(path).ToLower(), null, value)); + return this; + } - /// - /// Replace value at end of a list - /// - /// value type - /// target location - /// - public JsonPatchDocument Replace(Expression>> path, TProp value) - { - Operations.Add(new Operation("replace", ExpressionHelpers.GetPath>(path).ToLower() + "/-", null, value)); - return this; - } + /// + /// Replace value in a list at given position + /// + /// value type + /// target location + /// position + /// + public JsonPatchDocument Replace(Expression>> path, TProp value, int position) + { + Operations.Add(new Operation("replace", ExpressionHelpers.GetPath>(path).ToLower() + "/" + position, null, value)); + return this; + } - /// - /// Removes value at specified location and add it to the target location. Will result in, for example: - /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } - /// - /// - /// - /// - public JsonPatchDocument Move(Expression> from, Expression> path) - { - Operations.Add(new Operation("move", ExpressionHelpers.GetPath(path).ToLower() - , ExpressionHelpers.GetPath(from).ToLower())); - return this; - } + /// + /// Replace value at end of a list + /// + /// value type + /// target location + /// + public JsonPatchDocument Replace(Expression>> path, TProp value) + { + Operations.Add(new Operation("replace", ExpressionHelpers.GetPath>(path).ToLower() + "/-", null, value)); + return this; + } - /// - /// Move from a position in a list to a new location - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Move(Expression>> from, int positionFrom, Expression> path) - { - Operations.Add(new Operation("move", ExpressionHelpers.GetPath(path).ToLower() - , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); - return this; - } + /// + /// Removes value at specified location and add it to the target location. Will result in, for example: + /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } + /// + /// + /// + /// + public JsonPatchDocument Move(Expression> from, Expression> path) + { + Operations.Add(new Operation("move", ExpressionHelpers.GetPath(path).ToLower() + , ExpressionHelpers.GetPath(from).ToLower())); + return this; + } - /// - /// Move from a property to a location in a list - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Move(Expression> from, - Expression>> path, int positionTo) - { - Operations.Add(new Operation("move", ExpressionHelpers.GetPath>(path).ToLower() - + "/" + positionTo - , ExpressionHelpers.GetPath(from).ToLower())); - return this; - } + /// + /// Move from a position in a list to a new location + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Move(Expression>> from, int positionFrom, Expression> path) + { + Operations.Add(new Operation("move", ExpressionHelpers.GetPath(path).ToLower() + , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); + return this; + } - /// - /// Move from a position in a list to another location in a list - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Move(Expression>> from, int positionFrom, - Expression>> path, int positionTo) - { - Operations.Add(new Operation("move", ExpressionHelpers.GetPath>(path).ToLower() - + "/" + positionTo - , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); - return this; - } + /// + /// Move from a property to a location in a list + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Move(Expression> from, + Expression>> path, int positionTo) + { + Operations.Add(new Operation("move", ExpressionHelpers.GetPath>(path).ToLower() + + "/" + positionTo + , ExpressionHelpers.GetPath(from).ToLower())); + return this; + } - /// - /// Move from a position in a list to the end of another list - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Move(Expression>> from, int positionFrom, - Expression>> path) - { - Operations.Add(new Operation("move", ExpressionHelpers.GetPath>(path).ToLower() - + "/-" - , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); - return this; - } + /// + /// Move from a position in a list to another location in a list + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Move(Expression>> from, int positionFrom, + Expression>> path, int positionTo) + { + Operations.Add(new Operation("move", ExpressionHelpers.GetPath>(path).ToLower() + + "/" + positionTo + , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); + return this; + } - /// - /// Move to the end of a list - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Move(Expression> from, Expression>> path) - { - Operations.Add(new Operation("move", ExpressionHelpers.GetPath>(path).ToLower() + "/-" - , ExpressionHelpers.GetPath(from).ToLower())); - return this; - } + /// + /// Move from a position in a list to the end of another list + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Move(Expression>> from, int positionFrom, + Expression>> path) + { + Operations.Add(new Operation("move", ExpressionHelpers.GetPath>(path).ToLower() + + "/-" + , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); + return this; + } - /// - /// Copy the value at specified location to the target location. Willr esult in, for example: - /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } - /// - /// - /// - /// - public JsonPatchDocument Copy(Expression> from, Expression> path) - { - Operations.Add(new Operation("copy", ExpressionHelpers.GetPath(path).ToLower() - , ExpressionHelpers.GetPath(from).ToLower())); - return this; - } + /// + /// Move to the end of a list + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Move(Expression> from, Expression>> path) + { + Operations.Add(new Operation("move", ExpressionHelpers.GetPath>(path).ToLower() + "/-" + , ExpressionHelpers.GetPath(from).ToLower())); + return this; + } - /// - /// Copy from a position in a list to a new location - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Copy(Expression>> from, int positionFrom, Expression> path) - { - Operations.Add(new Operation("copy", ExpressionHelpers.GetPath(path).ToLower() - , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); - return this; - } + /// + /// Copy the value at specified location to the target location. Willr esult in, for example: + /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } + /// + /// + /// + /// + public JsonPatchDocument Copy(Expression> from, Expression> path) + { + Operations.Add(new Operation("copy", ExpressionHelpers.GetPath(path).ToLower() + , ExpressionHelpers.GetPath(from).ToLower())); + return this; + } - /// - /// Copy from a property to a location in a list - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Copy(Expression> from, - Expression>> path, int positionTo) - { - Operations.Add(new Operation("copy", ExpressionHelpers.GetPath>(path).ToLower() - + "/" + positionTo - , ExpressionHelpers.GetPath(from).ToLower())); - return this; - } + /// + /// Copy from a position in a list to a new location + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Copy(Expression>> from, int positionFrom, Expression> path) + { + Operations.Add(new Operation("copy", ExpressionHelpers.GetPath(path).ToLower() + , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); + return this; + } - /// - /// Copy from a position in a list to a new location in a list - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Copy(Expression>> from, int positionFrom, - Expression>> path, int positionTo) - { - Operations.Add(new Operation("copy", ExpressionHelpers.GetPath>(path).ToLower() - + "/" + positionTo - , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); - return this; - } + /// + /// Copy from a property to a location in a list + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Copy(Expression> from, + Expression>> path, int positionTo) + { + Operations.Add(new Operation("copy", ExpressionHelpers.GetPath>(path).ToLower() + + "/" + positionTo + , ExpressionHelpers.GetPath(from).ToLower())); + return this; + } - /// - /// Copy from a position in a list to the end of another list - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Copy(Expression>> from, int positionFrom, - Expression>> path) - { - Operations.Add(new Operation("copy", ExpressionHelpers.GetPath>(path).ToLower() - + "/-" - , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); - return this; - } + /// + /// Copy from a position in a list to a new location in a list + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Copy(Expression>> from, int positionFrom, + Expression>> path, int positionTo) + { + Operations.Add(new Operation("copy", ExpressionHelpers.GetPath>(path).ToLower() + + "/" + positionTo + , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); + return this; + } - /// - /// Copy to the end of a list - /// - /// - /// - /// - /// - /// - public JsonPatchDocument Copy(Expression> from, Expression>> path) - { - Operations.Add(new Operation("copy", ExpressionHelpers.GetPath>(path).ToLower() + "/-" - , ExpressionHelpers.GetPath(from).ToLower())); - return this; - } + /// + /// Copy from a position in a list to the end of another list + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Copy(Expression>> from, int positionFrom, + Expression>> path) + { + Operations.Add(new Operation("copy", ExpressionHelpers.GetPath>(path).ToLower() + + "/-" + , ExpressionHelpers.GetPath>(from).ToLower() + "/" + positionFrom)); + return this; + } - public void ApplyTo(T objectToApplyTo) - { - ApplyTo(objectToApplyTo, new SimpleObjectAdapter()); - } + /// + /// Copy to the end of a list + /// + /// + /// + /// + /// + /// + public JsonPatchDocument Copy(Expression> from, Expression>> path) + { + Operations.Add(new Operation("copy", ExpressionHelpers.GetPath>(path).ToLower() + "/-" + , ExpressionHelpers.GetPath(from).ToLower())); + return this; + } - public void ApplyTo(T objectToApplyTo, IObjectAdapter adapter) - { + public void ApplyTo(T objectToApplyTo) + { + ApplyTo(objectToApplyTo, new SimpleObjectAdapter(ContractResolver)); + } - // apply each operation in order - foreach (var op in Operations) - { - op.Apply(objectToApplyTo, adapter); - } - } + public void ApplyTo(T objectToApplyTo, IObjectAdapter adapter) + { - public List GetOperations() - { - var allOps = new List(); + // apply each operation in order + foreach (var op in Operations) + { + op.Apply(objectToApplyTo, adapter); + } + } - if (Operations != null) - { - foreach (var op in Operations) - { - var untypedOp = new Operation(); + public List GetOperations() + { + var allOps = new List(); - untypedOp.op = op.op; - untypedOp.value = op.value; - untypedOp.path = op.path; - untypedOp.from = op.from; + if (Operations != null) + { + foreach (var op in Operations) + { + var untypedOp = new Operation(); - allOps.Add(untypedOp); - } - } + untypedOp.op = op.op; + untypedOp.value = op.value; + untypedOp.path = op.path; + untypedOp.from = op.from; - return allOps; - } - } + allOps.Add(untypedOp); + } + } + return allOps; + } + } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/project.json b/src/Microsoft.AspNet.JsonPatch/project.json index 28b5edfa7a..eaf8c97b46 100644 --- a/src/Microsoft.AspNet.JsonPatch/project.json +++ b/src/Microsoft.AspNet.JsonPatch/project.json @@ -15,7 +15,7 @@ "System.Collections": "4.0.10-beta-*", "System.Collections.Concurrent": "4.0.10-beta-*", "System.Globalization": "4.0.10-beta-*", - "System.Runtime.Extensions": "4.0.10-beta-*" + "System.Runtime.Extensions": "4.0.10-beta-*" } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonPatchInputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonPatchInputFormatter.cs index 39f8e9c604..f076767743 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonPatchInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonPatchInputFormatter.cs @@ -1,126 +1,45 @@ -using System; -using System.IO; -using System.Text; +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.Threading.Tasks; +using Microsoft.AspNet.JsonPatch; using Microsoft.Framework.Internal; using Microsoft.Net.Http.Headers; -using Newtonsoft.Json; namespace Microsoft.AspNet.Mvc { - public class JsonPatchInputFormatter : InputFormatter + public class JsonPatchInputFormatter : JsonInputFormatter { - private const int DefaultMaxDepth = 32; - private JsonSerializerSettings _jsonSerializerSettings; - public JsonPatchInputFormatter() { - SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM); - SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian); + // Clear all values and only include json-patch+json value. + SupportedMediaTypes.Clear(); - SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/jsonpatch")); - - _jsonSerializerSettings = new JsonSerializerSettings - { - MissingMemberHandling = MissingMemberHandling.Ignore, - - // Limit the object graph we'll consume to a fixed depth. This prevents stackoverflow exceptions - // from deserialization errors that might occur from deeply nested objects. - MaxDepth = DefaultMaxDepth, - - // Do not change this setting - // Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types - TypeNameHandling = TypeNameHandling.None - }; - - _jsonSerializerSettings.ContractResolver = new JsonContractResolver(); - } - - /// - /// Gets or sets the used to configure the . - /// - public JsonSerializerSettings SerializerSettings - { - get { return _jsonSerializerSettings; } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _jsonSerializerSettings = value; - } + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json-patch+json")); } /// - public override Task ReadRequestBodyAsync([NotNull] InputFormatterContext context) + public async override Task ReadRequestBodyAsync([NotNull] InputFormatterContext context) { - var type = context.ModelType; - var request = context.ActionContext.HttpContext.Request; - MediaTypeHeaderValue requestContentType = null; - MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType); - - // Get the character encoding for the content - // Never non-null since SelectCharacterEncoding() throws in error / not found scenarios - var effectiveEncoding = SelectCharacterEncoding(requestContentType); - - using (var jsonReader = CreateJsonReader(context, request.Body, effectiveEncoding)) + var jsonPatchDocument = (IJsonPatchDocument)(await base.ReadRequestBodyAsync(context)); + if (jsonPatchDocument != null) { - jsonReader.CloseInput = false; - - var jsonSerializer = CreateJsonSerializer(); - - EventHandler errorHandler = null; - errorHandler = (sender, e) => - { - var exception = e.ErrorContext.Error; - context.ActionContext.ModelState.TryAddModelError(e.ErrorContext.Path, e.ErrorContext.Error); - - // Error must always be marked as handled - // Failure to do so can cause the exception to be rethrown at every recursive level and - // overflow the stack for x64 CLR processes - e.ErrorContext.Handled = true; - }; - jsonSerializer.Error += errorHandler; - - try - { - var contractObject = SerializerSettings.ContractResolver.ResolveContract(type); - return Task.FromResult(jsonSerializer.Deserialize(jsonReader, type)); - } - finally - { - // Clean up the error handler in case CreateJsonSerializer() reuses a serializer - if (errorHandler != null) - { - jsonSerializer.Error -= errorHandler; - } - } + jsonPatchDocument.ContractResolver = SerializerSettings.ContractResolver; } + + return (object)jsonPatchDocument; } - /// - /// Called during deserialization to get the . - /// - /// The for the read. - /// The from which to read. - /// The to use when reading. - /// The used during deserialization. - public virtual JsonReader CreateJsonReader([NotNull] InputFormatterContext context, - [NotNull] Stream readStream, - [NotNull] Encoding effectiveEncoding) + /// + public override bool CanRead(InputFormatterContext context) { - return new JsonTextReader(new StreamReader(readStream, effectiveEncoding)); - } + if (!typeof(IJsonPatchDocument).IsAssignableFrom(context.ModelType) || + !context.ModelType.IsGenericType()) + { + return false; + } - /// - /// Called during deserialization to get the . - /// - /// The used during serialization and deserialization. - public virtual JsonSerializer CreateJsonSerializer() - { - return JsonSerializer.Create(SerializerSettings); + return base.CanRead(context); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs index d0b12d5ec6..7938090321 100644 --- a/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs +++ b/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs @@ -201,6 +201,69 @@ namespace Microsoft.AspNet.JsonPatch.Test Assert.Equal(new List() { 4, 1, 2, 3 }, doc.SimpleDTO.IntegerList); } + [Fact] + public void AddToComplextTypeListSpecifyIndex() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTOList = new List() + { + new SimpleDTO + { + StringProperty = "String1" + }, + new SimpleDTO + { + StringProperty = "String2" + } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTOList[0].StringProperty, "ChangedString1"); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("ChangedString1", doc.SimpleDTOList[0].StringProperty); + } + + [Fact] + public void AddToComplextTypeListSpecifyIndexWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTOList = new List() + { + new SimpleDTO + { + StringProperty = "String1" + }, + new SimpleDTO + { + StringProperty = "String2" + } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTOList[0].StringProperty, "ChangedString1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("ChangedString1", doc.SimpleDTOList[0].StringProperty); + } + [Fact] public void AddToListInvalidPositionTooLarge() { diff --git a/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs index 9b1eeaf8a8..7af388099b 100644 --- a/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs +++ b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs @@ -1,20 +1,20 @@ -using System; +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; using System.Collections.Generic; namespace Microsoft.AspNet.JsonPatch.Test { - public class SimpleDTO - { - public List IntegerList { get; set; } - public int IntegerValue { get; set; } - public string StringProperty { get; set; } - public string AnotherStringProperty { get; set; } - public decimal DecimalValue { get; set; } - - public double DoubleValue { get; set; } - - public float FloatValue { get; set; } - - public Guid GuidValue { get; set; } - } + public class SimpleDTO + { + public List IntegerList { get; set; } + public int IntegerValue { get; set; } + public string StringProperty { get; set; } + public string AnotherStringProperty { get; set; } + public decimal DecimalValue { get; set; } + public double DoubleValue { get; set; } + public float FloatValue { get; set; } + public Guid GuidValue { get; set; } + } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTOWithNestedDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTOWithNestedDTO.cs index a9105c113f..fbc9f71fcd 100644 --- a/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTOWithNestedDTO.cs +++ b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTOWithNestedDTO.cs @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; + namespace Microsoft.AspNet.JsonPatch.Test { - public class SimpleDTOWithNestedDTO { public int IntegerValue { get; set; } @@ -12,10 +13,13 @@ namespace Microsoft.AspNet.JsonPatch.Test public SimpleDTO SimpleDTO { get; set; } + public List SimpleDTOList { get; set; } + public SimpleDTOWithNestedDTO() { this.NestedDTO = new NestedDTO(); this.SimpleDTO = new SimpleDTO(); + this.SimpleDTOList = new List(); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/project.json b/test/Microsoft.AspNet.JsonPatch.Test/project.json index dd66a23824..10f37a87b3 100644 --- a/test/Microsoft.AspNet.JsonPatch.Test/project.json +++ b/test/Microsoft.AspNet.JsonPatch.Test/project.json @@ -3,12 +3,12 @@ "warningsAsErrors": true }, "dependencies": { - "Microsoft.AspNet.Http.Extensions": "1.0.0-*", - "Microsoft.AspNet.JsonPatch": "1.0.0-*", - "Microsoft.AspNet.Mvc.Core": "6.0.0-*", - "Microsoft.AspNet.Testing": "1.0.0-*", - "Moq": "4.2.1312.1622", - "Newtonsoft.Json": "6.0.6", + "Microsoft.AspNet.Http.Extensions": "1.0.0-*", + "Microsoft.AspNet.JsonPatch": "1.0.0-*", + "Microsoft.AspNet.Mvc.Core": "6.0.0-*", + "Microsoft.AspNet.Testing": "1.0.0-*", + "Moq": "4.2.1312.1622", + "Newtonsoft.Json": "6.0.6", "xunit.runner.aspnet": "2.0.0-aspnet-*" }, "commands": { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/JsonPatchInputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/JsonPatchInputFormatterTest.cs new file mode 100644 index 0000000000..e65f399754 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Formatters/JsonPatchInputFormatterTest.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.JsonPatch; +using Microsoft.AspNet.Mvc.ModelBinding; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class JsonPatchInputFormatterTest + { + [Fact] + public async Task JsonPatchInputFormatter_ReadsOneOperation_Successfully() + { + // Arrange + var formatter = new JsonPatchInputFormatter(); + var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + var actionContext = GetActionContext(contentBytes); + var context = new InputFormatterContext(actionContext, typeof(JsonPatchDocument)); + + // Act + var model = await formatter.ReadAsync(context); + + // Assert + var patchDoc = Assert.IsType>(model); + Assert.Equal("add", patchDoc.Operations[0].op); + Assert.Equal("Customer/Name", patchDoc.Operations[0].path); + Assert.Equal("John", patchDoc.Operations[0].value); + } + + [Fact] + public async Task JsonPatchInputFormatter_ReadsMultipleOperations_Successfully() + { + // Arrange + var formatter = new JsonPatchInputFormatter(); + var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}," + + "{\"op\": \"remove\", \"path\" : \"Customer/Name\"}]"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + var actionContext = GetActionContext(contentBytes); + var context = new InputFormatterContext(actionContext, typeof(JsonPatchDocument)); + + // Act + var model = await formatter.ReadAsync(context); + + // Assert + var patchDoc = Assert.IsType>(model); + Assert.Equal("add", patchDoc.Operations[0].op); + Assert.Equal("Customer/Name", patchDoc.Operations[0].path); + Assert.Equal("John", patchDoc.Operations[0].value); + Assert.Equal("remove", patchDoc.Operations[1].op); + Assert.Equal("Customer/Name", patchDoc.Operations[1].path); + } + + [Theory] + [InlineData("application/json-patch+json", true)] + [InlineData("application/json", false)] + [InlineData("application/*", true)] + [InlineData("*/*", true)] + public void CanRead_ReturnsTrueOnlyForJsonPatchContentType(string requestContentType, bool expectedCanRead) + { + // Arrange + var formatter = new JsonPatchInputFormatter(); + var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + var actionContext = GetActionContext(contentBytes, contentType: requestContentType); + var formatterContext = new InputFormatterContext(actionContext, typeof(JsonPatchDocument)); + + // Act + var result = formatter.CanRead(formatterContext); + + // Assert + Assert.Equal(expectedCanRead, result); + } + + [Theory] + [InlineData(typeof(Customer))] + [InlineData(typeof(IJsonPatchDocument))] + public void CanRead_ReturnsFalse_NonJsonPatchContentType(Type modelType) + { + // Arrange + var formatter = new JsonPatchInputFormatter(); + var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + var actionContext = GetActionContext(contentBytes, contentType: "application/json-patch+json"); + var formatterContext = new InputFormatterContext(actionContext, modelType); + + // Act + var result = formatter.CanRead(formatterContext); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task JsonPatchInputFormatter_ReturnsModelStateErrors_InvalidModelType() + { + // Arrange + var exceptionMessage = "Cannot deserialize the current JSON array (e.g. [1,2,3]) into type " + + "'Microsoft.AspNet.Mvc.JsonPatchInputFormatterTest+Customer' because the type requires a JSON object "; + + var formatter = new JsonPatchInputFormatter(); + var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + var actionContext = GetActionContext(contentBytes, contentType: "application/json-patch+json"); + var context = new InputFormatterContext(actionContext, typeof(Customer)); + + // Act + var model = await formatter.ReadAsync(context); + + // Assert + Assert.Contains(exceptionMessage, actionContext.ModelState[""].Errors[0].Exception.Message); + } + + private static ActionContext GetActionContext(byte[] contentBytes, + string contentType = "application/json-patch+json") + { + return new ActionContext(GetHttpContext(contentBytes, contentType), + new AspNet.Routing.RouteData(), + new ActionDescriptor()); + } + + private static HttpContext GetHttpContext(byte[] contentBytes, + string contentType = "application/json-patch+json") + { + var request = new Mock(); + var headers = new Mock(); + request.SetupGet(r => r.Headers).Returns(headers.Object); + request.SetupGet(f => f.Body).Returns(new MemoryStream(contentBytes)); + request.SetupGet(f => f.ContentType).Returns(contentType); + + var httpContext = new Mock(); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + httpContext.SetupGet(c => c.Request).Returns(request.Object); + return httpContext.Object; + } + + private class Customer + { + public string Name { get; set; } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs index 6328a70e30..c1fcb862fe 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs @@ -103,8 +103,9 @@ namespace Microsoft.AspNet.Mvc setup.Configure(mvcOptions); // Assert - Assert.Equal(1, mvcOptions.InputFormatters.Count); + Assert.Equal(2, mvcOptions.InputFormatters.Count); Assert.IsType(mvcOptions.InputFormatters[0].Instance); + Assert.IsType(mvcOptions.InputFormatters[1].Instance); } [Fact] diff --git a/test/WebSites/FiltersWebSite/Controllers/JsonOnlyController.cs b/test/WebSites/FiltersWebSite/Controllers/JsonOnlyController.cs index a5800b93d1..5898155df4 100644 --- a/test/WebSites/FiltersWebSite/Controllers/JsonOnlyController.cs +++ b/test/WebSites/FiltersWebSite/Controllers/JsonOnlyController.cs @@ -25,7 +25,10 @@ namespace FiltersWebSite.Controllers public void OnResourceExecuting(ResourceExecutingContext context) { - var jsonFormatter = context.InputFormatters.OfType().Single(); + // InputFormatters collection contains JsonInputFormatter and JsonPatchInputFormatter. Picking + // JsonInputFormatter by matching the typename + var jsonFormatter = context.InputFormatters.OfType() + .Where(t => t.GetType() == typeof(JsonInputFormatter)).FirstOrDefault(); context.InputFormatters.Clear(); context.InputFormatters.Add(jsonFormatter);