From e0ac69c9f79d8486357531f0ea5125bbef3c42fa Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 21 Sep 2015 11:15:18 -0700 Subject: [PATCH] Add JsonPatch Moved from https://github.com/aspnet/Mvc --- JsonPatch.sln | 36 + NuGet.config | 2 +- .../Adapters/IObjectAdapter.cs | 19 + .../Adapters/ObjectAdapter.cs | 1105 +++++++++ .../Converters/JsonPatchDocumentConverter.cs | 76 + .../TypedJsonPatchDocumentConverter.cs | 65 + .../Exceptions/JsonPatchException.cs | 38 + .../Helpers/ActualPropertyPathResult.cs | 22 + .../Helpers/ConversionResult.cs | 17 + .../ExpandoObjectDictionaryExtensions.cs | 77 + .../Helpers/ExpressionHelpers.cs | 99 + .../Helpers/GetValueResult.cs | 30 + .../Helpers/JsonPatchProperty.cs | 43 + .../Helpers/ObjectTreeAnalyisResult.cs | 210 ++ .../Helpers/PathHelpers.cs | 31 + .../Helpers/RemovedPropertyTypeResult.cs | 38 + .../IJsonPatchDocument.cs | 16 + .../JsonPatchDocument.cs | 222 ++ .../JsonPatchDocumentOfT.cs | 700 ++++++ .../JsonPatchError.cs | 50 + .../Microsoft.AspNet.JsonPatch.xproj | 17 + .../Operations/Operation.cs | 74 + .../Operations/OperationBase.cs | 57 + .../Operations/OperationOfT.cs | 83 + .../Operations/OperationType.cs | 15 + .../Properties/AssemblyInfo.cs | 8 + .../Properties/Resources.Designer.cs | 270 +++ src/Microsoft.AspNet.JsonPatch/Resources.resx | 165 ++ src/Microsoft.AspNet.JsonPatch/project.json | 25 + .../Dynamic/AddOperationTests.cs | 569 +++++ .../Dynamic/AddTypedOperationTests.cs | 121 + .../Dynamic/CopyOperationTests.cs | 245 ++ .../Dynamic/CopyTypedOperationTests.cs | 268 +++ .../Dynamic/MoveOperationTests.cs | 338 +++ .../Dynamic/MoveTypedOperationTests.cs | 138 ++ .../Dynamic/NestedDTO.cs | 11 + .../Dynamic/PatchDocumentTests.cs | 95 + .../Dynamic/RemoveOperationTests.cs | 302 +++ .../Dynamic/RemoveTypedOperationTests.cs | 244 ++ .../Dynamic/ReplaceOperationTests.cs | 242 ++ .../Dynamic/ReplaceTypedOperationTests.cs | 191 ++ .../Dynamic/SimpleDTO.cs | 21 + .../Dynamic/SimpleDTOWithNestedDTO.cs | 22 + .../Microsoft.AspNet.JsonPatch.Test.xproj | 20 + .../NestedDTO.cs | 10 + .../NestedObjectTests.cs | 2041 +++++++++++++++++ .../ObjectAdapterTests.cs | 1737 ++++++++++++++ .../SimpleDTO.cs | 22 + .../SimpleDTOWithNestedDTO.cs | 27 + .../TestErrorLogger.cs | 15 + .../project.json | 18 + 51 files changed, 10306 insertions(+), 1 deletion(-) create mode 100644 JsonPatch.sln create mode 100644 src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Converters/JsonPatchDocumentConverter.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Exceptions/JsonPatchException.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/ActualPropertyPathResult.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/ConversionResult.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/ExpandoObjectDictionaryExtensions.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/ExpressionHelpers.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/GetValueResult.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/JsonPatchProperty.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/ObjectTreeAnalyisResult.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/PathHelpers.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/RemovedPropertyTypeResult.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/JsonPatchDocument.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/JsonPatchDocumentOfT.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/JsonPatchError.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Microsoft.AspNet.JsonPatch.xproj create mode 100644 src/Microsoft.AspNet.JsonPatch/Operations/Operation.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Operations/OperationBase.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Operations/OperationOfT.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Operations/OperationType.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.JsonPatch/Resources.resx create mode 100644 src/Microsoft.AspNet.JsonPatch/project.json create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/CopyOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/CopyTypedOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/MoveOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/MoveTypedOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/NestedDTO.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/PatchDocumentTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/ReplaceOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/ReplaceTypedOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTO.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTOWithNestedDTO.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Microsoft.AspNet.JsonPatch.Test.xproj create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/NestedDTO.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/SimpleDTOWithNestedDTO.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/TestErrorLogger.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/project.json diff --git a/JsonPatch.sln b/JsonPatch.sln new file mode 100644 index 0000000000..01e02f3125 --- /dev/null +++ b/JsonPatch.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{430B59ED-F960-4D3A-8FFE-3370008E168D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{36CD6341-AB44-44EB-B3AA-BF98C89FECDD}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch", "src\Microsoft.AspNet.JsonPatch\Microsoft.AspNet.JsonPatch.xproj", "{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}" +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 +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {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}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {430B59ED-F960-4D3A-8FFE-3370008E168D} + {81C20848-E063-4E12-AC40-0B55A532C16C} = {36CD6341-AB44-44EB-B3AA-BF98C89FECDD} + EndGlobalSection +EndGlobal diff --git a/NuGet.config b/NuGet.config index 52bf414192..5500f6d507 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,4 +1,4 @@ - + diff --git a/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs b/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs new file mode 100644 index 0000000000..724dfe3f79 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. 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; + +namespace Microsoft.AspNet.JsonPatch.Adapters +{ + /// + /// Defines the operations that can be performed on a JSON patch document. + /// + public interface IObjectAdapter + { + void Add(Operation operation, object objectToApplyTo); + void Copy(Operation operation, object objectToApplyTo); + void Move(Operation operation, object objectToApplyTo); + void Remove(Operation operation, object objectToApplyTo); + void Replace(Operation operation, object objectToApplyTo); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs b/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs new file mode 100644 index 0000000000..4640e97039 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs @@ -0,0 +1,1105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Microsoft.AspNet.JsonPatch.Helpers; +using Microsoft.AspNet.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch.Adapters +{ + /// + public class ObjectAdapter : IObjectAdapter + { + /// + /// Initializes a new instance of . + /// + /// The . + /// The for logging . + public ObjectAdapter( + IContractResolver contractResolver, + Action logErrorAction) + { + if (contractResolver == null) + { + throw new ArgumentNullException(nameof(contractResolver)); + } + + ContractResolver = contractResolver; + LogErrorAction = logErrorAction; + } + + /// + /// Gets or sets the . + /// + public IContractResolver ContractResolver { get; } + + /// + /// Action for logging . + /// + public Action LogErrorAction { get; } + + /// + /// 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, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(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, + object objectToApplyTo, + Operation operationToReport) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (operationToReport == null) + { + throw new ArgumentNullException(nameof(operationToReport)); + } + + // first up: if the path ends in a numeric value, we're inserting in a list and + // that value represents the position; if the path ends in "-", we're appending + // to the list. + + // get path result + var pathResult = GetActualPropertyPath( + path, + objectToApplyTo, + operationToReport); + if (pathResult == null) + { + return; + } + + var appendList = pathResult.ExecuteAtEnd; + var positionAsInteger = pathResult.NumericEnd; + var actualPathToProperty = pathResult.PathToProperty; + + var treeAnalysisResult = new ObjectTreeAnalysisResult( + objectToApplyTo, + actualPathToProperty, + ContractResolver); + + if (!treeAnalysisResult.IsValidPathForAdd) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatPropertyCannotBeAdded(path))); + return; + } + + if (treeAnalysisResult.UseDynamicLogic) + { + var container = treeAnalysisResult.Container; + if (container.ContainsCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent)) + { + // Existing property. + // If it's not an array, we need to check if the value fits the property type + // + // If it's an array, we need to check if the value fits in that array type, + // and add it at the correct position (if allowed). + if (appendList || positionAsInteger > -1) + { + // get the actual type + var propertyValue = container.GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent); + var typeOfPathProperty = propertyValue.GetType(); + + if (!IsNonStringArray(typeOfPathProperty)) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + return; + } + + // now, get the generic type of the enumerable + var genericTypeOfArray = GetIListType(typeOfPathProperty); + var conversionResult = ConvertToActualType(genericTypeOfArray, value); + if (!conversionResult.CanBeConverted) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidValueForProperty(value, path))); + return; + } + + // get value (it can be cast, we just checked that) + var array = treeAnalysisResult.Container.GetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent) as IList; + + if (appendList) + { + array.Add(conversionResult.ConvertedInstance); + treeAnalysisResult.Container.SetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent, array); + } + else + { + // specified index must not be greater than + // the amount of items in the array + if (positionAsInteger > array.Count) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + path))); + return; + } + + array.Insert(positionAsInteger, conversionResult.ConvertedInstance); + treeAnalysisResult.Container.SetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent, array); + } + } + else + { + // get the actual type + var typeOfPathProperty = treeAnalysisResult.Container + .GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent).GetType(); + + // can the value be converted to the actual type? + var conversionResult = ConvertToActualType(typeOfPathProperty, value); + if (conversionResult.CanBeConverted) + { + treeAnalysisResult.Container.SetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent, + conversionResult.ConvertedInstance); + } + else + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidValueForProperty(conversionResult.ConvertedInstance, path))); + return; + } + } + } + else + { + // New property - add it. + treeAnalysisResult.Container.Add(treeAnalysisResult.PropertyPathInParent, value); + } + } + else + { + // If it's an array, add to that array. If it's not, we replace. + + // is the path an array (but not a string (= char[]))? In this case, + // the path must end with "/position" or "/-", which we already determined before. + + var patchProperty = treeAnalysisResult.JsonPatchProperty; + + if (appendList || positionAsInteger > -1) + { + if (!IsNonStringArray(patchProperty.Property.PropertyType)) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + return; + } + + // now, get the generic type of the IList<> from Property type. + var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType); + var conversionResult = ConvertToActualType(genericTypeOfArray, value); + if (!conversionResult.CanBeConverted) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidValueForProperty(conversionResult.ConvertedInstance, path))); + return; + } + + if (!patchProperty.Property.Readable) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatCannotReadProperty(path))); + return; + } + + var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); + if (appendList) + { + array.Add(conversionResult.ConvertedInstance); + } + else if (positionAsInteger <= array.Count) + { + array.Insert(positionAsInteger, conversionResult.ConvertedInstance); + } + else + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + return; + } + } + else + { + var conversionResultTuple = ConvertToActualType( + patchProperty.Property.PropertyType, + value); + + if (!conversionResultTuple.CanBeConverted) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidValueForProperty(value, path))); + return; + } + + if (!patchProperty.Property.Writable) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatCannotUpdateProperty(path))); + return; + } + + patchProperty.Property.ValueProvider.SetValue( + patchProperty.Parent, + conversionResultTuple.ConvertedInstance); + } + } + } + + /// + /// 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, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + var valueAtFromLocationResult = GetValueAtLocation(operation.from, objectToApplyTo, operation); + + if (valueAtFromLocationResult.HasError) + { + // Error has already been logged in GetValueAtLocation. We + // must return, because remove / add should not be allowed to continue + return; + } + + // remove that value + var removeResult = Remove(operation.from, objectToApplyTo, operation); + + if (removeResult.HasError) + { + // Return => error has already been logged in remove method. We must + // return, because add should not be allowed to continue + return; + } + + // add that value to the path location + Add(operation.path, + valueAtFromLocationResult.PropertyValue, + 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, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(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. The return value + /// contains the type of the item that has been removed (and a bool possibly signifying an error) + /// This can be used by other methods, like replace, to ensure that we can pass in the correctly + /// typed value to whatever method follows. + /// + private RemovedPropertyTypeResult Remove(string path, object objectToApplyTo, Operation operationToReport) + { + // get path result + var pathResult = GetActualPropertyPath( + path, + objectToApplyTo, + operationToReport); + + if (pathResult == null) + { + return new RemovedPropertyTypeResult(null, true); + } + + var removeFromList = pathResult.ExecuteAtEnd; + var positionAsInteger = pathResult.NumericEnd; + var actualPathToProperty = pathResult.PathToProperty; + + var treeAnalysisResult = new ObjectTreeAnalysisResult( + objectToApplyTo, + actualPathToProperty, + ContractResolver); + + if (!treeAnalysisResult.IsValidPathForRemove) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatPropertyCannotBeRemoved(path))); + return new RemovedPropertyTypeResult(null, true); + } + + if (treeAnalysisResult.UseDynamicLogic) + { + // if it's not an array, we can remove the property from + // the dictionary. If it's an array, we need to check the position first. + if (removeFromList || positionAsInteger > -1) + { + var propertyValue = treeAnalysisResult.Container + .GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent); + + // we cannot continue when the value is null, because to be able to + // continue we need to be able to check if the array is a non-string array + if (propertyValue == null) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatCannotDeterminePropertyType(path))); + return new RemovedPropertyTypeResult(null, true); + } + + var typeOfPathProperty = propertyValue.GetType(); + + if (!IsNonStringArray(typeOfPathProperty)) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + return new RemovedPropertyTypeResult(null, true); + } + + // now, get the generic type of the enumerable (we'll return this type) + var genericTypeOfArray = GetIListType(typeOfPathProperty); + + // get the array + var array = (IList)treeAnalysisResult.Container.GetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent); + + if (array.Count == 0) + { + // if the array is empty, we should throw an error + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + path))); + return new RemovedPropertyTypeResult(null, true); + } + + if (removeFromList) + { + array.RemoveAt(array.Count - 1); + treeAnalysisResult.Container.SetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent, array); + + // return the type of the value that has been removed. + return new RemovedPropertyTypeResult(genericTypeOfArray, false); + } + else + { + if (positionAsInteger >= array.Count) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + path))); + return new RemovedPropertyTypeResult(null, true); + } + + array.RemoveAt(positionAsInteger); + treeAnalysisResult.Container.SetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent, array); + + // return the type of the value that has been removed. + return new RemovedPropertyTypeResult(genericTypeOfArray, false); + } + } + else + { + // get the property + var getResult = treeAnalysisResult.Container.GetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent); + + // remove the property + treeAnalysisResult.Container.RemoveValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent); + + // value is not null, we can determine the type + if (getResult != null) + { + var actualType = getResult.GetType(); + return new RemovedPropertyTypeResult(actualType, false); + } + else + { + return new RemovedPropertyTypeResult(null, false); + } + } + } + else + { + // not dynamic + var patchProperty = treeAnalysisResult.JsonPatchProperty; + + if (removeFromList || positionAsInteger > -1) + { + if (!IsNonStringArray(patchProperty.Property.PropertyType)) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + return new RemovedPropertyTypeResult(null, true); + } + + // now, get the generic type of the IList<> from Property type. + var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType); + + if (!patchProperty.Property.Readable) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatCannotReadProperty(path))); + return new RemovedPropertyTypeResult(null, true); + } + + var array = (IList)patchProperty.Property.ValueProvider + .GetValue(patchProperty.Parent); + + if (array.Count == 0) + { + // if the array is empty, we should throw an error + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + path))); + return new RemovedPropertyTypeResult(null, true); + } + + if (removeFromList) + { + array.RemoveAt(array.Count - 1); + + // return the type of the value that has been removed + return new RemovedPropertyTypeResult(genericTypeOfArray, false); + } + else + { + if (positionAsInteger >= array.Count) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + path))); + return new RemovedPropertyTypeResult(null, true); + } + + array.RemoveAt(positionAsInteger); + + // return the type of the value that has been removed + return new RemovedPropertyTypeResult(genericTypeOfArray, false); + } + } + else + { + if (!patchProperty.Property.Writable) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatCannotUpdateProperty(path))); + return new RemovedPropertyTypeResult(null, true); + } + + // setting the value to "null" will use the default value in case of value types, and + // null in case of reference types + object value = null; + + if (patchProperty.Property.PropertyType.GetTypeInfo().IsValueType + && Nullable.GetUnderlyingType(patchProperty.Property.PropertyType) == null) + { + value = Activator.CreateInstance(patchProperty.Property.PropertyType); + } + + patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, value); + return new RemovedPropertyTypeResult(patchProperty.Property.PropertyType, false); + } + } + } + + /// + /// 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, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + var removeResult = Remove(operation.path, objectToApplyTo, operation); + + if (removeResult.HasError) + { + // return => error has already been logged in remove method + return; + } + + if (!removeResult.HasError && removeResult.ActualType == null) + { + // the remove operation completed succesfully, but we could not determine the type. + LogError(new JsonPatchError( + objectToApplyTo, + operation, + Resources.FormatCannotDeterminePropertyType(operation.from))); + return; + } + + var conversionResult = ConvertToActualType(removeResult.ActualType, operation.value); + + if (!conversionResult.CanBeConverted) + { + // invalid value for path + LogError(new JsonPatchError( + objectToApplyTo, + operation, + Resources.FormatInvalidValueForProperty(operation.value, operation.path))); + return; + } + + Add(operation.path, conversionResult.ConvertedInstance, 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, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + // get value at from location and add that value to the path location + var valueAtFromLocationResult = GetValueAtLocation(operation.from, objectToApplyTo, operation); + + if (valueAtFromLocationResult.HasError) + { + // Return, error has already been logged in GetValueAtLocation + return; + } + + Add(operation.path, + valueAtFromLocationResult.PropertyValue, + objectToApplyTo, + operation); + } + + /// + /// Method is used by Copy and Move to avoid duplicate code + /// + /// Location where value should be + /// Object to inspect for the desired value + /// Operation to report in case of an error + /// GetValueResult containing value and a bool signifying a possible error + private GetValueResult GetValueAtLocation( + string location, + object objectToGetValueFrom, + Operation operationToReport) + { + if (location == null) + { + throw new ArgumentNullException(nameof(location)); + } + + if (objectToGetValueFrom == null) + { + throw new ArgumentNullException(nameof(objectToGetValueFrom)); + } + + if (operationToReport == null) + { + throw new ArgumentNullException(nameof(operationToReport)); + } + + // get path result + var pathResult = GetActualPropertyPath( + location, + objectToGetValueFrom, + operationToReport); + + if (pathResult == null) + { + return new GetValueResult(null, true); + } + + var getAtEndOfList = pathResult.ExecuteAtEnd; + var positionAsInteger = pathResult.NumericEnd; + var actualPathToProperty = pathResult.PathToProperty; + + var treeAnalysisResult = new ObjectTreeAnalysisResult( + objectToGetValueFrom, + actualPathToProperty, + ContractResolver); + + if (treeAnalysisResult.UseDynamicLogic) + { + // if it's not an array, we can remove the property from + // the dictionary. If it's an array, we need to check the position first. + if (getAtEndOfList || positionAsInteger > -1) + { + var propertyValue = treeAnalysisResult.Container + .GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent); + + // we cannot continue when the value is null, because to be able to + // continue we need to be able to check if the array is a non-string array + if (propertyValue == null) + { + LogError(new JsonPatchError( + objectToGetValueFrom, + operationToReport, + Resources.FormatCannotDeterminePropertyType(location))); + return new GetValueResult(null, true); + } + + var typeOfPathProperty = propertyValue.GetType(); + + if (!IsNonStringArray(typeOfPathProperty)) + { + LogError(new JsonPatchError( + objectToGetValueFrom, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, location))); + return new GetValueResult(null, true); + } + + // get the array + var array = (IList)treeAnalysisResult.Container.GetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent); + + if (positionAsInteger >= array.Count) + { + LogError(new JsonPatchError( + objectToGetValueFrom, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + location))); + return new GetValueResult(null, true); + } + + if (getAtEndOfList) + { + return new GetValueResult(array[array.Count - 1], false); + } + else + { + return new GetValueResult(array[positionAsInteger], false); + } + } + else + { + // get the property + var propertyValueAtLocation = treeAnalysisResult.Container.GetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent); + + return new GetValueResult(propertyValueAtLocation, false); + } + } + else + { + // not dynamic + var patchProperty = treeAnalysisResult.JsonPatchProperty; + + if (getAtEndOfList || positionAsInteger > -1) + { + if (!IsNonStringArray(patchProperty.Property.PropertyType)) + { + LogError(new JsonPatchError( + objectToGetValueFrom, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, location))); + return new GetValueResult(null, true); + } + + if (!patchProperty.Property.Readable) + { + LogError(new JsonPatchError( + objectToGetValueFrom, + operationToReport, + Resources.FormatCannotReadProperty(location))); + return new GetValueResult(null, true); + } + + var array = (IList)patchProperty.Property.ValueProvider + .GetValue(patchProperty.Parent); + + if (positionAsInteger >= array.Count) + { + LogError(new JsonPatchError( + objectToGetValueFrom, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + location))); + return new GetValueResult(null, true); + } + + if (getAtEndOfList) + { + return new GetValueResult(array[array.Count - 1], false); + } + else + { + return new GetValueResult(array[positionAsInteger], false); + } + } + else + { + if (!patchProperty.Property.Readable) + { + LogError(new JsonPatchError( + objectToGetValueFrom, + operationToReport, + Resources.FormatCannotReadProperty( + location))); + return new GetValueResult(null, true); + } + + var propertyValueAtLocation = patchProperty.Property.ValueProvider + .GetValue(patchProperty.Parent); + + return new GetValueResult(propertyValueAtLocation, false); + } + } + } + + private bool IsNonStringArray(Type type) + { + if (GetIListType(type) != null) + { + return true; + } + + return (!(type == typeof(string)) && typeof(IList).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())); + } + + private void LogError(JsonPatchError jsonPatchError) + { + if (LogErrorAction != null) + { + LogErrorAction(jsonPatchError); + } + else + { + throw new JsonPatchException(jsonPatchError); + } + } + + private 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); + } + } + + private Type GetIListType(Type type) + { + if (IsGenericListType(type)) + { + return type.GetGenericArguments()[0]; + } + + foreach (Type interfaceType in type.GetTypeInfo().ImplementedInterfaces) + { + if (IsGenericListType(interfaceType)) + { + return interfaceType.GetGenericArguments()[0]; + } + } + + return null; + } + + private bool IsGenericListType(Type type) + { + if (type.GetTypeInfo().IsGenericType && + type.GetGenericTypeDefinition() == typeof(IList<>)) + { + return true; + } + + return false; + } + + private ActualPropertyPathResult GetActualPropertyPath( + string propertyPath, + object objectToApplyTo, + Operation operationToReport) + { + if (propertyPath == null) + { + throw new ArgumentNullException(nameof(propertyPath)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (operationToReport == null) + { + throw new ArgumentNullException(nameof(operationToReport)); + } + + if (propertyPath.EndsWith("/-")) + { + return new ActualPropertyPathResult(-1, propertyPath.Substring(0, propertyPath.Length - 2), true); + } + else + { + var possibleIndex = propertyPath.Substring(propertyPath.LastIndexOf("/") + 1); + int castedIndex = -1; + if (int.TryParse(possibleIndex, out castedIndex)) + { + // has numeric end. + if (castedIndex > -1) + { + var pathToProperty = propertyPath.Substring( + 0, + propertyPath.LastIndexOf('/' + castedIndex.ToString())); + + return new ActualPropertyPathResult(castedIndex, pathToProperty, false); + } + else + { + // negative position - invalid path + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatNegativeIndexForArrayProperty(operationToReport.op, propertyPath))); + return null; + } + } + + return new ActualPropertyPathResult(-1, propertyPath, false); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Converters/JsonPatchDocumentConverter.cs b/src/Microsoft.AspNet.JsonPatch/Converters/JsonPatchDocumentConverter.cs new file mode 100644 index 0000000000..4551f64043 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Converters/JsonPatchDocumentConverter.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. 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 Microsoft.AspNet.JsonPatch.Exceptions; +using Microsoft.AspNet.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch.Converters +{ + public class JsonPatchDocumentConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return true; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + if (objectType != typeof(JsonPatchDocument)) + { + throw new ArgumentException(Resources.FormatParameterMustMatchType("objectType", "JsonPatchDocument"), "objectType"); + } + + try + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + // load jObject + var jObject = JArray.Load(reader); + + // Create target object for Json => list of operations + var targetOperations = new List(); + + // Create a new reader for this jObject, and set all properties + // to match the original reader. + var 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 JsonPatchDocument. + var container = new JsonPatchDocument(targetOperations, new DefaultContractResolver()); + + return container; + } + catch (Exception ex) + { + throw new JsonPatchException(Resources.FormatInvalidJsonPatchDocument(objectType.Name), 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); + } + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs b/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs new file mode 100644 index 0000000000..7b6bb77e17 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Converters/TypedJsonPatchDocumentConverter.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Microsoft.AspNet.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch.Converters +{ + public class TypedJsonPatchDocumentConverter : JsonPatchDocumentConverter + { + 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. + var 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, new DefaultContractResolver()); + + return container; + } + catch (Exception ex) + { + throw new JsonPatchException(Resources.FormatInvalidJsonPatchDocument(objectType.Name), ex); + } + } + } +} \ 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..ac5222a2bb --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Exceptions/JsonPatchException.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. 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 : Exception + { + public Operation FailedOperation { get; private set; } + public object AffectedObject { get; private set; } + + + public JsonPatchException() + { + + } + + public JsonPatchException(JsonPatchError jsonPatchError, Exception innerException) + : base(jsonPatchError.ErrorMessage, innerException) + { + FailedOperation = jsonPatchError.Operation; + AffectedObject = jsonPatchError.AffectedObject; + } + + public JsonPatchException(JsonPatchError jsonPatchError) + : this(jsonPatchError, null) + { + } + + public JsonPatchException(string message, Exception innerException) + : base (message, innerException) + { + + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/ActualPropertyPathResult.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/ActualPropertyPathResult.cs new file mode 100644 index 0000000000..d22d1bf830 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/ActualPropertyPathResult.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. 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 ActualPropertyPathResult + { + public int NumericEnd { get; private set; } + public string PathToProperty { get; set; } + public bool ExecuteAtEnd { get; set; } + + public ActualPropertyPathResult( + int numericEnd, + string pathToProperty, + bool executeAtEnd) + { + NumericEnd = numericEnd; + PathToProperty = pathToProperty; + ExecuteAtEnd = executeAtEnd; + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/ConversionResult.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/ConversionResult.cs new file mode 100644 index 0000000000..aba4913f2d --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/ConversionResult.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. 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/ExpandoObjectDictionaryExtensions.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/ExpandoObjectDictionaryExtensions.cs new file mode 100644 index 0000000000..a5911dded2 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/ExpandoObjectDictionaryExtensions.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.JsonPatch.Helpers +{ + // Helper methods to allow case-insensitive key search + internal static class ExpandoObjectDictionaryExtensions + { + internal static void SetValueForCaseInsensitiveKey( + this IDictionary propertyDictionary, + string key, + object value) + { + foreach (KeyValuePair kvp in propertyDictionary) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + propertyDictionary[kvp.Key] = value; + break; + } + } + } + + internal static void RemoveValueForCaseInsensitiveKey( + this IDictionary propertyDictionary, + string key) + { + string realKey = null; + foreach (KeyValuePair kvp in propertyDictionary) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + realKey = kvp.Key; + break; + } + } + + if (realKey != null) + { + propertyDictionary.Remove(realKey); + } + } + + internal static object GetValueForCaseInsensitiveKey( + this IDictionary propertyDictionary, + string key) + { + foreach (KeyValuePair kvp in propertyDictionary) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + return kvp.Value; + } + } + + throw new ArgumentException(Resources.FormatDictionaryKeyNotFound(key)); + } + + internal static bool ContainsCaseInsensitiveKey( + this IDictionary propertyDictionary, + string key) + { + foreach (KeyValuePair kvp in propertyDictionary) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/ExpressionHelpers.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/ExpressionHelpers.cs new file mode 100644 index 0000000000..6a0279876c --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/ExpressionHelpers.cs @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation. 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 TModel : 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 ? string.Empty : null; + default: + return string.Empty; + } + } + + 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/GetValueResult.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/GetValueResult.cs new file mode 100644 index 0000000000..89b7c93520 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/GetValueResult.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. 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 +{ + /// + /// Return value for the helper method used by Copy/Move. Needed to ensure we can make a different + /// decision in the calling method when the value is null because it cannot be fetched (HasError = true) + /// versus when it actually is null (much like why RemovedPropertyTypeResult is used for returning + /// type in the Remove operation). + /// + public class GetValueResult + { + public GetValueResult(object propertyValue, bool hasError) + { + PropertyValue = propertyValue; + HasError = hasError; + } + + /// + /// The value of the property we're trying to get + /// + public object PropertyValue { get; private set; } + + /// + /// HasError: true when an error occurred, the operation didn't complete succesfully + /// + public bool HasError { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/JsonPatchProperty.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/JsonPatchProperty.cs new file mode 100644 index 0000000000..0539d230a3 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/JsonPatchProperty.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. 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.Serialization; + +namespace Microsoft.AspNet.JsonPatch +{ + /// + /// Metadata for JsonProperty. + /// + public class JsonPatchProperty + { + /// + /// Initializes a new instance. + /// + public JsonPatchProperty(JsonProperty property, object parent) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } + + Property = property; + Parent = parent; + } + + /// + /// Gets or sets JsonProperty. + /// + public JsonProperty Property { get; set; } + + /// + /// Gets or sets Parent. + /// + public object Parent { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/ObjectTreeAnalyisResult.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/ObjectTreeAnalyisResult.cs new file mode 100644 index 0000000000..91d5ee129d --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/ObjectTreeAnalyisResult.cs @@ -0,0 +1,210 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch.Helpers +{ + internal class ObjectTreeAnalysisResult + { + // either the property is part of the container dictionary, + // or we have a direct reference to a JsonPatchProperty instance + + public bool UseDynamicLogic { get; private set; } + + public bool IsValidPathForAdd { get; private set; } + + public bool IsValidPathForRemove { get; private set; } + + public IDictionary Container { get; private set; } + + public string PropertyPathInParent { get; private set; } + + public JsonPatchProperty JsonPatchProperty { get; private set; } + + public ObjectTreeAnalysisResult( + object objectToSearch, + string propertyPath, + IContractResolver contractResolver) + { + // construct the analysis result. + + // split the propertypath, and if necessary, remove the first + // empty item (that's the case when it starts with a "/") + var propertyPathTree = propertyPath.Split( + new char[] { '/' }, + StringSplitOptions.RemoveEmptyEntries); + + // we've now got a split up property tree "base/property/otherproperty/..." + int lastPosition = 0; + object targetObject = objectToSearch; + for (int i = 0; i < propertyPathTree.Length; i++) + { + lastPosition = i; + + // if the current target object is an ExpandoObject (IDictionary), + // we cannot use the ContractResolver. + var dictionary = targetObject as IDictionary; + if (dictionary != null) + { + // find the value in the dictionary + if (dictionary.ContainsCaseInsensitiveKey(propertyPathTree[i])) + { + var possibleNewTargetObject = dictionary.GetValueForCaseInsensitiveKey(propertyPathTree[i]); + + // unless we're at the last item, we should set the targetobject + // to the new object. If we're at the last item, we need to stop + if (i != propertyPathTree.Length - 1) + { + targetObject = possibleNewTargetObject; + } + } + else + { + break; + } + } + else + { + // if the current part of the path is numeric, this means we're trying + // to get the propertyInfo of a specific object in an array. To allow + // for this, the previous value (targetObject) must be an IEnumerable, and + // the position must exist. + + int numericValue = -1; + if (int.TryParse(propertyPathTree[i], out numericValue)) + { + var element = GetElementAtFromObject(targetObject, numericValue); + if (element != null) + { + targetObject = element; + } + else + { + break; + } + } + else + { + var jsonContract = (JsonObjectContract)contractResolver.ResolveContract(targetObject.GetType()); + + // does the property exist? + var attemptedProperty = jsonContract + .Properties + .FirstOrDefault(p => string.Equals(p.PropertyName, propertyPathTree[i], StringComparison.OrdinalIgnoreCase)); + + if (attemptedProperty != null) + { + // unless we're at the last item, we should continue searching. + // If we're at the last item, we need to stop + if ((i != propertyPathTree.Length - 1)) + { + targetObject = attemptedProperty.ValueProvider.GetValue(targetObject); + } + } + else + { + // property cannot be found, and we're not working with dynamics. + // Stop, and return invalid path. + break; + } + } + } + } + + if (propertyPathTree.Length - lastPosition != 1) + { + IsValidPathForAdd = false; + IsValidPathForRemove = false; + return; + } + + // two things can happen now. The targetproperty can be an IDictionary - in that + // case, it's valid for add if there's 1 item left in the propertyPathTree. + // + // it can also be a property info. In that case, if there's nothing left in the path + // tree we're at the end, if there's one left we can try and set that. + if (targetObject is IDictionary) + { + UseDynamicLogic = true; + + Container = (IDictionary)targetObject; + IsValidPathForAdd = true; + PropertyPathInParent = propertyPathTree[propertyPathTree.Length - 1]; + + // to be able to remove this property, it must exist + IsValidPathForRemove = Container.ContainsCaseInsensitiveKey(PropertyPathInParent); + } + else if (targetObject is IList) + { + System.Diagnostics.Debugger.Launch(); + UseDynamicLogic = false; + + int index; + if (!Int32.TryParse(propertyPathTree[propertyPathTree.Length - 1], out index)) + { + // We only support indexing into a list + IsValidPathForAdd = false; + IsValidPathForRemove = false; + return; + } + + IsValidPathForAdd = true; + IsValidPathForRemove = ((IList)targetObject).Count > index; + PropertyPathInParent = propertyPathTree[propertyPathTree.Length - 1]; + } + else + { + UseDynamicLogic = false; + + var property = propertyPathTree[propertyPathTree.Length - 1]; + var jsonContract = (JsonObjectContract)contractResolver.ResolveContract(targetObject.GetType()); + var attemptedProperty = jsonContract + .Properties + .FirstOrDefault(p => string.Equals(p.PropertyName, property, StringComparison.OrdinalIgnoreCase)); + + if (attemptedProperty == null) + { + IsValidPathForAdd = false; + IsValidPathForRemove = false; + } + else + { + IsValidPathForAdd = true; + IsValidPathForRemove = true; + JsonPatchProperty = new JsonPatchProperty(attemptedProperty, targetObject); + PropertyPathInParent = property; + } + } + } + + private object GetElementAtFromObject(object targetObject, int numericValue) + { + if (numericValue > -1) + { + // Check if the targetobject is an IEnumerable, + // and if the position is valid. + if (targetObject is IEnumerable) + { + var indexable = ((IEnumerable)targetObject).Cast(); + + if (indexable.Count() >= numericValue) + { + return indexable.ElementAt(numericValue); + } + else { return null; } + } + else { return null; } + } + else + { + return null; + } + } + + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/PathHelpers.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/PathHelpers.cs new file mode 100644 index 0000000000..28683e7c98 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/PathHelpers.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. 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; + +namespace Microsoft.AspNet.JsonPatch.Helpers +{ + internal static class PathHelpers + { + internal static string NormalizePath(string path) + { + // check for most common path errors on create. This is not + // absolutely necessary, but it allows us to already catch mistakes + // on creation of the patch document rather than on execute. + + if (path.Contains(".") || path.Contains("//") || path.Contains(" ") || path.Contains("\\")) + { + throw new JsonPatchException(Resources.FormatInvalidValueForPath(path), null); + } + + if (!(path.StartsWith("/"))) + { + return "/" + path; + } + else + { + return path; + } + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/RemovedPropertyTypeResult.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/RemovedPropertyTypeResult.cs new file mode 100644 index 0000000000..9f5b97a868 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/RemovedPropertyTypeResult.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. 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.Helpers +{ + /// + /// Return value for Remove operation. The combination tells us what to do next (if this operation + /// is called from inside another operation, eg: Replace, Copy. + /// + /// Possible combo: + /// - ActualType contains type: operation succesfully completed, can continue when called from inside + /// another operation + /// - ActualType null and HasError true: operation not completed succesfully, should not be allowed to continue + /// - ActualType null and HasError false: operation completed succesfully, but we should not be allowed to + /// continue when called from inside another method as we could not verify the type of the removed property. + /// This happens when the value of an item in an ExpandoObject dictionary is null. + /// + internal class RemovedPropertyTypeResult + { + /// + /// The type of the removed property (value) + /// + public Type ActualType { get; private set; } + + /// + /// HasError: true when an error occurred, the operation didn't complete succesfully + /// + public bool HasError { get; set; } + + public RemovedPropertyTypeResult(Type actualType, bool hasError) + { + ActualType = actualType; + HasError = hasError; + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs b/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs new file mode 100644 index 0000000000..18033e82f0 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/IJsonPatchDocument.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. 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; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch +{ + public interface IJsonPatchDocument + { + IContractResolver ContractResolver { get; set; } + + IList GetOperations(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/JsonPatchDocument.cs b/src/Microsoft.AspNet.JsonPatch/JsonPatchDocument.cs new file mode 100644 index 0000000000..fc2e73030a --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/JsonPatchDocument.cs @@ -0,0 +1,222 @@ +// Copyright (c) .NET Foundation. 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 Microsoft.AspNet.JsonPatch.Adapters; +using Microsoft.AspNet.JsonPatch.Converters; +using Microsoft.AspNet.JsonPatch.Helpers; +using Microsoft.AspNet.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch +{ + // Implementation details: the purpose of this type of patch document is to allow creation of such + // documents for cases where there's no class/DTO to work on. Typical use case: backend not built in + // .NET or architecture doesn't contain a shared DTO layer. + [JsonConverter(typeof(JsonPatchDocumentConverter))] + public class JsonPatchDocument : IJsonPatchDocument + { + public List Operations { get; private set; } + + [JsonIgnore] + public IContractResolver ContractResolver { get; set; } + + public JsonPatchDocument() + { + Operations = new List(); + ContractResolver = new DefaultContractResolver(); + } + + public JsonPatchDocument(List operations, IContractResolver contractResolver) + { + if (operations == null) + { + throw new ArgumentNullException(nameof(operations)); + } + + if (contractResolver == null) + { + throw new ArgumentNullException(nameof(contractResolver)); + } + + Operations = operations; + ContractResolver = contractResolver; + } + + /// + /// Add operation. Will result in, for example, + /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } + /// + /// target location + /// value + /// + public JsonPatchDocument Add(string path, object value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("add", PathHelpers.NormalizePath(path), null, value)); + return this; + } + + /// + /// Remove value at target location. Will result in, for example, + /// { "op": "remove", "path": "/a/b/c" } + /// + /// target location + /// + public JsonPatchDocument Remove(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("remove", PathHelpers.NormalizePath(path), null, null)); + return this; + } + + /// + /// Replace value. Will result in, for example, + /// { "op": "replace", "path": "/a/b/c", "value": 42 } + /// + /// target location + /// value + /// + public JsonPatchDocument Replace(string path, object value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("replace", PathHelpers.NormalizePath(path), 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" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Move(string from, string path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("move", PathHelpers.NormalizePath(path), PathHelpers.NormalizePath(from))); + 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" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Copy(string from, string path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("copy", PathHelpers.NormalizePath(path), PathHelpers.NormalizePath(from))); + return this; + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + public void ApplyTo(object objectToApplyTo) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null)); + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// Action to log errors + public void ApplyTo(object objectToApplyTo, Action logErrorAction) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction)); + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// IObjectAdapter instance to use when applying + public void ApplyTo(object objectToApplyTo, IObjectAdapter adapter) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + // apply each operation in order + foreach (var op in Operations) + { + op.Apply(objectToApplyTo, adapter); + } + } + + IList IJsonPatchDocument.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; + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/JsonPatchDocumentOfT.cs b/src/Microsoft.AspNet.JsonPatch/JsonPatchDocumentOfT.cs new file mode 100644 index 0000000000..7b03f1644c --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/JsonPatchDocumentOfT.cs @@ -0,0 +1,700 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Microsoft.AspNet.JsonPatch.Adapters; +using Microsoft.AspNet.JsonPatch.Converters; +using Microsoft.AspNet.JsonPatch.Helpers; +using Microsoft.AspNet.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.JsonPatch +{ + // Implementation details: the purpose of this type of patch document is to ensure we can do type-checking + // when producing a JsonPatchDocument. However, we cannot send this "typed" over the wire, as that would require + // including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's + // not according to RFC 6902, and would thus break cross-platform compatibility. + [JsonConverter(typeof(TypedJsonPatchDocumentConverter))] + public class JsonPatchDocument : IJsonPatchDocument where TModel : class + { + public List> Operations { get; private set; } + + [JsonIgnore] + public IContractResolver ContractResolver { get; set; } + + public JsonPatchDocument() + { + Operations = new List>(); + ContractResolver = new DefaultContractResolver(); + } + + // Create from list of operations + public JsonPatchDocument(List> operations, IContractResolver contractResolver) + { + if (operations == null) + { + throw new ArgumentNullException(nameof(operations)); + } + + if (contractResolver == null) + { + throw new ArgumentNullException(nameof(contractResolver)); + } + + Operations = operations; + ContractResolver = contractResolver; + } + + /// + /// Add operation. Will result in, for example, + /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } + /// + /// value type + /// target location + /// value + /// + public JsonPatchDocument Add(Expression> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "add", + ExpressionHelpers.GetPath(path).ToLowerInvariant(), + from: null, + value: value)); + + return this; + } + + /// + /// Add value to list at given position + /// + /// value type + /// target location + /// value + /// position + /// + public JsonPatchDocument Add( + Expression>> path, + TProp value, + int position) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "add", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/" + position, + from: null, + value: value)); + + return this; + } + + /// + /// At value at end of list + /// + /// value type + /// target location + /// value + /// + public JsonPatchDocument Add(Expression>> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "add", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/-", + from: null, + value: value)); + + return this; + } + + /// + /// Remove value at target location. Will result in, for example, + /// { "op": "remove", "path": "/a/b/c" } + /// + /// target location + /// + public JsonPatchDocument Remove(Expression> path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("remove", ExpressionHelpers.GetPath(path).ToLowerInvariant(), from: null)); + + return this; + } + + /// + /// Remove value from list at given position + /// + /// value type + /// target location + /// position + /// + public JsonPatchDocument Remove(Expression>> path, int position) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "remove", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/" + position, + from: null)); + + return this; + } + + /// + /// Remove value from end of list + /// + /// value type + /// target location + /// + public JsonPatchDocument Remove(Expression>> path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "remove", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/-", + from: null)); + + return this; + } + + /// + /// Replace value. Will result in, for example, + /// { "op": "replace", "path": "/a/b/c", "value": 42 } + /// + /// target location + /// value + /// + public JsonPatchDocument Replace(Expression> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "replace", + ExpressionHelpers.GetPath(path).ToLowerInvariant(), + from: null, + value: value)); + + return this; + } + + /// + /// Replace value in a list at given position + /// + /// value type + /// target location + /// value + /// position + /// + public JsonPatchDocument Replace(Expression>> path, + TProp value, int position) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "replace", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/" + position, + from: null, + value: value)); + + return this; + } + + /// + /// Replace value at end of a list + /// + /// value type + /// target location + /// value + /// + public JsonPatchDocument Replace(Expression>> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "replace", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/-", + from: null, + value: 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" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Move( + Expression> from, + Expression> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + ExpressionHelpers.GetPath(path).ToLowerInvariant(), + ExpressionHelpers.GetPath(from).ToLowerInvariant())); + + return this; + } + + /// + /// Move from a position in a list to a new location + /// + /// + /// source location + /// position + /// target location + /// + public JsonPatchDocument Move( + Expression>> from, + int positionFrom, + Expression> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + ExpressionHelpers.GetPath(path).ToLowerInvariant(), + ExpressionHelpers.GetPath(from).ToLowerInvariant() + "/" + positionFrom)); + + return this; + } + + /// + /// Move from a property to a location in a list + /// + /// + /// source location + /// target location + /// position + /// + public JsonPatchDocument Move( + Expression> from, + Expression>> path, + int positionTo) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/" + positionTo, + ExpressionHelpers.GetPath(from).ToLowerInvariant())); + + return this; + } + + /// + /// Move from a position in a list to another location in a list + /// + /// + /// source location + /// position (source) + /// target location + /// position (target) + /// + public JsonPatchDocument Move( + Expression>> from, + int positionFrom, + Expression>> path, + int positionTo) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/" + positionTo, + ExpressionHelpers.GetPath(from).ToLowerInvariant() + "/" + positionFrom)); + + return this; + } + + /// + /// Move from a position in a list to the end of another list + /// + /// + /// source location + /// position + /// target location + /// + public JsonPatchDocument Move( + Expression>> from, + int positionFrom, + Expression>> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/-", + ExpressionHelpers.GetPath(from).ToLowerInvariant() + "/" + positionFrom)); + + return this; + } + + /// + /// Move to the end of a list + /// + /// + /// source location + /// target location + /// + public JsonPatchDocument Move( + Expression> from, + Expression>> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/-", + ExpressionHelpers.GetPath(from).ToLowerInvariant())); + + 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" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Copy( + Expression> from, + Expression> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + , ExpressionHelpers.GetPath(from).ToLowerInvariant())); + + return this; + } + + /// + /// Copy from a position in a list to a new location + /// + /// + /// source location + /// position + /// target location + /// + public JsonPatchDocument Copy( + Expression>> from, + int positionFrom, + Expression> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + ExpressionHelpers.GetPath(path).ToLowerInvariant(), + ExpressionHelpers.GetPath(from).ToLowerInvariant() + "/" + positionFrom)); + + return this; + } + + /// + /// Copy from a property to a location in a list + /// + /// + /// source location + /// target location + /// position + /// + public JsonPatchDocument Copy( + Expression> from, + Expression>> path, + int positionTo) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/" + positionTo, + ExpressionHelpers.GetPath(from).ToLowerInvariant())); + + return this; + } + + /// + /// Copy from a position in a list to a new location in a list + /// + /// + /// source location + /// position (source) + /// target location + /// position (target) + /// + public JsonPatchDocument Copy( + Expression>> from, + int positionFrom, + Expression>> path, + int positionTo) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/" + positionTo, + ExpressionHelpers.GetPath(from).ToLowerInvariant() + "/" + positionFrom)); + + return this; + } + + /// + /// Copy from a position in a list to the end of another list + /// + /// + /// source location + /// position + /// target location + /// + public JsonPatchDocument Copy( + Expression>> from, + int positionFrom, + Expression>> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/-", + ExpressionHelpers.GetPath(from).ToLowerInvariant() + "/" + positionFrom)); + + return this; + } + + /// + /// Copy to the end of a list + /// + /// + /// source location + /// target location + /// + public JsonPatchDocument Copy( + Expression> from, + Expression>> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + ExpressionHelpers.GetPath(path).ToLowerInvariant() + "/-", + ExpressionHelpers.GetPath(from).ToLowerInvariant())); + + return this; + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + public void ApplyTo(TModel objectToApplyTo) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null)); + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// Action to log errors + public void ApplyTo(TModel objectToApplyTo, Action logErrorAction) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction)); + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// IObjectAdapter instance to use when applying + public void ApplyTo(TModel objectToApplyTo, IObjectAdapter adapter) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + // apply each operation in order + foreach (var op in Operations) + { + op.Apply(objectToApplyTo, adapter); + } + } + + IList IJsonPatchDocument.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/JsonPatchError.cs b/src/Microsoft.AspNet.JsonPatch/JsonPatchError.cs new file mode 100644 index 0000000000..a140f0d4c1 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/JsonPatchError.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. 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 +{ + /// + /// Captures error message and the related entity and the operation that caused it. + /// + public class JsonPatchError + { + /// + /// Initializes a new instance of . + /// + /// The object that is affected by the error. + /// The that caused the error. + /// The error message. + public JsonPatchError( + object affectedObject, + Operation operation, + string errorMessage) + { + if (errorMessage == null) + { + throw new ArgumentNullException(nameof(errorMessage)); + } + + AffectedObject = affectedObject; + Operation = operation; + ErrorMessage = errorMessage; + } + + /// + /// Gets the object that is affected by the error. + /// + public object AffectedObject { get; } + + /// + /// Gets the that caused the error. + /// + public Operation Operation { get; } + + /// + /// Gets the error message. + /// + public string ErrorMessage { get; } + } +} \ 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..f9fb187ecd --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Microsoft.AspNet.JsonPatch.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 4d55f4d8-633b-462f-a5b1-feb84bd2d534 + ..\..\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..7f056f41bf --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Operations/Operation.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. 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.Adapters; +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) + { + } + + public void Apply(object objectToApplyTo, IObjectAdapter adapter) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(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: + throw new NotSupportedException(Resources.TestOperationNotSupported); + default: + break; + } + } + + public bool ShouldSerializevalue() + { + return (OperationType == OperationType.Add + || OperationType == OperationType.Replace + || OperationType == OperationType.Test); + } + } +} \ 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..17455f75e8 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Operations/OperationBase.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. 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) + { + if (op == null) + { + throw new ArgumentNullException(nameof(op)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + this.op = op; + this.path = path; + this.from = from; + } + + public bool ShouldSerializefrom() + { + return (OperationType == OperationType.Move + || OperationType == OperationType.Copy); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Operations/OperationOfT.cs b/src/Microsoft.AspNet.JsonPatch/Operations/OperationOfT.cs new file mode 100644 index 0000000000..63663f70d9 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Operations/OperationOfT.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. 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.Adapters; + +namespace Microsoft.AspNet.JsonPatch.Operations +{ + public class Operation : Operation where TModel : class + { + public Operation() + { + + } + + public Operation(string op, string path, string from, object value) + : base(op, path, from) + { + if (op == null) + { + throw new ArgumentNullException(nameof(op)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + this.value = value; + } + + public Operation(string op, string path, string from) + : base(op, path, from) + { + if (op == null) + { + throw new ArgumentNullException(nameof(op)); + } + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + } + + public void Apply(TModel objectToApplyTo, IObjectAdapter adapter) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(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: + throw new NotSupportedException(Resources.TestOperationNotSupported); + default: + break; + } + } + + } +} \ 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..a4d006bc51 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Operations/OperationType.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. 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/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.JsonPatch/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b2437d9ad6 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using System.Resources; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..f0ee7f2439 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs @@ -0,0 +1,270 @@ +// +namespace Microsoft.AspNet.JsonPatch +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.JsonPatch.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The type of the property at path '{0}' could not be determined. + /// + internal static string CannotDeterminePropertyType + { + get { return GetString("CannotDeterminePropertyType"); } + } + + /// + /// The type of the property at path '{0}' could not be determined. + /// + internal static string FormatCannotDeterminePropertyType(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CannotDeterminePropertyType"), p0); + } + + /// + /// The property at '{0}' could not be read. + /// + internal static string CannotReadProperty + { + get { return GetString("CannotReadProperty"); } + } + + /// + /// The property at '{0}' could not be read. + /// + internal static string FormatCannotReadProperty(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CannotReadProperty"), p0); + } + + /// + /// The property at path '{0}' could not be updated. + /// + internal static string CannotUpdateProperty + { + get { return GetString("CannotUpdateProperty"); } + } + + /// + /// The property at path '{0}' could not be updated. + /// + internal static string FormatCannotUpdateProperty(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CannotUpdateProperty"), p0); + } + + /// + /// The key '{0}' was not found. + /// + internal static string DictionaryKeyNotFound + { + get { return GetString("DictionaryKeyNotFound"); } + } + + /// + /// The key '{0}' was not found. + /// + internal static string FormatDictionaryKeyNotFound(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DictionaryKeyNotFound"), p0); + } + + /// + /// For operation '{0}' on array property at path '{1}', the index is larger than the array size. + /// + internal static string InvalidIndexForArrayProperty + { + get { return GetString("InvalidIndexForArrayProperty"); } + } + + /// + /// For operation '{0}' on array property at path '{1}', the index is larger than the array size. + /// + internal static string FormatInvalidIndexForArrayProperty(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidIndexForArrayProperty"), p0, p1); + } + + /// + /// The type '{0}' was malformed and could not be parsed. + /// + internal static string InvalidJsonPatchDocument + { + get { return GetString("InvalidJsonPatchDocument"); } + } + + /// + /// The type '{0}' was malformed and could not be parsed. + /// + internal static string FormatInvalidJsonPatchDocument(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidJsonPatchDocument"), p0); + } + + /// + /// For operation '{0}', the provided path is invalid for array property at path '{1}'. + /// + internal static string InvalidPathForArrayProperty + { + get { return GetString("InvalidPathForArrayProperty"); } + } + + /// + /// For operation '{0}', the provided path is invalid for array property at path '{1}'. + /// + internal static string FormatInvalidPathForArrayProperty(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidPathForArrayProperty"), p0, p1); + } + + /// + /// The provided string '{0}' is an invalid path. + /// + internal static string InvalidValueForPath + { + get { return GetString("InvalidValueForPath"); } + } + + /// + /// The provided string '{0}' is an invalid path. + /// + internal static string FormatInvalidValueForPath(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForPath"), p0); + } + + /// + /// The value '{0}' is invalid for property at path '{1}'. + /// + internal static string InvalidValueForProperty + { + get { return GetString("InvalidValueForProperty"); } + } + + /// + /// The value '{0}' is invalid for property at path '{1}'. + /// + internal static string FormatInvalidValueForProperty(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForProperty"), p0, p1); + } + + /// + /// For operation '{0}' on array property at path '{1}', the index is negative. + /// + internal static string NegativeIndexForArrayProperty + { + get { return GetString("NegativeIndexForArrayProperty"); } + } + + /// + /// For operation '{0}' on array property at path '{1}', the index is negative. + /// + internal static string FormatNegativeIndexForArrayProperty(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("NegativeIndexForArrayProperty"), p0, p1); + } + + /// + /// '{0}' must be of type '{1}'. + /// + internal static string ParameterMustMatchType + { + get { return GetString("ParameterMustMatchType"); } + } + + /// + /// '{0}' must be of type '{1}'. + /// + internal static string FormatParameterMustMatchType(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ParameterMustMatchType"), p0, p1); + } + + /// + /// The property at path '{0}' could not be added. + /// + internal static string PropertyCannotBeAdded + { + get { return GetString("PropertyCannotBeAdded"); } + } + + /// + /// The property at path '{0}' could not be added. + /// + internal static string FormatPropertyCannotBeAdded(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PropertyCannotBeAdded"), p0); + } + + /// + /// The property at path '{0}' could not be removed. + /// + internal static string PropertyCannotBeRemoved + { + get { return GetString("PropertyCannotBeRemoved"); } + } + + /// + /// The property at path '{0}' could not be removed. + /// + internal static string FormatPropertyCannotBeRemoved(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PropertyCannotBeRemoved"), p0); + } + + /// + /// Property does not exist at path '{0}'. + /// + internal static string PropertyDoesNotExist + { + get { return GetString("PropertyDoesNotExist"); } + } + + /// + /// Property does not exist at path '{0}'. + /// + internal static string FormatPropertyDoesNotExist(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PropertyDoesNotExist"), p0); + } + + /// + /// The test operation is not supported. + /// + internal static string TestOperationNotSupported + { + get { return GetString("TestOperationNotSupported"); } + } + + /// + /// The test operation is not supported. + /// + internal static string FormatTestOperationNotSupported() + { + return GetString("TestOperationNotSupported"); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Resources.resx b/src/Microsoft.AspNet.JsonPatch/Resources.resx new file mode 100644 index 0000000000..59ae2b59c3 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Resources.resx @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The type of the property at path '{0}' could not be determined. + + + The property at '{0}' could not be read. + + + The property at path '{0}' could not be updated. + + + The key '{0}' was not found. + + + For operation '{0}' on array property at path '{1}', the index is larger than the array size. + + + The type '{0}' was malformed and could not be parsed. + + + For operation '{0}', the provided path is invalid for array property at path '{1}'. + + + The provided string '{0}' is an invalid path. + + + The value '{0}' is invalid for property at path '{1}'. + + + For operation '{0}' on array property at path '{1}', the index is negative. + + + '{0}' must be of type '{1}'. + + + The property at path '{0}' could not be added. + + + The property at path '{0}' could not be removed. + + + Property does not exist at path '{0}'. + + + The test operation is not supported. + + \ 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..8d67ec1808 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/project.json @@ -0,0 +1,25 @@ +{ + "version": "1.0.0-*", + "repository": { + "type": "git", + "url": "git://github.com/aspnet/mvc" + }, + "dependencies": { + "Newtonsoft.Json": "6.0.6" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "Microsoft.CSharp": "4.0.1-beta-*", + "System.Collections.Concurrent": "4.0.11-beta-*", + "System.ComponentModel.TypeConverter": "4.0.1-beta-*", + "System.Globalization": "4.0.11-beta-*", + "System.Reflection.Extensions": "4.0.1-beta-*", + "System.Resources.ResourceManager": "4.0.1-beta-*", + "System.Runtime.Extensions": "4.0.11-beta-*", + "System.Text.Encoding.Extensions": "4.0.11-beta-*" + } + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddOperationTests.cs new file mode 100644 index 0000000000..3c2523c5fb --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddOperationTests.cs @@ -0,0 +1,569 @@ +// Copyright (c) .NET Foundation. 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.Dynamic; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class AddOperationTests + { + [Fact] + public void AddNewPropertyShouldFailIfRootIsNotAnExpandoObject() + { + dynamic doc = new + { + Test = 1 + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/NewInt' could not be added.", + exception.Message); + } + + [Fact] + public void AddNewProperty() + { + dynamic obj = new ExpandoObject(); + obj.Test = 1; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + + Assert.Equal(1, obj.NewInt); + Assert.Equal(1, obj.Test); + } + + [Fact] + public void AddNewPropertyToNestedAnonymousObjectShouldFail() + { + dynamic doc = new + { + Test = 1, + nested = new { } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("Nested/NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/Nested/NewInt' could not be added.", + exception.Message); + } + + [Fact] + public void AddNewPropertyToTypedObjectShouldFail() + { + dynamic doc = new + { + Test = 1, + nested = new NestedDTO() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("Nested/NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/Nested/NewInt' could not be added.", + exception.Message); + } + + [Fact] + public void AddToExistingPropertyOnNestedObject() + { + dynamic doc = new + { + Test = 1, + nested = new NestedDTO() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("Nested/StringProperty", "A"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.nested.StringProperty); + Assert.Equal(1, doc.Test); + } + + [Fact] + public void AddNewPropertyToExpandoOject() + { + dynamic doc = new + { + Test = 1, + nested = new ExpandoObject() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("Nested/NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.nested.NewInt); + Assert.Equal(1, doc.Test); + } + + [Fact] + public void AddNewPropertyToExpandoOjectInTypedObject() + { + var doc = new NestedDTO() + { + DynamicProperty = new ExpandoObject() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("DynamicProperty/NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.DynamicProperty.NewInt); + } + + [Fact] + public void AddNewPropertyToTypedObjectInExpandoObject() + { + dynamic dynamicProperty = new ExpandoObject(); + dynamicProperty.StringProperty = "A"; + + var doc = new NestedDTO() + { + DynamicProperty = dynamicProperty + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("DynamicProperty/StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.DynamicProperty.StringProperty); + } + + [Fact] + public void AddNewPropertyToAnonymousObjectShouldFail() + { + dynamic doc = new + { + Test = 1 + }; + + dynamic valueToAdd = new { IntValue = 1, StringValue = "test", GuidValue = Guid.NewGuid() }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("ComplexProperty", valueToAdd); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/ComplexProperty' could not be added.", + exception.Message); + } + + [Fact] + public void AddResultsReplaceShouldFailOnAnonymousDueToNoSetter() + { + var doc = new + { + StringProperty = "A" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/StringProperty' could not be updated.", + exception.Message); + } + + [Fact] + public void AddResultsShouldReplace() + { + dynamic doc = new ExpandoObject(); + doc.StringProperty = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.StringProperty); + } + + [Fact] + public void AddResultsShouldReplaceInNested() + { + dynamic doc = new ExpandoObject(); + doc.InBetweenFirst = new ExpandoObject(); + doc.InBetweenFirst.InBetweenSecond = new ExpandoObject(); + doc.InBetweenFirst.InBetweenSecond.StringProperty = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("/InBetweenFirst/InBetweenSecond/StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.InBetweenFirst.InBetweenSecond.StringProperty); + } + + [Fact] + public void AddResultsShouldReplaceInNestedInDynamic() + { + dynamic doc = new ExpandoObject(); + doc.Nested = new NestedDTO(); + doc.Nested.DynamicProperty = new ExpandoObject(); + doc.Nested.DynamicProperty.InBetweenFirst = new ExpandoObject(); + doc.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond = new ExpandoObject(); + doc.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond.StringProperty = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("/Nested/DynamicProperty/InBetweenFirst/InBetweenSecond/StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond.StringProperty); + } + + [Fact] + public void ShouldNotBeAbleToAddToNonExistingPropertyThatIsNotTheRoot() + { + //Adding to a Nonexistent Target + // + // An example target JSON document: + // { "foo": "bar" } + // A JSON Patch document: + // [ + // { "op": "add", "path": "/baz/bat", "value": "qux" } + // ] + // This JSON Patch document, applied to the target JSON document above, + // would result in an error (therefore, it would not be applied), + // because the "add" operation's target location that references neither + // the root of the document, nor a member of an existing object, nor a + // member of an existing array. + + var doc = new NestedDTO() + { + DynamicProperty = new ExpandoObject() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("DynamicProperty/OtherProperty/IntProperty", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/DynamicProperty/OtherProperty/IntProperty' could not be added.", + exception.Message); + } + + [Fact] + public void ShouldNotBeAbleToAddToNonExistingPropertyInNestedPropertyThatIsNotTheRoot() + { + //Adding to a Nonexistent Target + // + // An example target JSON document: + // { "foo": "bar" } + // A JSON Patch document: + // [ + // { "op": "add", "path": "/baz/bat", "value": "qux" } + // ] + // This JSON Patch document, applied to the target JSON document above, + // would result in an error (therefore, it would not be applied), + // because the "add" operation's target location that references neither + // the root of the document, nor a member of an existing object, nor a + // member of an existing array. + + var doc = new + { + Foo = "bar" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("baz/bat", "qux"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/baz/bat' could not be added.", + exception.Message); + } + + [Fact] + public void ShouldReplacePropertyWithDifferentCase() + { + dynamic doc = new ExpandoObject(); + doc.StringProperty = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("stringproperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.StringProperty); + } + + [Fact] + public void AddToList() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListNegativePosition() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/-1", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void ShouldAddToListWithDifferentCase() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("integerlist/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListInvalidPositionTooLarge() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/4", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/IntegerList/4', the index is larger than the array size.", + exception.Message); + } + + [Fact] + public void AddToListAtEndWithSerialization() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/3", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.IntegerList); + } + + [Fact] + public void AddToListAtBeginning() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListInvalidPositionTooSmall() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/-1", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void AddToListAppend() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/-", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.IntegerList); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs new file mode 100644 index 0000000000..4d2e8c646d --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class AddTypedOperationTests + { + [Fact] + public void AddToListNegativePosition() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/-1", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void AddToListInList() + { + var doc = new SimpleDTOWithNestedDTO() + { + ListOfSimpleDTO = new List() + { + new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("ListOfSimpleDTO/0/IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.ListOfSimpleDTO[0].IntegerList); + } + + [Fact] + public void AddToListInListInvalidPositionTooSmall() + { + var doc = new SimpleDTOWithNestedDTO() + { + ListOfSimpleDTO = new List() + { + new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("ListOfSimpleDTO/-1/IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/ListOfSimpleDTO/-1/IntegerList/0' could not be added.", + exception.Message); + } + + [Fact] + public void AddToListInListInvalidPositionTooLarge() + { + var doc = new SimpleDTOWithNestedDTO() + { + ListOfSimpleDTO = new List() + { + new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + } + }; + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("ListOfSimpleDTO/20/IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/ListOfSimpleDTO/20/IntegerList/0' could not be added.", + exception.Message); + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/CopyOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/CopyOperationTests.cs new file mode 100644 index 0000000000..3fa546faac --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/CopyOperationTests.cs @@ -0,0 +1,245 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Dynamic; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class CopyOperationTests + { + [Fact] + public void Copy() + { + dynamic doc = new ExpandoObject(); + + doc.StringProperty = "A"; + doc.AnotherStringProperty = "B"; + + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("StringProperty", "AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.AnotherStringProperty); + } + + [Fact] + public void CopyInList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerList/0", "IntegerList/1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void CopyFromListToEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerList/0", "IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 1 }, doc.IntegerList); + } + + [Fact] + public void CopyFromListToNonList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerList/0", "IntegerValue"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void CopyFromNonListToList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerValue = 5; + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerValue", "IntegerList/0"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void CopyToEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerValue = 5; + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerValue", "IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.IntegerList); + } + + [Fact] + public void NestedCopy() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/StringProperty", "SimpleDTO/AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.SimpleDTO.AnotherStringProperty); + } + + [Fact] + public void NestedCopyInList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerList/0", "SimpleDTO/IntegerList/1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedCopyFromListToEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerList/0", "SimpleDTO/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 1 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedCopyFromListToNonList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerList/0", "SimpleDTO/IntegerValue"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.SimpleDTO.IntegerValue); + } + + [Fact] + public void NestedCopyFromNonListToList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerValue", "SimpleDTO/IntegerList/0"); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedCopyToEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerValue", "SimpleDTO/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.SimpleDTO.IntegerList); + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/CopyTypedOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/CopyTypedOperationTests.cs new file mode 100644 index 0000000000..5b015416ac --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/CopyTypedOperationTests.cs @@ -0,0 +1,268 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class CopyTypedOperationTests + { + [Fact] + public void Copy() + { + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("StringProperty", "AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.AnotherStringProperty); + } + + [Fact] + public void CopyInList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerList/0", "IntegerList/1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void CopyFromListToEndOfList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerList/0", "IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 1 }, doc.IntegerList); + } + + [Fact] + public void CopyFromListToNonList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerList/0", "IntegerValue"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void CopyFromNonListToList() + { + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerValue", "IntegerList/0"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void CopyToEndOfList() + { + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("IntegerValue", "IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.IntegerList); + } + + [Fact] + public void NestedCopy() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/StringProperty", "SimpleDTO/AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.SimpleDTO.AnotherStringProperty); + } + + [Fact] + public void NestedCopyInList() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerList/0", "SimpleDTO/IntegerList/1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedCopyFromListToEndOfList() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerList/0", "SimpleDTO/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 1 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedCopyFromListToNonList() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerList/0", "SimpleDTO/IntegerValue"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.SimpleDTO.IntegerValue); + } + + [Fact] + public void NestedCopyFromNonListToList() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerValue", "SimpleDTO/IntegerList/0"); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedCopyToEndOfList() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("SimpleDTO/IntegerValue", "SimpleDTO/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.SimpleDTO.IntegerList); + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/MoveOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/MoveOperationTests.cs new file mode 100644 index 0000000000..dea2b6ee32 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/MoveOperationTests.cs @@ -0,0 +1,338 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Dynamic; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class MoveOperationTests + { + [Fact] + public void Move() + { + dynamic doc = new ExpandoObject(); + doc.StringProperty = "A"; + doc.AnotherStringProperty = "B"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("StringProperty", "AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.AnotherStringProperty); + + var cont = doc as IDictionary; + object valueFromDictionary; + cont.TryGetValue("StringProperty", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void MoveToNonExisting() + { + dynamic doc = new ExpandoObject(); + doc.StringProperty = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("StringProperty", "AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.AnotherStringProperty); + + var cont = doc as IDictionary; + object valueFromDictionary; + cont.TryGetValue("StringProperty", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void MoveDynamicToTyped() + { + dynamic doc = new ExpandoObject(); + doc.StringProperty = "A"; + doc.SimpleDTO = new SimpleDTO() { AnotherStringProperty = "B" }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("StringProperty", "SimpleDTO/AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.SimpleDTO.AnotherStringProperty); + + var cont = doc as IDictionary; + object valueFromDictionary; + cont.TryGetValue("StringProperty", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void MoveTypedToDynamic() + { + dynamic doc = new ExpandoObject(); + doc.StringProperty = "A"; + doc.SimpleDTO = new SimpleDTO() { AnotherStringProperty = "B" }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("SimpleDTO/AnotherStringProperty", "StringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.StringProperty); + Assert.Equal(null, doc.SimpleDTO.AnotherStringProperty); + } + + [Fact] + public void NestedMove() + { + dynamic doc = new ExpandoObject(); + doc.Nested = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("Nested/StringProperty", "Nested/AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.Nested.AnotherStringProperty); + Assert.Equal(null, doc.Nested.StringProperty); + } + + [Fact] + public void MoveInList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerList/0", "IntegerList/1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 2, 1, 3 }, doc.IntegerList); + } + + [Fact] + public void NestedMoveInList() + { + dynamic doc = new ExpandoObject(); + doc.Nested = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("Nested/IntegerList/0", "Nested/IntegerList/1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 2, 1, 3 }, doc.Nested.IntegerList); + } + + [Fact] + public void MoveFromListToEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerList/0", "IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 2, 3, 1 }, doc.IntegerList); + } + + [Fact] + public void NestedMoveFromListToEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.Nested = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("Nested/IntegerList/0", "Nested/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 2, 3, 1 }, doc.Nested.IntegerList); + } + + [Fact] + public void MoveFomListToNonList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerList/0", "IntegerValue"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 2, 3 }, doc.IntegerList); + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void NestedMoveFomListToNonList() + { + dynamic doc = new ExpandoObject(); + doc.Nested = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("Nested/IntegerList/0", "Nested/IntegerValue"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 2, 3 }, doc.Nested.IntegerList); + Assert.Equal(1, doc.Nested.IntegerValue); + } + + [Fact] + public void MoveFromNonListToList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerValue = 5; + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerValue", "IntegerList/0"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + var cont = doc as IDictionary; + object valueFromDictionary; + cont.TryGetValue("IntegerValue", out valueFromDictionary); + Assert.Null(valueFromDictionary); + + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void NestedMoveFromNonListToList() + { + dynamic doc = new ExpandoObject(); + doc.Nested = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("Nested/IntegerValue", "Nested/IntegerList/0"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(0, doc.Nested.IntegerValue); + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.Nested.IntegerList); + } + + [Fact] + public void MoveToEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerValue = 5; + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerValue", "IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + var cont = doc as IDictionary; + object valueFromDictionary; + cont.TryGetValue("IntegerValue", out valueFromDictionary); + Assert.Null(valueFromDictionary); + + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.IntegerList); + } + + [Fact] + public void NestedMoveToEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.Nested = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("Nested/IntegerValue", "Nested/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(0, doc.Nested.IntegerValue); + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.Nested.IntegerList); + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/MoveTypedOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/MoveTypedOperationTests.cs new file mode 100644 index 0000000000..3d356a3401 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/MoveTypedOperationTests.cs @@ -0,0 +1,138 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class MoveTypedOperationTests + { + [Fact] + public void Move() + { + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("StringProperty", "AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.AnotherStringProperty); + Assert.Equal(null, doc.StringProperty); + } + + [Fact] + public void MoveInList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerList/0", "IntegerList/1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 2, 1, 3 }, doc.IntegerList); + } + + [Fact] + public void MoveFromListToEndOfList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerList/0", "IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 2, 3, 1 }, doc.IntegerList); + } + + [Fact] + public void MoveFomListToNonList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerList/0", "IntegerValue"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 2, 3 }, doc.IntegerList); + Assert.Equal(1, doc.IntegerValue); + } + + [Fact] + public void MoveFromNonListToList() + { + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerValue", "IntegerList/0"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 5, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void MoveToEndOfList() + { + var doc = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Move("IntegerValue", "IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(0, doc.IntegerValue); + Assert.Equal(new List() { 1, 2, 3, 5 }, doc.IntegerList); + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/NestedDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/NestedDTO.cs new file mode 100644 index 0000000000..4d30cf60f2 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/NestedDTO.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. 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.Dynamic +{ + public class NestedDTO + { + public string StringProperty { get; set; } + public dynamic DynamicProperty { get; set; } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/PatchDocumentTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/PatchDocumentTests.cs new file mode 100644 index 0000000000..e15f85da8e --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/PatchDocumentTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. 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 Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class PatchDocumentTests + { + [Fact] + public void InvalidPathAtBeginningShouldThrowException() + { + JsonPatchDocument patchDoc = new JsonPatchDocument(); + var exception = Assert.Throws(() => + { + patchDoc.Add("//NewInt", 1); + }); + Assert.Equal( + "The provided string '//NewInt' is an invalid path.", + exception.Message); + } + + [Fact] + public void InvalidPathAtEndShouldThrowException() + { + JsonPatchDocument patchDoc = new JsonPatchDocument(); + var exception = Assert.Throws(() => + { + patchDoc.Add("NewInt//", 1); + }); + Assert.Equal( + "The provided string 'NewInt//' is an invalid path.", + exception.Message); + } + + [Fact] + public void InvalidPathWithDotShouldThrowException() + { + JsonPatchDocument patchDoc = new JsonPatchDocument(); + var exception = Assert.Throws(() => + { + patchDoc.Add("NewInt.Test", 1); + }); + Assert.Equal( + "The provided string 'NewInt.Test' is an invalid path.", + exception.Message); + } + + [Fact] + public void NonGenericPatchDocToGenericMustSerialize() + { + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("StringProperty", "AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.AnotherStringProperty); + } + + [Fact] + public void GenericPatchDocToNonGenericMustSerialize() + { + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + JsonPatchDocument patchDocTyped = new JsonPatchDocument(); + patchDocTyped.Copy(o => o.StringProperty, o => o.AnotherStringProperty); + + JsonPatchDocument patchDocUntyped = new JsonPatchDocument(); + patchDocUntyped.Copy("StringProperty", "AnotherStringProperty"); + + var serializedTyped = JsonConvert.SerializeObject(patchDocTyped); + var serializedUntyped = JsonConvert.SerializeObject(patchDocUntyped); + var deserialized = JsonConvert.DeserializeObject(serializedTyped); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.AnotherStringProperty); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveOperationTests.cs new file mode 100644 index 0000000000..9ef28ad7cc --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveOperationTests.cs @@ -0,0 +1,302 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Dynamic; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class RemoveOperationTests + { + [Fact] + public void RemovePropertyShouldFailIfRootIsAnonymous() + { + dynamic doc = new + { + Test = 1 + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("Test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/Test' could not be updated.", + exception.Message); + } + + [Fact] + public void RemovePropertyShouldFailIfItDoesntExist() + { + dynamic doc = new ExpandoObject(); + doc.Test = 1; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("NonExisting"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/NonExisting' could not be removed.", + exception.Message); + } + + [Fact] + public void RemovePropertyFromExpandoObject() + { + dynamic obj = new ExpandoObject(); + obj.Test = 1; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("Test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + + var cont = obj as IDictionary; + object valueFromDictionary; + + cont.TryGetValue("Test", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemovePropertyFromExpandoObjectMixedCase() + { + dynamic obj = new ExpandoObject(); + obj.Test = 1; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + + var cont = obj as IDictionary; + object valueFromDictionary; + + cont.TryGetValue("Test", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveNestedPropertyFromExpandoObject() + { + dynamic obj = new ExpandoObject(); + obj.Test = new ExpandoObject(); + obj.Test.AnotherTest = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("Test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + + var cont = obj as IDictionary; + object valueFromDictionary; + + cont.TryGetValue("Test", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveNestedPropertyFromExpandoObjectMixedCase() + { + dynamic obj = new ExpandoObject(); + obj.Test = new ExpandoObject(); + obj.Test.AnotherTest = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + var cont = obj as IDictionary; + + object valueFromDictionary; + cont.TryGetValue("Test", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void NestedRemove() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/StringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void NestedRemoveMixedCase() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("Simpledto/stringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void NestedRemoveFromList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/2"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedRemoveFromListMixedCase() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/Integerlist/2"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedRemoveFromListInvalidPositionTooLarge() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/3"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/3', the index is larger than the array size.", + exception.Message); + } + + [Fact] + public void NestedRemoveFromListInvalidPositionTooSmall() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/-1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void NestedRemoveFromEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs new file mode 100644 index 0000000000..db5d6502d1 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs @@ -0,0 +1,244 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class RemoveTypedOperationTests + { + [Fact] + public void Remove() + { + var doc = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("StringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(null, doc.StringProperty); + } + + [Fact] + public void RemoveFromList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("IntegerList/2"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.IntegerList); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLarge() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("IntegerList/3"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/IntegerList/3', the index is larger than the array size.", + exception.Message); + } + + [Fact] + public void RemoveFromListInvalidPositionTooSmall() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("IntegerList/-1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void RemoveFromEndOfList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.IntegerList); + } + + [Fact] + public void NestedRemove() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/StringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void NestedRemoveFromList() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/2"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedRemoveFromListInvalidPositionTooLarge() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/3"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/3', the index is larger than the array size.", + exception.Message); + } + + [Fact] + public void NestedRemoveFromListInvalidPositionTooSmall() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/-1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void NestedRemoveFromEndOfList() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/ReplaceOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/ReplaceOperationTests.cs new file mode 100644 index 0000000000..c7573c0286 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/ReplaceOperationTests.cs @@ -0,0 +1,242 @@ +// Copyright (c) .NET Foundation. 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 System.Dynamic; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class ReplaceOperationTests + { + [Fact] + public void ReplaceGuidTest() + { + dynamic doc = new SimpleDTO() + { + GuidValue = Guid.NewGuid() + }; + + var newGuid = Guid.NewGuid(); + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("GuidValue", newGuid); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserizalized = JsonConvert.DeserializeObject(serialized); + + deserizalized.ApplyTo(doc); + + Assert.Equal(newGuid, doc.GuidValue); + } + + [Fact] + public void ReplaceGuidTestExpandoObject() + { + dynamic doc = new ExpandoObject(); + doc.GuidValue = Guid.NewGuid(); + + var newGuid = Guid.NewGuid(); + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("GuidValue", newGuid); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserizalized = JsonConvert.DeserializeObject(serialized); + + deserizalized.ApplyTo(doc); + + Assert.Equal(newGuid, doc.GuidValue); + } + + [Fact] + public void ReplaceGuidTestExpandoObjectInAnonymous() + { + dynamic nestedObject = new ExpandoObject(); + nestedObject.GuidValue = Guid.NewGuid(); + + dynamic doc = new + { + NestedObject = nestedObject + }; + + var newGuid = Guid.NewGuid(); + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("nestedobject/GuidValue", newGuid); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserizalized = JsonConvert.DeserializeObject(serialized); + + deserizalized.ApplyTo(doc); + + Assert.Equal(newGuid, doc.NestedObject.GuidValue); + } + + [Fact] + public void ReplaceNestedObjectTest() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + var newDTO = new SimpleDTO() + { + DoubleValue = 1 + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("SimpleDTO", newDTO); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.SimpleDTO.DoubleValue); + Assert.Equal(0, doc.SimpleDTO.IntegerValue); + Assert.Equal(null, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceInList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList/0", 5); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 5, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList", new List() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceInListInList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTOList = new List() { + new SimpleDTO() { + IntegerList = new List(){1,2,3} + }}; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("SimpleDTOList/0/IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(4, doc.SimpleDTOList[0].IntegerList[0]); + } + + [Fact] + public void ReplaceInListInListAtEnd() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTOList = new List() { + new SimpleDTO() { + IntegerList = new List(){1,2,3} + }}; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("SimpleDTOList/0/IntegerList/-", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(4, doc.SimpleDTOList[0].IntegerList[2]); + } + + [Fact] + public void ReplaceFullListFromEnumerable() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList", new List() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullListWithCollection() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList", new Collection() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceAtEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList/-", 5); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 5 }, doc.IntegerList); + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/ReplaceTypedOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/ReplaceTypedOperationTests.cs new file mode 100644 index 0000000000..ccdd86f1a7 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/ReplaceTypedOperationTests.cs @@ -0,0 +1,191 @@ +// Copyright (c) .NET Foundation. 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 System.Linq; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class ReplaceTypedOperationTests + { + [Fact] + public void ReplaceGuidTest() + { + var doc = new SimpleDTO() + { + GuidValue = Guid.NewGuid() + }; + + var newGuid = Guid.NewGuid(); + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("GuidValue", newGuid); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserizalized = JsonConvert.DeserializeObject(serialized); + + deserizalized.ApplyTo(doc); + + Assert.Equal(newGuid, doc.GuidValue); + } + + [Fact] + public void SerializeAndReplaceNestedObjectTest() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + } + }; + + var newDTO = new SimpleDTO() + { + DoubleValue = 1 + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("SimpleDTO", newDTO); + + // serialize & deserialize + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.SimpleDTO.DoubleValue); + Assert.Equal(0, doc.SimpleDTO.IntegerValue); + Assert.Equal(null, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void ReplaceInList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList/0", 5); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 5, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList", new List() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceInListInList() + { + var doc = new SimpleDTO() + { + SimpleDTOList = new List() { + new SimpleDTO() { + IntegerList = new List(){1,2,3} + }} + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("SimpleDTOList/0/IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(4, doc.SimpleDTOList.First().IntegerList.First()); + } + + [Fact] + public void ReplaceFullListFromEnumerable() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList", new List() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceFullListWithCollection() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList", new Collection() { 4, 5, 6 }); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); + } + + [Fact] + public void ReplaceAtEndOfList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Replace("IntegerList/-", 5); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 5 }, doc.IntegerList); + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTO.cs new file mode 100644 index 0000000000..a6938dd992 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTO.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class SimpleDTO + { + public List SimpleDTOList { get; set; } + 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; } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTOWithNestedDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTOWithNestedDTO.cs new file mode 100644 index 0000000000..2147fcceb1 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTOWithNestedDTO.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class SimpleDTOWithNestedDTO + { + public int IntegerValue { get; set; } + public NestedDTO NestedDTO { get; set; } + public SimpleDTO SimpleDTO { get; set; } + public List ListOfSimpleDTO { get; set; } + + public SimpleDTOWithNestedDTO() + { + NestedDTO = new NestedDTO(); + SimpleDTO = new SimpleDTO(); + ListOfSimpleDTO = new List(); + } + } +} 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..74e0fef687 --- /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 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 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..6dd5ce8852 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/NestedDTO.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. 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..33649b7788 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs @@ -0,0 +1,2041 @@ +// Copyright (c) .NET Foundation. 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 AddToIntegerIList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerIList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => (List)o.SimpleDTO.IntegerIList, 4, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.SimpleDTO.IntegerIList); + } + + [Fact] + public void AddToIntegerIListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerIList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => (List)o.SimpleDTO.IntegerIList, 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.IntegerIList); + } + + [Fact] + public void AddToNestedIntegerIList() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTOIList = new List + { + new SimpleDTO + { + IntegerIList = new List() { 1, 2, 3 } + } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => (List)o.SimpleDTOIList[0].IntegerIList, 4, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.SimpleDTOIList[0].IntegerIList); + } + + [Fact] + public void AddToNestedIntegerIListWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTOIList = new List + { + new SimpleDTO + { + IntegerIList = new List() { 1, 2, 3 } + } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => (List)o.SimpleDTOIList[0].IntegerIList, 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.SimpleDTOIList[0].IntegerIList); + } + + [Fact] + public void AddToComplextTypeListSpecifyIndex() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTOList = new List() + { + new SimpleDTO + { + StringProperty = "String1" + }, + new SimpleDTO + { + StringProperty = "String2" + } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTOList[0].StringProperty, "ChangedString1"); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("ChangedString1", doc.SimpleDTOList[0].StringProperty); + } + + [Fact] + public void AddToComplextTypeListSpecifyIndexWithSerialization() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTOList = new List() + { + new SimpleDTO + { + StringProperty = "String1" + }, + new SimpleDTO + { + StringProperty = "String2" + } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTOList[0].StringProperty, "ChangedString1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(doc); + + // Assert + Assert.Equal("ChangedString1", doc.SimpleDTOList[0].StringProperty); + } + + [Fact] + public void AddToListInvalidPositionTooLarge() + { + // 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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal( + "For operation 'add' on array property at path '/simpledto/integerlist/4', the index is " + + "larger than the array size.", + exception.Message); + + } + + [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 + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/simpledto/integerlist/4', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void AddToListInvalidPositionTooLarge_LogsError() + { + // 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 logger = new TestErrorLogger(); + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + + //Assert + Assert.Equal( + "For operation 'add' on array property at path '/simpledto/integerlist/4', the index is larger than " + + "the array size.", + logger.ErrorMessage); + + } + + [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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal( + "For operation 'add' on array property at path '/simpledto/integerlist/-1', the index is negative.", + exception.Message); + } + + [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 + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/simpledto/integerlist/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void AddToListInvalidPositionTooSmall_LogsError() + { + // 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 logger = new TestErrorLogger(); + + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + + //Assert + Assert.Equal( + "For operation 'add' on array property at path '/simpledto/integerlist/-1', the index is negative.", + logger.ErrorMessage); + } + + [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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal( + "For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [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 + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLarge_LogsError() + { + // 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 logger = new TestErrorLogger(); + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal( + "For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + logger.ErrorMessage); + } + + [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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", exception.Message); + } + + [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 + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", exception.Message); + } + + [Fact] + public void RemoveFromListInvalidPositionTooSmall_LogsError() + { + // 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 logger = new TestErrorLogger(); + + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", logger.ErrorMessage); + } + + [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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal( + "For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [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 + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void ReplaceInListInvalid_PositionTooLarge_LogsError() + { + // 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 logger = new TestErrorLogger(); + + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + + // Assert + Assert.Equal( + "For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + logger.ErrorMessage); + } + + [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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal("For operation 'replace' on array property at path '/simpledto/integerlist/-1', the index is negative.", + exception.Message); + } + + [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 + var exception = Assert.Throws(() => { deserialized.ApplyTo(doc); }); + Assert.Equal( + "For operation 'replace' on array property at path '/simpledto/integerlist/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void ReplaceInListInvalidPositionTooSmall_LogsError() + { + // 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 logger = new TestErrorLogger(); + + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + + // Assert + Assert.Equal( + "For operation 'replace' on array property at path '/simpledto/integerlist/-1', the index is negative.", + logger.ErrorMessage); + } + + [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/ObjectAdapterTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs new file mode 100644 index 0000000000..0cc97e32c4 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs @@ -0,0 +1,1737 @@ +// Copyright (c) .NET Foundation. 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 ObjectAdapterTests + { + [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 AddToIntegerIList() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerIList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerIList, 4, 0); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerIList); + } + + [Fact] + public void AddToIntegerIListWithSerialization() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerIList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerIList, 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.IntegerIList); + } + + [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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal( + "For operation 'add' on array property at path '/integerlist/4', the index is " + + "larger than the array size.", + exception.Message); + } + + [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 + var exception = Assert.Throws(() => { deserialized.ApplyTo(doc); }); + Assert.Equal( + "For operation 'add' on array property at path '/integerlist/4', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void AddToListInvalidPositionTooLarge_LogsError() + { + // 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 logger = new TestErrorLogger(); + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + + // Assert + Assert.Equal( + "For operation 'add' on array property at path '/integerlist/4', the index is " + + "larger than the array size.", + logger.ErrorMessage); + } + + [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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal( + "For operation 'add' on array property at path '/integerlist/-1', the index is negative.", + exception.Message); + } + + [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 + var exception = Assert.Throws(() => { deserialized.ApplyTo(doc); }); + Assert.Equal( + "For operation 'add' on array property at path '/integerlist/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void AddToListInvalidPositionTooSmall_LogsError() + { + // 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 logger = new TestErrorLogger(); + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal( + "For operation 'add' on array property at path '/integerlist/-1', the index is negative.", + logger.ErrorMessage); + } + + [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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal( + "For operation 'remove' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [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 + var exception = Assert.Throws(() => { deserialized.ApplyTo(doc); }); + Assert.Equal( + "For operation 'remove' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLarge_LogsError() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, 3); + + var logger = new TestErrorLogger(); + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + + // Assert + Assert.Equal( + "For operation 'remove' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + logger.ErrorMessage); + } + + [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 + var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); + Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", exception.Message); + } + + [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 + var exception = Assert.Throws(() => { deserialized.ApplyTo(doc); }); + Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", exception.Message); + } + + [Fact] + public void RemoveFromListInvalidPositionTooSmall_LogsError() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, -1); + + var logger = new TestErrorLogger(); + + + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + + // Assert + Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", logger.ErrorMessage); + } + + [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 + var exception = Assert.Throws(() => + { + var deserialized + = JsonConvert.DeserializeObject>(serialized); + }); + + Assert.Equal("The type 'JsonPatchDocument`1' was malformed and could not be parsed.", exception.Message); + } + + [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 + var exception = Assert.Throws(() => + { + patchDoc.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'replace' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [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 + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'replace' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [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 + var exception = Assert.Throws(() => + { + patchDoc.ApplyTo(doc); + }); + Assert.Equal("For operation 'replace' on array property at path '/integerlist/-1', the index is negative.", exception.Message); + } + + [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 + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal("For operation 'replace' on array property at path '/integerlist/-1', the index is negative.", exception.Message); + } + + [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/SimpleDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs new file mode 100644 index 0000000000..0c29a86e83 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTO.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.AspNet.JsonPatch.Test +{ + public class SimpleDTO + { + public List IntegerList { get; set; } + public IList IntegerIList { 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..81aa38e682 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/SimpleDTOWithNestedDTO.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.JsonPatch.Test +{ + public class SimpleDTOWithNestedDTO + { + public int IntegerValue { get; set; } + + public NestedDTO NestedDTO { get; set; } + + public SimpleDTO SimpleDTO { get; set; } + + public List SimpleDTOList { get; set; } + + public IList SimpleDTOIList { get; set; } + + public SimpleDTOWithNestedDTO() + { + this.NestedDTO = new NestedDTO(); + this.SimpleDTO = new SimpleDTO(); + this.SimpleDTOList = new List(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/TestErrorLogger.cs b/test/Microsoft.AspNet.JsonPatch.Test/TestErrorLogger.cs new file mode 100644 index 0000000000..c254eab3e3 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/TestErrorLogger.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. 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 TestErrorLogger where T: class + { + public string ErrorMessage { get; set; } + + public void LogErrorMessage(JsonPatchError patchError) + { + ErrorMessage = patchError.ErrorMessage; + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/project.json b/test/Microsoft.AspNet.JsonPatch.Test/project.json new file mode 100644 index 0000000000..629b7a1be3 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/project.json @@ -0,0 +1,18 @@ +{ + "compilationOptions": { + "warningsAsErrors": true + }, + "dependencies": { + "Microsoft.AspNet.JsonPatch": "1.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