diff --git a/Mvc.NoFun.sln b/Mvc.NoFun.sln index 9d0aa0c826..af813aeb3e 100644 --- a/Mvc.NoFun.sln +++ b/Mvc.NoFun.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22609.0 +VisualStudioVersion = 14.0.22710.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -60,6 +60,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Common EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.TestCommon", "test\Microsoft.AspNet.Mvc.TestCommon\Microsoft.AspNet.Mvc.TestCommon.xproj", "{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch.Test", "test\Microsoft.AspNet.JsonPatch.Test\Microsoft.AspNet.JsonPatch.Test.xproj", "{81C20848-E063-4E12-AC40-0B55A532C16C}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch", "src\Microsoft.AspNet.JsonPatch\Microsoft.AspNet.JsonPatch.xproj", "{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -318,6 +322,29 @@ Global {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.ActiveCfg = Release|Any CPU {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.Build.0 = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|x86.ActiveCfg = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|x86.Build.0 = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Any CPU.Build.0 = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|x86.ActiveCfg = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|x86.Build.0 = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|x86.ActiveCfg = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|x86.Build.0 = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Any CPU.Build.0 = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -346,5 +373,7 @@ Global {4DA2D7C1-A7B6-4C01-B57D-89E6EA4609DE} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {0449D6D2-BE1B-4E29-8E1B-444420802C03} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {81C20848-E063-4E12-AC40-0B55A532C16C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} EndGlobalSection EndGlobal diff --git a/Mvc.sln b/Mvc.sln index c14e768c45..a7b76386b6 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,5 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.22711.0 MinimumVisualStudioVersion = 10.0.40219.1 @@ -156,6 +155,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CorsWebSite", "test\WebSite EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CorsMiddlewareWebSite", "test\WebSites\CorsMiddlewareWebSite\CorsMiddlewareWebSite.xproj", "{B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch.Test", "test\Microsoft.AspNet.JsonPatch.Test\Microsoft.AspNet.JsonPatch.Test.xproj", "{81C20848-E063-4E12-AC40-0B55A532C16C}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch", "src\Microsoft.AspNet.JsonPatch\Microsoft.AspNet.JsonPatch.xproj", "{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -898,6 +901,30 @@ Global {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.ActiveCfg = Release|Any CPU {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.Build.0 = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|x86.ActiveCfg = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|x86.Build.0 = Debug|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Any CPU.Build.0 = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|x86.ActiveCfg = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|x86.Build.0 = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|x86.ActiveCfg = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|x86.Build.0 = Debug|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Any CPU.Build.0 = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|x86.ActiveCfg = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|x86.Build.0 = Release|Any CPU {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Any CPU.Build.0 = Debug|Any CPU {94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -996,7 +1023,9 @@ Global {BCDB13A6-7D6E-485E-8424-A156432B71AC} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {8AEB631E-AB74-4D2E-83FB-8931EE10D9D3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {81C20848-E063-4E12-AC40-0B55A532C16C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {94BA134D-04B3-48AA-BA55-5A4DB8640F2D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection -EndGlobal +EndGlobal \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs b/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs new file mode 100644 index 0000000000..a4f8f10b87 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs @@ -0,0 +1,16 @@ +// 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. + +namespace Microsoft.AspNet.JsonPatch.Adapters +{ + public interface IObjectAdapter + where T : class + { + void Add(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); + void Copy(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); + void Move(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); + void Remove(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); + void Replace(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); + void Test(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Adapters/SimpleObjectAdapter.cs b/src/Microsoft.AspNet.JsonPatch/Adapters/SimpleObjectAdapter.cs new file mode 100644 index 0000000000..d2ae4cc840 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Adapters/SimpleObjectAdapter.cs @@ -0,0 +1,713 @@ +using System; +using Microsoft.AspNet.JsonPatch.Operations; +using Microsoft.AspNet.JsonPatch.Helpers; +using Microsoft.AspNet.JsonPatch.Exceptions; +using System.Reflection; +using System.Collections; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch.Adapters +{ + public class SimpleObjectAdapter : IObjectAdapter where T : class + { + /// + /// The "add" operation performs one of the following functions, + /// depending upon what the target location references: + /// + /// o If the target location specifies an array index, a new value is + /// inserted into the array at the specified index. + /// + /// o If the target location specifies an object member that does not + /// already exist, a new member is added to the object. + /// + /// o If the target location specifies an object member that does exist, + /// that member's value is replaced. + /// + /// The operation object MUST contain a "value" member whose content + /// specifies the value to be added. + /// + /// For example: + /// + /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } + /// + /// When the operation is applied, the target location MUST reference one + /// of: + /// + /// o The root of the target document - whereupon the specified value + /// becomes the entire content of the target document. + /// + /// o A member to add to an existing object - whereupon the supplied + /// value is added to that object at the indicated location. If the + /// member already exists, it is replaced by the specified value. + /// + /// o An element to add to an existing array - whereupon the supplied + /// value is added to the array at the indicated location. Any + /// elements at or above the specified index are shifted one position + /// to the right. The specified index MUST NOT be greater than the + /// number of elements in the array. If the "-" character is used to + /// index the end of the array (see [RFC6901]), this has the effect of + /// appending the value to the array. + /// + /// Because this operation is designed to add to existing objects and + /// arrays, its target location will often not exist. Although the + /// pointer's error handling algorithm will thus be invoked, this + /// specification defines the error handling behavior for "add" pointers + /// to ignore that error and add the value as specified. + /// + /// However, the object itself or an array containing it does need to + /// exist, and it remains an error for that not to be the case. For + /// example, an "add" with a target location of "/a/b" starting with this + /// document: + /// + /// { "a": { "foo": 1 } } + /// + /// is not an error, because "a" exists, and "b" will be added to its + /// value. It is an error in this document: + /// + /// { "q": { "bar": 2 } } + /// + /// because "a" does not exist. + /// + /// The add operation + /// Object to apply the operation to + public void Add(Operation operation, T objectToApplyTo) + { + Add(operation.path, operation.value, objectToApplyTo, operation); + } + + /// + /// 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 + /// + private void Add(string path, object value, T objectToApplyTo, Operation 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 pathProperty = PropertyHelpers + .FindProperty(objectToApplyTo, actualPathToProperty); + + + // 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); + } + + // 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) + { + // 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(operationToReport, + string.Format("Patch failed: provided value is invalid for array property type at location path: {0}", + path), + objectToApplyTo); + } + + // 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(operationToReport, + string.Format("Patch failed: provided path is invalid for array property type at " + + "location path: {0}: position doesn't exist in array", + 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), + objectToApplyTo); + } + } + else + { + var conversionResultTuple = PropertyHelpers.ConvertToActualType(pathProperty.PropertyType, value); + + // conversion successful + if (conversionResultTuple.CanBeConverted) + { + PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty, + conversionResultTuple.ConvertedInstance); + } + else + { + throw new JsonPatchException(operationToReport, + string.Format("Patch failed: provided value is invalid for property type at location path: {0}", + path), + objectToApplyTo); + } + + } + } + + /// + /// The "move" operation removes the value at a specified location and + /// adds it 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 move the value from. + /// + /// The "from" location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } + /// + /// This operation is functionally identical to a "remove" operation on + /// the "from" location, followed immediately by an "add" operation at + /// the target location with the value that was just removed. + /// + /// The "from" location MUST NOT be a proper prefix of the "path" + /// location; i.e., a location cannot be moved into one of its children. + /// + /// The move operation + /// 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) + { + actualFromProperty = operation.from.Substring(0, + operation.from.IndexOf('/' + positionAsInteger.ToString())); + } + + var fromProperty = PropertyHelpers + .FindProperty(objectToApplyTo, actualFromProperty); + + + // 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); + } + + + // 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) + { + // now, get the generic type of the enumerable + var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType); + + // get value (it can be cast, we just checked that) + var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty) as IList; + + 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); + } + + 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), + objectToApplyTo); + } + } + else + { + // no list, just get the value + + // set the new value + + valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty); + + } + + + // remove that value + + Remove(operation.from, objectToApplyTo, operation); + + // add that value to the path location + + Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); + + } + + /// + /// The "remove" operation removes the value at the target location. + /// + /// The target location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "remove", "path": "/a/b/c" } + /// + /// If removing an element from an array, any elements above the + /// specified index are shifted one position to the left. + /// + /// The remove operation + /// Object to apply the operation to + public void Remove(Operation operation, T objectToApplyTo) + { + Remove(operation.path, objectToApplyTo, operation); + } + + /// + /// 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 + /// + private void Remove(string path, T objectToApplyTo, Operation 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 pathProperty = PropertyHelpers + .FindProperty(objectToApplyTo, actualPathToProperty); + + // 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); + } + + // 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) + { + // now, get the generic type of the enumerable + var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType); + + // TODO: nested! + // get value (it can be cast, we just checked that) + var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList; + + if (removeFromList) + { + array.RemoveAt(array.Count - 1); + } + else + { + if (positionAsInteger < array.Count) + { + array.RemoveAt(positionAsInteger); + } + 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); + } + } + + } + else + { + throw new JsonPatchException(operationToReport, + 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); + } + + } + + + + + /// + /// 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" } + /// + /// The test operation + /// Object to apply the operation to + 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) + { + actualPathProperty = operation.path.Substring(0, + operation.path.IndexOf('/' + positionInPathAsInteger.ToString())); + } + + var pathProperty = PropertyHelpers + .FindProperty(objectToApplyTo, actualPathProperty); + + // 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); + } + + // 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) + { + // now, get the generic type of the enumerable + typeOfFinalPropertyAtPathLocation = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType); + + // get value (it can be cast, we just checked that) + var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathProperty) as IList; + + 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); + } + + 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), + 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); + } + + + + } + + + /// + /// 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 + /// 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 + /// 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) + { + actualFromProperty = operation.from.Substring(0, + operation.from.IndexOf('/' + positionAsInteger.ToString())); + } + + + var fromProperty = PropertyHelpers + .FindProperty(objectToApplyTo, actualFromProperty); + + // 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); + } + + // 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) + { + // now, get the generic type of the enumerable + var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType); + + // get value (it can be cast, we just checked that) + var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty) as IList; + + 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); + } + + 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), + objectToApplyTo); + } + } + else + { + // no list, just get the value + + // set the new value + + valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty); + + } + + // add operation to target location with that value. + + Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); + + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs b/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs new file mode 100644 index 0000000000..53474cf3a7 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNet.JsonPatch.Exceptions; +using Microsoft.AspNet.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.JsonPatch.Converters +{ + public class TypedJsonPatchDocumentConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + + return true; + } + + 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]; + + // 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); + + 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; + + // Populate the object properties + serializer.Populate(jObjectReader, targetOperations); + + // container target: the typed JsonPatchDocument. + var container = Activator.CreateInstance(objectType, targetOperations); + + return container; + + } + 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(); + + // write out the operations, no envelope + serializer.Serialize(writer, lst); + + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Exceptions/JsonPatchException.cs b/src/Microsoft.AspNet.JsonPatch/Exceptions/JsonPatchException.cs new file mode 100644 index 0000000000..1ca3f48f0a --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Exceptions/JsonPatchException.cs @@ -0,0 +1,34 @@ +// 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; + +namespace Microsoft.AspNet.JsonPatch.Exceptions +{ + public class JsonPatchException : Exception + { + public new Exception InnerException { get; internal set; } + + public object AffectedObject { get; private set; } + + private string _message = ""; + public override string Message + { + get + { + return _message; + } + } + + public JsonPatchException() + { + + } + + public JsonPatchException(string message, Exception innerException) + { + _message = message; + InnerException = innerException; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Exceptions/TypedJsonPatchException.cs b/src/Microsoft.AspNet.JsonPatch/Exceptions/TypedJsonPatchException.cs new file mode 100644 index 0000000000..d8e77d4b32 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Exceptions/TypedJsonPatchException.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.AspNet.JsonPatch.Operations; + +namespace Microsoft.AspNet.JsonPatch.Exceptions +{ + public class JsonPatchException : JsonPatchException where T : class + { + public Operation FailedOperation { get; private set; } + public new T AffectedObject { get; private set; } + + private string _message = ""; + public override string Message + { + get + { + return _message; + } + } + + public JsonPatchException() + { + + } + + public JsonPatchException(Operation operation, string message, T affectedObject) + { + FailedOperation = operation; + _message = message; + AffectedObject = affectedObject; + } + + public JsonPatchException(Operation operation, string message, T affectedObject, Exception innerException) + : this(operation, message, affectedObject) + { + InnerException = innerException; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/ConversionResult.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/ConversionResult.cs new file mode 100644 index 0000000000..2939dbe3fe --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/ConversionResult.cs @@ -0,0 +1,17 @@ +// 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. + +namespace Microsoft.AspNet.JsonPatch.Helpers +{ + internal class ConversionResult + { + public bool CanBeConverted { get; private set; } + public object ConvertedInstance { get; private set; } + + public ConversionResult(bool canBeConverted, object convertedInstance) + { + CanBeConverted = canBeConverted; + ConvertedInstance = convertedInstance; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/ExpressionHelpers.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/ExpressionHelpers.cs new file mode 100644 index 0000000000..041ffbb928 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/ExpressionHelpers.cs @@ -0,0 +1,99 @@ +// 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.Globalization; +using System.Linq.Expressions; + +namespace Microsoft.AspNet.JsonPatch.Helpers +{ + internal static class ExpressionHelpers + { + public static string GetPath(Expression> expr) where T : class + { + return "/" + GetPath(expr.Body, true); + } + + private static string GetPath(Expression expr, bool firstTime) + { + switch (expr.NodeType) + { + case ExpressionType.ArrayIndex: + var binaryExpression = (BinaryExpression)expr; + + if (ContinueWithSubPath(binaryExpression.Left.NodeType, false)) + { + var leftFromBinaryExpression = GetPath(binaryExpression.Left, false); + return leftFromBinaryExpression + "/" + binaryExpression.Right.ToString(); + } + else + { + return binaryExpression.Right.ToString(); + } + case ExpressionType.Call: + var methodCallExpression = (MethodCallExpression)expr; + + if (ContinueWithSubPath(methodCallExpression.Object.NodeType, false)) + { + var leftFromMemberCallExpression = GetPath(methodCallExpression.Object, false); + return leftFromMemberCallExpression + "/" + + GetIndexerInvocation(methodCallExpression.Arguments[0]); + } + else + { + return GetIndexerInvocation(methodCallExpression.Arguments[0]); + } + case ExpressionType.Convert: + return GetPath(((UnaryExpression)expr).Operand, false); + case ExpressionType.MemberAccess: + var memberExpression = expr as MemberExpression; + + if (ContinueWithSubPath(memberExpression.Expression.NodeType, false)) + { + var left = GetPath(memberExpression.Expression, false); + return left + "/" + memberExpression.Member.Name; + } + else + { + return memberExpression.Member.Name; + } + case ExpressionType.Parameter: + // Fits "x => x" (the whole document which is "" as JSON pointer) + return firstTime ? "" : null; + default: + return ""; + } + } + + private static bool ContinueWithSubPath(ExpressionType expressionType, bool firstTime) + { + if (firstTime) + { + return (expressionType == ExpressionType.ArrayIndex + || expressionType == ExpressionType.Call + || expressionType == ExpressionType.Convert + || expressionType == ExpressionType.MemberAccess + || expressionType == ExpressionType.Parameter); + } + else + { + return (expressionType == ExpressionType.ArrayIndex + || expressionType == ExpressionType.Call + || expressionType == ExpressionType.Convert + || expressionType == ExpressionType.MemberAccess); + } + } + + private static string GetIndexerInvocation(Expression expression) + { + var converted = Expression.Convert(expression, typeof(object)); + var fakeParameter = Expression.Parameter(typeof(object), null); + var lambda = Expression.Lambda>(converted, fakeParameter); + Func func; + + func = lambda.Compile(); + + return Convert.ToString(func(null), CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/PropertyHelpers.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/PropertyHelpers.cs new file mode 100644 index 0000000000..17c2b9e7ef --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/PropertyHelpers.cs @@ -0,0 +1,140 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +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) + { + try + { + var splitPath = propertyPath.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); + } + + var propertyToFind = targetObject.GetType().GetProperty(splitPath.Last(), + BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + + return propertyToFind; + } + catch (Exception) + { + // will result in JsonPatchException in calling class, as expected + return null; + } + } + + internal static ConversionResult ConvertToActualType(Type propertyType, object value) + { + try + { + var o = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), propertyType); + + return new ConversionResult(true, o); + } + catch (Exception) + { + return new ConversionResult(false, null); + } + } + + internal static Type GetEnumerableType(Type type) + { + 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<>)) + { + return interfaceType.GetGenericArguments()[0]; + } +#endif + + } + return null; + } + + internal static int GetNumericEnd(string path) + { + var possibleIndex = path.Substring(path.LastIndexOf("/") + 1); + var castedIndex = -1; + + if (int.TryParse(possibleIndex, out castedIndex)) + { + return castedIndex; + } + + 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 new file mode 100644 index 0000000000..a0fc3d14d3 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs @@ -0,0 +1,13 @@ +// 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 Microsoft.AspNet.JsonPatch.Operations; +using System.Collections.Generic; + +namespace Microsoft.AspNet.JsonPatch +{ + public interface IJsonPatchDocument + { + List GetOperations(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Microsoft.AspNet.JsonPatch.xproj b/src/Microsoft.AspNet.JsonPatch/Microsoft.AspNet.JsonPatch.xproj new file mode 100644 index 0000000000..e8a9a8f3c7 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Microsoft.AspNet.JsonPatch.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 4d55f4d8-633b-462f-a5b1-feb84bd2d534 + Microsoft.AspNet.JsonPatch + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Operations/Operation.cs b/src/Microsoft.AspNet.JsonPatch/Operations/Operation.cs new file mode 100644 index 0000000000..e6227e5c91 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Operations/Operation.cs @@ -0,0 +1,31 @@ +// 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; + +namespace Microsoft.AspNet.JsonPatch.Operations +{ + public class Operation : OperationBase + { + [JsonProperty("value")] + public object value { get; set; } + + public Operation() + { + + } + + public Operation(string op, string path, string from, object value) + : base(op, path, from) + { + this.value = value; + } + + public Operation(string op, string path, string from) + : base(op, path, from) + { + + } + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Operations/OperationBase.cs b/src/Microsoft.AspNet.JsonPatch/Operations/OperationBase.cs new file mode 100644 index 0000000000..77bf8914d7 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Operations/OperationBase.cs @@ -0,0 +1,47 @@ +// 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 Newtonsoft.Json; + +namespace Microsoft.AspNet.JsonPatch.Operations +{ + public class OperationBase + { + [JsonIgnore] + public OperationType OperationType + { + get + { + return (OperationType)Enum.Parse(typeof(OperationType), op, true); + } + } + + [JsonProperty("path")] + public string path { get; set; } + + [JsonProperty("op")] + public string op { get; set; } + + [JsonProperty("from")] + public string from { get; set; } + + public OperationBase() + { + + } + + public OperationBase(string op, string path, string from) + { + this.op = op; + this.path = path; + this.from = from; + } + + public bool ShouldSerializefrom() + { + return (OperationType == Operations.OperationType.Move + || OperationType == OperationType.Copy); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Operations/OperationType.cs b/src/Microsoft.AspNet.JsonPatch/Operations/OperationType.cs new file mode 100644 index 0000000000..63c5910bc5 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Operations/OperationType.cs @@ -0,0 +1,15 @@ +// 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. + +namespace Microsoft.AspNet.JsonPatch.Operations +{ + public enum OperationType + { + Add, + Remove, + Replace, + Move, + Copy, + Test + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Operations/TypedOperation.cs b/src/Microsoft.AspNet.JsonPatch/Operations/TypedOperation.cs new file mode 100644 index 0000000000..de315b99de --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Operations/TypedOperation.cs @@ -0,0 +1,61 @@ +// 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 Microsoft.AspNet.JsonPatch.Adapters; + +namespace Microsoft.AspNet.JsonPatch.Operations +{ + public class Operation : Operation where T : class + { + public Operation() + { + + } + + public Operation(string op, string path, string from, object value) + : base(op, path, from) + { + this.value = value; + } + + public Operation(string op, string path, string from) + : base(op, path, from) + { + + } + + public void Apply(T objectToApplyTo, IObjectAdapter adapter) + { + switch (OperationType) + { + case OperationType.Add: + adapter.Add(this, objectToApplyTo); + break; + case OperationType.Remove: + adapter.Remove(this, objectToApplyTo); + break; + case OperationType.Replace: + adapter.Replace(this, objectToApplyTo); + break; + case OperationType.Move: + adapter.Move(this, objectToApplyTo); + break; + case OperationType.Copy: + adapter.Copy(this, objectToApplyTo); + break; + case OperationType.Test: + adapter.Test(this, objectToApplyTo); + break; + default: + break; + } + } + + public bool ShouldSerializevalue() + { + return (OperationType == Operations.OperationType.Add + || OperationType == OperationType.Replace + || OperationType == OperationType.Test); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs b/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs new file mode 100644 index 0000000000..7ada538c33 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs @@ -0,0 +1,377 @@ +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; + +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; } + + public JsonPatchDocument() + { + Operations = new List>(); + } + + // Create from list of operations + public JsonPatchDocument(List> operations) + { + Operations = operations; + } + + /// + /// 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; + } + + /// + /// 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; + } + + /// + /// 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 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 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 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. 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 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 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; + } + + /// + /// 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 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 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 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 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 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 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 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 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 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 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 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) + { + ApplyTo(objectToApplyTo, new SimpleObjectAdapter()); + } + + public void ApplyTo(T objectToApplyTo, IObjectAdapter adapter) + { + + // apply each operation in order + foreach (var op in Operations) + { + op.Apply(objectToApplyTo, adapter); + } + } + + public List GetOperations() + { + var allOps = new List(); + + if (Operations != null) + { + foreach (var op in Operations) + { + var untypedOp = new Operation(); + + untypedOp.op = op.op; + untypedOp.value = op.value; + untypedOp.path = op.path; + untypedOp.from = op.from; + + 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 new file mode 100644 index 0000000000..28b5edfa7a --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/project.json @@ -0,0 +1,22 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Microsoft.AspNet.Http.Extensions": "1.0.0-*", + "Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" }, + "Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" }, + "Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" }, + "Newtonsoft.Json": "6.0.6" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "Microsoft.CSharp": "4.0.0-beta-*", + "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-*" + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonContractResolver.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonContractResolver.cs index 775938d478..2be2872abc 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonContractResolver.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonContractResolver.cs @@ -1,6 +1,7 @@ // 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.ComponentModel.DataAnnotations; using System.Reflection; using Newtonsoft.Json; diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonPatchInputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonPatchInputFormatter.cs new file mode 100644 index 0000000000..39f8e9c604 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/JsonPatchInputFormatter.cs @@ -0,0 +1,126 @@ +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Framework.Internal; +using Microsoft.Net.Http.Headers; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.Mvc +{ + public class JsonPatchInputFormatter : InputFormatter + { + private const int DefaultMaxDepth = 32; + private JsonSerializerSettings _jsonSerializerSettings; + + public JsonPatchInputFormatter() + { + SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM); + SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian); + + 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; + } + } + + /// + public 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)) + { + 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; + } + } + } + } + + /// + /// 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) + { + return new JsonTextReader(new StreamReader(readStream, effectiveEncoding)); + } + + /// + /// Called during deserialization to get the . + /// + /// The used during serialization and deserialization. + public virtual JsonSerializer CreateJsonSerializer() + { + return JsonSerializer.Create(SerializerSettings); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index a0dc0cc379..946b296fef 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -22,7 +22,8 @@ "Microsoft.Framework.PropertyActivator.Internal": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.Runtime.Interfaces": "1.0.0-*", - "Microsoft.Framework.WebEncoders": "1.0.0-*" + "Microsoft.Framework.WebEncoders": "1.0.0-*", + "Microsoft.AspNet.JsonPatch": "1.0.0-*" }, "frameworks": { "dnx451": {}, diff --git a/src/Microsoft.AspNet.Mvc/BuilderExtensions.cs b/src/Microsoft.AspNet.Mvc/BuilderExtensions.cs index 2d9a544359..5f24c2781a 100644 --- a/src/Microsoft.AspNet.Mvc/BuilderExtensions.cs +++ b/src/Microsoft.AspNet.Mvc/BuilderExtensions.cs @@ -60,4 +60,4 @@ namespace Microsoft.AspNet.Builder return app.UseRouter(routes.Build()); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index 5a228295dc..e9b0623fe0 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -55,6 +55,7 @@ namespace Microsoft.AspNet.Mvc // Set up default input formatters. options.InputFormatters.Add(new JsonInputFormatter()); + options.InputFormatters.Add(new JsonPatchInputFormatter()); // Set up ValueProviders options.ValueProviderFactories.Add(new RouteValueValueProviderFactory()); diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Microsoft.AspNet.JsonPatch.Test.xproj b/test/Microsoft.AspNet.JsonPatch.Test/Microsoft.AspNet.JsonPatch.Test.xproj new file mode 100644 index 0000000000..007bf19ed2 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Microsoft.AspNet.JsonPatch.Test.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 81c20848-e063-4e12-ac40-0b55a532c16c + + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/NestedDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/NestedDTO.cs new file mode 100644 index 0000000000..cb61d180cd --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/NestedDTO.cs @@ -0,0 +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. + +namespace Microsoft.AspNet.JsonPatch.Test +{ + public class NestedDTO + { + public string StringProperty { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs new file mode 100644 index 0000000000..d0b12d5ec6 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs @@ -0,0 +1,1655 @@ +// 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 Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test +{ + public class NestedObjectTests + { + [Fact] + public void ReplacePropertyInNestedObject() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + IntegerValue = 1 + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.NestedDTO.StringProperty, "B"); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.NestedDTO.StringProperty); + } + + [Fact] + public void ReplacePropertyInNestedObjectWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + IntegerValue = 1 + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.NestedDTO.StringProperty, "B"); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.NestedDTO.StringProperty); + } + + [Fact] + public void ReplaceNestedObject() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + IntegerValue = 1 + }; + + var newNested = new NestedDTO() { StringProperty = "B" }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.NestedDTO, newNested); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.NestedDTO.StringProperty); + } + + [Fact] + public void ReplaceNestedObjectWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + IntegerValue = 1 + }; + + var newNested = new NestedDTO() { StringProperty = "B" }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.NestedDTO, newNested); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.NestedDTO.StringProperty); + } + + [Fact] + public void AddResultsInReplace() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.StringProperty, "B"); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.SimpleDTO.StringProperty); + } + + [Fact] + public void AddResultsInReplaceWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.StringProperty, "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.SimpleDTO.StringProperty); + } + + [Fact] + public void AddToList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void AddToListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4, 0); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void AddToListInvalidPositionTooLarge() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4, 4); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void AddToListInvalidPositionTooLargeWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4, 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void AddToListInvalidPositionTooSmall() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4, -1); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void AddToListInvalidPositionTooSmallWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4, -1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void AddToListAppend() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void AddToListAppendWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void Remove() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.StringProperty); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void RemoveWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.StringProperty); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void RemoveFromList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList, 2); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void RemoveFromListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList, 2); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLarge() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList, 3); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLargeWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList, 3); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void RemoveFromListInvalidPositionTooSmall() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList, -1); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void RemoveFromListInvalidPositionTooSmallWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList, -1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void RemoveFromEndOfList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void RemoveFromEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void Replace() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A", + DecimalValue = 10 + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.StringProperty, "B"); + patchDoc.Replace(o => o.SimpleDTO.DecimalValue, 12); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.SimpleDTO.StringProperty); + Assert.Equal(12, doc.SimpleDTO.DecimalValue); + } + + [Fact] + public void ReplaceWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A", + DecimalValue = 10 + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.StringProperty, "B"); + patchDoc.Replace(o => o.SimpleDTO.DecimalValue, 12); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.SimpleDTO.StringProperty); + Assert.Equal(12, doc.SimpleDTO.DecimalValue); + } + + [Fact] + public void SerializationTests() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A", + DecimalValue = 10, + DoubleValue = 10, + FloatValue = 10, + IntegerValue = 10 + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.StringProperty, "B"); + patchDoc.Replace(o => o.SimpleDTO.DecimalValue, 12); + patchDoc.Replace(o => o.SimpleDTO.DoubleValue, 12); + patchDoc.Replace(o => o.SimpleDTO.FloatValue, 12); + patchDoc.Replace(o => o.SimpleDTO.IntegerValue, 12); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserizalized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserizalized.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.SimpleDTO.StringProperty); + Assert.Equal(12, doc.SimpleDTO.DecimalValue); + Assert.Equal(12, doc.SimpleDTO.DoubleValue); + Assert.Equal(12, doc.SimpleDTO.FloatValue); + Assert.Equal(12, doc.SimpleDTO.IntegerValue); + } + + [Fact] + public void ReplaceInList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 5, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceInListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5, 0); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 5, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceFullList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.SimpleDTO.IntegerList, new List() { 4, 5, 6 }); + + // Act + patchDoc.ApplyTo(doc); + + // Arrange + Assert.Equal(new List() { 4, 5, 6 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceFullListWithSerialiation() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.SimpleDTO.IntegerList, new List() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceFullListFromEnumerable() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.SimpleDTO.IntegerList, new List() { 4, 5, 6 }); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceFullListFromEnumerableWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.SimpleDTO.IntegerList, new List() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceFullListWithCollection() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.SimpleDTO.IntegerList, new Collection() { 4, 5, 6 }); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceFullListWithCollectionWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.SimpleDTO.IntegerList, new Collection() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceAtEndOfList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 5 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceAtEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 5 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceInListInvalidInvalidPositionTooLarge() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5, 3); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void ReplaceInListInvalidInvalidPositionTooLargeWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5, 3); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void ReplaceInListInvalidPositionTooSmall() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5, -1); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void ReplaceInListInvalidPositionTooSmallWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5, -1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void Copy() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.StringProperty, o => o.SimpleDTO.AnotherStringProperty); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("A", doc.SimpleDTO.AnotherStringProperty); + } + + [Fact] + public void CopyWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.StringProperty, o => o.SimpleDTO.AnotherStringProperty); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("A", doc.SimpleDTO.AnotherStringProperty); + } + + [Fact] + public void CopyInList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerList, 1); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void CopyInListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerList, 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void CopyFromListToEndOfList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 1 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void CopyFromListToEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 1 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void CopyFromListToNonList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerValue); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(1, doc.SimpleDTO.IntegerValue); + } + + [Fact] + public void CopyFromListToNonListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerValue); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(1, doc.SimpleDTO.IntegerValue); + } + + [Fact] + public void CopyFromNonListToList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerValue, o => o.SimpleDTO.IntegerList, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void CopyFromNonListToListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerValue, o => o.SimpleDTO.IntegerList, 0); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void CopyToEndOfList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerValue, o => o.SimpleDTO.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void CopyToEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.SimpleDTO.IntegerValue, o => o.SimpleDTO.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void Move() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.StringProperty, o => o.SimpleDTO.AnotherStringProperty); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("A", doc.SimpleDTO.AnotherStringProperty); + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void MoveWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.StringProperty, o => o.SimpleDTO.AnotherStringProperty); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("A", doc.SimpleDTO.AnotherStringProperty); + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void MoveInList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerList, 1); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 1, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void MoveInListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerList, 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 1, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void MoveFromListToEndOfList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3, 1 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void MoveFromListToEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3, 1 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void MoveFomListToNonList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerValue); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3 }, doc.SimpleDTO.IntegerList); + Assert.Equal(1, doc.SimpleDTO.IntegerValue); + } + + [Fact] + public void MoveFomListToNonListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerList, 0, o => o.SimpleDTO.IntegerValue); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3 }, doc.SimpleDTO.IntegerList); + Assert.Equal(1, doc.SimpleDTO.IntegerValue); + } + + [Fact] + public void MoveFomListToNonListBetweenHierarchy() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerList, 0, o => o.IntegerValue); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3 }, doc.SimpleDTO.IntegerList); + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void MoveFomListToNonListBetweenHierarchyWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerList, 0, o => o.IntegerValue); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3 }, doc.SimpleDTO.IntegerList); + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void MoveFromNonListToList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerValue, o => o.SimpleDTO.IntegerList, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void MoveFromNonListToListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerValue, o => o.SimpleDTO.IntegerList, 0); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void MoveToEndOfList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerValue, o => o.SimpleDTO.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void MoveToEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.SimpleDTO.IntegerValue, o => o.SimpleDTO.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.SimpleDTO.IntegerList); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs new file mode 100644 index 0000000000..9b1eeaf8a8 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs @@ -0,0 +1,20 @@ +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; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTOWithNestedDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTOWithNestedDTO.cs new file mode 100644 index 0000000000..a9105c113f --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTOWithNestedDTO.cs @@ -0,0 +1,21 @@ +// 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. + +namespace Microsoft.AspNet.JsonPatch.Test +{ + + public class SimpleDTOWithNestedDTO + { + public int IntegerValue { get; set; } + + public NestedDTO NestedDTO { get; set; } + + public SimpleDTO SimpleDTO { get; set; } + + public SimpleDTOWithNestedDTO() + { + this.NestedDTO = new NestedDTO(); + this.SimpleDTO = new SimpleDTO(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/SimpleObjectAdapterTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/SimpleObjectAdapterTests.cs new file mode 100644 index 0000000000..1f656b05c0 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/SimpleObjectAdapterTests.cs @@ -0,0 +1,1562 @@ +// 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.Collections.ObjectModel; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test +{ + public class SimpleObjectAdapterTests + { + [Fact] + public void AddResultsShouldReplace() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.StringProperty, "B"); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.StringProperty); + } + + [Fact] + public void AddResultsShouldReplaceWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.StringProperty, "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.StringProperty); + } + + [Fact] + public void AddToList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, 0); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListInvalidPositionTooLarge() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, 4); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void AddToListInvalidPositionTooLargeWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void AddToListAtEnd() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, 3); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.IntegerList); + } + + [Fact] + public void AddToListAtEndWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, 3); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.IntegerList); + } + + [Fact] + public void AddToListAtBeginning() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListAtBeginningWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, 0); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListInvalidPositionTooSmall() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, -1); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void AddToListInvalidPositionTooSmallWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, -1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void AddToListAppend() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.IntegerList); + } + + [Fact] + public void AddToListAppendWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.IntegerList); + } + + [Fact] + public void Remove() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.StringProperty); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(null, doc.StringProperty); + } + + [Fact] + public void RemoveWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.StringProperty); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(null, doc.StringProperty); + } + + [Fact] + public void RemoveFromList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, 2); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2 }, doc.IntegerList); + } + + [Fact] + public void RemoveFromListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, 2); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2 }, doc.IntegerList); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLarge() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, 3); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLargeWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, 3); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void RemoveFromListInvalidPositionTooSmall() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, -1); + + // Act & Assert + Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); + } + + [Fact] + public void RemoveFromListInvalidPositionTooSmallWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, -1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => { deserialized.ApplyTo(doc); }); + } + + [Fact] + public void RemoveFromEndOfList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2 }, doc.IntegerList); + } + + [Fact] + public void RemoveFromEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2 }, doc.IntegerList); + } + + [Fact] + public void Replace() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A", + DecimalValue = 10 + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.StringProperty, "B"); + + patchDoc.Replace(o => o.DecimalValue, 12); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.StringProperty); + Assert.Equal(12, doc.DecimalValue); + } + + [Fact] + public void ReplaceWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A", + DecimalValue = 10 + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.StringProperty, "B"); + + patchDoc.Replace(o => o.DecimalValue, 12); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.StringProperty); + Assert.Equal(12, doc.DecimalValue); + } + + [Fact] + public void SerializationMustNotIncudeEnvelope() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A", + DecimalValue = 10, + DoubleValue = 10, + FloatValue = 10, + IntegerValue = 10 + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.StringProperty, "B"); + patchDoc.Replace(o => o.DecimalValue, 12); + patchDoc.Replace(o => o.DoubleValue, 12); + patchDoc.Replace(o => o.FloatValue, 12); + patchDoc.Replace(o => o.IntegerValue, 12); + + // Act + var serialized = JsonConvert.SerializeObject(patchDoc); + + // Assert + Assert.Equal(false, serialized.Contains("operations")); + Assert.Equal(false, serialized.Contains("Operations")); + } + + [Fact] + public void DeserializationMustWorkWithoutEnvelope() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A", + DecimalValue = 10, + DoubleValue = 10, + FloatValue = 10, + IntegerValue = 10 + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.StringProperty, "B"); + patchDoc.Replace(o => o.DecimalValue, 12); + patchDoc.Replace(o => o.DoubleValue, 12); + patchDoc.Replace(o => o.FloatValue, 12); + patchDoc.Replace(o => o.IntegerValue, 12); + + // default: no envelope + var serialized = JsonConvert.SerializeObject(patchDoc); + + // Act + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Assert + Assert.IsType>(deserialized); + } + + [Fact] + public void DeserializationMustFailWithEnvelope() + { + // Arrange + string serialized = "{\"Operations\": [{ \"op\": \"replace\", \"path\": \"/title\", \"value\": \"New Title\"}]}"; + + // Act & Assert + Assert.Throws(() => + { + var deserialized + = JsonConvert.DeserializeObject>(serialized); + }); + } + + [Fact] + public void SerializationTests() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A", + DecimalValue = 10, + DoubleValue = 10, + FloatValue = 10, + IntegerValue = 10 + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.StringProperty, "B"); + patchDoc.Replace(o => o.DecimalValue, 12); + patchDoc.Replace(o => o.DoubleValue, 12); + patchDoc.Replace(o => o.FloatValue, 12); + patchDoc.Replace(o => o.IntegerValue, 12); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserizalized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserizalized.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.StringProperty); + Assert.Equal(12, doc.DecimalValue); + Assert.Equal(12, doc.DoubleValue); + Assert.Equal(12, doc.FloatValue); + Assert.Equal(12, doc.IntegerValue); + } + + [Fact] + public void SerializeAndReplaceGuidTest() + { + // Arrange + var doc = new SimpleDTO() + { + GuidValue = Guid.NewGuid() + }; + + var newGuid = Guid.NewGuid(); + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.GuidValue, newGuid); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserizalized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserizalized.ApplyTo(doc); + + // Assert + Assert.Equal(newGuid, doc.GuidValue); + } + + [Fact] + public void SerializeAndReplaceNestedObjectTest() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + var newDTO = new SimpleDTO() + { + DoubleValue = 1 + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO, newDTO); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(1, doc.SimpleDTO.DoubleValue); + Assert.Equal(0, doc.SimpleDTO.IntegerValue); + Assert.Equal(null, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceInList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.IntegerList, 5, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 5, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void ReplaceInListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.IntegerList, 5, 0); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 5, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.IntegerList, new List() { 4, 5, 6 }); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.IntegerList, new List() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullListFromEnumerable() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.IntegerList, new List() { 4, 5, 6 }); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullListFromEnumerableWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.IntegerList, new List() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullListWithCollection() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.IntegerList, new Collection() { 4, 5, 6 }); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullListWithCollectionWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace>(o => o.IntegerList, new Collection() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceAtEndOfList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.IntegerList, 5); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 5 }, doc.IntegerList); + } + + [Fact] + public void ReplaceAtEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.IntegerList, 5); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 5 }, doc.IntegerList); + } + + [Fact] + public void ReplaceInListInvalidInvalidPositionTooLarge() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.IntegerList, 5, 3); + + // Act & Assert + Assert.Throws>(() => + { + patchDoc.ApplyTo(doc); + }); + } + + [Fact] + public void ReplaceInListInvalidInvalidPositionTooLargeWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.IntegerList, 5, 3); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => + { + deserialized.ApplyTo(doc); + }); + } + + [Fact] + public void ReplaceInListInvalidPositionTooSmall() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.IntegerList, 5, -1); + + // Act & Assert + Assert.Throws>(() => + { + patchDoc.ApplyTo(doc); + }); + } + + [Fact] + public void ReplaceInListInvalidPositionTooSmallWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.IntegerList, 5, -1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act & Assert + Assert.Throws>(() => + { + deserialized.ApplyTo(doc); + }); + } + + [Fact] + public void Copy() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.StringProperty, o => o.AnotherStringProperty); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("A", doc.AnotherStringProperty); + } + + [Fact] + public void CopyWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.StringProperty, o => o.AnotherStringProperty); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("A", doc.AnotherStringProperty); + } + + [Fact] + public void CopyInList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerList, 0, o => o.IntegerList, 1); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void CopyInListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerList, 0, o => o.IntegerList, 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void CopyFromListToEndOfList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerList, 0, o => o.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 1 }, doc.IntegerList); + } + + [Fact] + public void CopyFromListToEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerList, 0, o => o.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 1 }, doc.IntegerList); + } + + [Fact] + public void CopyFromListToNonList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerList, 0, o => o.IntegerValue); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void CopyFromListToNonListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerList, 0, o => o.IntegerValue); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void CopyFromNonListToList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerValue, o => o.IntegerList, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void CopyFromNonListToListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerValue, o => o.IntegerList, 0); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void CopyToEndOfList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerValue, o => o.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.IntegerList); + } + + [Fact] + public void CopyToEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Copy(o => o.IntegerValue, o => o.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.IntegerList); + } + + [Fact] + public void Move() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.StringProperty, o => o.AnotherStringProperty); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("A", doc.AnotherStringProperty); + Assert.Equal(null, doc.StringProperty); + } + + [Fact] + public void MoveWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.StringProperty, o => o.AnotherStringProperty); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("A", doc.AnotherStringProperty); + Assert.Equal(null, doc.StringProperty); + } + + [Fact] + public void MoveInList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerList, 0, o => o.IntegerList, 1); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 1, 3 }, doc.IntegerList); + } + + [Fact] + public void MoveInListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerList, 0, o => o.IntegerList, 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 1, 3 }, doc.IntegerList); + } + + [Fact] + public void MoveFromListToEndOfList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerList, 0, o => o.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3, 1 }, doc.IntegerList); + } + + [Fact] + public void MoveFromListToEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerList, 0, o => o.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3, 1 }, doc.IntegerList); + } + + [Fact] + public void MoveFomListToNonList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerList, 0, o => o.IntegerValue); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3 }, doc.IntegerList); + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void MoveFomListToNonListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerList, 0, o => o.IntegerValue); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 2, 3 }, doc.IntegerList); + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void MoveFromNonListToList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerValue, o => o.IntegerList, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void MoveFromNonListToListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerValue, o => o.IntegerList, 0); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void MoveToEndOfList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerValue, o => o.IntegerList); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.IntegerList); + } + + [Fact] + public void MoveToEndOfListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Move(o => o.IntegerValue, o => o.IntegerList); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.IntegerList); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/project.json b/test/Microsoft.AspNet.JsonPatch.Test/project.json new file mode 100644 index 0000000000..dd66a23824 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/project.json @@ -0,0 +1,20 @@ +{ + "compilationOptions": { + "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", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { } + } +} \ No newline at end of file