diff --git a/src/Features/JsonPatch/src/Adapters/IObjectAdapter.cs b/src/Features/JsonPatch/src/Adapters/IObjectAdapter.cs new file mode 100644 index 0000000000..e5206bfa0d --- /dev/null +++ b/src/Features/JsonPatch/src/Adapters/IObjectAdapter.cs @@ -0,0 +1,112 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.JsonPatch.Operations; + +namespace Microsoft.AspNetCore.JsonPatch.Adapters +{ + /// + /// Defines the operations that can be performed on a JSON patch document. + /// + public interface IObjectAdapter + { + /// + /// Using the "add" operation a new value is inserted into the root of the target + /// document, into the target array at the specified valid index, or to a target object at + /// the specified location. + /// + /// When adding to arrays, the specified index MUST NOT be greater than the number of elements in the array. + /// To append the value to the array, the index of "-" character is used (see [RFC6901]). + /// + /// When adding to an object, if an object member does not already exist, a new member is added to the object at the + /// specified location or if an object member does exist, that member's value is replaced. + /// + /// The operation object MUST contain a "value" member whose content + /// specifies the value to be added. + /// + /// For example: + /// + /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-4 + /// + /// The add operation. + /// Object to apply the operation to. + void Add(Operation operation, object objectToApplyTo); + + /// + /// Using the "copy" operation, a value is copied from a specified location to the + /// target location. + /// + /// The operation object MUST contain a "from" member, which references the location in the + /// target document to copy the value from. + /// + /// The "from" location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-7 + /// + /// The copy operation. + /// Object to apply the operation to. + void Copy(Operation operation, object objectToApplyTo); + + /// + /// Using the "move" operation the value at a specified location is removed and + /// added to the target location. + /// + /// The operation object MUST contain a "from" member, which references the location in the + /// target document to move the value from. + /// + /// The "from" location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } + /// + /// A location cannot be moved into one of its children. + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-6 + /// + /// The move operation. + /// Object to apply the operation to. + void Move(Operation operation, object objectToApplyTo); + + /// + /// Using the "remove" operation the value at the target location is removed. + /// + /// The target location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "remove", "path": "/a/b/c" } + /// + /// If removing an element from an array, any elements above the + /// specified index are shifted one position to the left. + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-6 + /// + /// The remove operation. + /// Object to apply the operation to. + void Remove(Operation operation, object objectToApplyTo); + + /// + /// Using the "replace" operation he value at the target location is replaced + /// with a new value. The operation object MUST contain a "value" member + /// which specifies the replacement value. + /// + /// The target location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "replace", "path": "/a/b/c", "value": 42 } + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-6 + /// + /// The replace operation. + /// Object to apply the operation to. + void Replace(Operation operation, object objectToApplyTo); + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Adapters/IObjectAdapterWithTest.cs b/src/Features/JsonPatch/src/Adapters/IObjectAdapterWithTest.cs new file mode 100644 index 0000000000..e1b4ce7950 --- /dev/null +++ b/src/Features/JsonPatch/src/Adapters/IObjectAdapterWithTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.JsonPatch.Operations; + +namespace Microsoft.AspNetCore.JsonPatch.Adapters +{ + /// + /// Defines the operations that can be performed on a JSON patch document, including "test". + /// + public interface IObjectAdapterWithTest : IObjectAdapter + { + /// + /// Using the "test" operation a value at the target location is compared for + /// equality to a specified value. + /// + /// The operation object MUST contain a "value" member that specifies + /// value to be compared to the target location's value. + /// + /// The target location MUST be equal to the "value" value for the + /// operation to be considered successful. + /// + /// For example: + /// { "op": "test", "path": "/a/b/c", "value": "foo" } + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-7 + /// + /// The test operation. + /// Object to apply the operation to. + void Test(Operation operation, object objectToApplyTo); + } +} diff --git a/src/Features/JsonPatch/src/Adapters/ObjectAdapter.cs b/src/Features/JsonPatch/src/Adapters/ObjectAdapter.cs new file mode 100644 index 0000000000..73095b52c2 --- /dev/null +++ b/src/Features/JsonPatch/src/Adapters/ObjectAdapter.cs @@ -0,0 +1,328 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.JsonPatch.Internal; +using Microsoft.AspNetCore.JsonPatch.Operations; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Adapters +{ + /// + public class ObjectAdapter : IObjectAdapterWithTest + { + /// + /// Initializes a new instance of . + /// + /// The . + /// The for logging . + public ObjectAdapter( + IContractResolver contractResolver, + Action logErrorAction) + { + ContractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver)); + LogErrorAction = logErrorAction; + } + + /// + /// Gets or sets the . + /// + public IContractResolver ContractResolver { get; } + + /// + /// Action for logging . + /// + public Action LogErrorAction { get; } + + public void Add(Operation operation, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + Add(operation.path, operation.value, objectToApplyTo, operation); + } + + /// + /// Add is used by various operations (eg: add, copy, ...), yet through different operations; + /// This method allows code reuse yet reporting the correct operation on error + /// + private void Add( + string path, + object value, + object objectToApplyTo, + Operation operation) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + var parsedPath = new ParsedPath(path); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); + + var target = objectToApplyTo; + if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) + { + var error = CreatePathNotFoundError(objectToApplyTo, path, operation, errorMessage); + ErrorReporter(error); + return; + } + + if (!adapter.TryAdd(target, parsedPath.LastSegment, ContractResolver, value, out errorMessage)) + { + var error = CreateOperationFailedError(objectToApplyTo, path, operation, errorMessage); + ErrorReporter(error); + return; + } + } + + public void Move(Operation operation, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + // Get value at 'from' location and add that value to the 'path' location + if (TryGetValue(operation.from, objectToApplyTo, operation, out var propertyValue)) + { + // remove that value + Remove(operation.from, objectToApplyTo, operation); + + // add that value to the path location + Add(operation.path, + propertyValue, + objectToApplyTo, + operation); + } + } + + public void Remove(Operation operation, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + Remove(operation.path, objectToApplyTo, operation); + } + + /// + /// Remove is used by various operations (eg: remove, move, ...), yet through different operations; + /// This method allows code reuse yet reporting the correct operation on error. The return value + /// contains the type of the item that has been removed (and a bool possibly signifying an error) + /// This can be used by other methods, like replace, to ensure that we can pass in the correctly + /// typed value to whatever method follows. + /// + private void Remove(string path, object objectToApplyTo, Operation operationToReport) + { + var parsedPath = new ParsedPath(path); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); + + var target = objectToApplyTo; + if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) + { + var error = CreatePathNotFoundError(objectToApplyTo, path, operationToReport, errorMessage); + ErrorReporter(error); + return; + } + + if (!adapter.TryRemove(target, parsedPath.LastSegment, ContractResolver, out errorMessage)) + { + var error = CreateOperationFailedError(objectToApplyTo, path, operationToReport, errorMessage); + ErrorReporter(error); + return; + } + } + + public void Replace(Operation operation, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + var parsedPath = new ParsedPath(operation.path); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); + + var target = objectToApplyTo; + if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) + { + var error = CreatePathNotFoundError(objectToApplyTo, operation.path, operation, errorMessage); + ErrorReporter(error); + return; + } + + if (!adapter.TryReplace(target, parsedPath.LastSegment, ContractResolver, operation.value, out errorMessage)) + { + var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, errorMessage); + ErrorReporter(error); + return; + } + } + + public void Copy(Operation operation, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + // Get value at 'from' location and add that value to the 'path' location + if (TryGetValue(operation.from, objectToApplyTo, operation, out var propertyValue)) + { + // Create deep copy + var copyResult = ConversionResultProvider.CopyTo(propertyValue, propertyValue.GetType()); + if (copyResult.CanBeConverted) + { + Add(operation.path, + copyResult.ConvertedInstance, + objectToApplyTo, + operation); + } + else + { + var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, Resources.FormatCannotCopyProperty(operation.from)); + ErrorReporter(error); + return; + } + } + } + + public void Test(Operation operation, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + var parsedPath = new ParsedPath(operation.path); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); + + var target = objectToApplyTo; + if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) + { + var error = CreatePathNotFoundError(objectToApplyTo, operation.path, operation, errorMessage); + ErrorReporter(error); + return; + } + + if (!adapter.TryTest(target, parsedPath.LastSegment, ContractResolver, operation.value, out errorMessage)) + { + var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, errorMessage); + ErrorReporter(error); + return; + } + } + + private bool TryGetValue( + string fromLocation, + object objectToGetValueFrom, + Operation operation, + out object propertyValue) + { + if (fromLocation == null) + { + throw new ArgumentNullException(nameof(fromLocation)); + } + + if (objectToGetValueFrom == null) + { + throw new ArgumentNullException(nameof(objectToGetValueFrom)); + } + + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + propertyValue = null; + + var parsedPath = new ParsedPath(fromLocation); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); + + var target = objectToGetValueFrom; + if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) + { + var error = CreatePathNotFoundError(objectToGetValueFrom, fromLocation, operation, errorMessage); + ErrorReporter(error); + return false; + } + + if (!adapter.TryGet(target, parsedPath.LastSegment, ContractResolver, out propertyValue, out errorMessage)) + { + var error = CreateOperationFailedError(objectToGetValueFrom, fromLocation, operation, errorMessage); + ErrorReporter(error); + return false; + } + + return true; + } + + private Action ErrorReporter + { + get + { + return LogErrorAction ?? Internal.ErrorReporter.Default; + } + } + + private JsonPatchError CreateOperationFailedError(object target, string path, Operation operation, string errorMessage) + { + return new JsonPatchError( + target, + operation, + errorMessage ?? Resources.FormatCannotPerformOperation(operation.op, path)); + } + + private JsonPatchError CreatePathNotFoundError(object target, string path, Operation operation, string errorMessage) + { + return new JsonPatchError( + target, + operation, + errorMessage ?? Resources.FormatTargetLocationNotFound(operation.op, path)); + } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Converters/JsonPatchDocumentConverter.cs b/src/Features/JsonPatch/src/Converters/JsonPatchDocumentConverter.cs new file mode 100644 index 0000000000..aed9c48474 --- /dev/null +++ b/src/Features/JsonPatch/src/Converters/JsonPatchDocumentConverter.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Microsoft.AspNetCore.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Converters +{ + public class JsonPatchDocumentConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return true; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + if (objectType != typeof(JsonPatchDocument)) + { + throw new ArgumentException(Resources.FormatParameterMustMatchType("objectType", "JsonPatchDocument"), "objectType"); + } + + try + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + // load jObject + var jObject = JArray.Load(reader); + + // Create target object for Json => list of operations + var targetOperations = new List(); + + // Create a new reader for this jObject, and set all properties + // to match the original reader. + var jObjectReader = jObject.CreateReader(); + jObjectReader.Culture = reader.Culture; + jObjectReader.DateParseHandling = reader.DateParseHandling; + jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling; + jObjectReader.FloatParseHandling = reader.FloatParseHandling; + + // Populate the object properties + serializer.Populate(jObjectReader, targetOperations); + + // container target: the JsonPatchDocument. + var container = new JsonPatchDocument(targetOperations, new DefaultContractResolver()); + + return container; + } + catch (Exception ex) + { + throw new JsonSerializationException(Resources.InvalidJsonPatchDocument, ex); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is IJsonPatchDocument) + { + var jsonPatchDoc = (IJsonPatchDocument)value; + var lst = jsonPatchDoc.GetOperations(); + + // write out the operations, no envelope + serializer.Serialize(writer, lst); + } + } + } +} diff --git a/src/Features/JsonPatch/src/Converters/TypedJsonPatchDocumentConverter.cs b/src/Features/JsonPatch/src/Converters/TypedJsonPatchDocumentConverter.cs new file mode 100644 index 0000000000..fd779ba4ee --- /dev/null +++ b/src/Features/JsonPatch/src/Converters/TypedJsonPatchDocumentConverter.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNetCore.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch.Converters +{ + public class TypedJsonPatchDocumentConverter : JsonPatchDocumentConverter + { + public override object ReadJson( + JsonReader reader, + Type objectType, + object existingValue, + JsonSerializer serializer) + { + try + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + var genericType = objectType.GetTypeInfo().GenericTypeArguments[0]; + + // load jObject + var jObject = JArray.Load(reader); + + // Create target object for Json => list of operations, typed to genericType + var genericOperation = typeof(Operation<>); + var concreteOperationType = genericOperation.MakeGenericType(genericType); + + var genericList = typeof(List<>); + var concreteList = genericList.MakeGenericType(concreteOperationType); + + var targetOperations = Activator.CreateInstance(concreteList); + + //Create a new reader for this jObject, and set all properties to match the original reader. + var jObjectReader = jObject.CreateReader(); + jObjectReader.Culture = reader.Culture; + jObjectReader.DateParseHandling = reader.DateParseHandling; + jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling; + jObjectReader.FloatParseHandling = reader.FloatParseHandling; + + // Populate the object properties + serializer.Populate(jObjectReader, targetOperations); + + // container target: the typed JsonPatchDocument. + var container = Activator.CreateInstance(objectType, targetOperations, new DefaultContractResolver()); + + return container; + } + catch (Exception ex) + { + throw new JsonSerializationException(Resources.InvalidJsonPatchDocument, ex); + } + } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Exceptions/JsonPatchException.cs b/src/Features/JsonPatch/src/Exceptions/JsonPatchException.cs new file mode 100644 index 0000000000..90e080575a --- /dev/null +++ b/src/Features/JsonPatch/src/Exceptions/JsonPatchException.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.JsonPatch.Operations; + +namespace Microsoft.AspNetCore.JsonPatch.Exceptions +{ + public class JsonPatchException : Exception + { + public Operation FailedOperation { get; private set; } + public object AffectedObject { get; private set; } + + + public JsonPatchException() + { + + } + + public JsonPatchException(JsonPatchError jsonPatchError, Exception innerException) + : base(jsonPatchError.ErrorMessage, innerException) + { + FailedOperation = jsonPatchError.Operation; + AffectedObject = jsonPatchError.AffectedObject; + } + + public JsonPatchException(JsonPatchError jsonPatchError) + : this(jsonPatchError, null) + { + } + + public JsonPatchException(string message, Exception innerException) + : base (message, innerException) + { + + } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Helpers/GetValueResult.cs b/src/Features/JsonPatch/src/Helpers/GetValueResult.cs new file mode 100644 index 0000000000..e2e739a027 --- /dev/null +++ b/src/Features/JsonPatch/src/Helpers/GetValueResult.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.JsonPatch.Helpers +{ + /// + /// Return value for the helper method used by Copy/Move. Needed to ensure we can make a different + /// decision in the calling method when the value is null because it cannot be fetched (HasError = true) + /// versus when it actually is null (much like why RemovedPropertyTypeResult is used for returning + /// type in the Remove operation). + /// + public class GetValueResult + { + public GetValueResult(object propertyValue, bool hasError) + { + PropertyValue = propertyValue; + HasError = hasError; + } + + /// + /// The value of the property we're trying to get + /// + public object PropertyValue { get; private set; } + + /// + /// HasError: true when an error occurred, the operation didn't complete succesfully + /// + public bool HasError { get; private set; } + } +} diff --git a/src/Features/JsonPatch/src/Helpers/JsonPatchProperty.cs b/src/Features/JsonPatch/src/Helpers/JsonPatchProperty.cs new file mode 100644 index 0000000000..041b0104ac --- /dev/null +++ b/src/Features/JsonPatch/src/Helpers/JsonPatchProperty.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch +{ + /// + /// Metadata for JsonProperty. + /// + public class JsonPatchProperty + { + /// + /// Initializes a new instance. + /// + public JsonPatchProperty(JsonProperty property, object parent) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + if (parent == null) + { + throw new ArgumentNullException(nameof(parent)); + } + + Property = property; + Parent = parent; + } + + /// + /// Gets or sets JsonProperty. + /// + public JsonProperty Property { get; set; } + + /// + /// Gets or sets Parent. + /// + public object Parent { get; set; } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/IJsonPatchDocument.cs b/src/Features/JsonPatch/src/IJsonPatchDocument.cs new file mode 100644 index 0000000000..fc5f5bd4d1 --- /dev/null +++ b/src/Features/JsonPatch/src/IJsonPatchDocument.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.JsonPatch.Operations; +using System.Collections.Generic; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public interface IJsonPatchDocument + { + IContractResolver ContractResolver { get; set; } + + IList GetOperations(); + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Internal/ConversionResult.cs b/src/Features/JsonPatch/src/Internal/ConversionResult.cs new file mode 100644 index 0000000000..77181eb18d --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/ConversionResult.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class ConversionResult + { + 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/Features/JsonPatch/src/Internal/ConversionResultProvider.cs b/src/Features/JsonPatch/src/Internal/ConversionResultProvider.cs new file mode 100644 index 0000000000..71af0d27fc --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/ConversionResultProvider.cs @@ -0,0 +1,75 @@ +// 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.Reflection; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public static class ConversionResultProvider + { + public static ConversionResult ConvertTo(object value, Type typeToConvertTo) + { + if (value == null) + { + return new ConversionResult(IsNullableType(typeToConvertTo), null); + } + else if (typeToConvertTo.IsAssignableFrom(value.GetType())) + { + // No need to convert + return new ConversionResult(true, value); + } + else + { + try + { + var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), typeToConvertTo); + return new ConversionResult(true, deserialized); + } + catch + { + return new ConversionResult(canBeConverted: false, convertedInstance: null); + } + } + } + + public static ConversionResult CopyTo(object value, Type typeToConvertTo) + { + var targetType = typeToConvertTo; + if (value == null) + { + return new ConversionResult(IsNullableType(typeToConvertTo), null); + } + else if (typeToConvertTo.IsAssignableFrom(value.GetType())) + { + // Keep original type + targetType = value.GetType(); + } + try + { + var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), targetType); + return new ConversionResult(true, deserialized); + } + catch + { + return new ConversionResult(canBeConverted: false, convertedInstance: null); + } + } + + private static bool IsNullableType(Type type) + { + var typeInfo = type.GetTypeInfo(); + if (typeInfo.IsValueType) + { + // value types are only nullable if they are Nullable + return typeInfo.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + else + { + // reference types are always nullable + return true; + } + } + } +} diff --git a/src/Features/JsonPatch/src/Internal/DictionaryAdapterOfTU.cs b/src/Features/JsonPatch/src/Internal/DictionaryAdapterOfTU.cs new file mode 100644 index 0000000000..8a344e24ee --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/DictionaryAdapterOfTU.cs @@ -0,0 +1,245 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +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 contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType()); + var key = contract.DictionaryKeyResolver(segment); + var dictionary = (IDictionary)target; + + // As per JsonPatch spec, if a key already exists, adding should replace the existing value + if (!TryConvertKey(key, out var convertedKey, out errorMessage)) + { + return false; + } + + if (!TryConvertValue(value, out var convertedValue, out errorMessage)) + { + return false; + } + + dictionary[convertedKey] = convertedValue; + errorMessage = null; + return true; + } + + public bool TryGet( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage) + { + var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType()); + var key = contract.DictionaryKeyResolver(segment); + var dictionary = (IDictionary)target; + + if (!TryConvertKey(key, out var convertedKey, out errorMessage)) + { + value = null; + return false; + } + + if (!dictionary.ContainsKey(convertedKey)) + { + value = null; + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + value = dictionary[convertedKey]; + errorMessage = null; + return true; + } + + public bool TryRemove( + object target, + string segment, + IContractResolver contractResolver, + out string errorMessage) + { + var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType()); + var key = contract.DictionaryKeyResolver(segment); + var dictionary = (IDictionary)target; + + if (!TryConvertKey(key, out var convertedKey, out errorMessage)) + { + return false; + } + + // As per JsonPatch spec, the target location must exist for remove to be successful + if (!dictionary.ContainsKey(convertedKey)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + dictionary.Remove(convertedKey); + + errorMessage = null; + return true; + } + + public bool TryReplace( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType()); + var key = contract.DictionaryKeyResolver(segment); + var dictionary = (IDictionary)target; + + if (!TryConvertKey(key, out var convertedKey, out errorMessage)) + { + return false; + } + + // As per JsonPatch spec, the target location must exist for remove to be successful + if (!dictionary.ContainsKey(convertedKey)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!TryConvertValue(value, out var convertedValue, out errorMessage)) + { + return false; + } + + dictionary[convertedKey] = convertedValue; + + errorMessage = null; + return true; + } + + public bool TryTest( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType()); + var key = contract.DictionaryKeyResolver(segment); + var dictionary = (IDictionary)target; + + if (!TryConvertKey(key, out var convertedKey, out errorMessage)) + { + return false; + } + + // As per JsonPatch spec, the target location must exist for test to be successful + if (!dictionary.ContainsKey(convertedKey)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!TryConvertValue(value, out var convertedValue, out errorMessage)) + { + return false; + } + + var currentValue = dictionary[convertedKey]; + + // The target segment does not have an assigned value to compare the test value with + if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString())) + { + errorMessage = Resources.FormatValueForTargetSegmentCannotBeNullOrEmpty(segment); + return false; + } + + if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(convertedValue))) + { + errorMessage = Resources.FormatValueNotEqualToTestValue(currentValue, value, segment); + return false; + } + else + { + errorMessage = null; + return true; + } + } + + public bool TryTraverse( + object target, + string segment, + IContractResolver contractResolver, + out object nextTarget, + out string errorMessage) + { + var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType()); + var key = contract.DictionaryKeyResolver(segment); + var dictionary = (IDictionary)target; + + if (!TryConvertKey(key, out var convertedKey, out errorMessage)) + { + nextTarget = null; + return false; + } + + if (dictionary.ContainsKey(convertedKey)) + { + nextTarget = dictionary[convertedKey]; + errorMessage = null; + return true; + } + else + { + nextTarget = null; + errorMessage = null; + return false; + } + } + + private bool TryConvertKey(string key, out TKey convertedKey, out string errorMessage) + { + var conversionResult = ConversionResultProvider.ConvertTo(key, typeof(TKey)); + if (conversionResult.CanBeConverted) + { + errorMessage = null; + convertedKey = (TKey)conversionResult.ConvertedInstance; + return true; + } + else + { + errorMessage = Resources.FormatInvalidPathSegment(key); + convertedKey = default(TKey); + return false; + } + } + + private bool TryConvertValue(object value, out TValue convertedValue, out string errorMessage) + { + var conversionResult = ConversionResultProvider.ConvertTo(value, typeof(TValue)); + if (conversionResult.CanBeConverted) + { + errorMessage = null; + convertedValue = (TValue)conversionResult.ConvertedInstance; + return true; + } + else + { + errorMessage = Resources.FormatInvalidValueForProperty(value); + convertedValue = default(TValue); + return false; + } + } + } +} diff --git a/src/Features/JsonPatch/src/Internal/DynamicObjectAdapter.cs b/src/Features/JsonPatch/src/Internal/DynamicObjectAdapter.cs new file mode 100644 index 0000000000..fb4adeb1f2 --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/DynamicObjectAdapter.cs @@ -0,0 +1,243 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.CSharp.RuntimeBinder; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using CSharpBinder = Microsoft.CSharp.RuntimeBinder; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class DynamicObjectAdapter : IAdapter + { + public bool TryAdd( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + if (!TrySetDynamicObjectProperty(target, contractResolver, segment, value, out errorMessage)) + { + return false; + } + + errorMessage = null; + return true; + } + + public bool TryGet( + object target, + string segment, + IContractResolver contractResolver, + out object value, + out string errorMessage) + { + if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out value, out errorMessage)) + { + value = null; + return false; + } + + errorMessage = null; + return true; + } + + public bool TryRemove( + object target, + string segment, + IContractResolver contractResolver, + out string errorMessage) + { + if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out var property, out errorMessage)) + { + 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 (property.GetType().GetTypeInfo().IsValueType + && Nullable.GetUnderlyingType(property.GetType()) == null) + { + value = Activator.CreateInstance(property.GetType()); + } + + if (!TrySetDynamicObjectProperty(target, contractResolver, segment, value, out errorMessage)) + { + return false; + } + + errorMessage = null; + return true; + + } + + public bool TryReplace( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out var property, out errorMessage)) + { + return false; + } + + if (!TryConvertValue(value, property.GetType(), out var convertedValue)) + { + errorMessage = Resources.FormatInvalidValueForProperty(value); + return false; + } + + if (!TrySetDynamicObjectProperty(target, contractResolver, segment, convertedValue, out errorMessage)) + { + return false; + } + + errorMessage = null; + return true; + } + + public bool TryTest( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out var property, out errorMessage)) + { + return false; + } + + if (!TryConvertValue(value, property.GetType(), out var convertedValue)) + { + errorMessage = Resources.FormatInvalidValueForProperty(value); + return false; + } + + if (!JToken.DeepEquals(JsonConvert.SerializeObject(property), JsonConvert.SerializeObject(convertedValue))) + { + errorMessage = Resources.FormatValueNotEqualToTestValue(property, value, segment); + return false; + } + else + { + errorMessage = null; + return true; + } + } + + public bool TryTraverse( + object target, + string segment, + IContractResolver contractResolver, + out object nextTarget, + out string errorMessage) + { + if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out var property, out errorMessage)) + { + nextTarget = null; + return false; + } + else + { + nextTarget = property; + errorMessage = null; + return true; + } + } + + private bool TryGetDynamicObjectProperty( + object target, + IContractResolver contractResolver, + string segment, + out object value, + out string errorMessage) + { + var jsonDynamicContract = (JsonDynamicContract)contractResolver.ResolveContract(target.GetType()); + + var propertyName = jsonDynamicContract.PropertyNameResolver(segment); + + var binder = CSharpBinder.Binder.GetMember( + CSharpBinderFlags.None, + propertyName, + target.GetType(), + new List + { + CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) + }); + + var callsite = CallSite>.Create(binder); + + try + { + value = callsite.Target(callsite, target); + errorMessage = null; + return true; + } + catch (RuntimeBinderException) + { + value = null; + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + } + + private bool TrySetDynamicObjectProperty( + object target, + IContractResolver contractResolver, + string segment, + object value, + out string errorMessage) + { + var jsonDynamicContract = (JsonDynamicContract)contractResolver.ResolveContract(target.GetType()); + + var propertyName = jsonDynamicContract.PropertyNameResolver(segment); + + var binder = CSharpBinder.Binder.SetMember( + CSharpBinderFlags.None, + propertyName, + target.GetType(), + new List + { + CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), + CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) + }); + + var callsite = CallSite>.Create(binder); + + try + { + callsite.Target(callsite, target, value); + errorMessage = null; + return true; + } + catch (RuntimeBinderException) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + 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/Features/JsonPatch/src/Internal/ErrorReporter.cs b/src/Features/JsonPatch/src/Internal/ErrorReporter.cs new file mode 100644 index 0000000000..76b55a6144 --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/ErrorReporter.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.JsonPatch.Exceptions; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + internal static class ErrorReporter + { + public static readonly Action Default = (error) => + { + throw new JsonPatchException(error); + }; + } +} diff --git a/src/Features/JsonPatch/src/Internal/IAdapter.cs b/src/Features/JsonPatch/src/Internal/IAdapter.cs new file mode 100644 index 0000000000..ec28131f7d --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/IAdapter.cs @@ -0,0 +1,51 @@ +// 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); + + bool TryTest( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage); + } +} diff --git a/src/Features/JsonPatch/src/Internal/ListAdapter.cs b/src/Features/JsonPatch/src/Internal/ListAdapter.cs new file mode 100644 index 0000000000..d1348fd5c6 --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/ListAdapter.cs @@ -0,0 +1,349 @@ +// 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; +using Newtonsoft.Json.Linq; +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; + + if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage)) + { + return false; + } + + if (!TryGetPositionInfo(list, segment, OperationType.Add, out var positionInfo, out errorMessage)) + { + return false; + } + + if (!TryConvertValue(value, typeArgument, segment, out var 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; + + if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage)) + { + value = null; + return false; + } + + if (!TryGetPositionInfo(list, segment, OperationType.Get, out var 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; + + if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage)) + { + return false; + } + + if (!TryGetPositionInfo(list, segment, OperationType.Remove, out var 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; + + if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage)) + { + return false; + } + + if (!TryGetPositionInfo(list, segment, OperationType.Replace, out var positionInfo, out errorMessage)) + { + return false; + } + + if (!TryConvertValue(value, typeArgument, segment, out var 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 TryTest( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var list = (IList)target; + + if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage)) + { + return false; + } + + if (!TryGetPositionInfo(list, segment, OperationType.Replace, out var positionInfo, out errorMessage)) + { + return false; + } + + if (!TryConvertValue(value, typeArgument, segment, out var convertedValue, out errorMessage)) + { + return false; + } + + var currentValue = list[positionInfo.Index]; + if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(convertedValue))) + { + errorMessage = Resources.FormatValueAtListPositionNotEqualToTestValue(currentValue, value, positionInfo.Index); + return false; + } + else + { + 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; + } + + var 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, + OperationType operationType, + out PositionInfo positionInfo, + out string errorMessage) + { + if (segment == "-") + { + positionInfo = new PositionInfo(PositionType.EndOfList, -1); + errorMessage = null; + return true; + } + + var position = -1; + if (int.TryParse(segment, out position)) + { + if (position >= 0 && position < list.Count) + { + positionInfo = new PositionInfo(PositionType.Index, position); + errorMessage = null; + return true; + } + // As per JSON Patch spec, for Add operation the index value representing the number of elements is valid, + // where as for other operations like Remove, Replace, Move and Copy the target index MUST exist. + else if (position == list.Count && operationType == OperationType.Add) + { + positionInfo = new PositionInfo(PositionType.EndOfList, -1); + errorMessage = null; + return true; + } + else + { + positionInfo = new PositionInfo(PositionType.OutOfBounds, position); + errorMessage = Resources.FormatIndexOutOfBounds(segment); + return false; + } + } + else + { + positionInfo = new PositionInfo(PositionType.Invalid, -1); + 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 + } + + private enum OperationType + { + Add, + Remove, + Get, + Replace + } + } +} diff --git a/src/Features/JsonPatch/src/Internal/ObjectVisitor.cs b/src/Features/JsonPatch/src/Internal/ObjectVisitor.cs new file mode 100644 index 0000000000..8994f0aa52 --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/ObjectVisitor.cs @@ -0,0 +1,72 @@ +// 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 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) + { + _path = path; + _contractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver)); + } + + public bool TryVisit(ref object target, out IAdapter adapter, out string errorMessage) + { + if (target == null) + { + adapter = null; + errorMessage = null; + return false; + } + + adapter = SelectAdapter(target); + + // Traverse until the penultimate segment to get the target object and adapter + for (var i = 0; i < _path.Segments.Count - 1; i++) + { + if (!adapter.TryTraverse(target, _path.Segments[i], _contractResolver, out var next, out errorMessage)) + { + adapter = null; + return false; + } + + target = next; + adapter = SelectAdapter(target); + } + + errorMessage = null; + return true; + } + + private IAdapter SelectAdapter(object targetObject) + { + var jsonContract = _contractResolver.ResolveContract(targetObject.GetType()); + + if (targetObject is IList) + { + return new ListAdapter(); + } + else if (jsonContract is JsonDictionaryContract jsonDictionaryContract) + { + var type = typeof(DictionaryAdapter<,>).MakeGenericType(jsonDictionaryContract.DictionaryKeyType, jsonDictionaryContract.DictionaryValueType); + return (IAdapter)Activator.CreateInstance(type); + } + else if (jsonContract is JsonDynamicContract) + { + return new DynamicObjectAdapter(); + } + else + { + return new PocoAdapter(); + } + } + } +} diff --git a/src/Features/JsonPatch/src/Internal/ParsedPath.cs b/src/Features/JsonPatch/src/Internal/ParsedPath.cs new file mode 100644 index 0000000000..8d0e69aa4d --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/ParsedPath.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using System; +using System.Collections.Generic; +using System.Text; + +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 = ParsePath(path); + } + + public string LastSegment + { + get + { + if (_segments == null || _segments.Length == 0) + { + return null; + } + + return _segments[_segments.Length - 1]; + } + } + + public IReadOnlyList Segments => _segments ?? Empty; + + private static string[] ParsePath(string path) + { + var strings = new List(); + var sb = new StringBuilder(path.Length); + + for (var i = 0; i < path.Length; i++) + { + if (path[i] == '/') + { + if (sb.Length > 0) + { + strings.Add(sb.ToString()); + sb.Length = 0; + } + } + else if (path[i] == '~') + { + ++i; + if (i >= path.Length) + { + throw new JsonPatchException(Resources.FormatInvalidValueForPath(path), null); + } + + if (path[i] == '0') + { + sb.Append('~'); + } + else if (path[i] == '1') + { + sb.Append('/'); + } + else + { + throw new JsonPatchException(Resources.FormatInvalidValueForPath(path), null); + } + } + else + { + sb.Append(path[i]); + } + } + + if (sb.Length > 0) + { + strings.Add(sb.ToString()); + } + + return strings.ToArray(); + } + } +} diff --git a/src/Features/JsonPatch/src/Internal/PathHelpers.cs b/src/Features/JsonPatch/src/Internal/PathHelpers.cs new file mode 100644 index 0000000000..f0afedb60e --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/PathHelpers.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using System; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + internal static class PathHelpers + { + internal static string ValidateAndNormalizePath(string path) + { + // check for most common path errors on create. This is not + // absolutely necessary, but it allows us to already catch mistakes + // on creation of the patch document rather than on execute. + + if (path.Contains("//")) + { + throw new JsonPatchException(Resources.FormatInvalidValueForPath(path), null); + } + + if (!path.StartsWith("/", StringComparison.Ordinal)) + { + return "/" + path; + } + else + { + return path; + } + } + } +} diff --git a/src/Features/JsonPatch/src/Internal/PocoAdapter.cs b/src/Features/JsonPatch/src/Internal/PocoAdapter.cs new file mode 100644 index 0000000000..0eee0fc889 --- /dev/null +++ b/src/Features/JsonPatch/src/Internal/PocoAdapter.cs @@ -0,0 +1,236 @@ +// 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; +using Newtonsoft.Json.Linq; +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) + { + if (!TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!jsonProperty.Writable) + { + errorMessage = Resources.FormatCannotUpdateProperty(segment); + return false; + } + + if (!TryConvertValue(value, jsonProperty.PropertyType, out var 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) + { + if (!TryGetJsonProperty(target, contractResolver, segment, out var 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) + { + if (!TryGetJsonProperty(target, contractResolver, segment, out var 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) + { + if (!TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!jsonProperty.Writable) + { + errorMessage = Resources.FormatCannotUpdateProperty(segment); + return false; + } + + if (!TryConvertValue(value, jsonProperty.PropertyType, out var convertedValue)) + { + errorMessage = Resources.FormatInvalidValueForProperty(value); + return false; + } + + jsonProperty.ValueProvider.SetValue(target, convertedValue); + + errorMessage = null; + return true; + } + + public bool TryTest( + object target, + string segment, + IContractResolver + contractResolver, + object value, + out string errorMessage) + { + if (!TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!jsonProperty.Readable) + { + errorMessage = Resources.FormatCannotReadProperty(segment); + return false; + } + + if (!TryConvertValue(value, jsonProperty.PropertyType, out var convertedValue)) + { + errorMessage = Resources.FormatInvalidValueForProperty(value); + return false; + } + + var currentValue = jsonProperty.ValueProvider.GetValue(target); + if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(convertedValue))) + { + errorMessage = Resources.FormatValueNotEqualToTestValue(currentValue, value, segment); + return false; + } + + 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; + } + + if (TryGetJsonProperty(target, contractResolver, segment, out var 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) + { + if (contractResolver.ResolveContract(target.GetType()) is JsonObjectContract jsonObjectContract) + { + 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/Features/JsonPatch/src/JsonPatchDocument.cs b/src/Features/JsonPatch/src/JsonPatchDocument.cs new file mode 100644 index 0000000000..b6539caae8 --- /dev/null +++ b/src/Features/JsonPatch/src/JsonPatchDocument.cs @@ -0,0 +1,256 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.JsonPatch.Adapters; +using Microsoft.AspNetCore.JsonPatch.Converters; +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Microsoft.AspNetCore.JsonPatch.Internal; +using Microsoft.AspNetCore.JsonPatch.Operations; +using Newtonsoft.Json; +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 + // .NET or architecture doesn't contain a shared DTO layer. + [JsonConverter(typeof(JsonPatchDocumentConverter))] + public class JsonPatchDocument : IJsonPatchDocument + { + public List Operations { get; private set; } + + [JsonIgnore] + public IContractResolver ContractResolver { get; set; } + + public JsonPatchDocument() + { + Operations = new List(); + ContractResolver = new DefaultContractResolver(); + } + + public JsonPatchDocument(List operations, IContractResolver contractResolver) + { + if (operations == null) + { + throw new ArgumentNullException(nameof(operations)); + } + + if (contractResolver == null) + { + throw new ArgumentNullException(nameof(contractResolver)); + } + + Operations = operations; + ContractResolver = contractResolver; + } + + /// + /// Add operation. Will result in, for example, + /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } + /// + /// target location + /// value + /// + public JsonPatchDocument Add(string path, object value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("add", PathHelpers.ValidateAndNormalizePath(path), null, value)); + return this; + } + + /// + /// Remove value at target location. Will result in, for example, + /// { "op": "remove", "path": "/a/b/c" } + /// + /// target location + /// + public JsonPatchDocument Remove(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("remove", PathHelpers.ValidateAndNormalizePath(path), null, null)); + return this; + } + + /// + /// Replace value. Will result in, for example, + /// { "op": "replace", "path": "/a/b/c", "value": 42 } + /// + /// target location + /// value + /// + public JsonPatchDocument Replace(string path, object value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("replace", PathHelpers.ValidateAndNormalizePath(path), null, value)); + return this; + } + + /// + /// Test value. Will result in, for example, + /// { "op": "test", "path": "/a/b/c", "value": 42 } + /// + /// target location + /// value + /// + public JsonPatchDocument Test(string path, object value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("test", PathHelpers.ValidateAndNormalizePath(path), null, value)); + return this; + } + + /// + /// Removes value at specified location and add it to the target location. Will result in, for example: + /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Move(string from, string path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("move", PathHelpers.ValidateAndNormalizePath(path), PathHelpers.ValidateAndNormalizePath(from))); + return this; + } + + /// + /// Copy the value at specified location to the target location. Will result in, for example: + /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Copy(string from, string path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("copy", PathHelpers.ValidateAndNormalizePath(path), PathHelpers.ValidateAndNormalizePath(from))); + return this; + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + public void ApplyTo(object objectToApplyTo) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null)); + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// Action to log errors + public void ApplyTo(object objectToApplyTo, Action logErrorAction) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + var adapter = new ObjectAdapter(ContractResolver, logErrorAction); + foreach (var op in Operations) + { + try + { + op.Apply(objectToApplyTo, adapter); + } + catch (JsonPatchException jsonPatchException) + { + var errorReporter = logErrorAction ?? ErrorReporter.Default; + errorReporter(new JsonPatchError(objectToApplyTo, op, jsonPatchException.Message)); + + // As per JSON Patch spec if an operation results in error, further operations should not be executed. + break; + } + } + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// IObjectAdapter instance to use when applying + public void ApplyTo(object objectToApplyTo, IObjectAdapter adapter) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + // apply each operation in order + foreach (var op in Operations) + { + op.Apply(objectToApplyTo, adapter); + } + } + + IList IJsonPatchDocument.GetOperations() + { + var allOps = new List(); + + if (Operations != null) + { + foreach (var op in Operations) + { + var untypedOp = new Operation(); + + untypedOp.op = op.op; + untypedOp.value = op.value; + untypedOp.path = op.path; + untypedOp.from = op.from; + + allOps.Add(untypedOp); + } + } + + return allOps; + } + } +} diff --git a/src/Features/JsonPatch/src/JsonPatchDocumentOfT.cs b/src/Features/JsonPatch/src/JsonPatchDocumentOfT.cs new file mode 100644 index 0000000000..8ae1430185 --- /dev/null +++ b/src/Features/JsonPatch/src/JsonPatchDocumentOfT.cs @@ -0,0 +1,869 @@ +// 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.Collections.Generic; +using System.Globalization; +using System.Linq.Expressions; +using Microsoft.AspNetCore.JsonPatch.Adapters; +using Microsoft.AspNetCore.JsonPatch.Converters; +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Microsoft.AspNetCore.JsonPatch.Internal; +using Microsoft.AspNetCore.JsonPatch.Operations; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNetCore.JsonPatch +{ + // Implementation details: the purpose of this type of patch document is to ensure we can do type-checking + // when producing a JsonPatchDocument. However, we cannot send this "typed" over the wire, as that would require + // including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's + // not according to RFC 6902, and would thus break cross-platform compatibility. + [JsonConverter(typeof(TypedJsonPatchDocumentConverter))] + public class JsonPatchDocument : IJsonPatchDocument where TModel : class + { + public List> Operations { get; private set; } + + [JsonIgnore] + public IContractResolver ContractResolver { get; set; } + + public JsonPatchDocument() + { + Operations = new List>(); + ContractResolver = new DefaultContractResolver(); + } + + // Create from list of operations + public JsonPatchDocument(List> operations, IContractResolver contractResolver) + { + Operations = operations ?? throw new ArgumentNullException(nameof(operations)); + ContractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver)); + } + + /// + /// Add operation. Will result in, for example, + /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } + /// + /// value type + /// target location + /// value + /// + public JsonPatchDocument Add(Expression> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "add", + GetPath(path, null), + from: null, + value: value)); + + return this; + } + + /// + /// Add value to list at given position + /// + /// value type + /// target location + /// value + /// position + /// + public JsonPatchDocument Add( + Expression>> path, + TProp value, + int position) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "add", + GetPath(path, position.ToString()), + from: null, + value: value)); + + return this; + } + + /// + /// Add value to the end of the list + /// + /// value type + /// target location + /// value + /// + public JsonPatchDocument Add(Expression>> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "add", + GetPath(path, "-"), + from: null, + value: value)); + + return this; + } + + /// + /// Remove value at target location. Will result in, for example, + /// { "op": "remove", "path": "/a/b/c" } + /// + /// target location + /// + public JsonPatchDocument Remove(Expression> path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("remove", GetPath(path, null), from: null)); + + return this; + } + + /// + /// Remove value from list at given position + /// + /// value type + /// target location + /// position + /// + public JsonPatchDocument Remove(Expression>> path, int position) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "remove", + GetPath(path, position.ToString()), + from: null)); + + return this; + } + + /// + /// Remove value from end of list + /// + /// value type + /// target location + /// + public JsonPatchDocument Remove(Expression>> path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "remove", + GetPath(path, "-"), + from: null)); + + return this; + } + + /// + /// Replace value. Will result in, for example, + /// { "op": "replace", "path": "/a/b/c", "value": 42 } + /// + /// target location + /// value + /// + public JsonPatchDocument Replace(Expression> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "replace", + GetPath(path, null), + from: null, + value: value)); + + return this; + } + + /// + /// Replace value in a list at given position + /// + /// value type + /// target location + /// value + /// position + /// + public JsonPatchDocument Replace(Expression>> path, + TProp value, int position) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "replace", + GetPath(path, position.ToString()), + from: null, + value: value)); + + return this; + } + + /// + /// Replace value at end of a list + /// + /// value type + /// target location + /// value + /// + public JsonPatchDocument Replace(Expression>> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "replace", + GetPath(path, "-"), + from: null, + value: value)); + + return this; + } + + /// + /// Test value. Will result in, for example, + /// { "op": "test", "path": "/a/b/c", "value": 42 } + /// + /// target location + /// value + /// + public JsonPatchDocument Test(Expression> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "test", + GetPath(path, null), + from: null, + value: value)); + + return this; + } + + /// + /// Test value in a list at given position + /// + /// value type + /// target location + /// value + /// position + /// + public JsonPatchDocument Test(Expression>> path, + TProp value, int position) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "test", + GetPath(path, position.ToString()), + from: null, + value: value)); + + return this; + } + + /// + /// Test value at end of a list + /// + /// value type + /// target location + /// value + /// + public JsonPatchDocument Test(Expression>> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "test", + GetPath(path, "-"), + from: null, + value: value)); + + return this; + } + + /// + /// Removes value at specified location and add it to the target location. Will result in, for example: + /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Move( + Expression> from, + Expression> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + GetPath(path, null), + GetPath(from, null))); + + return this; + } + + /// + /// Move from a position in a list to a new location + /// + /// + /// source location + /// position + /// target location + /// + public JsonPatchDocument Move( + Expression>> from, + int positionFrom, + Expression> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + GetPath(path, null), + GetPath(from, positionFrom.ToString()))); + + return this; + } + + /// + /// Move from a property to a location in a list + /// + /// + /// source location + /// target location + /// position + /// + public JsonPatchDocument Move( + Expression> from, + Expression>> path, + int positionTo) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + GetPath(path, positionTo.ToString()), + GetPath(from, null))); + + return this; + } + + /// + /// Move from a position in a list to another location in a list + /// + /// + /// source location + /// position (source) + /// target location + /// position (target) + /// + public JsonPatchDocument Move( + Expression>> from, + int positionFrom, + Expression>> path, + int positionTo) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + GetPath(path, positionTo.ToString()), + GetPath(from, positionFrom.ToString()))); + + return this; + } + + /// + /// Move from a position in a list to the end of another list + /// + /// + /// source location + /// position + /// target location + /// + public JsonPatchDocument Move( + Expression>> from, + int positionFrom, + Expression>> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + GetPath(path, "-"), + GetPath(from, positionFrom.ToString()))); + + return this; + } + + /// + /// Move to the end of a list + /// + /// + /// source location + /// target location + /// + public JsonPatchDocument Move( + Expression> from, + Expression>> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "move", + GetPath(path, "-"), + GetPath(from, null))); + + return this; + } + + /// + /// Copy the value at specified location to the target location. Willr esult in, for example: + /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } + /// + /// source location + /// target location + /// + public JsonPatchDocument Copy( + Expression> from, + Expression> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + GetPath(path, null), + GetPath(from, null))); + + return this; + } + + /// + /// Copy from a position in a list to a new location + /// + /// + /// source location + /// position + /// target location + /// + public JsonPatchDocument Copy( + Expression>> from, + int positionFrom, + Expression> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + GetPath(path, null), + GetPath(from, positionFrom.ToString()))); + + return this; + } + + /// + /// Copy from a property to a location in a list + /// + /// + /// source location + /// target location + /// position + /// + public JsonPatchDocument Copy( + Expression> from, + Expression>> path, + int positionTo) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + GetPath(path, positionTo.ToString()), + GetPath(from, null))); + + return this; + } + + /// + /// Copy from a position in a list to a new location in a list + /// + /// + /// source location + /// position (source) + /// target location + /// position (target) + /// + public JsonPatchDocument Copy( + Expression>> from, + int positionFrom, + Expression>> path, + int positionTo) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + GetPath(path, positionTo.ToString()), + GetPath(from, positionFrom.ToString()))); + + return this; + } + + /// + /// Copy from a position in a list to the end of another list + /// + /// + /// source location + /// position + /// target location + /// + public JsonPatchDocument Copy( + Expression>> from, + int positionFrom, + Expression>> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + GetPath(path, "-"), + GetPath(from, positionFrom.ToString()))); + + return this; + } + + /// + /// Copy to the end of a list + /// + /// + /// source location + /// target location + /// + public JsonPatchDocument Copy( + Expression> from, + Expression>> path) + { + if (from == null) + { + throw new ArgumentNullException(nameof(from)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "copy", + GetPath(path, "-"), + GetPath(from, null))); + + return this; + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + public void ApplyTo(TModel objectToApplyTo) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null)); + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// Action to log errors + public void ApplyTo(TModel objectToApplyTo, Action logErrorAction) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + var adapter = new ObjectAdapter(ContractResolver, logErrorAction); + foreach (var op in Operations) + { + try + { + op.Apply(objectToApplyTo, adapter); + } + catch (JsonPatchException jsonPatchException) + { + var errorReporter = logErrorAction ?? ErrorReporter.Default; + errorReporter(new JsonPatchError(objectToApplyTo, op, jsonPatchException.Message)); + + // As per JSON Patch spec if an operation results in error, further operations should not be executed. + break; + } + } + } + + /// + /// Apply this JsonPatchDocument + /// + /// Object to apply the JsonPatchDocument to + /// IObjectAdapter instance to use when applying + public void ApplyTo(TModel objectToApplyTo, IObjectAdapter adapter) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + // apply each operation in order + foreach (var op in Operations) + { + op.Apply(objectToApplyTo, adapter); + } + } + + IList IJsonPatchDocument.GetOperations() + { + var allOps = new List(); + + if (Operations != null) + { + foreach (var op in Operations) + { + var untypedOp = new Operation + { + op = op.op, + value = op.value, + path = op.path, + from = op.from + }; + + allOps.Add(untypedOp); + } + } + + return allOps; + } + + // Internal for testing + internal string GetPath(Expression> expr, string position) + { + var segments = GetPathSegments(expr.Body); + var path = String.Join("/", segments); + if (position != null) + { + path += "/" + position; + if (segments.Count == 0) + { + return path; + } + } + + return "/" + path; + } + + private List GetPathSegments(Expression expr) + { + var listOfSegments = new List(); + switch (expr.NodeType) + { + case ExpressionType.ArrayIndex: + var binaryExpression = (BinaryExpression)expr; + listOfSegments.AddRange(GetPathSegments(binaryExpression.Left)); + listOfSegments.Add(binaryExpression.Right.ToString()); + return listOfSegments; + + case ExpressionType.Call: + var methodCallExpression = (MethodCallExpression)expr; + listOfSegments.AddRange(GetPathSegments(methodCallExpression.Object)); + listOfSegments.Add(EvaluateExpression(methodCallExpression.Arguments[0])); + return listOfSegments; + + case ExpressionType.Convert: + listOfSegments.AddRange(GetPathSegments(((UnaryExpression)expr).Operand)); + return listOfSegments; + + case ExpressionType.MemberAccess: + var memberExpression = expr as MemberExpression; + listOfSegments.AddRange(GetPathSegments(memberExpression.Expression)); + // Get property name, respecting JsonProperty attribute + listOfSegments.Add(GetPropertyNameFromMemberExpression(memberExpression)); + return listOfSegments; + + case ExpressionType.Parameter: + // Fits "x => x" (the whole document which is "" as JSON pointer) + return listOfSegments; + + default: + throw new InvalidOperationException(Resources.FormatExpressionTypeNotSupported(expr)); + } + } + + private string GetPropertyNameFromMemberExpression(MemberExpression memberExpression) + { + var jsonObjectContract = ContractResolver.ResolveContract(memberExpression.Expression.Type) as JsonObjectContract; + if (jsonObjectContract != null) + { + return jsonObjectContract.Properties + .First(jsonProperty => jsonProperty.UnderlyingName == memberExpression.Member.Name) + .PropertyName; + } + + return null; + } + + private static bool ContinueWithSubPath(ExpressionType expressionType) + { + return (expressionType == ExpressionType.ArrayIndex + || expressionType == ExpressionType.Call + || expressionType == ExpressionType.Convert + || expressionType == ExpressionType.MemberAccess); + + } + + // Evaluates the value of the key or index which may be an int or a string, + // or some other expression type. + // The expression is converted to a delegate and the result of executing the delegate is returned as a string. + private static string EvaluateExpression(Expression expression) + { + var converted = Expression.Convert(expression, typeof(object)); + var fakeParameter = Expression.Parameter(typeof(object), null); + var lambda = Expression.Lambda>(converted, fakeParameter); + var func = lambda.Compile(); + + return Convert.ToString(func(null), CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/JsonPatchError.cs b/src/Features/JsonPatch/src/JsonPatchError.cs new file mode 100644 index 0000000000..a49af7a4e2 --- /dev/null +++ b/src/Features/JsonPatch/src/JsonPatchError.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.JsonPatch.Operations; + +namespace Microsoft.AspNetCore.JsonPatch +{ + /// + /// Captures error message and the related entity and the operation that caused it. + /// + public class JsonPatchError + { + /// + /// Initializes a new instance of . + /// + /// The object that is affected by the error. + /// The that caused the error. + /// The error message. + public JsonPatchError( + object affectedObject, + Operation operation, + string errorMessage) + { + if (errorMessage == null) + { + throw new ArgumentNullException(nameof(errorMessage)); + } + + AffectedObject = affectedObject; + Operation = operation; + ErrorMessage = errorMessage; + } + + /// + /// Gets the object that is affected by the error. + /// + public object AffectedObject { get; } + + /// + /// Gets the that caused the error. + /// + public Operation Operation { get; } + + /// + /// Gets the error message. + /// + public string ErrorMessage { get; } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj b/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj new file mode 100644 index 0000000000..3708e92927 --- /dev/null +++ b/src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj @@ -0,0 +1,17 @@ + + + + ASP.NET Core support for JSON PATCH. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;json;jsonpatch + + + + + + + + + diff --git a/src/Features/JsonPatch/src/Operations/Operation.cs b/src/Features/JsonPatch/src/Operations/Operation.cs new file mode 100644 index 0000000000..690ade4776 --- /dev/null +++ b/src/Features/JsonPatch/src/Operations/Operation.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.JsonPatch.Adapters; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.JsonPatch.Operations +{ + public class Operation : OperationBase + { + [JsonProperty("value")] + public object value { get; set; } + + public Operation() + { + + } + + public Operation(string op, string path, string from, object value) + : base(op, path, from) + { + this.value = value; + } + + public Operation(string op, string path, string from) + : base(op, path, from) + { + } + + public void Apply(object objectToApplyTo, IObjectAdapter adapter) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + switch (OperationType) + { + case OperationType.Add: + adapter.Add(this, objectToApplyTo); + break; + case OperationType.Remove: + adapter.Remove(this, objectToApplyTo); + break; + case OperationType.Replace: + adapter.Replace(this, objectToApplyTo); + break; + case OperationType.Move: + adapter.Move(this, objectToApplyTo); + break; + case OperationType.Copy: + adapter.Copy(this, objectToApplyTo); + break; + case OperationType.Test: + if (adapter is IObjectAdapterWithTest adapterWithTest) + { + adapterWithTest.Test(this, objectToApplyTo); + break; + } + else + { + throw new NotSupportedException(Resources.TestOperationNotSupported); + } + default: + break; + } + } + + public bool ShouldSerializevalue() + { + return (OperationType == OperationType.Add + || OperationType == OperationType.Replace + || OperationType == OperationType.Test); + } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Operations/OperationBase.cs b/src/Features/JsonPatch/src/Operations/OperationBase.cs new file mode 100644 index 0000000000..e629e2308d --- /dev/null +++ b/src/Features/JsonPatch/src/Operations/OperationBase.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 Newtonsoft.Json; + +namespace Microsoft.AspNetCore.JsonPatch.Operations +{ + public class OperationBase + { + private string _op; + private OperationType _operationType; + + [JsonIgnore] + public OperationType OperationType + { + get + { + return _operationType; + } + } + + [JsonProperty("path")] + public string path { get; set; } + + [JsonProperty("op")] + public string op + { + get + { + return _op; + } + set + { + OperationType result; + if (!Enum.TryParse(value, ignoreCase: true, result: out result)) + { + result = OperationType.Invalid; + } + _operationType = result; + _op = value; + } + } + + [JsonProperty("from")] + public string from { get; set; } + + public OperationBase() + { + + } + + public OperationBase(string op, string path, string from) + { + if (op == null) + { + throw new ArgumentNullException(nameof(op)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + this.op = op; + this.path = path; + this.from = from; + } + + public bool ShouldSerializefrom() + { + return (OperationType == OperationType.Move + || OperationType == OperationType.Copy); + } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Operations/OperationOfT.cs b/src/Features/JsonPatch/src/Operations/OperationOfT.cs new file mode 100644 index 0000000000..bd13528775 --- /dev/null +++ b/src/Features/JsonPatch/src/Operations/OperationOfT.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.JsonPatch.Adapters; +using Microsoft.AspNetCore.JsonPatch.Exceptions; + +namespace Microsoft.AspNetCore.JsonPatch.Operations +{ + public class Operation : Operation where TModel : class + { + public Operation() + { + + } + + public Operation(string op, string path, string from, object value) + : base(op, path, from) + { + if (op == null) + { + throw new ArgumentNullException(nameof(op)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + this.value = value; + } + + public Operation(string op, string path, string from) + : base(op, path, from) + { + if (op == null) + { + throw new ArgumentNullException(nameof(op)); + } + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + } + + public void Apply(TModel objectToApplyTo, IObjectAdapter adapter) + { + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + if (adapter == null) + { + throw new ArgumentNullException(nameof(adapter)); + } + + switch (OperationType) + { + case OperationType.Add: + adapter.Add(this, objectToApplyTo); + break; + case OperationType.Remove: + adapter.Remove(this, objectToApplyTo); + break; + case OperationType.Replace: + adapter.Replace(this, objectToApplyTo); + break; + case OperationType.Move: + adapter.Move(this, objectToApplyTo); + break; + case OperationType.Copy: + adapter.Copy(this, objectToApplyTo); + break; + case OperationType.Test: + if (adapter is IObjectAdapterWithTest adapterWithTest) + { + adapterWithTest.Test(this, objectToApplyTo); + break; + } + else + { + throw new JsonPatchException(new JsonPatchError(objectToApplyTo, this, Resources.TestOperationNotSupported)); + } + case OperationType.Invalid: + throw new JsonPatchException( + Resources.FormatInvalidJsonPatchOperation(op), innerException: null); + default: + break; + } + } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Operations/OperationType.cs b/src/Features/JsonPatch/src/Operations/OperationType.cs new file mode 100644 index 0000000000..725646df3a --- /dev/null +++ b/src/Features/JsonPatch/src/Operations/OperationType.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.JsonPatch.Operations +{ + public enum OperationType + { + Add, + Remove, + Replace, + Move, + Copy, + Test, + Invalid + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/src/Properties/AssemblyInfo.cs b/src/Features/JsonPatch/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..11fa956b64 --- /dev/null +++ b/src/Features/JsonPatch/src/Properties/AssemblyInfo.cs @@ -0,0 +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. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.JsonPatch.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Features/JsonPatch/src/Properties/Resources.Designer.cs b/src/Features/JsonPatch/src/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..c314465238 --- /dev/null +++ b/src/Features/JsonPatch/src/Properties/Resources.Designer.cs @@ -0,0 +1,338 @@ +// +namespace Microsoft.AspNetCore.JsonPatch +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.JsonPatch.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The property at '{0}' could not be copied. + /// + internal static string CannotCopyProperty + { + get => GetString("CannotCopyProperty"); + } + + /// + /// The property at '{0}' could not be copied. + /// + internal static string FormatCannotCopyProperty(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("CannotCopyProperty"), p0); + + /// + /// The type of the property at path '{0}' could not be determined. + /// + internal static string CannotDeterminePropertyType + { + get => GetString("CannotDeterminePropertyType"); + } + + /// + /// The type of the property at path '{0}' could not be determined. + /// + internal static string FormatCannotDeterminePropertyType(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("CannotDeterminePropertyType"), p0); + + /// + /// The '{0}' operation at path '{1}' could not be performed. + /// + internal static string CannotPerformOperation + { + get => GetString("CannotPerformOperation"); + } + + /// + /// The '{0}' operation at path '{1}' could not be performed. + /// + internal static string FormatCannotPerformOperation(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("CannotPerformOperation"), p0, p1); + + /// + /// The property at '{0}' could not be read. + /// + internal static string CannotReadProperty + { + get => GetString("CannotReadProperty"); + } + + /// + /// The property at '{0}' could not be read. + /// + internal static string FormatCannotReadProperty(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("CannotReadProperty"), p0); + + /// + /// The property at path '{0}' could not be updated. + /// + internal static string CannotUpdateProperty + { + get => GetString("CannotUpdateProperty"); + } + + /// + /// The property at path '{0}' could not be updated. + /// + internal static string FormatCannotUpdateProperty(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("CannotUpdateProperty"), p0); + + /// + /// The expression '{0}' is not supported. Supported expressions include member access and indexer expressions. + /// + internal static string ExpressionTypeNotSupported + { + get => GetString("ExpressionTypeNotSupported"); + } + + /// + /// The expression '{0}' is not supported. Supported expressions include member access and indexer expressions. + /// + internal static string FormatExpressionTypeNotSupported(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ExpressionTypeNotSupported"), p0); + + /// + /// The index value provided by path segment '{0}' is out of bounds of the array size. + /// + internal static string IndexOutOfBounds + { + get => GetString("IndexOutOfBounds"); + } + + /// + /// The index value provided by path segment '{0}' is out of bounds of the array size. + /// + internal static string FormatIndexOutOfBounds(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("IndexOutOfBounds"), p0); + + /// + /// The path segment '{0}' is invalid for an array index. + /// + internal static string InvalidIndexValue + { + get => GetString("InvalidIndexValue"); + } + + /// + /// The path segment '{0}' is invalid for an array index. + /// + internal static string FormatInvalidIndexValue(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("InvalidIndexValue"), p0); + + /// + /// The JSON patch document was malformed and could not be parsed. + /// + internal static string InvalidJsonPatchDocument + { + get => GetString("InvalidJsonPatchDocument"); + } + + /// + /// The JSON patch document was malformed and could not be parsed. + /// + internal static string FormatInvalidJsonPatchDocument() + => GetString("InvalidJsonPatchDocument"); + + /// + /// Invalid JsonPatch operation '{0}'. + /// + internal static string InvalidJsonPatchOperation + { + get => GetString("InvalidJsonPatchOperation"); + } + + /// + /// Invalid JsonPatch operation '{0}'. + /// + internal static string FormatInvalidJsonPatchOperation(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("InvalidJsonPatchOperation"), p0); + + /// + /// The provided path segment '{0}' cannot be converted to the target type. + /// + internal static string InvalidPathSegment + { + get => GetString("InvalidPathSegment"); + } + + /// + /// The provided path segment '{0}' cannot be converted to the target type. + /// + internal static string FormatInvalidPathSegment(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("InvalidPathSegment"), p0); + + /// + /// The provided string '{0}' is an invalid path. + /// + internal static string InvalidValueForPath + { + get => GetString("InvalidValueForPath"); + } + + /// + /// The provided string '{0}' is an invalid path. + /// + internal static string FormatInvalidValueForPath(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForPath"), p0); + + /// + /// The value '{0}' is invalid for target location. + /// + internal static string InvalidValueForProperty + { + get => GetString("InvalidValueForProperty"); + } + + /// + /// The value '{0}' is invalid for target location. + /// + internal static string FormatInvalidValueForProperty(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForProperty"), p0); + + /// + /// '{0}' must be of type '{1}'. + /// + internal static string ParameterMustMatchType + { + get => GetString("ParameterMustMatchType"); + } + + /// + /// '{0}' must be of type '{1}'. + /// + internal static string FormatParameterMustMatchType(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("ParameterMustMatchType"), p0, p1); + + /// + /// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size. + /// + internal static string PatchNotSupportedForArrays + { + get => GetString("PatchNotSupportedForArrays"); + } + + /// + /// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size. + /// + internal static string FormatPatchNotSupportedForArrays(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForArrays"), p0); + + /// + /// 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 PatchNotSupportedForNonGenericLists + { + get => GetString("PatchNotSupportedForNonGenericLists"); + } + + /// + /// 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 FormatPatchNotSupportedForNonGenericLists(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForNonGenericLists"), p0); + + /// + /// The target location specified by path segment '{0}' was not found. + /// + internal static string TargetLocationAtPathSegmentNotFound + { + get => GetString("TargetLocationAtPathSegmentNotFound"); + } + + /// + /// The target location specified by path segment '{0}' was not found. + /// + internal static string FormatTargetLocationAtPathSegmentNotFound(object p0) + => 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 => GetString("TargetLocationNotFound"); + } + + /// + /// For operation '{0}', the target location specified by path '{1}' was not found. + /// + internal static string FormatTargetLocationNotFound(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("TargetLocationNotFound"), p0, p1); + + /// + /// The test operation is not supported. + /// + internal static string TestOperationNotSupported + { + get => GetString("TestOperationNotSupported"); + } + + /// + /// The test operation is not supported. + /// + internal static string FormatTestOperationNotSupported() + => GetString("TestOperationNotSupported"); + + /// + /// The current value '{0}' at position '{2}' is not equal to the test value '{1}'. + /// + internal static string ValueAtListPositionNotEqualToTestValue + { + get => GetString("ValueAtListPositionNotEqualToTestValue"); + } + + /// + /// The current value '{0}' at position '{2}' is not equal to the test value '{1}'. + /// + internal static string FormatValueAtListPositionNotEqualToTestValue(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("ValueAtListPositionNotEqualToTestValue"), p0, p1, p2); + + /// + /// The value at '{0}' cannot be null or empty to perform the test operation. + /// + internal static string ValueForTargetSegmentCannotBeNullOrEmpty + { + get => GetString("ValueForTargetSegmentCannotBeNullOrEmpty"); + } + + /// + /// The value at '{0}' cannot be null or empty to perform the test operation. + /// + internal static string FormatValueForTargetSegmentCannotBeNullOrEmpty(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ValueForTargetSegmentCannotBeNullOrEmpty"), p0); + + /// + /// The current value '{0}' at path '{2}' is not equal to the test value '{1}'. + /// + internal static string ValueNotEqualToTestValue + { + get => GetString("ValueNotEqualToTestValue"); + } + + /// + /// The current value '{0}' at path '{2}' is not equal to the test value '{1}'. + /// + internal static string FormatValueNotEqualToTestValue(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("ValueNotEqualToTestValue"), p0, p1, p2); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Features/JsonPatch/src/Resources.resx b/src/Features/JsonPatch/src/Resources.resx new file mode 100644 index 0000000000..87cc399c62 --- /dev/null +++ b/src/Features/JsonPatch/src/Resources.resx @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The property at '{0}' could not be copied. + + + 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 expression '{0}' is not supported. Supported expressions include member access and indexer expressions. + + + The index value provided by path segment '{0}' is out of bounds of the array size. + + + The path segment '{0}' is invalid for an array index. + + + The JSON patch document was malformed and could not be parsed. + + + Invalid JsonPatch operation '{0}'. + + + The provided path segment '{0}' cannot be converted to the target type. + + + The provided string '{0}' is an invalid path. + + + The value '{0}' is invalid for target location. + + + '{0}' must be of type '{1}'. + + + The type '{0}' which is an array is not supported for json patch operations as it has a fixed size. + + + The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported. + + + 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. + + + The current value '{0}' at position '{2}' is not equal to the test value '{1}'. + + + The value at '{0}' cannot be null or empty to perform the test operation. + + + The current value '{0}' at path '{2}' is not equal to the test value '{1}'. + + \ No newline at end of file diff --git a/src/Features/JsonPatch/src/baseline.netcore.json b/src/Features/JsonPatch/src/baseline.netcore.json new file mode 100644 index 0000000000..3d90a8f017 --- /dev/null +++ b/src/Features/JsonPatch/src/baseline.netcore.json @@ -0,0 +1,1985 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.JsonPatch, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.JsonPatch.JsonPatchProperty", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Property", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Serialization.JsonProperty", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Property", + "Parameters": [ + { + "Name": "value", + "Type": "Newtonsoft.Json.Serialization.JsonProperty" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Parent", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Parent", + "Parameters": [ + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "property", + "Type": "Newtonsoft.Json.Serialization.JsonProperty" + }, + { + "Name": "parent", + "Type": "System.Object" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.IJsonPatchDocument", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_ContractResolver", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Serialization.IContractResolver", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContractResolver", + "Parameters": [ + { + "Name": "value", + "Type": "Newtonsoft.Json.Serialization.IContractResolver" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetOperations", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.JsonPatch.IJsonPatchDocument" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Operations", + "Parameters": [], + "ReturnType": "System.Collections.Generic.List", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContractResolver", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Serialization.IContractResolver", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.IJsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContractResolver", + "Parameters": [ + { + "Name": "value", + "Type": "Newtonsoft.Json.Serialization.IContractResolver" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.IJsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "path", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Replace", + "Parameters": [ + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Test", + "Parameters": [ + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Move", + "Parameters": [ + { + "Name": "from", + "Type": "System.String" + }, + { + "Name": "path", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [ + { + "Name": "from", + "Type": "System.String" + }, + { + "Name": "path", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ApplyTo", + "Parameters": [ + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ApplyTo", + "Parameters": [ + { + "Name": "objectToApplyTo", + "Type": "System.Object" + }, + { + "Name": "logErrorAction", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ApplyTo", + "Parameters": [ + { + "Name": "objectToApplyTo", + "Type": "System.Object" + }, + { + "Name": "adapter", + "Type": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "operations", + "Type": "System.Collections.Generic.List" + }, + { + "Name": "contractResolver", + "Type": "Newtonsoft.Json.Serialization.IContractResolver" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.JsonPatch.IJsonPatchDocument" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Operations", + "Parameters": [], + "ReturnType": "System.Collections.Generic.List>", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContractResolver", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Serialization.IContractResolver", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.IJsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ContractResolver", + "Parameters": [ + { + "Name": "value", + "Type": "Newtonsoft.Json.Serialization.IContractResolver" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.IJsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>" + }, + { + "Name": "value", + "Type": "T0" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "value", + "Type": "T0" + }, + { + "Name": "position", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "value", + "Type": "T0" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "position", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Replace", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>" + }, + { + "Name": "value", + "Type": "T0" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Replace", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "value", + "Type": "T0" + }, + { + "Name": "position", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Replace", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "value", + "Type": "T0" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Test", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>" + }, + { + "Name": "value", + "Type": "T0" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Test", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "value", + "Type": "T0" + }, + { + "Name": "position", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Test", + "Parameters": [ + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "value", + "Type": "T0" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Move", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Move", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionFrom", + "Type": "System.Int32" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Move", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionTo", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Move", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionFrom", + "Type": "System.Int32" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionTo", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Move", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionFrom", + "Type": "System.Int32" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Move", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionFrom", + "Type": "System.Int32" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionTo", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionFrom", + "Type": "System.Int32" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionTo", + "Type": "System.Int32" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>>" + }, + { + "Name": "positionFrom", + "Type": "System.Int32" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [ + { + "Name": "from", + "Type": "System.Linq.Expressions.Expression>" + }, + { + "Name": "path", + "Type": "System.Linq.Expressions.Expression>>" + } + ], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.JsonPatchDocument", + "Visibility": "Public", + "GenericParameter": [ + { + "ParameterName": "TProp", + "ParameterPosition": 0, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Kind": "Method", + "Name": "ApplyTo", + "Parameters": [ + { + "Name": "objectToApplyTo", + "Type": "T0" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ApplyTo", + "Parameters": [ + { + "Name": "objectToApplyTo", + "Type": "T0" + }, + { + "Name": "logErrorAction", + "Type": "System.Action" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ApplyTo", + "Parameters": [ + { + "Name": "objectToApplyTo", + "Type": "T0" + }, + { + "Name": "adapter", + "Type": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "operations", + "Type": "System.Collections.Generic.List>" + }, + { + "Name": "contractResolver", + "Type": "Newtonsoft.Json.Serialization.IContractResolver" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TModel", + "ParameterPosition": 0, + "Class": true, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.JsonPatchError", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_AffectedObject", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Operation", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.Operations.Operation", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ErrorMessage", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "affectedObject", + "Type": "System.Object" + }, + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "errorMessage", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Operations.Operation", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.JsonPatch.Operations.OperationBase", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_value", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_value", + "Parameters": [ + { + "Name": "value", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Apply", + "Parameters": [ + { + "Name": "objectToApplyTo", + "Type": "System.Object" + }, + { + "Name": "adapter", + "Type": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ShouldSerializevalue", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "op", + "Type": "System.String" + }, + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "from", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "op", + "Type": "System.String" + }, + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "from", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Operations.OperationBase", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_OperationType", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.Operations.OperationType", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_path", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_path", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_op", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_op", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_from", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_from", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ShouldSerializefrom", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "op", + "Type": "System.String" + }, + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "from", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Operations.Operation", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.JsonPatch.Operations.Operation", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Apply", + "Parameters": [ + { + "Name": "objectToApplyTo", + "Type": "T0" + }, + { + "Name": "adapter", + "Type": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "op", + "Type": "System.String" + }, + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "from", + "Type": "System.String" + }, + { + "Name": "value", + "Type": "System.Object" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "op", + "Type": "System.String" + }, + { + "Name": "path", + "Type": "System.String" + }, + { + "Name": "from", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [ + { + "ParameterName": "TModel", + "ParameterPosition": 0, + "Class": true, + "BaseTypeOrInterfaces": [] + } + ] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Operations.OperationType", + "Visibility": "Public", + "Kind": "Enumeration", + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Add", + "Parameters": [], + "GenericParameter": [], + "Literal": "0" + }, + { + "Kind": "Field", + "Name": "Remove", + "Parameters": [], + "GenericParameter": [], + "Literal": "1" + }, + { + "Kind": "Field", + "Name": "Replace", + "Parameters": [], + "GenericParameter": [], + "Literal": "2" + }, + { + "Kind": "Field", + "Name": "Move", + "Parameters": [], + "GenericParameter": [], + "Literal": "3" + }, + { + "Kind": "Field", + "Name": "Copy", + "Parameters": [], + "GenericParameter": [], + "Literal": "4" + }, + { + "Kind": "Field", + "Name": "Test", + "Parameters": [], + "GenericParameter": [], + "Literal": "5" + }, + { + "Kind": "Field", + "Name": "Invalid", + "Parameters": [], + "GenericParameter": [], + "Literal": "6" + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Helpers.GetValueResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_PropertyValue", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_HasError", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "propertyValue", + "Type": "System.Object" + }, + { + "Name": "hasError", + "Type": "System.Boolean" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Exceptions.JsonPatchException", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "System.Exception", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_FailedOperation", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.JsonPatch.Operations.Operation", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_AffectedObject", + "Parameters": [], + "ReturnType": "System.Object", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "jsonPatchError", + "Type": "Microsoft.AspNetCore.JsonPatch.JsonPatchError" + }, + { + "Name": "innerException", + "Type": "System.Exception" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "jsonPatchError", + "Type": "Microsoft.AspNetCore.JsonPatch.JsonPatchError" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "message", + "Type": "System.String" + }, + { + "Name": "innerException", + "Type": "System.Exception" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Converters.JsonPatchDocumentConverter", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Newtonsoft.Json.JsonConverter", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "CanConvert", + "Parameters": [ + { + "Name": "objectType", + "Type": "System.Type" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ReadJson", + "Parameters": [ + { + "Name": "reader", + "Type": "Newtonsoft.Json.JsonReader" + }, + { + "Name": "objectType", + "Type": "System.Type" + }, + { + "Name": "existingValue", + "Type": "System.Object" + }, + { + "Name": "serializer", + "Type": "Newtonsoft.Json.JsonSerializer" + } + ], + "ReturnType": "System.Object", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteJson", + "Parameters": [ + { + "Name": "writer", + "Type": "Newtonsoft.Json.JsonWriter" + }, + { + "Name": "value", + "Type": "System.Object" + }, + { + "Name": "serializer", + "Type": "Newtonsoft.Json.JsonSerializer" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Converters.TypedJsonPatchDocumentConverter", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.JsonPatch.Converters.JsonPatchDocumentConverter", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "ReadJson", + "Parameters": [ + { + "Name": "reader", + "Type": "Newtonsoft.Json.JsonReader" + }, + { + "Name": "objectType", + "Type": "System.Type" + }, + { + "Name": "existingValue", + "Type": "System.Object" + }, + { + "Name": "serializer", + "Type": "Newtonsoft.Json.JsonSerializer" + } + ], + "ReturnType": "System.Object", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Move", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Replace", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapterWithTest", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Test", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.JsonPatch.Adapters.ObjectAdapter", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapterWithTest" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Add", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Copy", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Move", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Remove", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Replace", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapter", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ContractResolver", + "Parameters": [], + "ReturnType": "Newtonsoft.Json.Serialization.IContractResolver", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_LogErrorAction", + "Parameters": [], + "ReturnType": "System.Action", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Test", + "Parameters": [ + { + "Name": "operation", + "Type": "Microsoft.AspNetCore.JsonPatch.Operations.Operation" + }, + { + "Name": "objectToApplyTo", + "Type": "System.Object" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.JsonPatch.Adapters.IObjectAdapterWithTest", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "contractResolver", + "Type": "Newtonsoft.Json.Serialization.IContractResolver" + }, + { + "Name": "logErrorAction", + "Type": "System.Action" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Features/JsonPatch/test/CustomNamingStrategyTests.cs b/src/Features/JsonPatch/test/CustomNamingStrategyTests.cs new file mode 100644 index 0000000000..ebc45874d9 --- /dev/null +++ b/src/Features/JsonPatch/test/CustomNamingStrategyTests.cs @@ -0,0 +1,153 @@ +// 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 +{ + public class CustomNamingStrategyTests + { + [Fact] + public void AddProperty_ToDynamicTestObject_WithCustomNamingStrategy() + { + // Arrange + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new TestNamingStrategy() + }; + + dynamic targetObject = new DynamicTestObject(); + targetObject.Test = 1; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("NewInt", 1); + patchDocument.ContractResolver = contractResolver; + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(1, targetObject.customNewInt); + Assert.Equal(1, targetObject.Test); + } + + [Fact] + public void CopyPropertyValue_ToDynamicTestObject_WithCustomNamingStrategy() + { + // Arrange + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new TestNamingStrategy() + }; + + dynamic targetObject = new DynamicTestObject(); + targetObject.customStringProperty = "A"; + targetObject.customAnotherStringProperty = "B"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy("StringProperty", "AnotherStringProperty"); + patchDocument.ContractResolver = contractResolver; + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("A", targetObject.customAnotherStringProperty); + } + + [Fact] + public void MovePropertyValue_ForExpandoObject_WithCustomNamingStrategy() + { + // Arrange + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new TestNamingStrategy() + }; + + dynamic targetObject = new ExpandoObject(); + targetObject.customStringProperty = "A"; + targetObject.customAnotherStringProperty = "B"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move("StringProperty", "AnotherStringProperty"); + patchDocument.ContractResolver = contractResolver; + + // Act + patchDocument.ApplyTo(targetObject); + var cont = targetObject as IDictionary; + cont.TryGetValue("customStringProperty", out var valueFromDictionary); + + // Assert + Assert.Equal("A", targetObject.customAnotherStringProperty); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveProperty_FromDictionaryObject_WithCustomNamingStrategy() + { + // Arrange + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new TestNamingStrategy() + }; + + var targetObject = new Dictionary() + { + { "customTest", 1}, + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("Test"); + patchDocument.ContractResolver = contractResolver; + + // Act + patchDocument.ApplyTo(targetObject); + var cont = targetObject as IDictionary; + cont.TryGetValue("customTest", out var valueFromDictionary); + + // Assert + Assert.Equal(0, valueFromDictionary); + } + + [Fact] + public void ReplacePropertyValue_ForExpandoObject_WithCustomNamingStrategy() + { + // Arrange + var contractResolver = new DefaultContractResolver + { + NamingStrategy = new TestNamingStrategy() + }; + + dynamic targetObject = new ExpandoObject(); + targetObject.customTest = 1; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace("Test", 2); + patchDocument.ContractResolver = contractResolver; + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(2, targetObject.customTest); + } + + private class TestNamingStrategy : NamingStrategy + { + public new bool ProcessDictionaryKeys => true; + + public override string GetDictionaryKey(string key) + { + return "custom" + key; + } + + protected override string ResolvePropertyName(string name) + { + return name; + } + } + } +} diff --git a/src/Features/JsonPatch/test/IntegrationTests/AnonymousObjectIntegrationTest.cs b/src/Features/JsonPatch/test/IntegrationTests/AnonymousObjectIntegrationTest.cs new file mode 100644 index 0000000000..4f290aae2f --- /dev/null +++ b/src/Features/JsonPatch/test/IntegrationTests/AnonymousObjectIntegrationTest.cs @@ -0,0 +1,190 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests +{ + public class AnonymousObjectIntegrationTest + { + [Fact] + public void AddNewProperty_ShouldFail() + { + // Arrange + var targetObject = new { }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("NewProperty", 4); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The target location specified by path segment 'NewProperty' was not found.", + exception.Message); + } + + [Fact] + public void AddNewProperty_ToNestedAnonymousObject_ShouldFail() + { + // Arrange + dynamic targetObject = new + { + Test = 1, + nested = new { } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("Nested/NewInt", 1); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The target location specified by path segment 'NewInt' was not found.", + exception.Message); + } + + [Fact] + public void AddDoesNotReplace() + { + // Arrange + var targetObject = new + { + StringProperty = "A" + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("StringProperty", "B"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The property at path 'StringProperty' could not be updated.", + exception.Message); + } + + [Fact] + public void RemoveProperty_ShouldFail() + { + // Arrange + dynamic targetObject = new + { + Test = 1 + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("Test"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The property at path 'Test' could not be updated.", + exception.Message); + } + + [Fact] + public void ReplaceProperty_ShouldFail() + { + // Arrange + var targetObject = new + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace("StringProperty", "AnotherStringProperty"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The property at path 'StringProperty' could not be updated.", + exception.Message); + } + + [Fact] + public void MoveProperty_ShouldFail() + { + // Arrange + var targetObject = new + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move("StringProperty", "AnotherStringProperty"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The property at path 'StringProperty' could not be updated.", + exception.Message); + } + + [Fact] + public void TestStringProperty_IsSucessful() + { + // Arrange + var targetObject = new + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test("StringProperty", "A"); + + // Act & Assert + patchDocument.ApplyTo(targetObject); + } + + [Fact] + public void TestStringProperty_Fails() + { + // Arrange + var targetObject = new + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test("StringProperty", "B"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The current value 'A' at path 'StringProperty' is not equal to the test value 'B'.", + exception.Message); + } + } +} diff --git a/src/Features/JsonPatch/test/IntegrationTests/DictionaryIntegrationTest.cs b/src/Features/JsonPatch/test/IntegrationTests/DictionaryIntegrationTest.cs new file mode 100644 index 0000000000..da990e3e8c --- /dev/null +++ b/src/Features/JsonPatch/test/IntegrationTests/DictionaryIntegrationTest.cs @@ -0,0 +1,319 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests +{ + public class DictionaryTest + { + [Fact] + public void TestIntegerValue_IsSuccessful() + { + // Arrange + var model = new IntDictionary(); + model.DictionaryOfStringToInteger["one"] = 1; + model.DictionaryOfStringToInteger["two"] = 2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Test("/DictionaryOfStringToInteger/two", 2); + + // Act & Assert + patchDocument.ApplyTo(model); + } + + [Fact] + public void AddIntegerValue_Succeeds() + { + // Arrange + var model = new IntDictionary(); + 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 RemoveIntegerValue_Succeeds() + { + // Arrange + var model = new IntDictionary(); + 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 MoveIntegerValue_Succeeds() + { + // Arrange + var model = new IntDictionary(); + model.DictionaryOfStringToInteger["one"] = 1; + model.DictionaryOfStringToInteger["two"] = 2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Move("/DictionaryOfStringToInteger/one", "/DictionaryOfStringToInteger/two"); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(1, model.DictionaryOfStringToInteger.Count); + Assert.Equal(1, model.DictionaryOfStringToInteger["two"]); + } + + [Fact] + public void ReplaceIntegerValue_Succeeds() + { + // Arrange + var model = new IntDictionary(); + 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"]); + } + + [Fact] + public void CopyIntegerValue_Succeeds() + { + // Arrange + var model = new IntDictionary(); + model.DictionaryOfStringToInteger["one"] = 1; + model.DictionaryOfStringToInteger["two"] = 2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy("/DictionaryOfStringToInteger/one", "/DictionaryOfStringToInteger/two"); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(2, model.DictionaryOfStringToInteger.Count); + Assert.Equal(1, model.DictionaryOfStringToInteger["one"]); + Assert.Equal(1, 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 IntDictionary + { + public IDictionary DictionaryOfStringToInteger { get; } = new Dictionary(); + } + + private class CustomerDictionary + { + public IDictionary DictionaryOfStringToCustomer { get; } = new Dictionary(); + } + + [Fact] + public void TestPocoObject_Succeeds() + { + // Arrange + var key1 = 100; + var value1 = new Customer() { Name = "James" }; + var model = new CustomerDictionary(); + model.DictionaryOfStringToCustomer[key1] = value1; + var patchDocument = new JsonPatchDocument(); + patchDocument.Test($"/DictionaryOfStringToCustomer/{key1}/Name", "James"); + + // Act & Assert + patchDocument.ApplyTo(model); + } + + [Fact] + public void TestPocoObject_FailsWhenTestValueIsNotEqualToObjectValue() + { + // Arrange + var key1 = 100; + var value1 = new Customer() { Name = "James" }; + var model = new CustomerDictionary(); + model.DictionaryOfStringToCustomer[key1] = value1; + var patchDocument = new JsonPatchDocument(); + patchDocument.Test($"/DictionaryOfStringToCustomer/{key1}/Name", "Mike"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(model); + }); + + // Assert + Assert.Equal("The current value 'James' at path 'Name' is not equal to the test value 'Mike'.", exception.Message); + } + + [Fact] + public void AddReplacesPocoObject_Succeeds() + { + // Arrange + var key1 = 100; + var value1 = new Customer() { Name = "Jamesss" }; + var key2 = 200; + var value2 = new Customer() { Name = "Mike" }; + var model = new CustomerDictionary(); + model.DictionaryOfStringToCustomer[key1] = value1; + model.DictionaryOfStringToCustomer[key2] = value2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Add($"/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 RemovePocoObject_Succeeds() + { + // Arrange + var key1 = 100; + var value1 = new Customer() { Name = "Jamesss" }; + var key2 = 200; + var value2 = new Customer() { Name = "Mike" }; + var model = new CustomerDictionary(); + model.DictionaryOfStringToCustomer[key1] = value1; + model.DictionaryOfStringToCustomer[key2] = value2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove($"/DictionaryOfStringToCustomer/{key1}/Name"); + + // Act + patchDocument.ApplyTo(model); + + // Assert + var actualValue1 = model.DictionaryOfStringToCustomer[key1]; + Assert.Null(actualValue1.Name); + } + + [Fact] + public void MovePocoObject_Succeeds() + { + // Arrange + var key1 = 100; + var value1 = new Customer() { Name = "James" }; + var key2 = 200; + var value2 = new Customer() { Name = "Mike" }; + var model = new CustomerDictionary(); + model.DictionaryOfStringToCustomer[key1] = value1; + model.DictionaryOfStringToCustomer[key2] = value2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Move($"/DictionaryOfStringToCustomer/{key1}/Name", $"/DictionaryOfStringToCustomer/{key2}/Name"); + + // Act + patchDocument.ApplyTo(model); + + // Assert + var actualValue2 = model.DictionaryOfStringToCustomer[key2]; + Assert.NotNull(actualValue2); + Assert.Equal("James", actualValue2.Name); + } + + [Fact] + public void CopyPocoObject_Succeeds() + { + // Arrange + var key1 = 100; + var value1 = new Customer() { Name = "James" }; + var key2 = 200; + var value2 = new Customer() { Name = "Mike" }; + var model = new CustomerDictionary(); + model.DictionaryOfStringToCustomer[key1] = value1; + model.DictionaryOfStringToCustomer[key2] = value2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy($"/DictionaryOfStringToCustomer/{key1}/Name", $"/DictionaryOfStringToCustomer/{key2}/Name"); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(2, model.DictionaryOfStringToCustomer.Count); + var actualValue2 = model.DictionaryOfStringToCustomer[key2]; + Assert.NotNull(actualValue2); + Assert.Equal("James", actualValue2.Name); + } + + [Fact] + public void ReplacePocoObject_Succeeds() + { + // Arrange + var key1 = 100; + var value1 = new Customer() { Name = "Jamesss" }; + var key2 = 200; + var value2 = new Customer() { Name = "Mike" }; + var model = new CustomerDictionary(); + 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 ReplacePocoObject_WithEscaping_Succeeds() + { + // Arrange + var key1 = "Foo/Name"; + var value1 = 100; + var key2 = "Foo"; + var value2 = 200; + var model = new IntDictionary(); + model.DictionaryOfStringToInteger[key1] = value1; + model.DictionaryOfStringToInteger[key2] = value2; + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace($"/DictionaryOfStringToInteger/Foo~1Name", 300); + + // Act + patchDocument.ApplyTo(model); + + // Assert + Assert.Equal(2, model.DictionaryOfStringToInteger.Count); + var actualValue1 = model.DictionaryOfStringToInteger[key1]; + var actualValue2 = model.DictionaryOfStringToInteger[key2]; + Assert.Equal(300, actualValue1); + Assert.Equal(200, actualValue2); + } + } +} diff --git a/src/Features/JsonPatch/test/IntegrationTests/DynamicObjectIntegrationTest.cs b/src/Features/JsonPatch/test/IntegrationTests/DynamicObjectIntegrationTest.cs new file mode 100644 index 0000000000..ec15951a85 --- /dev/null +++ b/src/Features/JsonPatch/test/IntegrationTests/DynamicObjectIntegrationTest.cs @@ -0,0 +1,236 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests +{ + public class DynamicObjectIntegrationTest + { + [Fact] + public void AddResults_ShouldReplaceExistingPropertyValue_InNestedDynamicObject() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.Nested = new NestedObject(); + dynamicTestObject.Nested.DynamicProperty = new DynamicTestObject(); + dynamicTestObject.Nested.DynamicProperty.InBetweenFirst = new DynamicTestObject(); + dynamicTestObject.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond = new DynamicTestObject(); + dynamicTestObject.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond.StringProperty = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("/Nested/DynamicProperty/InBetweenFirst/InBetweenSecond/StringProperty", "B"); + + // Act + patchDocument.ApplyTo(dynamicTestObject); + + // Assert + Assert.Equal("B", dynamicTestObject.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond.StringProperty); + } + + [Fact] + public void ShouldNotBeAbleToAdd_ToNonExistingProperty_ThatIsNotTheRoot() + { + //Adding to a Nonexistent Target + // + // An example target JSON document: + // { "foo": "bar" } + // A JSON Patch document: + // [ + // { "op": "add", "path": "/baz/bat", "value": "qux" } + // ] + // This JSON Patch document, applied to the target JSON document above, + // would result in an error (therefore, it would not be applied), + // because the "add" operation's target location that references neither + // the root of the document, nor a member of an existing object, nor a + // member of an existing array. + + // Arrange + var nestedObject = new NestedObject() + { + DynamicProperty = new DynamicTestObject() + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("DynamicProperty/OtherProperty/IntProperty", 1); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(nestedObject); + }); + + // Assert + Assert.Equal("The target location specified by path segment 'OtherProperty' was not found.", exception.Message); + } + + [Fact] + public void CopyProperties_InNestedDynamicObject() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.NestedDynamicObject = new DynamicTestObject(); + dynamicTestObject.NestedDynamicObject.StringProperty = "A"; + dynamicTestObject.NestedDynamicObject.AnotherStringProperty = "B"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy("NestedDynamicObject/StringProperty", "NestedDynamicObject/AnotherStringProperty"); + + // Act + patchDocument.ApplyTo(dynamicTestObject); + + // Assert + Assert.Equal("A", dynamicTestObject.NestedDynamicObject.AnotherStringProperty); + } + + + [Fact] + public void MoveToNonExistingProperty_InDynamicObject_ShouldAddNewProperty() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.StringProperty = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move("StringProperty", "AnotherStringProperty"); + + // Act + patchDocument.ApplyTo(dynamicTestObject); + dynamicTestObject.TryGetValue("StringProperty", out object valueFromDictionary); + + // Assert + Assert.Equal("A", dynamicTestObject.AnotherStringProperty); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void MovePropertyValue_FromDynamicObject_ToTypedObject() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.StringProperty = "A"; + dynamicTestObject.SimpleObject = new SimpleObject() { AnotherStringProperty = "B" }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move("StringProperty", "SimpleObject/AnotherStringProperty"); + + // Act + patchDocument.ApplyTo(dynamicTestObject); + dynamicTestObject.TryGetValue("StringProperty", out object valueFromDictionary); + + // Assert + Assert.Equal("A", dynamicTestObject.SimpleObject.AnotherStringProperty); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveNestedProperty_FromDynamicObject() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.Test = new DynamicTestObject(); + dynamicTestObject.Test.AnotherTest = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("Test"); + + // Act + patchDocument.ApplyTo(dynamicTestObject); + dynamicTestObject.TryGetValue("Test", out object valueFromDictionary); + + // Assert + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveFromNestedObject_InDynamicObject_MixedCase_ThrowsPathNotFoundException() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.SimpleObject = new SimpleObject() + { + StringProperty = "A" + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("Simpleobject/stringProperty"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(dynamicTestObject); + }); + + // Assert + Assert.Equal("The target location specified by path segment 'Simpleobject' was not found.", exception.Message); + } + + + [Fact] + public void ReplaceNestedTypedObject_InDynamicObject() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.SimpleObject = new SimpleObject() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + var newObject = new SimpleObject() + { + DoubleValue = 1 + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace("SimpleObject", newObject); + + // Act + patchDocument.ApplyTo(dynamicTestObject); + + // Assert + Assert.Equal(1, dynamicTestObject.SimpleObject.DoubleValue); + Assert.Equal(0, dynamicTestObject.SimpleObject.IntegerValue); + Assert.Null(dynamicTestObject.SimpleObject.IntegerList); + } + + [Fact] + public void TestStringPropertyValue_IsSuccessful() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.Property = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test("Property", "A"); + + // Act & Assert + patchDocument.ApplyTo(dynamicTestObject); + } + + [Fact] + public void TestIntegerPropertyValue_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.Nested = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test("Nested/IntegerList/0", 2); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(dynamicTestObject); + }); + + // Assert + Assert.Equal("The current value '1' at position '0' is not equal to the test value '2'.", exception.Message); + } + } +} diff --git a/src/Features/JsonPatch/test/IntegrationTests/ExpandoObjectIntegrationTest.cs b/src/Features/JsonPatch/test/IntegrationTests/ExpandoObjectIntegrationTest.cs new file mode 100644 index 0000000000..b1b58556f0 --- /dev/null +++ b/src/Features/JsonPatch/test/IntegrationTests/ExpandoObjectIntegrationTest.cs @@ -0,0 +1,327 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Dynamic; +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests +{ + public class ExpandoObjectIntegrationTest + { + [Fact] + public void AddNewIntProperty() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.Test = 1; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("NewInt", 1); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(1, targetObject.NewInt); + Assert.Equal(1, targetObject.Test); + } + + [Fact] + public void AddNewProperty_ToTypedObject_InExpandoObject() + { + // Arrange + dynamic dynamicProperty = new ExpandoObject(); + dynamicProperty.StringProperty = "A"; + + var targetObject = new NestedObject() + { + DynamicProperty = dynamicProperty + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("DynamicProperty/StringProperty", "B"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("B", targetObject.DynamicProperty.StringProperty); + } + + [Fact] + public void AddReplaces_ExistingProperty() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.StringProperty = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("StringProperty", "B"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("B", targetObject.StringProperty); + } + + [Fact] + public void AddReplaces_ExistingProperty_InNestedExpandoObject() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.InBetweenFirst = new ExpandoObject(); + targetObject.InBetweenFirst.InBetweenSecond = new ExpandoObject(); + targetObject.InBetweenFirst.InBetweenSecond.StringProperty = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("/InBetweenFirst/InBetweenSecond/StringProperty", "B"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("B", targetObject.InBetweenFirst.InBetweenSecond.StringProperty); + } + + [Fact] + public void ShouldNotReplaceProperty_WithDifferentCase() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.StringProperty = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("stringproperty", "B"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("A", targetObject.StringProperty); + Assert.Equal("B", targetObject.stringproperty); + } + + [Fact] + public void TestIntegerProperty_IsSucessful() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.Test = 1; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test("Test", 1); + + // Act & Assert + patchDocument.ApplyTo(targetObject); + } + + [Fact] + public void TestStringProperty_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.Test = "Value"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test("Test", "TestValue"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The current value 'Value' at path 'Test' is not equal to the test value 'TestValue'.", + exception.Message); + } + + [Fact] + public void CopyStringProperty_ToAnotherStringProperty() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + + targetObject.StringProperty = "A"; + targetObject.AnotherStringProperty = "B"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy("StringProperty", "AnotherStringProperty"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("A", targetObject.AnotherStringProperty); + } + + [Fact] + public void MoveIntegerValue_ToAnotherIntegerProperty() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.IntegerValue = 100; + targetObject.AnotherIntegerValue = 200; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move("IntegerValue", "AnotherIntegerValue"); + + // Act + patchDocument.ApplyTo(targetObject); + + Assert.Equal(100, targetObject.AnotherIntegerValue); + + var cont = targetObject as IDictionary; + cont.TryGetValue("IntegerValue", out object valueFromDictionary); + + // Assert + Assert.Null(valueFromDictionary); + } + + [Fact] + public void Move_ToNonExistingProperty() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.StringProperty = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move("StringProperty", "AnotherStringProperty"); + + // Act + patchDocument.ApplyTo(targetObject); + + Assert.Equal("A", targetObject.AnotherStringProperty); + + var cont = targetObject as IDictionary; + cont.TryGetValue("StringProperty", out var valueFromDictionary); + + // Assert + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveProperty_ShouldFail_IfItDoesntExist() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.Test = 1; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("NonExisting"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The target location specified by path segment 'NonExisting' was not found.", exception.Message); + } + + [Fact] + public void RemoveStringProperty() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.Test = 1; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("Test"); + + // Act + patchDocument.ApplyTo(targetObject); + + var cont = targetObject as IDictionary; + cont.TryGetValue("Test", out object valueFromDictionary); + + // Assert + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveProperty_MixedCase_ThrowsPathNotFoundException() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.Test = 1; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("test"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The target location specified by path segment 'test' was not found.", exception.Message); + } + + [Fact] + public void RemoveNestedProperty() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.Test = new ExpandoObject(); + targetObject.Test.AnotherTest = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("Test"); + + // Act + patchDocument.ApplyTo(targetObject); + + var cont = targetObject as IDictionary; + cont.TryGetValue("Test", out object valueFromDictionary); + + // Assert + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveNestedProperty_MixedCase_ThrowsPathNotFoundException() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.Test = new ExpandoObject(); + targetObject.Test.AnotherTest = "A"; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("test"); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal("The target location specified by path segment 'test' was not found.", exception.Message); + } + + [Fact] + public void ReplaceGuid() + { + // Arrange + dynamic targetObject = new ExpandoObject(); + targetObject.GuidValue = Guid.NewGuid(); + + var newGuid = Guid.NewGuid(); + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace("GuidValue", newGuid); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(newGuid, targetObject.GuidValue); + } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/test/IntegrationTests/ListIntegrationTest.cs b/src/Features/JsonPatch/test/IntegrationTests/ListIntegrationTest.cs new file mode 100644 index 0000000000..5e261ea08b --- /dev/null +++ b/src/Features/JsonPatch/test/IntegrationTests/ListIntegrationTest.cs @@ -0,0 +1,366 @@ +// 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.Collections.ObjectModel; +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests +{ + public class ListIntegrationTest + { + [Fact] + public void TestInList_IsSuccessful() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test(o => o.SimpleObject.IntegerList, 3, 2); + + // Act & Assert + patchDocument.ApplyTo(targetObject); + } + + [Fact] + public void TestInList_InvalidPosition() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test(o => o.SimpleObject.IntegerList, 4, -1); + + // Act & Assert + var exception = Assert.Throws(() => { patchDocument.ApplyTo(targetObject); }); + Assert.Equal("The index value provided by path segment '-1' is out of bounds of the array size.", + exception.Message); + } + + [Fact] + public void AddToIntegerIList() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + IntegerIList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add(o => (List)o.SimpleObject.IntegerIList, 4, 0); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(new List() { 4, 1, 2, 3 }, targetObject.SimpleObject.IntegerIList); + } + + [Fact] + public void AddToComplextTypeList_SpecifyIndex() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObjectList = new List() + { + new SimpleObject + { + StringProperty = "String1" + }, + new SimpleObject + { + StringProperty = "String2" + } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add(o => o.SimpleObjectList[0].StringProperty, "ChangedString1"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("ChangedString1", targetObject.SimpleObjectList[0].StringProperty); + } + + [Fact] + public void AddToListAppend() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add(o => o.SimpleObject.IntegerList, 4); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 4 }, targetObject.SimpleObject.IntegerList); + } + + [Fact] + public void RemoveFromList() + { + // Arrange + var targetObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("IntegerList/2"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(new List() { 1, 2 }, targetObject.IntegerList); + } + + [Theory] + [InlineData("3")] + [InlineData("-1")] + public void RemoveFromList_InvalidPosition(string position) + { + // Arrange + var targetObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("IntegerList/" + position); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.ApplyTo(targetObject); + }); + + // Assert + Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", exception.Message); + } + + [Fact] + public void Remove_FromEndOfList() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove(o => o.SimpleObject.IntegerList); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(new List() { 1, 2 }, targetObject.SimpleObject.IntegerList); + } + + [Fact] + public void ReplaceFullList_WithCollection() + { + // Arrange + var targetObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace("IntegerList", new Collection() { 4, 5, 6 }); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(new List() { 4, 5, 6 }, targetObject.IntegerList); + } + + [Fact] + public void Replace_AtEndOfList() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace(o => o.SimpleObject.IntegerList, 5); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(new List() { 1, 2, 5 }, targetObject.SimpleObject.IntegerList); + } + + [Fact] + public void Replace_InList_InvalidPosition() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace(o => o.SimpleObject.IntegerList, 5, -1); + + // Act + var exception = Assert.Throws(() => { patchDocument.ApplyTo(targetObject); }); + + // Assert + Assert.Equal("The index value provided by path segment '-1' is out of bounds of the array size.", exception.Message); + } + + [Fact] + public void CopyFromListToEndOfList() + { + // Arrange + var targetObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy("IntegerList/0", "IntegerList/-"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(new List() { 1, 2, 3, 1 }, targetObject.IntegerList); + } + + [Fact] + public void CopyFromListToNonList() + { + // Arrange + var targetObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy("IntegerList/0", "IntegerValue"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(1, targetObject.IntegerValue); + } + + [Fact] + public void MoveToEndOfList() + { + // Arrange + var targetObject = new SimpleObject() + { + IntegerValue = 5, + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move("IntegerValue", "IntegerList/-"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(0, targetObject.IntegerValue); + Assert.Equal(new List() { 1, 2, 3, 5 }, targetObject.IntegerList); + } + + [Fact] + public void Move_KeepsObjectReferenceInList() + { + // Arrange + var simpleObject1 = new SimpleObject() { IntegerValue = 1 }; + var simpleObject2 = new SimpleObject() { IntegerValue = 2 }; + var simpleObject3 = new SimpleObject() { IntegerValue = 3 }; + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObjectList = new List() { + simpleObject1, + simpleObject2, + simpleObject3 + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move(o => o.SimpleObjectList, 0, o => o.SimpleObjectList, 1); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(new List() { simpleObject2, simpleObject1, simpleObject3 }, targetObject.SimpleObjectList); + Assert.Equal(2, targetObject.SimpleObjectList[0].IntegerValue); + Assert.Equal(1, targetObject.SimpleObjectList[1].IntegerValue); + Assert.Same(simpleObject2, targetObject.SimpleObjectList[0]); + Assert.Same(simpleObject1, targetObject.SimpleObjectList[1]); + } + + [Fact] + public void MoveFromList_ToNonList_BetweenHierarchy() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move(o => o.SimpleObject.IntegerList, 0, o => o.IntegerValue); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(new List() { 2, 3 }, targetObject.SimpleObject.IntegerList); + Assert.Equal(1, targetObject.IntegerValue); + } + } +} diff --git a/src/Features/JsonPatch/test/IntegrationTests/NestedObjectIntegrationTest.cs b/src/Features/JsonPatch/test/IntegrationTests/NestedObjectIntegrationTest.cs new file mode 100644 index 0000000000..92c0e7fb2d --- /dev/null +++ b/src/Features/JsonPatch/test/IntegrationTests/NestedObjectIntegrationTest.cs @@ -0,0 +1,342 @@ +// 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.Dynamic; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests +{ + public class NestedObjectIntegrationTest + { + [Fact] + public void Replace_DTOWithNullCheck() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObjectWithNullCheck() + { + SimpleObjectWithNullCheck = new SimpleObjectWithNullCheck() + { + StringProperty = "A" + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace(o => o.SimpleObjectWithNullCheck.StringProperty, "B"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("B", targetObject.SimpleObjectWithNullCheck.StringProperty); + } + + [Fact] + public void ReplaceNestedObject_WithSerialization() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + IntegerValue = 1 + }; + + var newNested = new NestedObject() { StringProperty = "B" }; + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace(o => o.NestedObject, newNested); + + var serialized = JsonConvert.SerializeObject(patchDocument); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(targetObject); + + // Assert + Assert.Equal("B", targetObject.NestedObject.StringProperty); + } + + [Fact] + public void TestStringProperty_InNestedObject() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + NestedObject = new NestedObject() { StringProperty = "A"} + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test(o => o.StringProperty, "A"); + + // Act + patchDocument.ApplyTo(targetObject.NestedObject); + + // Assert + Assert.Equal("A", targetObject.NestedObject.StringProperty); + } + + [Fact] + public void TestNestedObject() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + NestedObject = new NestedObject() { StringProperty = "B"} + }; + + var testNested = new NestedObject() { StringProperty = "B" }; + var patchDocument = new JsonPatchDocument(); + patchDocument.Test(o => o.NestedObject, testNested); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("B", targetObject.NestedObject.StringProperty); + } + + [Fact] + public void AddReplaces_ExistingStringProperty() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + StringProperty = "A" + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add(o => o.SimpleObject.StringProperty, "B"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("B", targetObject.SimpleObject.StringProperty); + } + + [Fact] + public void AddNewProperty_ToExpandoOject_InTypedObject() + { + var targetObject = new NestedObject() + { + DynamicProperty = new ExpandoObject() + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("DynamicProperty/NewInt", 1); + + patchDocument.ApplyTo(targetObject); + + Assert.Equal(1, targetObject.DynamicProperty.NewInt); + } + + [Fact] + public void RemoveStringProperty() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + StringProperty = "A" + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove(o => o.SimpleObject.StringProperty); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Null(targetObject.SimpleObject.StringProperty); + } + + [Fact] + public void CopyStringProperty_ToAnotherStringProperty() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + StringProperty = "A", + AnotherStringProperty = "B" + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy(o => o.SimpleObject.StringProperty, o => o.SimpleObject.AnotherStringProperty); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("A", targetObject.SimpleObject.AnotherStringProperty); + } + + [Fact] + public void Copy_DeepClonesObject() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + StringProperty = "A", + AnotherStringProperty = "B" + }, + InheritedObject = new InheritedObject() + { + StringProperty = "C", + AnotherStringProperty = "D" + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy(o => o.InheritedObject, o => o.SimpleObject); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("C", targetObject.SimpleObject.StringProperty); + Assert.Equal("D", targetObject.SimpleObject.AnotherStringProperty); + Assert.Equal("C", targetObject.InheritedObject.StringProperty); + Assert.Equal("D", targetObject.InheritedObject.AnotherStringProperty); + Assert.NotSame(targetObject.SimpleObject.StringProperty, targetObject.InheritedObject.StringProperty); + } + + [Fact] + public void Copy_KeepsObjectType() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject(), + InheritedObject = new InheritedObject() + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy(o => o.InheritedObject, o => o.SimpleObject); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(typeof(InheritedObject), targetObject.SimpleObject.GetType()); + } + + [Fact] + public void Copy_BreaksObjectReference() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject(), + InheritedObject = new InheritedObject() + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy(o => o.InheritedObject, o => o.SimpleObject); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.NotSame(targetObject.SimpleObject, targetObject.InheritedObject); + } + + [Fact] + public void MoveIntegerValue_ToAnotherIntegerProperty() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = new SimpleObject() + { + IntegerValue = 2, + AnotherIntegerValue = 3 + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move(o => o.SimpleObject.IntegerValue, o => o.SimpleObject.AnotherIntegerValue); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(2, targetObject.SimpleObject.AnotherIntegerValue); + Assert.Equal(0, targetObject.SimpleObject.IntegerValue); + } + + [Fact] + public void Move_KeepsObjectReference() + { + // Arrange + var sDto = new SimpleObject() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + var iDto = new InheritedObject() + { + StringProperty = "C", + AnotherStringProperty = "D" + }; + var targetObject = new SimpleObjectWithNestedObject() + { + SimpleObject = sDto, + InheritedObject = iDto + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move(o => o.InheritedObject, o => o.SimpleObject); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("C", targetObject.SimpleObject.StringProperty); + Assert.Equal("D", targetObject.SimpleObject.AnotherStringProperty); + Assert.Same(iDto, targetObject.SimpleObject); + Assert.Null(targetObject.InheritedObject); + } + + private class SimpleObjectWithNullCheck + { + private string stringProperty; + + public string StringProperty + { + get + { + return stringProperty; + } + + set + { + if (value == null) + { + throw new ArgumentNullException(); + } + + stringProperty = value; + } + } + } + + private class SimpleObjectWithNestedObjectWithNullCheck + { + public SimpleObjectWithNullCheck SimpleObjectWithNullCheck { get; set; } + + public SimpleObjectWithNestedObjectWithNullCheck() + { + SimpleObjectWithNullCheck = new SimpleObjectWithNullCheck(); + } + } + } +} diff --git a/src/Features/JsonPatch/test/IntegrationTests/SimpleObjectIntegrationTest.cs b/src/Features/JsonPatch/test/IntegrationTests/SimpleObjectIntegrationTest.cs new file mode 100644 index 0000000000..669c6d7af4 --- /dev/null +++ b/src/Features/JsonPatch/test/IntegrationTests/SimpleObjectIntegrationTest.cs @@ -0,0 +1,128 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.IntegrationTests +{ + public class SimpleObjectIntegrationTest + { + [Fact] + public void TestDoubleValueProperty() + { + // Arrange + var targetObject = new SimpleObject() + { + DoubleValue = 9.8 + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test("DoubleValue", 9.8); + + // Act & Assert + patchDocument.ApplyTo(targetObject); + } + + [Fact] + public void CopyStringProperty_ToAnotherStringProperty() + { + // Arrange + var targetObject = new SimpleObject() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy("StringProperty", "AnotherStringProperty"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal("A", targetObject.AnotherStringProperty); + } + + [Fact] + public void MoveIntegerProperty_ToAnotherIntegerProperty() + { + // Arrange + var targetObject = new SimpleObject() + { + IntegerValue = 2, + AnotherIntegerValue = 3 + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Move("IntegerValue", "AnotherIntegerValue"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(2, targetObject.AnotherIntegerValue); + Assert.Equal(0, targetObject.IntegerValue); + } + + [Fact] + public void RemoveDecimalPropertyValue() + { + // Arrange + var targetObject = new SimpleObject() + { + DecimalValue = 9.8M + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("DecimalValue"); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(0, targetObject.DecimalValue); + } + + [Fact] + public void ReplaceGuid() + { + // Arrange + var targetObject = new SimpleObject() + { + GuidValue = Guid.NewGuid() + }; + + var newGuid = Guid.NewGuid(); + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace("GuidValue", newGuid); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(newGuid, targetObject.GuidValue); + } + + [Fact] + public void AddReplacesGuid() + { + // Arrange + var targetObject = new SimpleObject() + { + GuidValue = Guid.NewGuid() + }; + + var newGuid = Guid.NewGuid(); + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("GuidValue", newGuid); + + // Act + patchDocument.ApplyTo(targetObject); + + // Assert + Assert.Equal(newGuid, targetObject.GuidValue); + } + + } +} diff --git a/src/Features/JsonPatch/test/Internal/DictionaryAdapterTest.cs b/src/Features/JsonPatch/test/Internal/DictionaryAdapterTest.cs new file mode 100644 index 0000000000..a0dcd82ab0 --- /dev/null +++ b/src/Features/JsonPatch/test/Internal/DictionaryAdapterTest.cs @@ -0,0 +1,311 @@ +// 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 Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class DictionaryAdapterTest + { + [Fact] + public void Add_KeyWhichAlreadyExists_ReplacesExistingValue() + { + // Arrange + var key = "Status"; + var dictionary = new Dictionary(StringComparer.Ordinal); + dictionary[key] = 404; + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + + // Act + var addStatus = dictionaryAdapter.TryAdd(dictionary, key, resolver, 200, out var message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Single(dictionary); + Assert.Equal(200, dictionary[key]); + } + + [Fact] + public void Add_IntKeyWhichAlreadyExists_ReplacesExistingValue() + { + // Arrange + var intKey = 1; + var dictionary = new Dictionary(); + dictionary[intKey] = "Mike"; + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + + // Act + var addStatus = dictionaryAdapter.TryAdd(dictionary, intKey.ToString(), resolver, "James", out var message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Single(dictionary); + Assert.Equal("James", dictionary[intKey]); + } + + [Fact] + public void GetInvalidKey_ThrowsInvalidPathSegmentException() + { + // Arrange + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + var key = 1; + var dictionary = new Dictionary(); + + // Act + var addStatus = dictionaryAdapter.TryAdd(dictionary, key.ToString(), resolver, "James", out var message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Single(dictionary); + Assert.Equal("James", dictionary[key]); + + // Act + var guidKey = new Guid(); + var getStatus = dictionaryAdapter.TryGet(dictionary, guidKey.ToString(), resolver, out var outValue, out message); + + // Assert + Assert.False(getStatus); + Assert.Equal($"The provided path segment '{guidKey.ToString()}' cannot be converted to the target type.", message); + Assert.Null(outValue); + } + + [Fact] + public void Get_UsingCaseSensitiveKey_FailureScenario() + { + // Arrange + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + + // Act + var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver, "James", out var message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Single(dictionary); + Assert.Equal("James", dictionary[nameKey]); + + // Act + var getStatus = dictionaryAdapter.TryGet(dictionary, nameKey.ToUpper(), resolver, out var outValue, out message); + + // Assert + Assert.False(getStatus); + Assert.Equal("The target location specified by path segment 'NAME' was not found.", message); + Assert.Null(outValue); + } + + [Fact] + public void Get_UsingCaseSensitiveKey_SuccessScenario() + { + // Arrange + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + + // Act + var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver, "James", out var message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Single(dictionary); + Assert.Equal("James", dictionary[nameKey]); + + // Act + addStatus = dictionaryAdapter.TryGet(dictionary, nameKey, resolver, out var 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 DefaultContractResolver(); + + // Act + var replaceStatus = dictionaryAdapter.TryReplace(dictionary, nameKey, resolver, "James", out var message); + + // Assert + Assert.True(replaceStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Single(dictionary); + Assert.Equal("James", dictionary[nameKey]); + } + + [Fact] + public void ReplacingExistingItem_WithGuidKey() + { + // Arrange + var guidKey = new Guid(); + var dictionary = new Dictionary(); + dictionary.Add(guidKey, "Mike"); + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + + // Act + var replaceStatus = dictionaryAdapter.TryReplace(dictionary, guidKey.ToString(), resolver, "James", out var message); + + // Assert + Assert.True(replaceStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Single(dictionary); + Assert.Equal("James", dictionary[guidKey]); + } + + [Fact] + public void ReplacingWithInvalidValue_ThrowsInvalidValueForPropertyException() + { + // Arrange + var guidKey = new Guid(); + var dictionary = new Dictionary(); + dictionary.Add(guidKey, 5); + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + + // Act + var replaceStatus = dictionaryAdapter.TryReplace(dictionary, guidKey.ToString(), resolver, "test", out var message); + + // Assert + Assert.False(replaceStatus); + Assert.Equal("The value 'test' is invalid for target location.", message); + Assert.Equal(5, dictionary[guidKey]); + } + + [Fact] + public void Replace_NonExistingKey_Fails() + { + // Arrange + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + + // Act + var replaceStatus = dictionaryAdapter.TryReplace(dictionary, nameKey, resolver, "Mike", out var message); + + // Assert + Assert.False(replaceStatus); + Assert.Equal("The target location specified by path segment 'Name' was not found.", message); + Assert.Empty(dictionary); + } + + [Fact] + public void Remove_NonExistingKey_Fails() + { + // Arrange + var nameKey = "Name"; + var dictionary = new Dictionary(StringComparer.Ordinal); + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + + // Act + var removeStatus = dictionaryAdapter.TryRemove(dictionary, nameKey, resolver, out var message); + + // Assert + Assert.False(removeStatus); + Assert.Equal("The target location specified by path segment 'Name' was not found.", message); + Assert.Empty(dictionary); + } + + [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 DefaultContractResolver(); + + // Act + var removeStatus = dictionaryAdapter.TryRemove(dictionary, nameKey, resolver, out var message); + + //Assert + Assert.True(removeStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Empty(dictionary); + } + + [Fact] + public void Remove_RemovesFromDictionary_WithUriKey() + { + // Arrange + var uriKey = new Uri("http://www.test.com/name"); + var dictionary = new Dictionary(); + dictionary[uriKey] = "James"; + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + + // Act + var removeStatus = dictionaryAdapter.TryRemove(dictionary, uriKey.ToString(), resolver, out var message); + + //Assert + Assert.True(removeStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Empty(dictionary); + } + + [Fact] + public void Test_DoesNotThrowException_IfTestIsSuccessful() + { + // Arrange + var key = "Name"; + var dictionary = new Dictionary>(); + var value = new List() + { + "James", + 2, + new Customer("James", 25) + }; + dictionary[key] = value; + var dictionaryAdapter = new DictionaryAdapter>(); + var resolver = new DefaultContractResolver(); + + // Act + var testStatus = dictionaryAdapter.TryTest(dictionary, key, resolver, value, out var message); + + //Assert + Assert.True(testStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + } + + [Fact] + public void Test_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + var key = "Name"; + var dictionary = new Dictionary(); + dictionary[key] = "James"; + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + var expectedErrorMessage = "The current value 'James' at path 'Name' is not equal to the test value 'John'."; + + // Act + var testStatus = dictionaryAdapter.TryTest(dictionary, key, resolver, "John", out var errorMessage); + + //Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + } +} diff --git a/src/Features/JsonPatch/test/Internal/DynamicObjectAdapterTest.cs b/src/Features/JsonPatch/test/Internal/DynamicObjectAdapterTest.cs new file mode 100644 index 0000000000..96b1aee935 --- /dev/null +++ b/src/Features/JsonPatch/test/Internal/DynamicObjectAdapterTest.cs @@ -0,0 +1,274 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class DynamicObjectAdapterTest + { + [Fact] + public void TryAdd_AddsNewProperty() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act + var status = adapter.TryAdd(target, segment, resolver, "new", out string errorMessage); + + // Assert + Assert.True(status); + Assert.Null(errorMessage); + Assert.Equal("new", target.NewProperty); + } + + [Fact] + public void TryAdd_ReplacesExistingPropertyValue() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + target.List = new List() { 1, 2, 3 }; + var value = new List() { "stringValue1", "stringValue2" }; + var segment = "List"; + var resolver = new DefaultContractResolver(); + + // Act + var status = adapter.TryAdd(target, segment, resolver, value, out string errorMessage); + + // Assert + Assert.True(status); + Assert.Null(errorMessage); + Assert.Equal(value, target.List); + } + + [Fact] + public void TryGet_GetsPropertyValue_ForExistingProperty() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act 1 + var addStatus = adapter.TryAdd(target, segment, resolver, "new", out string errorMessage); + + // Assert 1 + Assert.True(addStatus); + Assert.Null(errorMessage); + Assert.Equal("new", target.NewProperty); + + // Act 2 + var getStatus = adapter.TryGet(target, segment, resolver, out object getValue, out string getErrorMessage); + + // Assert 2 + Assert.True(getStatus); + Assert.Null(getErrorMessage); + Assert.Equal(getValue, target.NewProperty); + } + + [Fact] + public void TryGet_ThrowsPathNotFoundException_ForNonExistingProperty() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act + var getStatus = adapter.TryGet(target, segment, resolver, out object getValue, out string getErrorMessage); + + // Assert + Assert.False(getStatus); + Assert.Null(getValue); + Assert.Equal($"The target location specified by path segment '{segment}' was not found.", getErrorMessage); + } + + [Fact] + public void TryTraverse_FindsNextTarget() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + target.NestedObject = new DynamicTestObject(); + target.NestedObject.NewProperty = "A"; + var segment = "NestedObject"; + var resolver = new DefaultContractResolver(); + + // Act + var status = adapter.TryTraverse(target, segment, resolver, out object nextTarget, out string errorMessage); + + // Assert + Assert.True(status); + Assert.Null(errorMessage); + Assert.Equal(target.NestedObject, nextTarget); + } + + [Fact] + public void TryTraverse_ThrowsPathNotFoundException_ForNonExistingProperty() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + target.NestedObject = new DynamicTestObject(); + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act + var status = adapter.TryTraverse(target.NestedObject, segment, resolver, out object nextTarget, out string errorMessage); + + // Assert + Assert.False(status); + Assert.Equal($"The target location specified by path segment '{segment}' was not found.", errorMessage); + } + + [Fact] + public void TryReplace_ReplacesPropertyValue() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + target.NewProperty = new object(); + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act + var status = adapter.TryReplace(target, segment, resolver, "new", out string errorMessage); + + // Assert + Assert.True(status); + Assert.Null(errorMessage); + Assert.Equal("new", target.NewProperty); + } + + [Fact] + public void TryReplace_ThrowsPathNotFoundException_ForNonExistingProperty() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act + var status = adapter.TryReplace(target, segment, resolver, "test", out string errorMessage); + + // Assert + Assert.False(status); + Assert.Equal($"The target location specified by path segment '{segment}' was not found.", errorMessage); + } + + [Fact] + public void TryReplace_ThrowsPropertyInvalidException_IfNewValueIsNotTheSameTypeAsInitialValue() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + target.NewProperty = 1; + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act + var status = adapter.TryReplace(target, segment, resolver, "test", out string errorMessage); + + // Assert + Assert.False(status); + Assert.Equal($"The value 'test' is invalid for target location.", errorMessage); + } + + [Theory] + [InlineData(1, 0)] + [InlineData("new", null)] + public void TryRemove_SetsPropertyToDefaultOrNull(object value, object expectedValue) + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act 1 + var addStatus = adapter.TryAdd(target, segment, resolver, value, out string errorMessage); + + // Assert 1 + Assert.True(addStatus); + Assert.Null(errorMessage); + Assert.Equal(value, target.NewProperty); + + // Act 2 + var removeStatus = adapter.TryRemove(target, segment, resolver, out string removeErrorMessage); + + // Assert 2 + Assert.True(removeStatus); + Assert.Null(removeErrorMessage); + Assert.Equal(expectedValue, target.NewProperty); + } + + [Fact] + public void TryRemove_ThrowsPathNotFoundException_ForNonExistingProperty() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act + var removeStatus = adapter.TryRemove(target, segment, resolver, out string removeErrorMessage); + + // Assert + Assert.False(removeStatus); + Assert.Equal($"The target location specified by path segment '{segment}' was not found.", removeErrorMessage); + } + + [Fact] + public void TryTest_DoesNotThrowException_IfTestSuccessful() + { + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + var value = new List() + { + "Joana", + 2, + new Customer("Joana", 25) + }; + target.NewProperty = value; + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act + var testStatus = adapter.TryTest(target, segment, resolver, value, out string errorMessage); + + // Assert + Assert.Equal(value, target.NewProperty); + Assert.True(testStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryTest_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + target.NewProperty = "Joana"; + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + var expectedErrorMessage = $"The current value 'Joana' at path '{segment}' is not equal to the test value 'John'."; + + // Act + var testStatus = adapter.TryTest(target, segment, resolver, "John", out string errorMessage); + + // Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + } +} diff --git a/src/Features/JsonPatch/test/Internal/ListAdapterTest.cs b/src/Features/JsonPatch/test/Internal/ListAdapterTest.cs new file mode 100644 index 0000000000..f31e57541b --- /dev/null +++ b/src/Features/JsonPatch/test/Internal/ListAdapterTest.cs @@ -0,0 +1,497 @@ +// 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(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "0", resolver.Object, "40", out var message); + + // Assert + Assert.False(addStatus); + Assert.Equal($"The type '{targetObject.GetType().FullName}' which is an array is not supported for json patch operations as it has a fixed size.", 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(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "40", out var message); + + // Assert + Assert.False(addStatus); + Assert.Equal($"The type '{targetObject.GetType().FullName}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.", message); + } + + [Fact] + public void Add_WithIndexSameAsNumberOfElements_Works() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { "James", "Mike" }; + var listAdapter = new ListAdapter(); + var position = targetObject.Count.ToString(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "Rob", out var message); + + // Assert + Assert.Null(message); + Assert.True(addStatus); + Assert.Equal(3, targetObject.Count); + Assert.Equal(new List() { "James", "Mike", "Rob" }, targetObject); + } + + [Theory] + [InlineData("-1")] + [InlineData("-2")] + [InlineData("3")] + public void Add_WithOutOfBoundsIndex_Fails(string position) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { "James", "Mike" }; + var listAdapter = new ListAdapter(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out var message); + + // Assert + Assert.False(addStatus); + Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", 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(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out var message); + + // Assert + Assert.False(addStatus); + Assert.Equal($"The path segment '{position}' is invalid for an array index.", 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(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "20", out var 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" }; + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, value: null, errorMessage: out var 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_CompatibleTypeWorks() + { + // Arrange + var sDto = new SimpleObject(); + var iDto = new InheritedObject(); + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { sDto }; + var listAdapter = new ListAdapter(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, iDto, out var message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(2, targetObject.Count); + Assert.Equal(new List() { sDto, iDto }, targetObject); + } + + [Fact] + public void Add_NonCompatibleType_Fails() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "James", out var message); + + // Assert + Assert.False(addStatus); + Assert.Equal("The value 'James' is invalid for target location.", 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(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, value, out var message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(expected.Count, targetObject.Count); + Assert.Equal(expected, targetObject); + } + + public static TheoryData AddingKeepsObjectReferenceData + { + get + { + var sDto1 = new SimpleObject(); + var sDto2 = new SimpleObject(); + var sDto3 = new SimpleObject(); + return new TheoryData() + { + { + new List() { }, + sDto1, + "-", + new List() { sDto1 } + }, + { + new List() { sDto1, sDto2 }, + sDto3, + "-", + new List() { sDto1, sDto2, sDto3 } + }, + { + new List() { sDto1, sDto2 }, + sDto3, + "0", + new List() { sDto3, sDto1, sDto2 } + }, + { + new List() { sDto1, sDto2 }, + sDto3, + "1", + new List() { sDto1, sDto3, sDto2 } + } + }; + } + } + + [Theory] + [MemberData(nameof(AddingKeepsObjectReferenceData))] + public void Add_KeepsObjectReference(IList targetObject, object value, string position, IList expected) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var listAdapter = new ListAdapter(); + + // Act + var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, value, out var message); + + // Assert + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(expected.Count, targetObject.Count); + Assert.Equal(expected, targetObject); + } + + [Theory] + [InlineData(new int[] { }, "0")] + [InlineData(new[] { 10, 20 }, "-1")] + [InlineData(new[] { 10, 20 }, "2")] + public void Get_IndexOutOfBounds(int[] input, string position) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List(input); + var listAdapter = new ListAdapter(); + + // Act + var getStatus = listAdapter.TryGet(targetObject, position, resolver.Object, out var value, out var message); + + // Assert + Assert.False(getStatus); + Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", message); + } + + [Theory] + [InlineData(new[] { 10, 20 }, "0", 10)] + [InlineData(new[] { 10, 20 }, "1", 20)] + [InlineData(new[] { 10 }, "0", 10)] + public void Get(int[] input, string position, object expected) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List(input); + var listAdapter = new ListAdapter(); + + // Act + var getStatus = listAdapter.TryGet(targetObject, position, resolver.Object, out var value, out var message); + + // Assert + Assert.True(getStatus); + Assert.Equal(expected, value); + Assert.Equal(new List(input), targetObject); + } + + [Theory] + [InlineData(new int[] { }, "0")] + [InlineData(new[] { 10, 20 }, "-1")] + [InlineData(new[] { 10, 20 }, "2")] + public void Remove_IndexOutOfBounds(int[] input, string position) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List(input); + var listAdapter = new ListAdapter(); + + // Act + var removeStatus = listAdapter.TryRemove(targetObject, position, resolver.Object, out var message); + + // Assert + Assert.False(removeStatus); + Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", message); + } + + [Theory] + [InlineData(new[] { 10, 20 }, "0", new[] { 20 })] + [InlineData(new[] { 10, 20 }, "1", new[] { 10 })] + [InlineData(new[] { 10 }, "0", new int[] { })] + public void Remove(int[] input, string position, int[] expected) + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List(input); + var listAdapter = new ListAdapter(); + + // Act + var removeStatus = listAdapter.TryRemove(targetObject, position, resolver.Object, out var message); + + // Assert + Assert.True(removeStatus); + Assert.Equal(new List(expected), targetObject); + } + + [Fact] + public void Replace_NonCompatibleType_Fails() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + + // Act + var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "James", out var message); + + // Assert + Assert.False(replaceStatus); + Assert.Equal("The value 'James' is invalid for target location.", message); + } + + [Fact] + public void Replace_ReplacesValue_AtTheEnd() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + + // Act + var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "30", out var 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(); + + // Act + var replaceStatus = listAdapter.TryReplace(targetObject, position, resolver.Object, "30", out var message); + + // Assert + Assert.True(replaceStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Equal(expected, targetObject); + } + + [Fact] + public void Test_DoesNotThrowException_IfTestIsSuccessful() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + + // Act + var testStatus = listAdapter.TryTest(targetObject, "0", resolver.Object, "10", out var message); + + //Assert + Assert.True(testStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + } + + [Fact] + public void Test_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + var expectedErrorMessage = "The current value '20' at position '1' is not equal to the test value '10'."; + + // Act + var testStatus = listAdapter.TryTest(targetObject, "1", resolver.Object, "10", out var errorMessage); + + //Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void Test_ThrowsJsonPatchException_IfListPositionOutOfBounds() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + var expectedErrorMessage = "The index value provided by path segment '2' is out of bounds of the array size."; + + // Act + var testStatus = listAdapter.TryTest(targetObject, "2", resolver.Object, "10", out var errorMessage); + + //Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + } +} diff --git a/src/Features/JsonPatch/test/Internal/ObjectVisitorTest.cs b/src/Features/JsonPatch/test/Internal/ObjectVisitorTest.cs new file mode 100644 index 0000000000..cb299cf0dc --- /dev/null +++ b/src/Features/JsonPatch/test/Internal/ObjectVisitorTest.cs @@ -0,0 +1,224 @@ +// 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()); + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var 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()); + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(expectedTargetObject, targetObject); + Assert.Equal(typeof(DictionaryAdapter), adapter.GetType()); + } + + public static IEnumerable ReturnsExpandoAdapterData + { + get + { + 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 contractResolver = new DefaultContractResolver(); + var visitor = new ObjectVisitor(new ParsedPath(path), contractResolver); + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.Same(expectedTargetObject, targetObject); + Assert.Same(typeof(DictionaryAdapter), adapter.GetType()); + } + + 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()); + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var 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; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); + + // Assert + Assert.False(visitStatus); + Assert.Equal($"The index value provided by path segment '{position}' is out of bounds of the array size.", 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; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); + + // Assert + Assert.False(visitStatus); + Assert.Equal($"The path segment '{position}' is invalid for an array index.", message); + } + + [Fact] + public void Visit_DoesNotValidate_FinalPathSegment() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath($"/NonExisting"), new DefaultContractResolver()); + var model = new Class1(); + object targetObject = model; + + // Act + var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); + + // Assert + Assert.True(visitStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + Assert.IsType(adapter); + } + + [Fact] + public void Visit_NullTarget_ReturnsNullAdapter() + { + // Arrange + var visitor = new ObjectVisitor(new ParsedPath("test"), new DefaultContractResolver()); + + // Act + object target = null; + var visitStatus = visitor.TryVisit(ref target, out var adapter, out var message); + + // Assert + Assert.False(visitStatus); + Assert.Null(adapter); + Assert.Null(message); + } + } +} diff --git a/src/Features/JsonPatch/test/Internal/ParsedPathTests.cs b/src/Features/JsonPatch/test/Internal/ParsedPathTests.cs new file mode 100644 index 0000000000..c23abcdb5e --- /dev/null +++ b/src/Features/JsonPatch/test/Internal/ParsedPathTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class ParsedPathTests + { + [Theory] + [InlineData("foo/bar~0baz", new string[] { "foo", "bar~baz" })] + [InlineData("foo/bar~00baz", new string[] { "foo", "bar~0baz" })] + [InlineData("foo/bar~01baz", new string[] { "foo", "bar~1baz" })] + [InlineData("foo/bar~10baz", new string[] { "foo", "bar/0baz" })] + [InlineData("foo/bar~1baz", new string[] { "foo", "bar/baz" })] + [InlineData("foo/bar~0/~0/~1~1/~0~0/baz", new string[] { "foo", "bar~", "~", "//", "~~", "baz" })] + [InlineData("~0~1foo", new string[] { "~/foo" })] + public void ParsingValidPathShouldSucceed(string path, string[] expected) + { + // Arrange & Act + var parsedPath = new ParsedPath(path); + + // Assert + Assert.Equal(expected, parsedPath.Segments); + } + + [Theory] + [InlineData("foo/bar~")] + [InlineData("~")] + [InlineData("~2")] + [InlineData("foo~3bar")] + public void PathWithInvalidEscapeSequenceShouldFail(string path) + { + // Arrange, Act & Assert + Assert.Throws(() => + { + var parsedPath = new ParsedPath(path); + }); + } + } +} diff --git a/src/Features/JsonPatch/test/Internal/PocoAdapterTest.cs b/src/Features/JsonPatch/test/Internal/PocoAdapterTest.cs new file mode 100644 index 0000000000..9a31ea11a8 --- /dev/null +++ b/src/Features/JsonPatch/test/Internal/PocoAdapterTest.cs @@ -0,0 +1,241 @@ +// 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; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class PocoAdapterTest + { + [Fact] + public void TryAdd_ReplacesExistingProperty() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var addStatus = adapter.TryAdd(model, "Name", contractResolver, "John", out var errorMessage); + + // Assert + Assert.Equal("John", model.Name); + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryAdd_ThrowsJsonPatchException_IfPropertyDoesNotExist() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found."; + + // Act + var addStatus = adapter.TryAdd(model, "LastName", contractResolver, "Smith", out var errorMessage); + + // Assert + Assert.False(addStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryGet_ExistingProperty() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var getStatus = adapter.TryGet(model, "Name", contractResolver, out var value, out var errorMessage); + + // Assert + Assert.Equal("Joana", value); + Assert.True(getStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryGet_ThrowsJsonPatchException_IfPropertyDoesNotExist() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found."; + + // Act + var getStatus = adapter.TryGet(model, "LastName", contractResolver, out var value, out var errorMessage); + + // Assert + Assert.Null(value); + Assert.False(getStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryRemove_SetsPropertyToNull() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var removeStatus = adapter.TryRemove(model, "Name", contractResolver, out var errorMessage); + + // Assert + Assert.Null(model.Name); + Assert.True(removeStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryRemove_ThrowsJsonPatchException_IfPropertyDoesNotExist() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found."; + + // Act + var removeStatus = adapter.TryRemove(model, "LastName", contractResolver, out var errorMessage); + + // Assert + Assert.False(removeStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryReplace_OverwritesExistingValue() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var replaceStatus = adapter.TryReplace(model, "Name", contractResolver, "John", out var errorMessage); + + // Assert + Assert.Equal("John", model.Name); + Assert.True(replaceStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryReplace_ThrowsJsonPatchException_IfNewValueIsInvalidType() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Age = 25 + }; + + var expectedErrorMessage = "The value 'TwentySix' is invalid for target location."; + + // Act + var replaceStatus = adapter.TryReplace(model, "Age", contractResolver, "TwentySix", out var errorMessage); + + // Assert + Assert.Equal(25, model.Age); + Assert.False(replaceStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryReplace_ThrowsJsonPatchException_IfPropertyDoesNotExist() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found."; + + // Act + var replaceStatus = adapter.TryReplace(model, "LastName", contractResolver, "Smith", out var errorMessage); + + // Assert + Assert.Equal("Joana", model.Name); + Assert.False(replaceStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryTest_DoesNotThrowException_IfTestSuccessful() + { + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var testStatus = adapter.TryTest(model, "Name", contractResolver, "Joana", out var errorMessage); + + // Assert + Assert.Equal("Joana", model.Name); + Assert.True(testStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryTest_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The current value 'Joana' at path 'Name' is not equal to the test value 'John'."; + + // Act + var testStatus = adapter.TryTest(model, "Name", contractResolver, "John", out var errorMessage); + + // Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + private class Customer + { + public string Name { get; set; } + + public int Age { get; set; } + } + } +} diff --git a/src/Features/JsonPatch/test/JsonPatchDocumentGetPathTest.cs b/src/Features/JsonPatch/test/JsonPatchDocumentGetPathTest.cs new file mode 100644 index 0000000000..266202c7a5 --- /dev/null +++ b/src/Features/JsonPatch/test/JsonPatchDocumentGetPathTest.cs @@ -0,0 +1,122 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class JsonPatchDocumentGetPathTest + { + [Fact] + public void ExpressionType_MemberAccess() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + var path = patchDocument.GetPath(p => p.SimpleObject.IntegerList, "-"); + + // Assert + Assert.Equal("/SimpleObject/IntegerList/-", path); + } + + [Fact] + public void ExpressionType_ArrayIndex() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + var path = patchDocument.GetPath(p => p[3], null); + + // Assert + Assert.Equal("/3", path); + } + + [Fact] + public void ExpressionType_Call() + { + // Arrange + var patchDocument = new JsonPatchDocument>(); + + // Act + var path = patchDocument.GetPath(p => p["key"], "3"); + + // Assert + Assert.Equal("/key/3", path); + } + + [Fact] + public void ExpressionType_Parameter_NullPosition() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + var path = patchDocument.GetPath(p => p, null); + + // Assert + Assert.Equal("/", path); + } + + [Fact] + public void ExpressionType_Parameter_WithPosition() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + var path = patchDocument.GetPath(p => p, "-"); + + // Assert + Assert.Equal("/-", path); + } + + [Fact] + public void ExpressionType_Convert() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + var path = patchDocument.GetPath(p => (BaseClass)p.DerivedObject, null); + + // Assert + Assert.Equal("/DerivedObject", path); + } + + [Fact] + public void ExpressionType_NotSupported() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.GetPath(p => p.IntegerValue >= 4, null); + }); + + // Assert + Assert.Equal("The expression '(p.IntegerValue >= 4)' is not supported. Supported expressions include member access and indexer expressions.", exception.Message); + } + } + + internal class DerivedClass : BaseClass + { + public DerivedClass() + { + } + } + + internal class NestedObjectWithDerivedClass + { + public DerivedClass DerivedObject { get; set; } + } + + internal class BaseClass + { + } +} diff --git a/src/Features/JsonPatch/test/JsonPatchDocumentJsonPropertyAttributeTest.cs b/src/Features/JsonPatch/test/JsonPatchDocumentJsonPropertyAttributeTest.cs new file mode 100644 index 0000000000..a9c3d8500b --- /dev/null +++ b/src/Features/JsonPatch/test/JsonPatchDocumentJsonPropertyAttributeTest.cs @@ -0,0 +1,90 @@ +// 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.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class JsonPatchDocumentJsonPropertyAttributeTest + { + [Fact] + public void Add_RespectsJsonPropertyAttribute() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + patchDocument.Add(p => p.Name, "John"); + + // Assert + var pathToCheck = patchDocument.Operations.First().path; + Assert.Equal("/AnotherName", pathToCheck); + } + + [Fact] + public void Add_RespectsJsonPropertyAttribute_WithDotWhitespaceAndBackslashInName() + { + // Arrange + var obj = new JsonPropertyObjectWithStrangeNames(); + var patchDocument = new JsonPatchDocument(); + + // Act + patchDocument.Add("/First Name.", "John"); + patchDocument.Add("Last\\Name", "Doe"); + patchDocument.ApplyTo(obj); + + // Assert + Assert.Equal("John", obj.FirstName); + Assert.Equal("Doe", obj.LastName); + } + + [Fact] + public void Move_FallsbackToPropertyName_WhenJsonPropertyAttributeName_IsEmpty() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + patchDocument.Move(m => m.StringProperty, m => m.StringProperty2); + + // Assert + var fromPath = patchDocument.Operations.First().from; + Assert.Equal("/StringProperty", fromPath); + var toPath = patchDocument.Operations.First().path; + Assert.Equal("/StringProperty2", toPath); + } + + private class JsonPropertyObject + { + [JsonProperty("AnotherName")] + public string Name { get; set; } + } + + private class JsonPropertyObjectWithStrangeNames + { + [JsonProperty("First Name.")] + public string FirstName { get; set; } + + [JsonProperty("Last\\Name")] + public string LastName { get; set; } + } + + private class JsonPropertyWithNoPropertyName + { + [JsonProperty] + public string StringProperty { get; set; } + + [JsonProperty] + public string[] ArrayProperty { get; set; } + + [JsonProperty] + public string StringProperty2 { get; set; } + + [JsonProperty] + public string SSN { get; set; } + } + } +} diff --git a/src/Features/JsonPatch/test/JsonPatchDocumentTest.cs b/src/Features/JsonPatch/test/JsonPatchDocumentTest.cs new file mode 100644 index 0000000000..197e514cee --- /dev/null +++ b/src/Features/JsonPatch/test/JsonPatchDocumentTest.cs @@ -0,0 +1,162 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class JsonPatchDocumentTest + { + [Fact] + public void InvalidPathAtBeginningShouldThrowException() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.Add("//NewInt", 1); + }); + + // Assert + Assert.Equal( + "The provided string '//NewInt' is an invalid path.", + exception.Message); + } + + [Fact] + public void InvalidPathAtEndShouldThrowException() + { + // Arrange + var patchDocument = new JsonPatchDocument(); + + // Act + var exception = Assert.Throws(() => + { + patchDocument.Add("NewInt//", 1); + }); + + // Assert + Assert.Equal( + "The provided string 'NewInt//' is an invalid path.", + exception.Message); + } + + [Fact] + public void NonGenericPatchDocToGenericMustSerialize() + { + // Arrange + var targetObject = new SimpleObject() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Copy("StringProperty", "AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDocument); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Act + deserialized.ApplyTo(targetObject); + + // Assert + Assert.Equal("A", targetObject.AnotherStringProperty); + } + + [Fact] + public void GenericPatchDocToNonGenericMustSerialize() + { + // Arrange + var targetObject = new SimpleObject() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + var patchDocTyped = new JsonPatchDocument(); + patchDocTyped.Copy(o => o.StringProperty, o => o.AnotherStringProperty); + + var patchDocUntyped = new JsonPatchDocument(); + patchDocUntyped.Copy("StringProperty", "AnotherStringProperty"); + + var serializedTyped = JsonConvert.SerializeObject(patchDocTyped); + var serializedUntyped = JsonConvert.SerializeObject(patchDocUntyped); + var deserialized = JsonConvert.DeserializeObject(serializedTyped); + + // Act + deserialized.ApplyTo(targetObject); + + // Assert + Assert.Equal("A", targetObject.AnotherStringProperty); + } + + [Fact] + public void Deserialization_Successful_ForValidJsonPatchDocument() + { + // Arrange + var doc = new SimpleObject() + { + StringProperty = "A", + DecimalValue = 10, + DoubleValue = 10, + FloatValue = 10, + IntegerValue = 10 + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Replace(o => o.StringProperty, "B"); + patchDocument.Replace(o => o.DecimalValue, 12); + patchDocument.Replace(o => o.DoubleValue, 12); + patchDocument.Replace(o => o.FloatValue, 12); + patchDocument.Replace(o => o.IntegerValue, 12); + + // default: no envelope + var serialized = JsonConvert.SerializeObject(patchDocument); + + // Act + var deserialized = JsonConvert.DeserializeObject>(serialized); + + // Assert + Assert.IsType>(deserialized); + } + + [Fact] + public void Deserialization_Fails_ForInvalidJsonPatchDocument() + { + // Arrange + var serialized = "{\"Operations\": [{ \"op\": \"replace\", \"path\": \"/title\", \"value\": \"New Title\"}]}"; + + // Act + var exception = Assert.Throws(() => + { + var deserialized + = JsonConvert.DeserializeObject(serialized); + }); + + // Assert + Assert.Equal("The JSON patch document was malformed and could not be parsed.", exception.Message); + } + + [Fact] + public void Deserialization_Fails_ForInvalidTypedJsonPatchDocument() + { + // Arrange + var serialized = "{\"Operations\": [{ \"op\": \"replace\", \"path\": \"/title\", \"value\": \"New Title\"}]}"; + + // Act + var exception = Assert.Throws(() => + { + var deserialized + = JsonConvert.DeserializeObject>(serialized); + }); + + // Assert + Assert.Equal("The JSON patch document was malformed and could not be parsed.", exception.Message); + } + } +} diff --git a/src/Features/JsonPatch/test/Microsoft.AspNetCore.JsonPatch.Tests.csproj b/src/Features/JsonPatch/test/Microsoft.AspNetCore.JsonPatch.Tests.csproj new file mode 100644 index 0000000000..5aca855c9c --- /dev/null +++ b/src/Features/JsonPatch/test/Microsoft.AspNetCore.JsonPatch.Tests.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp2.1;net461 + + + + + + + diff --git a/src/Features/JsonPatch/test/OperationBaseTests.cs b/src/Features/JsonPatch/test/OperationBaseTests.cs new file mode 100644 index 0000000000..955344404f --- /dev/null +++ b/src/Features/JsonPatch/test/OperationBaseTests.cs @@ -0,0 +1,41 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Operations +{ + public class OperationBaseTests + { + [Theory] + [InlineData("ADd", OperationType.Add)] + [InlineData("Copy", OperationType.Copy)] + [InlineData("mOVE", OperationType.Move)] + [InlineData("REMOVE", OperationType.Remove)] + [InlineData("replace", OperationType.Replace)] + [InlineData("TeSt", OperationType.Test)] + public void SetValidOperationType(string op, OperationType operationType) + { + // Arrange + var operationBase = new OperationBase(); + operationBase.op = op; + + // Act & Assert + Assert.Equal(operationType, operationBase.OperationType); + } + + [Theory] + [InlineData("invalid", OperationType.Invalid)] + [InlineData("coppy", OperationType.Invalid)] + [InlineData("notvalid", OperationType.Invalid)] + public void InvalidOperationType_SetsOperationTypeInvalid(string op, OperationType operationType) + { + // Arrange + var operationBase = new OperationBase(); + operationBase.op = op; + + // Act & Assert + Assert.Equal(operationType, operationBase.OperationType); + } + } +} diff --git a/src/Features/JsonPatch/test/TestErrorLogger.cs b/src/Features/JsonPatch/test/TestErrorLogger.cs new file mode 100644 index 0000000000..2cd6a4453e --- /dev/null +++ b/src/Features/JsonPatch/test/TestErrorLogger.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class TestErrorLogger where T : class + { + public string ErrorMessage { get; set; } + + public void LogErrorMessage(JsonPatchError patchError) + { + ErrorMessage = patchError.ErrorMessage; + } + } +} diff --git a/src/Features/JsonPatch/test/TestObjectModels/Customer.cs b/src/Features/JsonPatch/test/TestObjectModels/Customer.cs new file mode 100644 index 0000000000..c8a5aa22b1 --- /dev/null +++ b/src/Features/JsonPatch/test/TestObjectModels/Customer.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + internal class Customer + { + private string _name; + private int _age; + + public Customer(string name, int age) + { + _name = name; + _age = age; + } + } +} diff --git a/src/Features/JsonPatch/test/TestObjectModels/DynamicTestObject.cs b/src/Features/JsonPatch/test/TestObjectModels/DynamicTestObject.cs new file mode 100644 index 0000000000..94ecf1685d --- /dev/null +++ b/src/Features/JsonPatch/test/TestObjectModels/DynamicTestObject.cs @@ -0,0 +1,87 @@ +// 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; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class DynamicTestObject : DynamicObject + { + private Dictionary _dictionary = new Dictionary(); + + public object this[string key] { get => ((IDictionary)_dictionary)[key]; set => ((IDictionary)_dictionary)[key] = value; } + + public ICollection Keys => ((IDictionary)_dictionary).Keys; + + public ICollection Values => ((IDictionary)_dictionary).Values; + + public int Count => ((IDictionary)_dictionary).Count; + + public bool IsReadOnly => ((IDictionary)_dictionary).IsReadOnly; + + public void Add(string key, object value) + { + ((IDictionary)_dictionary).Add(key, value); + } + + public void Add(KeyValuePair item) + { + ((IDictionary)_dictionary).Add(item); + } + + public void Clear() + { + ((IDictionary)_dictionary).Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((IDictionary)_dictionary).Contains(item); + } + + public bool ContainsKey(string key) + { + return ((IDictionary)_dictionary).ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)_dictionary).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return ((IDictionary)_dictionary).GetEnumerator(); + } + + public bool Remove(string key) + { + return ((IDictionary)_dictionary).Remove(key); + } + + public bool Remove(KeyValuePair item) + { + return ((IDictionary)_dictionary).Remove(item); + } + + public bool TryGetValue(string key, out object value) + { + return ((IDictionary)_dictionary).TryGetValue(key, out value); + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var name = binder.Name; + + return TryGetValue(name, out result); + } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + _dictionary[binder.Name] = value; + + return true; + } + } +} diff --git a/src/Features/JsonPatch/test/TestObjectModels/InheritedObject.cs b/src/Features/JsonPatch/test/TestObjectModels/InheritedObject.cs new file mode 100644 index 0000000000..37b8a10dc9 --- /dev/null +++ b/src/Features/JsonPatch/test/TestObjectModels/InheritedObject.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class InheritedObject : SimpleObject + { + public string AdditionalStringProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/test/TestObjectModels/NestedObject.cs b/src/Features/JsonPatch/test/TestObjectModels/NestedObject.cs new file mode 100644 index 0000000000..1b42d0d7ef --- /dev/null +++ b/src/Features/JsonPatch/test/TestObjectModels/NestedObject.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class NestedObject + { + public string StringProperty { get; set; } + public dynamic DynamicProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/test/TestObjectModels/SimpleObject.cs b/src/Features/JsonPatch/test/TestObjectModels/SimpleObject.cs new file mode 100644 index 0000000000..651a91bdcf --- /dev/null +++ b/src/Features/JsonPatch/test/TestObjectModels/SimpleObject.cs @@ -0,0 +1,23 @@ +// 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 +{ + public class SimpleObject + { + public List SimpleObjectList { get; set; } + public List IntegerList { get; set; } + public IList IntegerIList { get; set; } + public int IntegerValue { get; set; } + public int AnotherIntegerValue { get; set; } + public string StringProperty { get; set; } + public string AnotherStringProperty { get; set; } + public decimal DecimalValue { get; set; } + public double DoubleValue { get; set; } + public float FloatValue { get; set; } + public Guid GuidValue { get; set; } + } +} \ No newline at end of file diff --git a/src/Features/JsonPatch/test/TestObjectModels/SimpleObjectWithNestedObject.cs b/src/Features/JsonPatch/test/TestObjectModels/SimpleObjectWithNestedObject.cs new file mode 100644 index 0000000000..4d7c0e2bd8 --- /dev/null +++ b/src/Features/JsonPatch/test/TestObjectModels/SimpleObjectWithNestedObject.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.JsonPatch +{ + public class SimpleObjectWithNestedObject + { + public int IntegerValue { get; set; } + + public NestedObject NestedObject { get; set; } + + public SimpleObject SimpleObject { get; set; } + + public InheritedObject InheritedObject { get; set; } + + public List SimpleObjectList { get; set; } + + public IList SimpleObjectIList { get; set; } + + public SimpleObjectWithNestedObject() + { + NestedObject = new NestedObject(); + SimpleObject = new SimpleObject(); + InheritedObject = new InheritedObject(); + SimpleObjectList = new List(); + } + } +} \ No newline at end of file