From 393c25988ae2a57b6be4235ee4848212c3dcd861 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Wed, 21 Sep 2016 22:56:55 -0700 Subject: [PATCH] [Fixes #33] Dictionary operations fail due to contract issues --- .../Adapters/ObjectAdapter.cs | 853 ++---------------- .../Helpers/ActualPropertyPathResult.cs | 22 - .../ExpandoObjectDictionaryExtensions.cs | 77 -- .../Helpers/ObjectTreeAnalyisResult.cs | 209 ----- .../Helpers/RemovedPropertyTypeResult.cs | 38 - .../{Helpers => Internal}/ConversionResult.cs | 10 +- .../Internal/ConversionResultProvider.cs | 24 + .../Internal/DictionaryAdapter.cs | 111 +++ .../Internal/ExpandoObjectAdapter.cs | 143 +++ .../ExpandoObjectDictionaryExtensions.cs | 26 + .../ExpressionHelpers.cs | 11 +- .../Internal/IAdapter.cs | 44 + .../Internal/ListAdapter.cs | 299 ++++++ .../Internal/ObjectVisitor.cs | 76 ++ .../Internal/ParsedPath.cs | 40 + .../{Helpers => Internal}/PathHelpers.cs | 2 +- .../Internal/PocoAdapter.cs | 205 +++++ .../JsonPatchDocument.cs | 10 +- .../JsonPatchDocumentOfT.cs | 10 +- .../Properties/Resources.Designer.cs | 132 +-- .../Resources.resx | 34 +- .../project.json | 12 +- .../DictionaryAdapterTest.cs | 176 ++++ .../Dynamic/AddOperationTests.cs | 43 +- .../Dynamic/AddTypedOperationTests.cs | 6 +- .../Dynamic/RemoveOperationTests.cs | 10 +- .../Dynamic/RemoveTypedOperationTests.cs | 10 +- .../Dynamic/ReplaceOperationTests.cs | 10 +- .../ListAdapterTest.cs | 302 +++++++ .../NestedDTO.cs | 2 +- .../NestedObjectTests.cs | 54 +- .../ObjectAdapterTests.cs | 667 ++++++++++++-- .../ObjectVisitorTest.cs | 230 +++++ .../ObjectVisitorTest.cs~RF1ad82e13.TMP | 119 +++ .../ObjectVisitorTest.cs~RF1ae034c3.TMP | 131 +++ .../SimpleDTO.cs | 2 +- .../SimpleDTOWithNestedDTO.cs | 2 +- .../TestErrorLogger.cs | 2 +- 38 files changed, 2796 insertions(+), 1358 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.JsonPatch/Helpers/ActualPropertyPathResult.cs delete mode 100644 src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpandoObjectDictionaryExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.JsonPatch/Helpers/ObjectTreeAnalyisResult.cs delete mode 100644 src/Microsoft.AspNetCore.JsonPatch/Helpers/RemovedPropertyTypeResult.cs rename src/Microsoft.AspNetCore.JsonPatch/{Helpers => Internal}/ConversionResult.cs (64%) create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResultProvider.cs create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Internal/DictionaryAdapter.cs create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Internal/ExpandoObjectAdapter.cs create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Internal/ExpandoObjectDictionaryExtensions.cs rename src/Microsoft.AspNetCore.JsonPatch/{Helpers => Internal}/ExpressionHelpers.cs (95%) create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Internal/IAdapter.cs create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Internal/ListAdapter.cs create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Internal/ParsedPath.cs rename src/Microsoft.AspNetCore.JsonPatch/{Helpers => Internal}/PathHelpers.cs (95%) create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Internal/PocoAdapter.cs create mode 100644 test/Microsoft.AspNetCore.JsonPatch.Test/DictionaryAdapterTest.cs create mode 100644 test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs create mode 100644 test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs create mode 100644 test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs~RF1ad82e13.TMP create mode 100644 test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs~RF1ae034c3.TMP diff --git a/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs index b70ff697c3..ee01fbeba7 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs @@ -1,14 +1,11 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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.AspNetCore.JsonPatch.Exceptions; using Microsoft.AspNetCore.JsonPatch.Helpers; +using Microsoft.AspNetCore.JsonPatch.Internal; using Microsoft.AspNetCore.JsonPatch.Operations; -using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.JsonPatch.Adapters @@ -127,7 +124,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters string path, object value, object objectToApplyTo, - Operation operationToReport) + Operation operation) { if (path == null) { @@ -139,228 +136,30 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters throw new ArgumentNullException(nameof(objectToApplyTo)); } - if (operationToReport == null) + if (operation == null) { - throw new ArgumentNullException(nameof(operationToReport)); + throw new ArgumentNullException(nameof(operation)); } - // first up: if the path ends in a numeric value, we're inserting in a list and - // that value represents the position; if the path ends in "-", we're appending - // to the list. + var parsedPath = new ParsedPath(path); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); - // get path result - var pathResult = GetActualPropertyPath( - path, - objectToApplyTo, - operationToReport); - if (pathResult == null) + IAdapter adapter; + var target = objectToApplyTo; + string errorMessage; + if (!visitor.TryVisit(ref target, out adapter, out errorMessage)) { + var error = CreatePathNotFoundError(objectToApplyTo, path, operation, errorMessage); + ReportError(error); return; } - var appendList = pathResult.ExecuteAtEnd; - var positionAsInteger = pathResult.NumericEnd; - var actualPathToProperty = pathResult.PathToProperty; - - var treeAnalysisResult = new ObjectTreeAnalysisResult( - objectToApplyTo, - actualPathToProperty, - ContractResolver); - - if (!treeAnalysisResult.IsValidPathForAdd) + if (!adapter.TryAdd(target, parsedPath.LastSegment, ContractResolver, value, out errorMessage)) { - LogError(new JsonPatchError( - objectToApplyTo, - operationToReport, - Resources.FormatPropertyCannotBeAdded(path))); + var error = CreateOperationFailedError(objectToApplyTo, path, operation, errorMessage); + ReportError(error); 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); - } - } } /// @@ -398,30 +197,19 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters throw new ArgumentNullException(nameof(objectToApplyTo)); } - var valueAtFromLocationResult = GetValueAtLocation(operation.from, objectToApplyTo, operation); - - if (valueAtFromLocationResult.HasError) + object propertyValue; + // Get value at 'from' location and add that value to the 'path' location + if (TryGetValue(operation.from, objectToApplyTo, operation, out propertyValue)) { - // Error has already been logged in GetValueAtLocation. We - // must return, because remove / add should not be allowed to continue - return; + // remove that value + Remove(operation.from, objectToApplyTo, operation); + + // add that value to the path location + Add(operation.path, + propertyValue, + objectToApplyTo, + operation); } - - // 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); } /// @@ -453,240 +241,33 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters 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 + /// 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) + private void Remove(string path, object objectToApplyTo, Operation operationToReport) { - // get path result - var pathResult = GetActualPropertyPath( - path, - objectToApplyTo, - operationToReport); + var parsedPath = new ParsedPath(path); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); - if (pathResult == null) + IAdapter adapter; + var target = objectToApplyTo; + string errorMessage; + if (!visitor.TryVisit(ref target, out adapter, out errorMessage)) { - return new RemovedPropertyTypeResult(null, true); + var error = CreatePathNotFoundError(objectToApplyTo, path, operationToReport, errorMessage); + ReportError(error); + return; } - var removeFromList = pathResult.ExecuteAtEnd; - var positionAsInteger = pathResult.NumericEnd; - var actualPathToProperty = pathResult.PathToProperty; - - var treeAnalysisResult = new ObjectTreeAnalysisResult( - objectToApplyTo, - actualPathToProperty, - ContractResolver); - - if (!treeAnalysisResult.IsValidPathForRemove) + if (!adapter.TryRemove(target, parsedPath.LastSegment, ContractResolver, out errorMessage)) { - 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); - } + var error = CreateOperationFailedError(objectToApplyTo, path, operationToReport, errorMessage); + ReportError(error); + return; } } @@ -722,37 +303,25 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters throw new ArgumentNullException(nameof(objectToApplyTo)); } - var removeResult = Remove(operation.path, objectToApplyTo, operation); + var parsedPath = new ParsedPath(operation.path); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); - if (removeResult.HasError) + IAdapter adapter; + var target = objectToApplyTo; + string errorMessage; + if (!visitor.TryVisit(ref target, out adapter, out errorMessage)) { - // return => error has already been logged in remove method + var error = CreatePathNotFoundError(objectToApplyTo, operation.path, operation, errorMessage); + ReportError(error); return; } - if (!removeResult.HasError && removeResult.ActualType == null) + if (!adapter.TryReplace(target, parsedPath.LastSegment, ContractResolver, operation.value, out errorMessage)) { - // the remove operation completed succesfully, but we could not determine the type. - LogError(new JsonPatchError( - objectToApplyTo, - operation, - Resources.FormatCannotDeterminePropertyType(operation.from))); + var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, errorMessage); + ReportError(error); 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); } /// @@ -789,36 +358,26 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters 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) + object propertyValue; + // Get value at 'from' location and add that value to the 'path' location + if (TryGetValue(operation.from, objectToApplyTo, operation, out propertyValue)) { - // Return, error has already been logged in GetValueAtLocation - return; + Add(operation.path, + propertyValue, + objectToApplyTo, + operation); } - - 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, + private bool TryGetValue( + string fromLocation, object objectToGetValueFrom, - Operation operationToReport) + Operation operation, + out object propertyValue) { - if (location == null) + if (fromLocation == null) { - throw new ArgumentNullException(nameof(location)); + throw new ArgumentNullException(nameof(fromLocation)); } if (objectToGetValueFrom == null) @@ -826,280 +385,62 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters throw new ArgumentNullException(nameof(objectToGetValueFrom)); } - if (operationToReport == null) + if (operation == null) { - throw new ArgumentNullException(nameof(operationToReport)); + throw new ArgumentNullException(nameof(operation)); } - // get path result - var pathResult = GetActualPropertyPath( - location, - objectToGetValueFrom, - operationToReport); + propertyValue = null; - if (pathResult == null) + var parsedPath = new ParsedPath(fromLocation); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); + + IAdapter adapter; + var target = objectToGetValueFrom; + string errorMessage; + if (!visitor.TryVisit(ref target, out adapter, out errorMessage)) { - return new GetValueResult(null, true); + var error = CreatePathNotFoundError(objectToGetValueFrom, fromLocation, operation, errorMessage); + ReportError(error); + return false; } - var getAtEndOfList = pathResult.ExecuteAtEnd; - var positionAsInteger = pathResult.NumericEnd; - var actualPathToProperty = pathResult.PathToProperty; - - var treeAnalysisResult = new ObjectTreeAnalysisResult( - objectToGetValueFrom, - actualPathToProperty, - ContractResolver); - - if (treeAnalysisResult.UseDynamicLogic) + if (!adapter.TryGet(target, parsedPath.LastSegment, ContractResolver, out propertyValue, out errorMessage)) { - // 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); - } + var error = CreateOperationFailedError(objectToGetValueFrom, fromLocation, operation, errorMessage); + ReportError(error); + return 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); - } - } + return true; } - 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) + private void ReportError(JsonPatchError error) { if (LogErrorAction != null) { - LogErrorAction(jsonPatchError); + LogErrorAction(error); } else { - throw new JsonPatchException(jsonPatchError); + throw new JsonPatchException(error); } } - private ConversionResult ConvertToActualType(Type propertyType, object value) + private JsonPatchError CreateOperationFailedError(object target, string path, Operation operation, string errorMessage) { - try - { - var o = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), propertyType); - - return new ConversionResult(true, o); - } - catch (Exception) - { - return new ConversionResult(false, null); - } + return new JsonPatchError( + target, + operation, + errorMessage ?? Resources.FormatCannotPerformOperation(operation.op, path)); } - private Type GetIListType(Type type) + private JsonPatchError CreatePathNotFoundError(object target, string path, Operation operation, string errorMessage) { - if (IsGenericListType(type)) - { - return type.GetTypeInfo().GenericTypeArguments[0]; - } - - foreach (Type interfaceType in type.GetTypeInfo().ImplementedInterfaces) - { - if (IsGenericListType(interfaceType)) - { - return interfaceType.GetTypeInfo().GenericTypeArguments[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); - } + return new JsonPatchError( + target, + operation, + errorMessage ?? Resources.FormatTargetLocationNotFound(operation.op, path)); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.JsonPatch/Helpers/ActualPropertyPathResult.cs b/src/Microsoft.AspNetCore.JsonPatch/Helpers/ActualPropertyPathResult.cs deleted file mode 100644 index 1967bf6d90..0000000000 --- a/src/Microsoft.AspNetCore.JsonPatch/Helpers/ActualPropertyPathResult.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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.AspNetCore.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.AspNetCore.JsonPatch/Helpers/ExpandoObjectDictionaryExtensions.cs b/src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpandoObjectDictionaryExtensions.cs deleted file mode 100644 index aa9fa2158b..0000000000 --- a/src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpandoObjectDictionaryExtensions.cs +++ /dev/null @@ -1,77 +0,0 @@ -// 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.AspNetCore.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.AspNetCore.JsonPatch/Helpers/ObjectTreeAnalyisResult.cs b/src/Microsoft.AspNetCore.JsonPatch/Helpers/ObjectTreeAnalyisResult.cs deleted file mode 100644 index 782859ad5f..0000000000 --- a/src/Microsoft.AspNetCore.JsonPatch/Helpers/ObjectTreeAnalyisResult.cs +++ /dev/null @@ -1,209 +0,0 @@ -// 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.AspNetCore.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) - { - 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.AspNetCore.JsonPatch/Helpers/RemovedPropertyTypeResult.cs b/src/Microsoft.AspNetCore.JsonPatch/Helpers/RemovedPropertyTypeResult.cs deleted file mode 100644 index c548f55c6e..0000000000 --- a/src/Microsoft.AspNetCore.JsonPatch/Helpers/RemovedPropertyTypeResult.cs +++ /dev/null @@ -1,38 +0,0 @@ -// 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.AspNetCore.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.AspNetCore.JsonPatch/Helpers/ConversionResult.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResult.cs similarity index 64% rename from src/Microsoft.AspNetCore.JsonPatch/Helpers/ConversionResult.cs rename to src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResult.cs index 00a7513d44..77181eb18d 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Helpers/ConversionResult.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResult.cs @@ -1,17 +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.AspNetCore.JsonPatch.Helpers +namespace Microsoft.AspNetCore.JsonPatch.Internal { - internal class ConversionResult + public class ConversionResult { - public bool CanBeConverted { get; private set; } - public object ConvertedInstance { get; private set; } - public ConversionResult(bool canBeConverted, object convertedInstance) { CanBeConverted = canBeConverted; ConvertedInstance = convertedInstance; } + + public bool CanBeConverted { get; } + public object ConvertedInstance { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResultProvider.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResultProvider.cs new file mode 100644 index 0000000000..bdf4122394 --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ConversionResultProvider.cs @@ -0,0 +1,24 @@ +// 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.AspNetCore.JsonPatch.Internal +{ + public static class ConversionResultProvider + { + public static ConversionResult ConvertTo(object value, Type typeToConvertTo) + { + try + { + var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), typeToConvertTo); + return new ConversionResult(true, deserialized); + } + catch + { + return new ConversionResult(canBeConverted: false, convertedInstance: null); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/DictionaryAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/DictionaryAdapter.cs new file mode 100644 index 0000000000..4c7651e475 --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/DictionaryAdapter.cs @@ -0,0 +1,111 @@ +// 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; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class DictionaryAdapter : IAdapter + { + public bool TryAdd( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var dictionary = (IDictionary)target; + + // As per JsonPatch spec, if a key already exists, adding should replace the existing value + dictionary[segment] = value; + + errorMessage = null; + return true; + } + + public bool TryGet( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage) + { + var dictionary = (IDictionary)target; + + value = dictionary[segment]; + errorMessage = null; + return true; + } + + public bool TryRemove( + object target, + string segment, + IContractResolver contractResolver, + out string errorMessage) + { + var dictionary = (IDictionary)target; + + // As per JsonPatch spec, the target location must exist for remove to be successful + if (!dictionary.Contains(segment)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + dictionary.Remove(segment); + errorMessage = null; + return true; + } + + public bool TryReplace( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var dictionary = (IDictionary)target; + + // As per JsonPatch spec, the target location must exist for remove to be successful + if (!dictionary.Contains(segment)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + dictionary[segment] = value; + errorMessage = null; + return true; + } + + public bool TryTraverse( + object target, + string segment, + IContractResolver contractResolver, + out object nextTarget, + out string errorMessage) + { + var dictionary = target as IDictionary; + if (dictionary == null) + { + nextTarget = null; + errorMessage = null; + return false; + } + + if (dictionary.Contains(segment)) + { + nextTarget = dictionary[segment]; + errorMessage = null; + return true; + } + else + { + nextTarget = null; + errorMessage = null; + return false; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/ExpandoObjectAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ExpandoObjectAdapter.cs new file mode 100644 index 0000000000..681b5309e6 --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ExpandoObjectAdapter.cs @@ -0,0 +1,143 @@ +// 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.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class ExpandoObjectAdapter : IAdapter + { + public bool TryAdd( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var dictionary = (IDictionary)target; + + var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment); + + // As per JsonPatch spec, if a key already exists, adding should replace the existing value + dictionary[key] = ConvertValue(dictionary, key, value); + + errorMessage = null; + return true; + } + + public bool TryGet( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage) + { + var dictionary = (IDictionary)target; + + var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment); + value = dictionary[key]; + + errorMessage = null; + return true; + } + + public bool TryRemove( + object target, + string segment, + IContractResolver contractResolver, + out string errorMessage) + { + var dictionary = (IDictionary)target; + + var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment); + + // As per JsonPatch spec, the target location must exist for remove to be successful + if (!dictionary.ContainsKey(key)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + dictionary.Remove(key); + + errorMessage = null; + return true; + } + + public bool TryReplace( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var dictionary = (IDictionary)target; + + var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment); + + // As per JsonPatch spec, the target location must exist for remove to be successful + if (!dictionary.ContainsKey(key)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + dictionary[key] = ConvertValue(dictionary, key, value); + + errorMessage = null; + return true; + } + + public bool TryTraverse( + object target, + string segment, + IContractResolver contractResolver, + out object nextTarget, + out string errorMessage) + { + var expandoObject = target as ExpandoObject; + if (expandoObject == null) + { + errorMessage = null; + nextTarget = null; + return false; + } + + var dictionary = (IDictionary)expandoObject; + + var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment); + + if (dictionary.ContainsKey(key)) + { + nextTarget = dictionary[key]; + errorMessage = null; + return true; + } + else + { + nextTarget = null; + errorMessage = null; + return false; + } + } + + private object ConvertValue(IDictionary dictionary, string key, object newValue) + { + object existingValue = null; + if (dictionary.TryGetValue(key, out existingValue)) + { + if (existingValue != null) + { + var conversionResult = ConversionResultProvider.ConvertTo(newValue, existingValue.GetType()); + if (conversionResult.CanBeConverted) + { + return conversionResult.ConvertedInstance; + } + } + } + return newValue; + } + } +} diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/ExpandoObjectDictionaryExtensions.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ExpandoObjectDictionaryExtensions.cs new file mode 100644 index 0000000000..8bb60dd2a7 --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ExpandoObjectDictionaryExtensions.cs @@ -0,0 +1,26 @@ +// 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.AspNetCore.JsonPatch.Internal +{ + // Helper methods to allow case-insensitive key search + public static class ExpandoObjectDictionaryExtensions + { + internal static string GetKeyUsingCaseInsensitiveSearch( + this IDictionary propertyDictionary, + string key) + { + foreach (var keyInDictionary in propertyDictionary.Keys) + { + if (string.Equals(key, keyInDictionary, StringComparison.OrdinalIgnoreCase)) + { + return keyInDictionary; + } + } + return key; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpressionHelpers.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ExpressionHelpers.cs similarity index 95% rename from src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpressionHelpers.cs rename to src/Microsoft.AspNetCore.JsonPatch/Internal/ExpressionHelpers.cs index d9b516cc0e..c978330e5c 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Helpers/ExpressionHelpers.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ExpressionHelpers.cs @@ -1,16 +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. -using Newtonsoft.Json; using System; using System.Globalization; -using System.Linq; using System.Linq.Expressions; using System.Reflection; +using Newtonsoft.Json; -namespace Microsoft.AspNetCore.JsonPatch.Helpers +namespace Microsoft.AspNetCore.JsonPatch.Internal { - internal static class ExpressionHelpers + public static class ExpressionHelpers { public static string GetPath(Expression> expr) where TModel : class { @@ -59,7 +58,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Helpers } else { - // Get property name, respecting JsonProperty attribute + // Get property name, respecting JsonProperty attribute return GetPropertyNameFromMemberExpression(memberExpression); } case ExpressionType.Parameter: @@ -73,7 +72,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Helpers private static string GetPropertyNameFromMemberExpression(MemberExpression memberExpression) { // if there's a JsonProperty attribute, we must return the PropertyName - // from the attribute rather than the member name + // from the attribute rather than the member name var jsonPropertyAttribute = memberExpression.Member.GetCustomAttribute( typeof(JsonPropertyAttribute), true); diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/IAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/IAdapter.cs new file mode 100644 index 0000000000..1866b42ed4 --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/IAdapter.cs @@ -0,0 +1,44 @@ +// 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 Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public interface IAdapter + { + bool TryTraverse( + object target, + string segment, + IContractResolver contractResolver, + out object nextTarget, + out string errorMessage); + + bool TryAdd( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage); + + bool TryRemove( + object target, + string segment, + IContractResolver contractResolver, + out string errorMessage); + + bool TryGet( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage); + + bool TryReplace( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage); + } +} diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/ListAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ListAdapter.cs new file mode 100644 index 0000000000..c3da14fe5a --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ListAdapter.cs @@ -0,0 +1,299 @@ +// 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 Microsoft.Extensions.Internal; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class ListAdapter : IAdapter + { + public bool TryAdd( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var list = (IList)target; + + Type typeArgument = null; + if (!TryGetListTypeArgument(list, out typeArgument, out errorMessage)) + { + return false; + } + + PositionInfo positionInfo; + if (!TryGetPositionInfo(list, segment, out positionInfo, out errorMessage)) + { + return false; + } + + object convertedValue = null; + if (!TryConvertValue(value, typeArgument, segment, out convertedValue, out errorMessage)) + { + return false; + } + + if (positionInfo.Type == PositionType.EndOfList) + { + list.Add(convertedValue); + } + else + { + list.Insert(positionInfo.Index, convertedValue); + } + + errorMessage = null; + return true; + } + + public bool TryGet( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage) + { + var list = (IList)target; + + Type typeArgument = null; + if (!TryGetListTypeArgument(list, out typeArgument, out errorMessage)) + { + value = null; + return false; + } + + PositionInfo positionInfo; + if (!TryGetPositionInfo(list, segment, out positionInfo, out errorMessage)) + { + value = null; + return false; + } + + if (positionInfo.Type == PositionType.EndOfList) + { + value = list[list.Count - 1]; + } + else + { + value = list[positionInfo.Index]; + } + + errorMessage = null; + return true; + } + + public bool TryRemove( + object target, + string segment, + IContractResolver contractResolver, + out string errorMessage) + { + var list = (IList)target; + + Type typeArgument = null; + if (!TryGetListTypeArgument(list, out typeArgument, out errorMessage)) + { + return false; + } + + PositionInfo positionInfo; + if (!TryGetPositionInfo(list, segment, out positionInfo, out errorMessage)) + { + return false; + } + + if (positionInfo.Type == PositionType.EndOfList) + { + list.RemoveAt(list.Count - 1); + } + else + { + list.RemoveAt(positionInfo.Index); + } + + errorMessage = null; + return true; + } + + public bool TryReplace( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var list = (IList)target; + + Type typeArgument = null; + if (!TryGetListTypeArgument(list, out typeArgument, out errorMessage)) + { + return false; + } + + PositionInfo positionInfo; + if (!TryGetPositionInfo(list, segment, out positionInfo, out errorMessage)) + { + return false; + } + + object convertedValue = null; + if (!TryConvertValue(value, typeArgument, segment, out convertedValue, out errorMessage)) + { + return false; + } + + if (positionInfo.Type == PositionType.EndOfList) + { + list[list.Count - 1] = convertedValue; + } + else + { + list[positionInfo.Index] = convertedValue; + } + + errorMessage = null; + return true; + } + + public bool TryTraverse( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage) + { + var list = target as IList; + if (list == null) + { + value = null; + errorMessage = null; + return false; + } + + int index = -1; + if (!int.TryParse(segment, out index)) + { + value = null; + errorMessage = Resources.FormatInvalidIndexValue(segment); + return false; + } + + if (index < 0 || index >= list.Count) + { + value = null; + errorMessage = Resources.FormatIndexOutOfBounds(segment); + return false; + } + + value = list[index]; + errorMessage = null; + return true; + } + + private bool TryConvertValue( + object originalValue, + Type listTypeArgument, + string segment, + out object convertedValue, + out string errorMessage) + { + var conversionResult = ConversionResultProvider.ConvertTo(originalValue, listTypeArgument); + if (!conversionResult.CanBeConverted) + { + convertedValue = null; + errorMessage = Resources.FormatInvalidValueForProperty(originalValue); + return false; + } + + convertedValue = conversionResult.ConvertedInstance; + errorMessage = null; + return true; + } + + private bool TryGetListTypeArgument(IList list, out Type listTypeArgument, out string errorMessage) + { + // Arrays are not supported as they have fixed size and operations like Add, Insert do not make sense + var listType = list.GetType(); + if (listType.IsArray) + { + errorMessage = Resources.FormatPatchNotSupportedForArrays(listType.FullName); + listTypeArgument = null; + return false; + } + else + { + var genericList = ClosedGenericMatcher.ExtractGenericInterface(listType, typeof(IList<>)); + if (genericList == null) + { + errorMessage = Resources.FormatPatchNotSupportedForNonGenericLists(listType.FullName); + listTypeArgument = null; + return false; + } + else + { + listTypeArgument = genericList.GenericTypeArguments[0]; + errorMessage = null; + return true; + } + } + } + + private bool TryGetPositionInfo(IList list, string segment, out PositionInfo positionInfo, out string errorMessage) + { + if (segment == "-") + { + positionInfo = new PositionInfo(PositionType.EndOfList, -1); + errorMessage = null; + return true; + } + + int position = -1; + if (int.TryParse(segment, out position)) + { + if (position >= 0 && position < list.Count) + { + positionInfo = new PositionInfo(PositionType.Index, position); + errorMessage = null; + return true; + } + else + { + positionInfo = default(PositionInfo); + errorMessage = Resources.FormatIndexOutOfBounds(segment); + return false; + } + } + else + { + positionInfo = default(PositionInfo); + errorMessage = Resources.FormatInvalidIndexValue(segment); + return false; + } + } + + private struct PositionInfo + { + public PositionInfo(PositionType type, int index) + { + Type = type; + Index = index; + } + + public PositionType Type { get; } + public int Index { get; } + } + + private enum PositionType + { + Index, // valid index + EndOfList, // '-' + Invalid, // Ex: not an integer + OutOfBounds + } + } +} diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs new file mode 100644 index 0000000000..b604f65a17 --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ObjectVisitor.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; +using System.Dynamic; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class ObjectVisitor + { + private readonly IContractResolver _contractResolver; + private readonly ParsedPath _path; + + public ObjectVisitor(ParsedPath path, IContractResolver contractResolver) + { + if (contractResolver == null) + { + throw new ArgumentNullException(nameof(contractResolver)); + } + + _path = path; + _contractResolver = contractResolver; + } + + public bool TryVisit(ref object target, out IAdapter adapter, out string errorMessage) + { + if (target == null) + { + adapter = null; + errorMessage = null; + return false; + } + + adapter = SelectAdapater(target); + + // Traverse until the penultimate segment to get the target object and adapter + for (var i = 0; i < _path.Segments.Count - 1; i++) + { + object next; + if (!adapter.TryTraverse(target, _path.Segments[i], _contractResolver, out next, out errorMessage)) + { + adapter = null; + return false; + } + + target = next; + adapter = SelectAdapater(target); + } + + errorMessage = null; + return true; + } + + private IAdapter SelectAdapater(object targetObject) + { + if (targetObject is ExpandoObject) + { + return new ExpandoObjectAdapter(); + } + else if (targetObject is IDictionary) + { + return new DictionaryAdapter(); + } + else if (targetObject is IList) + { + return new ListAdapter(); + } + else + { + return new PocoAdapter(); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/ParsedPath.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ParsedPath.cs new file mode 100644 index 0000000000..fe786b86c1 --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ParsedPath.cs @@ -0,0 +1,40 @@ +// 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.AspNetCore.JsonPatch.Internal +{ + public struct ParsedPath + { + private static readonly string[] Empty = null; + + private readonly string[] _segments; + + public ParsedPath(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + _segments = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string LastSegment + { + get + { + if (_segments == null || _segments.Length == 0) + { + return null; + } + + return _segments[_segments.Length - 1]; + } + } + + public IReadOnlyList Segments => _segments ?? Empty; + } +} diff --git a/src/Microsoft.AspNetCore.JsonPatch/Helpers/PathHelpers.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/PathHelpers.cs similarity index 95% rename from src/Microsoft.AspNetCore.JsonPatch/Helpers/PathHelpers.cs rename to src/Microsoft.AspNetCore.JsonPatch/Internal/PathHelpers.cs index 99bb2f1536..7ef8fe7baf 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Helpers/PathHelpers.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/PathHelpers.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.JsonPatch.Exceptions; -namespace Microsoft.AspNetCore.JsonPatch.Helpers +namespace Microsoft.AspNetCore.JsonPatch.Internal { internal static class PathHelpers { diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/PocoAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/PocoAdapter.cs new file mode 100644 index 0000000000..0755e132e1 --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/PocoAdapter.cs @@ -0,0 +1,205 @@ +// 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.Linq; +using System.Reflection; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class PocoAdapter : IAdapter + { + public bool TryAdd( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + JsonProperty jsonProperty = null; + if (!TryGetJsonProperty(target, contractResolver, segment, out jsonProperty)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!jsonProperty.Writable) + { + errorMessage = Resources.FormatCannotUpdateProperty(segment); + return false; + } + + object convertedValue = null; + if (!TryConvertValue(value, jsonProperty.PropertyType, out convertedValue)) + { + errorMessage = Resources.FormatInvalidValueForProperty(value); + return false; + } + + jsonProperty.ValueProvider.SetValue(target, convertedValue); + + errorMessage = null; + return true; + } + + public bool TryGet( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage) + { + JsonProperty jsonProperty = null; + if (!TryGetJsonProperty(target, contractResolver, segment, out jsonProperty)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + value = null; + return false; + } + + if (!jsonProperty.Readable) + { + errorMessage = Resources.FormatCannotReadProperty(segment); + value = null; + return false; + } + + value = jsonProperty.ValueProvider.GetValue(target); + errorMessage = null; + return true; + } + + public bool TryRemove( + object target, + string segment, + IContractResolver contractResolver, + out string errorMessage) + { + JsonProperty jsonProperty = null; + if (!TryGetJsonProperty(target, contractResolver, segment, out jsonProperty)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!jsonProperty.Writable) + { + errorMessage = Resources.FormatCannotUpdateProperty(segment); + return false; + } + + // 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 (jsonProperty.PropertyType.GetTypeInfo().IsValueType + && Nullable.GetUnderlyingType(jsonProperty.PropertyType) == null) + { + value = Activator.CreateInstance(jsonProperty.PropertyType); + } + + jsonProperty.ValueProvider.SetValue(target, value); + + errorMessage = null; + return true; + } + + public bool TryReplace( + object target, + string segment, + IContractResolver + contractResolver, + object value, + out string errorMessage) + { + JsonProperty jsonProperty = null; + if (!TryGetJsonProperty(target, contractResolver, segment, out jsonProperty)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!jsonProperty.Writable) + { + errorMessage = Resources.FormatCannotUpdateProperty(segment); + return false; + } + + object convertedValue = null; + if (!TryConvertValue(value, jsonProperty.PropertyType, out convertedValue)) + { + errorMessage = Resources.FormatInvalidValueForProperty(value); + return false; + } + + jsonProperty.ValueProvider.SetValue(target, convertedValue); + + errorMessage = null; + return true; + } + + public bool TryTraverse( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage) + { + if (target == null) + { + value = null; + errorMessage = null; + return false; + } + + JsonProperty jsonProperty = null; + if (TryGetJsonProperty(target, contractResolver, segment, out jsonProperty)) + { + value = jsonProperty.ValueProvider.GetValue(target); + errorMessage = null; + return true; + } + + value = null; + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + private bool TryGetJsonProperty( + object target, + IContractResolver contractResolver, + string segment, + out JsonProperty jsonProperty) + { + var jsonObjectContract = contractResolver.ResolveContract(target.GetType()) as JsonObjectContract; + if (jsonObjectContract != null) + { + var pocoProperty = jsonObjectContract + .Properties + .FirstOrDefault(p => string.Equals(p.PropertyName, segment, StringComparison.OrdinalIgnoreCase)); + + if (pocoProperty != null) + { + jsonProperty = pocoProperty; + return true; + } + } + + jsonProperty = null; + return false; + } + + private bool TryConvertValue(object value, Type propertyType, out object convertedValue) + { + var conversionResult = ConversionResultProvider.ConvertTo(value, propertyType); + if (!conversionResult.CanBeConverted) + { + convertedValue = null; + return false; + } + + convertedValue = conversionResult.ConvertedInstance; + return true; + } + } +} diff --git a/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocument.cs b/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocument.cs index c9c34459a9..72e93e0302 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocument.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocument.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.JsonPatch.Adapters; using Microsoft.AspNetCore.JsonPatch.Converters; -using Microsoft.AspNetCore.JsonPatch.Helpers; +using Microsoft.AspNetCore.JsonPatch.Internal; using Microsoft.AspNetCore.JsonPatch.Operations; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -13,7 +13,7 @@ using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.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 + // 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 @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// Apply this JsonPatchDocument + /// Apply this JsonPatchDocument /// /// Object to apply the JsonPatchDocument to public void ApplyTo(object objectToApplyTo) @@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// Apply this JsonPatchDocument + /// Apply this JsonPatchDocument /// /// Object to apply the JsonPatchDocument to /// Action to log errors @@ -174,7 +174,7 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// Apply this JsonPatchDocument + /// Apply this JsonPatchDocument /// /// Object to apply the JsonPatchDocument to /// IObjectAdapter instance to use when applying diff --git a/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocumentOfT.cs b/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocumentOfT.cs index 530b414060..afb5eb3ff1 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocumentOfT.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocumentOfT.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Linq.Expressions; using Microsoft.AspNetCore.JsonPatch.Adapters; using Microsoft.AspNetCore.JsonPatch.Converters; -using Microsoft.AspNetCore.JsonPatch.Helpers; +using Microsoft.AspNetCore.JsonPatch.Internal; using Microsoft.AspNetCore.JsonPatch.Operations; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -502,7 +502,7 @@ namespace Microsoft.AspNetCore.JsonPatch /// Copy from a property to a location in a list /// /// - /// source location + /// source location /// target location /// position /// @@ -623,7 +623,7 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// Apply this JsonPatchDocument + /// Apply this JsonPatchDocument /// /// Object to apply the JsonPatchDocument to public void ApplyTo(TModel objectToApplyTo) @@ -637,7 +637,7 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// Apply this JsonPatchDocument + /// Apply this JsonPatchDocument /// /// Object to apply the JsonPatchDocument to /// Action to log errors @@ -652,7 +652,7 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// Apply this JsonPatchDocument + /// Apply this JsonPatchDocument /// /// Object to apply the JsonPatchDocument to /// IObjectAdapter instance to use when applying diff --git a/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs index 70f76f1fa3..9e00cc2f00 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs @@ -26,6 +26,22 @@ namespace Microsoft.AspNetCore.JsonPatch return string.Format(CultureInfo.CurrentCulture, GetString("CannotDeterminePropertyType"), p0); } + /// + /// The '{0}' operation at path '{1}' could not be performed. + /// + internal static string CannotPerformOperation + { + get { return GetString("CannotPerformOperation"); } + } + + /// + /// The '{0}' operation at path '{1}' could not be performed. + /// + internal static string FormatCannotPerformOperation(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CannotPerformOperation"), p0, p1); + } + /// /// The property at '{0}' could not be read. /// @@ -59,35 +75,35 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// The key '{0}' was not found. + /// The index value provided by path segment '{0}' is out of bounds of the array size. /// - internal static string DictionaryKeyNotFound + internal static string IndexOutOfBounds { - get { return GetString("DictionaryKeyNotFound"); } + get { return GetString("IndexOutOfBounds"); } } /// - /// The key '{0}' was not found. + /// The index value provided by path segment '{0}' is out of bounds of the array size. /// - internal static string FormatDictionaryKeyNotFound(object p0) + internal static string FormatIndexOutOfBounds(object p0) { - return string.Format(CultureInfo.CurrentCulture, GetString("DictionaryKeyNotFound"), p0); + return string.Format(CultureInfo.CurrentCulture, GetString("IndexOutOfBounds"), p0); } /// - /// For operation '{0}' on array property at path '{1}', the index is larger than the array size. + /// The path segment '{0}' is invalid for an array index. /// - internal static string InvalidIndexForArrayProperty + internal static string InvalidIndexValue { - get { return GetString("InvalidIndexForArrayProperty"); } + get { return GetString("InvalidIndexValue"); } } /// - /// For operation '{0}' on array property at path '{1}', the index is larger than the array size. + /// The path segment '{0}' is invalid for an array index. /// - internal static string FormatInvalidIndexForArrayProperty(object p0, object p1) + internal static string FormatInvalidIndexValue(object p0) { - return string.Format(CultureInfo.CurrentCulture, GetString("InvalidIndexForArrayProperty"), p0, p1); + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidIndexValue"), p0); } /// @@ -106,22 +122,6 @@ namespace Microsoft.AspNetCore.JsonPatch 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. /// @@ -139,7 +139,7 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// The value '{0}' is invalid for property at path '{1}'. + /// The value '{0}' is invalid for target location. /// internal static string InvalidValueForProperty { @@ -147,27 +147,11 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// The value '{0}' is invalid for property at path '{1}'. + /// The value '{0}' is invalid for target location. /// - internal static string FormatInvalidValueForProperty(object p0, object p1) + internal static string FormatInvalidValueForProperty(object p0) { - 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); + return string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForProperty"), p0); } /// @@ -187,51 +171,67 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// The property at path '{0}' could not be added. + /// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size. /// - internal static string PropertyCannotBeAdded + internal static string PatchNotSupportedForArrays { - get { return GetString("PropertyCannotBeAdded"); } + get { return GetString("PatchNotSupportedForArrays"); } } /// - /// The property at path '{0}' could not be added. + /// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size. /// - internal static string FormatPropertyCannotBeAdded(object p0) + internal static string FormatPatchNotSupportedForArrays(object p0) { - return string.Format(CultureInfo.CurrentCulture, GetString("PropertyCannotBeAdded"), p0); + return string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForArrays"), p0); } /// - /// The property at path '{0}' could not be removed. + /// The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported. /// - internal static string PropertyCannotBeRemoved + internal static string PatchNotSupportedForNonGenericLists { - get { return GetString("PropertyCannotBeRemoved"); } + get { return GetString("PatchNotSupportedForNonGenericLists"); } } /// - /// The property at path '{0}' could not be removed. + /// The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported. /// - internal static string FormatPropertyCannotBeRemoved(object p0) + internal static string FormatPatchNotSupportedForNonGenericLists(object p0) { - return string.Format(CultureInfo.CurrentCulture, GetString("PropertyCannotBeRemoved"), p0); + return string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForNonGenericLists"), p0); } /// - /// Property does not exist at path '{0}'. + /// The target location specified by path segment '{0}' was not found. /// - internal static string PropertyDoesNotExist + internal static string TargetLocationAtPathSegmentNotFound { - get { return GetString("PropertyDoesNotExist"); } + get { return GetString("TargetLocationAtPathSegmentNotFound"); } } /// - /// Property does not exist at path '{0}'. + /// The target location specified by path segment '{0}' was not found. /// - internal static string FormatPropertyDoesNotExist(object p0) + internal static string FormatTargetLocationAtPathSegmentNotFound(object p0) { - return string.Format(CultureInfo.CurrentCulture, GetString("PropertyDoesNotExist"), p0); + return string.Format(CultureInfo.CurrentCulture, GetString("TargetLocationAtPathSegmentNotFound"), p0); + } + + /// + /// For operation '{0}', the target location specified by path '{1}' was not found. + /// + internal static string TargetLocationNotFound + { + get { return GetString("TargetLocationNotFound"); } + } + + /// + /// For operation '{0}', the target location specified by path '{1}' was not found. + /// + internal static string FormatTargetLocationNotFound(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TargetLocationNotFound"), p0, p1); } /// diff --git a/src/Microsoft.AspNetCore.JsonPatch/Resources.resx b/src/Microsoft.AspNetCore.JsonPatch/Resources.resx index 59ae2b59c3..a7b50520ac 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Resources.resx +++ b/src/Microsoft.AspNetCore.JsonPatch/Resources.resx @@ -120,44 +120,44 @@ The type of the property at path '{0}' could not be determined. + + The '{0}' operation at path '{1}' could not be performed. + The property at '{0}' could not be read. The property at path '{0}' could not be updated. - - The key '{0}' was not found. + + The index value provided by path segment '{0}' is out of bounds of the array size. - - For operation '{0}' on array property at path '{1}', the index is larger than the array size. + + The path segment '{0}' is invalid for an array index. 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. + The value '{0}' is invalid for target location. '{0}' must be of type '{1}'. - - The property at path '{0}' could not be added. + + The type '{0}' which is an array is not supported for json patch operations as it has a fixed size. - - The property at path '{0}' could not be removed. + + The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported. - - Property does not exist at path '{0}'. + + The target location specified by path segment '{0}' was not found. + + + For operation '{0}', the target location specified by path '{1}' was not found. The test operation is not supported. diff --git a/src/Microsoft.AspNetCore.JsonPatch/project.json b/src/Microsoft.AspNetCore.JsonPatch/project.json index 5180ee7e23..c9beba3639 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/project.json +++ b/src/Microsoft.AspNetCore.JsonPatch/project.json @@ -22,14 +22,18 @@ }, "dependencies": { "NETStandard.Library": "1.6.1-*", - "Newtonsoft.Json": "9.0.1" + "Newtonsoft.Json": "9.0.1", + "Microsoft.Extensions.ClosedGenericMatcher.Sources": { + "type": "build", + "version": "1.1.0-*" + } }, "frameworks": { - "netstandard1.1": { + "net451": {}, + "netstandard1.3": { "dependencies": { "Microsoft.CSharp": "4.3.0-*", - "System.ComponentModel.TypeConverter": "4.3.0-*", - "System.Runtime.Serialization.Primitives": "4.3.0-*" + "System.Reflection.TypeExtensions": "4.1.0-*" } } } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/DictionaryAdapterTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/DictionaryAdapterTest.cs new file mode 100644 index 0000000000..6a88bd6cc7 --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/DictionaryAdapterTest.cs @@ -0,0 +1,176 @@ +// 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 Moq; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class DictionaryAdapterTest + { + [Fact] + public void Add_KeyWhichAlreadyExists_ReplacesExistingValue() + { + // Arrange + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + dictionary[nameKey] = "Mike"; + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new Mock(MockBehavior.Strict); + string message = null; + + // Act + var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver.Object, "James", out message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(1, dictionary.Count); + Assert.Equal("James", dictionary[nameKey]); + } + + [Fact] + public void Get_UsingCaseSensitiveKey_FailureScenario() + { + // Arrange + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new Mock(MockBehavior.Strict); + string message = null; + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + + // Act + var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver.Object, "James", out message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(1, dictionary.Count); + Assert.Equal("James", dictionary[nameKey]); + + // Act + object outValue = null; + addStatus = dictionaryAdapter.TryGet(dictionary, nameKey.ToUpper(), resolver.Object, out outValue, out message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Null(outValue); + } + + [Fact] + public void Get_UsingCaseSensitiveKey_SuccessScenario() + { + // Arrange + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new Mock(MockBehavior.Strict); + string message = null; + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + + // Act + var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver.Object, "James", out message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(1, dictionary.Count); + Assert.Equal("James", dictionary[nameKey]); + + // Act + object outValue = null; + addStatus = dictionaryAdapter.TryGet(dictionary, nameKey, resolver.Object, out outValue, out message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal("James", outValue?.ToString()); + } + + [Fact] + public void ReplacingExistingItem() + { + // Arrange + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + dictionary.Add(nameKey, "Mike"); + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new Mock(MockBehavior.Strict); + string message = null; + + // Act + var replaceStatus = dictionaryAdapter.TryReplace(dictionary, nameKey, resolver.Object, "James", out message); + + // Assert + Assert.True(replaceStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(1, dictionary.Count); + Assert.Equal("James", dictionary[nameKey]); + } + + [Fact] + public void Replace_NonExistingKey_Fails() + { + // Arrange + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new Mock(MockBehavior.Strict); + string message = null; + + // Act + var replaceStatus = dictionaryAdapter.TryReplace(dictionary, nameKey, resolver.Object, "Mike", out message); + + // Assert + Assert.False(replaceStatus); + Assert.Equal( + string.Format("The target location specified by path segment '{0}' was not found.", nameKey), + message); + Assert.Equal(0, dictionary.Count); + } + + [Fact] + public void Remove_NonExistingKey_Fails() + { + // Arrange + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new Mock(MockBehavior.Strict); + string message = null; + + // Act + var removeStatus = dictionaryAdapter.TryRemove(dictionary, nameKey, resolver.Object, out message); + + // Assert + Assert.False(removeStatus); + Assert.Equal( + string.Format("The target location specified by path segment '{0}' was not found.", nameKey), + message); + Assert.Equal(0, dictionary.Count); + } + + [Fact] + public void Remove_RemovesFromDictionary() + { + // Arrange + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + dictionary[nameKey] = "James"; + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new Mock(MockBehavior.Strict); + string message = null; + + // Act + var removeStatus = dictionaryAdapter.TryRemove(dictionary, nameKey, resolver.Object, out message); + + //Assert + Assert.True(removeStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(0, dictionary.Count); + } + } +} diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/AddOperationTests.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/AddOperationTests.cs index cb5f090d0f..cbd44a7ef9 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/AddOperationTests.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/AddOperationTests.cs @@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/NewInt' could not be added.", + string.Format("The target location specified by path segment '{0}' was not found.", "NewInt"), exception.Message); } @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/Nested/NewInt' could not be added.", + string.Format("The target location specified by path segment '{0}' was not found.", "NewInt"), exception.Message); } @@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/Nested/NewInt' could not be added.", + string.Format("The target location specified by path segment '{0}' was not found.", "NewInt"), exception.Message); } @@ -214,7 +214,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/ComplexProperty' could not be added.", + string.Format("The target location specified by path segment '{0}' was not found.", "ComplexProperty"), exception.Message); } @@ -238,7 +238,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/StringProperty' could not be updated.", + string.Format("The property at path '{0}' could not be updated.", "StringProperty"), exception.Message); } @@ -336,7 +336,10 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/DynamicProperty/OtherProperty/IntProperty' could not be added.", + string.Format( + "For operation '{0}', the target location specified by path '{1}' was not found.", + "add", + "/DynamicProperty/OtherProperty/IntProperty"), exception.Message); } @@ -374,7 +377,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/baz/bat' could not be added.", + string.Format("The target location specified by path segment '{0}' was not found.", "baz"), exception.Message); } @@ -434,7 +437,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'add' on array property at path '/IntegerList/-1', the index is negative.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -478,30 +481,10 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'add' on array property at path '/IntegerList/4', the index is larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"), 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() { @@ -542,7 +525,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'add' on array property at path '/IntegerList/-1', the index is negative.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs index 098e97d850..f28d877ea4 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'add' on array property at path '/IntegerList/-1', the index is negative.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/ListOfSimpleDTO/-1/IntegerList/0' could not be added.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/ListOfSimpleDTO/20/IntegerList/0' could not be added.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "20"), exception.Message); } } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/RemoveOperationTests.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/RemoveOperationTests.cs index b436bb4d23..6e903bf2ec 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/RemoveOperationTests.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/RemoveOperationTests.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/Test' could not be updated.", + string.Format("The property at path '{0}' could not be updated.", "Test"), exception.Message); } @@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "The property at path '/NonExisting' could not be removed.", + string.Format("The target location specified by path segment '{0}' was not found.", "NonExisting"), exception.Message); } @@ -250,7 +250,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/3', the index is larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -275,8 +275,8 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/-1', the index is negative.", - exception.Message); + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), + exception.Message); } [Fact] diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs index d716dafe9a..126706eef2 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs @@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'remove' on array property at path '/IntegerList/3', the index is larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'remove' on array property at path '/IntegerList/-1', the index is negative.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -187,8 +187,8 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic 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); + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), + exception.Message); } [Fact] @@ -214,7 +214,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/-1', the index is negative.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/ReplaceOperationTests.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/ReplaceOperationTests.cs index 8dff9b6a98..b8e7196068 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/ReplaceOperationTests.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Dynamic/ReplaceOperationTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Dynamic; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Xunit; namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic @@ -25,7 +26,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic JsonPatchDocument patchDoc = new JsonPatchDocument(); patchDoc.Replace("GuidValue", newGuid); - // serialize & deserialize + // serialize & deserialize var serialized = JsonConvert.SerializeObject(patchDoc); var deserizalized = JsonConvert.DeserializeObject(serialized); @@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic JsonPatchDocument patchDoc = new JsonPatchDocument(); patchDoc.Replace("GuidValue", newGuid); - // serialize & deserialize + // serialize & deserialize var serialized = JsonConvert.SerializeObject(patchDoc); var deserizalized = JsonConvert.DeserializeObject(serialized); @@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic JsonPatchDocument patchDoc = new JsonPatchDocument(); patchDoc.Replace("nestedobject/GuidValue", newGuid); - // serialize & deserialize + // serialize & deserialize var serialized = JsonConvert.SerializeObject(patchDoc); var deserizalized = JsonConvert.DeserializeObject(serialized); @@ -98,7 +99,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic JsonPatchDocument patchDoc = new JsonPatchDocument(); patchDoc.Replace("SimpleDTO", newDTO); - // serialize & deserialize + // serialize & deserialize var serialized = JsonConvert.SerializeObject(patchDoc); var deserialized = JsonConvert.DeserializeObject(serialized); @@ -201,6 +202,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic var deserialized = JsonConvert.DeserializeObject(serialized); deserialized.ApplyTo(doc); + Assert.Equal(new List() { 4, 5, 6 }, doc.IntegerList); } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs new file mode 100644 index 0000000000..ee331a2b30 --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.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; +using System.Collections.Generic; +using Moq; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class ListAdapterTest + { + [Fact] + public void Patch_OnArrayObject_Fails() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new[] { 20, 30 }; + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "0", resolver.Object, "40", out message); + + // Assert + Assert.False(addStatus); + Assert.Equal( + string.Format( + "The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.", + targetObject.GetType().FullName), + message); + } + + [Fact] + public void Patch_OnNonGenericListObject_Fails() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new ArrayList(); + targetObject.Add(20); + targetObject.Add(30); + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "40", out message); + + // Assert + Assert.False(addStatus); + Assert.Equal( + string.Format( + "The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.", + targetObject.GetType().FullName), + message); + } + + [Theory] + [InlineData("-1")] + [InlineData("-2")] + [InlineData("2")] + [InlineData("3")] + public void Patch_WithOutOfBoundsIndex_Fails(string position) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { "James", "Mike" }; + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out message); + + // Assert + Assert.False(addStatus); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", position), + message); + } + + [Theory] + [InlineData("_")] + [InlineData("blah")] + public void Patch_WithInvalidPositionFormat_Fails(string position) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { "James", "Mike" }; + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out message); + + // Assert + Assert.False(addStatus); + Assert.Equal( + string.Format("The path segment '{0}' is invalid for an array index.", position), + message); + } + + public static TheoryData, List> AppendAtEndOfListData + { + get + { + return new TheoryData, List>() + { + { + new List() { }, + new List() { 20 } + }, + { + new List() { 5, 10 }, + new List() { 5, 10, 20 } + } + }; + } + } + + [Theory] + [MemberData(nameof(AppendAtEndOfListData))] + public void Add_Appends_AtTheEnd(List targetObject, List expected) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "20", out message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(expected.Count, targetObject.Count); + Assert.Equal(expected, targetObject); + } + + [Fact] + public void Add_NullObject_ToReferenceTypeListWorks() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var listAdapter = new ListAdapter(); + var targetObject = new List() { "James", "Mike" }; + string message = null; + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, value: null, errorMessage: out message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(3, targetObject.Count); + Assert.Equal(new List() { "James", "Mike", null }, targetObject); + } + + [Fact] + public void Add_NonCompatibleType_Fails() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = (new List() { 10, 20 }).AsReadOnly(); + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "James", out message); + + // Assert + Assert.False(addStatus); + Assert.Equal(string.Format("The value '{0}' is invalid for target location.", "James"), message); + } + + public static TheoryData AddingDifferentComplexTypeWorksData + { + get + { + return new TheoryData() + { + { + new List() { }, + "a", + "-", + new List() { "a" } + }, + { + new List() { "a", "b" }, + "c", + "-", + new List() { "a", "b", "c" } + }, + { + new List() { "a", "b" }, + "c", + "0", + new List() { "c", "a", "b" } + }, + { + new List() { "a", "b" }, + "c", + "1", + new List() { "a", "c", "b" } + } + }; + } + } + + [Theory] + [MemberData(nameof(AddingDifferentComplexTypeWorksData))] + public void Add_DifferentComplexTypeWorks(IList targetObject, object value, string position, IList expected) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, value, out message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(expected.Count, targetObject.Count); + Assert.Equal(expected, targetObject); + } + + [Fact] + public void Replace_NonCompatibleType_Fails() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = (new List() { 10, 20 }).AsReadOnly(); + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "James", out message); + + // Assert + Assert.False(replaceStatus); + Assert.Equal( + string.Format("The value '{0}' is invalid for target location.", "James"), + message); + } + + [Fact] + public void Replace_ReplacesValue_AtTheEnd() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "30", out message); + + // Assert + Assert.True(replaceStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(new List() { 10, 30 }, targetObject); + } + + public static TheoryData> ReplacesValuesAtPositionData + { + get + { + return new TheoryData>() + { + { + "0", + new List() { 30, 20 } + }, + { + "1", + new List() { 10, 30 } + } + }; + } + } + + [Theory] + [MemberData(nameof(ReplacesValuesAtPositionData))] + public void Replace_ReplacesValue_AtGivenPosition(string position, List expected) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + string message = null; + + // Act + var replaceStatus = listAdapter.TryReplace(targetObject, position, resolver.Object, "30", out message); + + // Assert + Assert.True(replaceStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(expected, targetObject); + } + } +} diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/NestedDTO.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/NestedDTO.cs index aa767557c3..90be5152cb 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/NestedDTO.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/NestedDTO.cs @@ -1,7 +1,7 @@ // 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.AspNetCore.JsonPatch.Test +namespace Microsoft.AspNetCore.JsonPatch { public class NestedDTO { diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/NestedObjectTests.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/NestedObjectTests.cs index 6215da6bcd..345ffe2201 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/NestedObjectTests.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/NestedObjectTests.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.JsonPatch.Exceptions; using Newtonsoft.Json; using Xunit; -namespace Microsoft.AspNetCore.JsonPatch.Test +namespace Microsoft.AspNetCore.JsonPatch { public class NestedObjectTests { @@ -386,8 +386,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"), exception.Message); } @@ -417,8 +416,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'add' on array property at path '/simpledto/integerlist/4', the index is " + - "larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"), exception.Message); } @@ -445,8 +443,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test //Assert Assert.Equal( - "For operation 'add' on array property at path '/simpledto/integerlist/4', the index is larger than " + - "the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"), logger.ErrorMessage); } @@ -470,7 +467,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -499,7 +496,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'add' on array property at path '/simpledto/integerlist/-1', the index is negative.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -527,7 +524,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test //Assert Assert.Equal( - "For operation 'add' on array property at path '/simpledto/integerlist/-1', the index is negative.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), logger.ErrorMessage); } @@ -697,8 +694,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -727,8 +723,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + - "larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -754,8 +749,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // Assert Assert.Equal( - "For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + - "larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), logger.ErrorMessage); } @@ -777,7 +771,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), + exception.Message); } [Fact] @@ -804,7 +800,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test { deserialized.ApplyTo(doc); }); - Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", exception.Message); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), + exception.Message); } [Fact] @@ -829,7 +827,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test 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); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), + logger.ErrorMessage); } [Fact] @@ -1239,8 +1239,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -1269,8 +1268,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + - "larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -1298,8 +1296,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // Assert Assert.Equal( - "For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + - "larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), logger.ErrorMessage); } @@ -1321,7 +1318,8 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -1347,7 +1345,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -1375,7 +1373,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // Assert Assert.Equal( - "For operation 'replace' on array property at path '/simpledto/integerlist/-1', the index is negative.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), logger.ErrorMessage); } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectAdapterTests.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectAdapterTests.cs index 98a72e1bf8..88693fdbd5 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectAdapterTests.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectAdapterTests.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.JsonPatch.Exceptions; using Newtonsoft.Json; using Xunit; -namespace Microsoft.AspNetCore.JsonPatch.Test +namespace Microsoft.AspNetCore.JsonPatch.Adapters { public class ObjectAdapterTests { @@ -157,8 +157,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"), exception.Message); } @@ -181,8 +180,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"), exception.Message); } @@ -206,54 +204,10 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // Assert Assert.Equal( - "For operation 'add' on array property at path '/integerlist/4', the index is " + - "larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"), 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() { @@ -313,7 +267,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -336,7 +290,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), exception.Message); } @@ -359,7 +313,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // Assert Assert.Equal( - "For operation 'add' on array property at path '/integerlist/-1', the index is negative.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), logger.ErrorMessage); } @@ -508,8 +462,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -532,8 +485,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -557,8 +509,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // Assert Assert.Equal( - "For operation 'remove' on array property at path '/integerlist/3', the index is " + - "larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), logger.ErrorMessage); } @@ -577,7 +528,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), + exception.Message); } [Fact] @@ -598,7 +551,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // 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); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), + exception.Message); } [Fact] @@ -621,7 +576,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test // Assert - Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", logger.ErrorMessage); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), + logger.ErrorMessage); } [Fact] @@ -1123,8 +1080,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test patchDoc.ApplyTo(doc); }); Assert.Equal( - "For operation 'replace' on array property at path '/integerlist/3', the index is " + - "larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -1149,8 +1105,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test deserialized.ApplyTo(doc); }); Assert.Equal( - "For operation 'replace' on array property at path '/integerlist/3', the index is " + - "larger than the array size.", + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"), exception.Message); } @@ -1172,7 +1127,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test { patchDoc.ApplyTo(doc); }); - Assert.Equal("For operation 'replace' on array property at path '/integerlist/-1', the index is negative.", exception.Message); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), + exception.Message); } [Fact] @@ -1196,7 +1153,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test { deserialized.ApplyTo(doc); }); - Assert.Equal("For operation 'replace' on array property at path '/integerlist/-1', the index is negative.", exception.Message); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"), + exception.Message); } [Fact] @@ -1733,5 +1692,577 @@ namespace Microsoft.AspNetCore.JsonPatch.Test Assert.Equal(0, doc.IntegerValue); Assert.Equal(new List() { 1, 2, 3, 5 }, doc.IntegerList); } + + private class Class6 + { + public IDictionary DictionaryOfStringToInteger { get; } = new Dictionary(); + } + + [Fact] + public void Add_WhenDictionary_ValueIsNonObject_Succeeds() + { + // Arrange + var model = new Class6(); + model.DictionaryOfStringToInteger["one"] = 1; + model.DictionaryOfStringToInteger["two"] = 2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("/DictionaryOfStringToInteger/three", 3); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(3, model.DictionaryOfStringToInteger.Count); + Assert.Equal(1, model.DictionaryOfStringToInteger["one"]); + Assert.Equal(2, model.DictionaryOfStringToInteger["two"]); + Assert.Equal(3, model.DictionaryOfStringToInteger["three"]); + } + + [Fact] + public void Remove_WhenDictionary_ValueIsNonObject_Succeeds() + { + // Arrange + var model = new Class6(); + model.DictionaryOfStringToInteger["one"] = 1; + model.DictionaryOfStringToInteger["two"] = 2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("/DictionaryOfStringToInteger/two"); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(1, model.DictionaryOfStringToInteger.Count); + Assert.Equal(1, model.DictionaryOfStringToInteger["one"]); + } + + [Fact] + public void Replace_WhenDictionary_ValueIsNonObject_Succeeds() + { + // Arrange + var model = new Class6(); + model.DictionaryOfStringToInteger["one"] = 1; + model.DictionaryOfStringToInteger["two"] = 2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace("/DictionaryOfStringToInteger/two", 20); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(2, model.DictionaryOfStringToInteger.Count); + Assert.Equal(1, model.DictionaryOfStringToInteger["one"]); + Assert.Equal(20, model.DictionaryOfStringToInteger["two"]); + } + + private class Customer + { + public string Name { get; set; } + public Address Address { get; set; } + } + + private class Address + { + public string City { get; set; } + } + + private class Class8 + { + public IDictionary DictionaryOfStringToCustomer { get; } = new Dictionary(); + } + + [Fact] + public void Replace_WhenDictionary_ValueAPocoType_Succeeds() + { + // Arrange + var key1 = "key1"; + var value1 = new Customer() { Name = "Jamesss" }; + var key2 = "key2"; + var value2 = new Customer() { Name = "Mike" }; + var model = new Class8(); + model.DictionaryOfStringToCustomer[key1] = value1; + model.DictionaryOfStringToCustomer[key2] = value2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Name", "James"); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(2, model.DictionaryOfStringToCustomer.Count); + var actualValue1 = model.DictionaryOfStringToCustomer[key1]; + Assert.NotNull(actualValue1); + Assert.Equal("James", actualValue1.Name); + } + + [Fact] + public void Replace_WhenDictionary_ValueAPocoType_Succeeds_WithSerialization() + { + // Arrange + var key1 = "key1"; + var value1 = new Customer() { Name = "Jamesss" }; + var key2 = "key2"; + var value2 = new Customer() { Name = "Mike" }; + var model = new Class8(); + model.DictionaryOfStringToCustomer[key1] = value1; + model.DictionaryOfStringToCustomer[key2] = value2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Name", "James"); + var serialized = JsonConvert.SerializeObject(patchDocument); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(2, model.DictionaryOfStringToCustomer.Count); + var actualValue1 = model.DictionaryOfStringToCustomer[key1]; + Assert.NotNull(actualValue1); + Assert.Equal("James", actualValue1.Name); + } + + [Fact] + public void Replace_DeepNested_DictionaryValue_Succeeds() + { + // Arrange + var key1 = "key1"; + var value1 = new Customer() { Name = "Jamesss" }; + var key2 = "key2"; + var value2 = new Customer() { Name = "Mike" }; + var model = new Class8(); + model.DictionaryOfStringToCustomer[key1] = value1; + model.DictionaryOfStringToCustomer[key2] = value2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Name", "James"); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(2, model.DictionaryOfStringToCustomer.Count); + var actualValue1 = model.DictionaryOfStringToCustomer[key1]; + Assert.NotNull(actualValue1); + Assert.Equal("James", actualValue1.Name); + } + + [Fact] + public void Replace_DeepNested_DictionaryValue_Succeeds_WithSerialization() + { + // Arrange + var key1 = "key1"; + var value1 = new Customer() { Name = "James", Address = new Address { City = "Redmond" } }; + var key2 = "key2"; + var value2 = new Customer() { Name = "Mike", Address = new Address { City = "Seattle" } }; + var model = new Class8(); + model.DictionaryOfStringToCustomer[key1] = value1; + model.DictionaryOfStringToCustomer[key2] = value2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Address/City", "Bellevue"); + var serialized = JsonConvert.SerializeObject(patchDocument); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(2, model.DictionaryOfStringToCustomer.Count); + var actualValue1 = model.DictionaryOfStringToCustomer[key1]; + Assert.NotNull(actualValue1); + Assert.Equal("James", actualValue1.Name); + var address = actualValue1.Address; + Assert.NotNull(address); + Assert.Equal("Bellevue", address.City); + } + + class Class9 + { + public List StringList { get; set; } = new List(); + } + + [Fact] + public void AddToNonIntegerListAtEnd() + { + // Arrange + var model = new Class9() + { + StringList = new List() + }; + model.StringList.Add("string1"); + model.StringList.Add("string2"); + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/StringList/0", "string3"); + + // Act + patchDoc.ApplyTo(model); + + // Assert + Assert.Equal(new List() { "string3", "string1", "string2" }, model.StringList); + } + + [Fact] + public void AddMember_OnPOCO_WithNullPropertyValue_ShouldAddPropertyValue() + { + // Arrange + var doc = new SimpleDTO() + { + StringProperty = null + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.StringProperty, "B"); + + // Act + patchDoc.ApplyTo(doc); + + // Assert + Assert.Equal("B", doc.StringProperty); + } + + private class Class1 + { + public IDictionary USStates { get; set; } = new Dictionary(); + } + + [Fact] + public void AddMember_OnDictionaryProperty_ShouldAddKeyValueMember() + { + // Arrange + var expected = "Washington"; + var model = new Class1(); + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/USStates/WA", expected); + + // Act + patchDoc.ApplyTo(model); + + // Assert + Assert.Equal(1, model.USStates.Count); + Assert.Equal(expected, model.USStates["WA"]); + } + + [Fact] + public void AddMember_OnDictionaryProperty_ShouldAddKeyValueMember_WithSerialization() + { + // Arrange + var expected = "Washington"; + var model = new Class1(); + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/USStates/WA", expected); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(model); + + // Assert + Assert.Equal(1, model.USStates.Count); + Assert.Equal(expected, model.USStates["WA"]); + } + + private class Class2 + { + public Class1 Class1Property { get; set; } = new Class1(); + } + + [Fact] + public void AddMember_OnDictionaryPropertyDeeplyNested_ShouldAddKeyValueMember() + { + // Arrange + var expected = "Washington"; + var model = new Class2(); + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/Class1Property/USStates/WA", expected); + + // Act + patchDoc.ApplyTo(model); + + // Assert + Assert.Equal(1, model.Class1Property.USStates.Count); + Assert.Equal(expected, model.Class1Property.USStates["WA"]); + } + + [Fact] + public void AddMember_OnDictionaryPropertyDeeplyNested_ShouldAddKeyValueMember_WithSerialization() + { + // Arrange + var expected = "Washington"; + var model = new Class2(); + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/Class1Property/USStates/WA", expected); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(model); + + // Assert + Assert.Equal(1, model.Class1Property.USStates.Count); + Assert.Equal(expected, model.Class1Property.USStates["WA"]); + } + + [Fact] + public void AddMember_OnDictionaryObjectDirectly_ShouldAddKeyValueMember() + { + // Arrange + var expected = "Washington"; + var model = new Dictionary(); + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/WA", expected); + + // Act + patchDoc.ApplyTo(model); + + // Assert + Assert.Equal(1, model.Count); + Assert.Equal(expected, model["WA"]); + } + + [Fact] + public void AddMember_OnDictionaryObjectDirectly_ShouldAddKeyValueMember_WithSerialization() + { + // Arrange + var expected = "Washington"; + var model = new Dictionary(); + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/WA", expected); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>>(serialized); + + // Act + deserialized.ApplyTo(model); + + // Assert + Assert.Equal(1, model.Count); + Assert.Equal(expected, model["WA"]); + } + + [Fact] + public void AddElement_ToListDirectly_ShouldAppendValue() + { + // Arrange + var model = new List() { 1, 2, 3 }; + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/-", value: 4); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>>(serialized); + + // Act + deserialized.ApplyTo(model); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 4 }, model); + } + + [Fact] + public void AddElement_ToListDirectly_ShouldAppendValue_WithSerialization() + { + // Arrange + var model = new List() { 1, 2, 3 }; + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/-", value: 4); + + // Act + patchDoc.ApplyTo(model); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 4 }, model); + } + + [Fact] + public void AddElement_ToListDirectly_ShouldAddValue_AtSuppliedPosition() + { + // Arrange + var model = new List() { 1, 2, 3 }; + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/0", value: 4); + + // Act + patchDoc.ApplyTo(model); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, model); + } + + [Fact] + public void AddElement_ToListDirectly_ShouldAddValue_AtSuppliedPosition_WithSerialization() + { + // Arrange + var model = new List() { 1, 2, 3 }; + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/0", value: 4); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>>(serialized); + + // Act + deserialized.ApplyTo(model); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, model); + } + + class ListOnDictionary + { + public IDictionary> NamesAndBadgeIds { get; set; } = new Dictionary>(); + } + + [Fact] + public void AddElement_ToList_OnDictionary_ShouldAddValue_AtSuppliedPosition() + { + // Arrange + var model = new ListOnDictionary(); + model.NamesAndBadgeIds["James"] = new List(); + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/NamesAndBadgeIds/James/-", 200); + + // Act + patchDoc.ApplyTo(model); + + // Assert + var list = model.NamesAndBadgeIds["James"]; + Assert.NotNull(list); + Assert.Equal(new List() { 200 }, list); + } + + [Fact] + public void AddElement_ToList_OnPOCO_ShouldAddValue_AtSuppliedPosition() + { + // 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); + } + + class Class3 + { + public SimpleDTO SimpleDTOProperty { get; set; } = new SimpleDTO(); + } + + [Fact] + public void AddElement_ToDeeplyNestedListProperty_OnPOCO_ShouldAddValue_AtSuppliedPosition() + { + // Arrange + var model = new Class3() + { + SimpleDTOProperty = new SimpleDTO() + { + IntegerIList = new List() { 1, 2, 3 } + } + }; + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTOProperty.IntegerIList, value: 4, position: 0); + + // Act + patchDoc.ApplyTo(model); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, model.SimpleDTOProperty.IntegerIList); + } + + [Fact] + public void AddElement_ToDeeplyNestedListProperty_OnPOCO_ShouldAddValue_AtSuppliedPosition_WithSerialization() + { + // Arrange + var model = new Class3() + { + SimpleDTOProperty = new SimpleDTO() + { + IntegerIList = new List() { 1, 2, 3 } + } + }; + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTOProperty.IntegerIList, value: 4, position: 0); + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(model); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, model.SimpleDTOProperty.IntegerIList); + } + + class Class4 + { + public int IntegerProperty { get; set; } + } + + [Fact] + public void Remove_OnNonReferenceType_POCOProperty_ShouldSetDefaultValue() + { + // Arrange + var model = new Class4() + { + IntegerProperty = 10 + }; + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerProperty); + + // Act + patchDoc.ApplyTo(model); + + // Assert + Assert.Equal(0, model.IntegerProperty); + } + + [Fact] + public void Remove_OnNonReferenceType_POCOProperty_ShouldSetDefaultValue_WithSerialization() + { + // 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); + } + + class ClassWithPrivateProperties + { + public string Name { get; set; } + private int Age { get; set; } = 45; + } + + [Fact] + public void Add_OnPrivateProperties_FailesWithException() + { + // Arrange + var doc = new ClassWithPrivateProperties() + { + Name = "James" + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add("/Age", 30); + + // Act & Assert + var exception = Assert.Throws(() => patchDoc.ApplyTo(doc)); + Assert.Equal( + string.Format("The target location specified by path segment '{0}' was not found.", "Age"), + exception.Message); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs new file mode 100644 index 0000000000..a03b830e02 --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs @@ -0,0 +1,230 @@ +// 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.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class ObjectVisitorTest + { + private class Class1 + { + public string Name { get; set; } + public IList States { get; set; } = new List(); + public IDictionary Countries = new Dictionary(); + public dynamic Items { get; set; } = new ExpandoObject(); + } + + private class Class1Nested + { + public List Customers { get; set; } = new List(); + } + + public static IEnumerable ReturnsListAdapterData + { + get + { + var model = new Class1(); + yield return new object[] { model, "/States/-", model.States }; + yield return new object[] { model.States, "/-", model.States }; + + var nestedModel = new Class1Nested(); + nestedModel.Customers.Add(new Class1()); + yield return new object[] { nestedModel, "/Customers/0/States/-", nestedModel.Customers[0].States }; + yield return new object[] { nestedModel, "/Customers/0/States/0", nestedModel.Customers[0].States }; + yield return new object[] { nestedModel.Customers, "/0/States/-", nestedModel.Customers[0].States }; + yield return new object[] { nestedModel.Customers[0], "/States/-", nestedModel.Customers[0].States }; + } + } + + [Theory] + [MemberData(nameof(ReturnsListAdapterData))] + public void Visit_ValidPathToArray_ReturnsListAdapter(object targetObject, string path, object expectedTargetObject) + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver()); + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(expectedTargetObject, targetObject); + Assert.IsType(adapter); + } + + public static IEnumerable ReturnsDictionaryAdapterData + { + get + { + var model = new Class1(); + yield return new object[] { model, "/Countries/USA", model.Countries }; + yield return new object[] { model.Countries, "/USA", model.Countries }; + + var nestedModel = new Class1Nested(); + nestedModel.Customers.Add(new Class1()); + yield return new object[] { nestedModel, "/Customers/0/Countries/USA", nestedModel.Customers[0].Countries }; + yield return new object[] { nestedModel.Customers, "/0/Countries/USA", nestedModel.Customers[0].Countries }; + yield return new object[] { nestedModel.Customers[0], "/Countries/USA", nestedModel.Customers[0].Countries }; + } + } + + [Theory] + [MemberData(nameof(ReturnsDictionaryAdapterData))] + public void Visit_ValidPathToDictionary_ReturnsDictionaryAdapter(object targetObject, string path, object expectedTargetObject) + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver()); + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(expectedTargetObject, targetObject); + Assert.IsType(adapter); + } + + public static IEnumerable ReturnsExpandoAdapterData + { + get + { + var model = new Class1(); + yield return new object[] { model, "/Items/Name", model.Items }; + yield return new object[] { model.Items, "/Name", model.Items }; + + var nestedModel = new Class1Nested(); + nestedModel.Customers.Add(new Class1()); + yield return new object[] { nestedModel, "/Customers/0/Items/Name", nestedModel.Customers[0].Items }; + yield return new object[] { nestedModel.Customers, "/0/Items/Name", nestedModel.Customers[0].Items }; + yield return new object[] { nestedModel.Customers[0], "/Items/Name", nestedModel.Customers[0].Items }; + } + } + + [Theory] + [MemberData(nameof(ReturnsExpandoAdapterData))] + public void Visit_ValidPathToExpandoObject_ReturnsExpandoAdapter(object targetObject, string path, object expectedTargetObject) + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver()); + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(expectedTargetObject, targetObject); + Assert.IsType(adapter); + } + + public static IEnumerable ReturnsPocoAdapterData + { + get + { + var model = new Class1(); + yield return new object[] { model, "/Name", model }; + + var nestedModel = new Class1Nested(); + nestedModel.Customers.Add(new Class1()); + yield return new object[] { nestedModel, "/Customers/0/Name", nestedModel.Customers[0] }; + yield return new object[] { nestedModel.Customers, "/0/Name", nestedModel.Customers[0] }; + yield return new object[] { nestedModel.Customers[0], "/Name", nestedModel.Customers[0] }; + } + } + + [Theory] + [MemberData(nameof(ReturnsPocoAdapterData))] + public void Visit_ValidPath_ReturnsExpandoAdapter(object targetObject, string path, object expectedTargetObject) + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver()); + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(expectedTargetObject, targetObject); + Assert.IsType(adapter); + } + + [Theory] + [InlineData("0")] + [InlineData("-1")] + public void Visit_InvalidIndexToArray_Fails(string position) + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{position}/States/-"), new DefaultContractResolver()); + var automobileDepartment = new Class1Nested(); + object targetObject = automobileDepartment; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.False(visitStatus); + Assert.Equal( + string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", position), + message); + } + + [Theory] + [InlineData("-")] + [InlineData("foo")] + public void Visit_InvalidIndexFormatToArray_Fails(string position) + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{position}/States/-"), new DefaultContractResolver()); + var automobileDepartment = new Class1Nested(); + object targetObject = automobileDepartment; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.False(visitStatus); + Assert.Equal(string.Format( + "The path segment '{0}' is invalid for an array index.", position), + message); + } + + // The adapter takes care of the responsibility of validating the final segment + [Fact] + public void Visit_DoesNotValidate_FinalPathSegment() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath($"/NonExisting"), new DefaultContractResolver()); + var model = new Class1(); + object targetObject = model; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.IsType(adapter); + } + } +} diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs~RF1ad82e13.TMP b/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs~RF1ad82e13.TMP new file mode 100644 index 0000000000..694c33c633 --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs~RF1ad82e13.TMP @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class ObjectVisitorTest + { + private class Class1 + { + public IList States { get; set; } = new List(); + public IDictionary Countries = new Dictionary(); + } + + [Fact] + public void Visit_ValidPathToArray_ReturnsListAdapter() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath("/States/-"), new DefaultContractResolver()); + var model = new Class1(); + object targetObject = model; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(model.States, targetObject); + Assert.IsType(adapter); + } + + [Fact] + public void Visit_ValidPathToDictionary_ReturnsDictionaryAdapter() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath("/Countries/USA"), new DefaultContractResolver()); + var model = new Class1(); + object targetObject = model; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(model.Countries, targetObject); + Assert.IsType(adapter); + } + + private class AutomobileDepartment + { + public List Customers { get; set; } = new List(); + } + + [Fact] + public void Visit_ValidPathToArray_ReturnsListAdapter_ForDeepNestedPath() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath("/Customers/0/States/-"), new DefaultContractResolver()); + var customer = new Class1(); + var automobileDepartment = new AutomobileDepartment(); + automobileDepartment.Customers.Add(customer); + object targetObject = automobileDepartment; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(customer.States, targetObject); + Assert.IsType(adapter); + } + + [Fact] + public void Visit_InvalidPathToArray_Fails() + { + // Arrange + var invalidIndex = 2; + var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{invalidIndex}/States/-"), new DefaultContractResolver()); + var automobileDepartment = new AutomobileDepartment(); + object targetObject = automobileDepartment; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.False(visitStatus); + Assert.Equal(string.Format(ErrorMessageFormats.IndexOutOfBounds, invalidIndex), message); + } + + [Fact] + public void Visit_DoesNotValidate_FinalPathSegment() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath($"/NonExisting"), new DefaultContractResolver()); + var model = new Class1(); + object targetObject = model; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.False(visitStatus); + Assert.Equal(string.Format(ErrorMessageFormats.TargetLocationAtPathSegmentNotFound, "NonExisting"), message); + } + } +} diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs~RF1ae034c3.TMP b/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs~RF1ae034c3.TMP new file mode 100644 index 0000000000..88139198ac --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/ObjectVisitorTest.cs~RF1ae034c3.TMP @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class ObjectVisitorTest + { + private class Class1 + { + public IList States { get; set; } = new List(); + public IDictionary Countries = new Dictionary(); + } + + public static IEnumerable ReturnsListAdapterData + { + get + { + var model = new Class1(); + yield return new object[] { model, "/States/-", model.States }; + + yield return new object[] { model.States, "/-", model.States }; + } + } + + [Theory] + [MemberData(nameof(ReturnsListAdapterData))] + public void Visit_ValidPathToArray_ReturnsListAdapter(object targetObject, string path, object expectedTargetObject) + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver()); + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(expectedTargetObject, targetObject); + Assert.IsType(adapter); + } + + [Fact] + public void Visit_ValidPathToDictionary_ReturnsDictionaryAdapter() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath("/Countries/USA"), new DefaultContractResolver()); + var model = new Class1(); + object targetObject = model; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(model.Countries, targetObject); + Assert.IsType(adapter); + } + + private class AutomobileDepartment + { + public List Customers { get; set; } = new List(); + } + + [Fact] + public void Visit_ValidPathToArray_ReturnsListAdapter_ForDeepNestedPath() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath("/Customers/0/States/-"), new DefaultContractResolver()); + var customer = new Class1(); + var automobileDepartment = new AutomobileDepartment(); + automobileDepartment.Customers.Add(customer); + object targetObject = automobileDepartment; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(customer.States, targetObject); + Assert.IsType(adapter); + } + + [Fact] + public void Visit_InvalidPathToArray_Fails() + { + // Arrange + var invalidIndex = 2; + var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{invalidIndex}/States/-"), new DefaultContractResolver()); + var automobileDepartment = new AutomobileDepartment(); + object targetObject = automobileDepartment; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.False(visitStatus); + Assert.Equal(string.Format(ErrorMessageFormats.IndexOutOfBounds, invalidIndex), message); + } + + // The adapter takes care of the responsibility of validating the final segment + [Fact] + public void Visit_DoesNotValidate_FinalPathSegment() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath($"/NonExisting"), new DefaultContractResolver()); + var model = new Class1(); + object targetObject = model; + IAdapter adapter = null; + string message = null; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.IsType(adapter); + } + } +} diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTO.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTO.cs index 1206cd108f..6dc1f173de 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTO.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTO.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.JsonPatch.Test +namespace Microsoft.AspNetCore.JsonPatch { public class SimpleDTO { diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTOWithNestedDTO.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTOWithNestedDTO.cs index dbded242ee..879d820277 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTOWithNestedDTO.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/SimpleDTOWithNestedDTO.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.JsonPatch.Test +namespace Microsoft.AspNetCore.JsonPatch { public class SimpleDTOWithNestedDTO { diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/TestErrorLogger.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/TestErrorLogger.cs index 6db8a42684..2cd6a4453e 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/TestErrorLogger.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/TestErrorLogger.cs @@ -1,7 +1,7 @@ // 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.AspNetCore.JsonPatch.Test +namespace Microsoft.AspNetCore.JsonPatch { public class TestErrorLogger where T : class {