Merge the source code of aspnet/JsonPatch into this repo
This commit is contained in:
commit
fd6b46bc7f
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the operations that can be performed on a JSON patch document.
|
||||
/// </summary>
|
||||
public interface IObjectAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="operation">The add operation.</param>
|
||||
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
|
||||
void Add(Operation operation, object objectToApplyTo);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="operation">The copy operation.</param>
|
||||
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
|
||||
void Copy(Operation operation, object objectToApplyTo);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="operation">The move operation.</param>
|
||||
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
|
||||
void Move(Operation operation, object objectToApplyTo);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="operation">The remove operation.</param>
|
||||
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
|
||||
void Remove(Operation operation, object objectToApplyTo);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="operation">The replace operation.</param>
|
||||
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
|
||||
void Replace(Operation operation, object objectToApplyTo);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the operations that can be performed on a JSON patch document, including "test".
|
||||
/// </summary>
|
||||
public interface IObjectAdapterWithTest : IObjectAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="operation">The test operation.</param>
|
||||
/// <param name="objectToApplyTo">Object to apply the operation to.</param>
|
||||
void Test(Operation operation, object objectToApplyTo);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ObjectAdapter : IObjectAdapterWithTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ObjectAdapter"/>.
|
||||
/// </summary>
|
||||
/// <param name="contractResolver">The <see cref="IContractResolver"/>.</param>
|
||||
/// <param name="logErrorAction">The <see cref="Action"/> for logging <see cref="JsonPatchError"/>.</param>
|
||||
public ObjectAdapter(
|
||||
IContractResolver contractResolver,
|
||||
Action<JsonPatchError> logErrorAction)
|
||||
{
|
||||
ContractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver));
|
||||
LogErrorAction = logErrorAction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IContractResolver"/>.
|
||||
/// </summary>
|
||||
public IContractResolver ContractResolver { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Action for logging <see cref="JsonPatchError"/>.
|
||||
/// </summary>
|
||||
public Action<JsonPatchError> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<JsonPatchError> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Operation>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public class GetValueResult
|
||||
{
|
||||
public GetValueResult(object propertyValue, bool hasError)
|
||||
{
|
||||
PropertyValue = propertyValue;
|
||||
HasError = hasError;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The value of the property we're trying to get
|
||||
/// </summary>
|
||||
public object PropertyValue { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// HasError: true when an error occurred, the operation didn't complete succesfully
|
||||
/// </summary>
|
||||
public bool HasError { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata for JsonProperty.
|
||||
/// </summary>
|
||||
public class JsonPatchProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets JsonProperty.
|
||||
/// </summary>
|
||||
public JsonProperty Property { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets Parent.
|
||||
/// </summary>
|
||||
public object Parent { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Operation> GetOperations();
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T>
|
||||
return typeInfo.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
|
||||
}
|
||||
else
|
||||
{
|
||||
// reference types are always nullable
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TKey, TValue> : 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<TKey, TValue>)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<TKey, TValue>)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<TKey, TValue>)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<TKey, TValue>)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<TKey, TValue>)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<TKey, TValue>)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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
{
|
||||
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
|
||||
});
|
||||
|
||||
var callsite = CallSite<Func<CallSite, object, object>>.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>
|
||||
{
|
||||
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
|
||||
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
|
||||
});
|
||||
|
||||
var callsite = CallSite<Func<CallSite, object, object, object>>.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<JsonPatchError> Default = (error) =>
|
||||
{
|
||||
throw new JsonPatchException(error);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> Segments => _segments ?? Empty;
|
||||
|
||||
private static string[] ParsePath(string path)
|
||||
{
|
||||
var strings = new List<string>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Operation> Operations { get; private set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IContractResolver ContractResolver { get; set; }
|
||||
|
||||
public JsonPatchDocument()
|
||||
{
|
||||
Operations = new List<Operation>();
|
||||
ContractResolver = new DefaultContractResolver();
|
||||
}
|
||||
|
||||
public JsonPatchDocument(List<Operation> operations, IContractResolver contractResolver)
|
||||
{
|
||||
if (operations == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(operations));
|
||||
}
|
||||
|
||||
if (contractResolver == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(contractResolver));
|
||||
}
|
||||
|
||||
Operations = operations;
|
||||
ContractResolver = contractResolver;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add operation. Will result in, for example,
|
||||
/// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
|
||||
/// </summary>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove value at target location. Will result in, for example,
|
||||
/// { "op": "remove", "path": "/a/b/c" }
|
||||
/// </summary>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace value. Will result in, for example,
|
||||
/// { "op": "replace", "path": "/a/b/c", "value": 42 }
|
||||
/// </summary>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test value. Will result in, for example,
|
||||
/// { "op": "test", "path": "/a/b/c", "value": 42 }
|
||||
/// </summary>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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" }
|
||||
/// </summary>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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" }
|
||||
/// </summary>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply this JsonPatchDocument
|
||||
/// </summary>
|
||||
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
|
||||
public void ApplyTo(object objectToApplyTo)
|
||||
{
|
||||
if (objectToApplyTo == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(objectToApplyTo));
|
||||
}
|
||||
|
||||
ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply this JsonPatchDocument
|
||||
/// </summary>
|
||||
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
|
||||
/// <param name="logErrorAction">Action to log errors</param>
|
||||
public void ApplyTo(object objectToApplyTo, Action<JsonPatchError> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply this JsonPatchDocument
|
||||
/// </summary>
|
||||
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
|
||||
/// <param name="adapter">IObjectAdapter instance to use when applying</param>
|
||||
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<Operation> IJsonPatchDocument.GetOperations()
|
||||
{
|
||||
var allOps = new List<Operation>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TModel> : IJsonPatchDocument where TModel : class
|
||||
{
|
||||
public List<Operation<TModel>> Operations { get; private set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IContractResolver ContractResolver { get; set; }
|
||||
|
||||
public JsonPatchDocument()
|
||||
{
|
||||
Operations = new List<Operation<TModel>>();
|
||||
ContractResolver = new DefaultContractResolver();
|
||||
}
|
||||
|
||||
// Create from list of operations
|
||||
public JsonPatchDocument(List<Operation<TModel>> operations, IContractResolver contractResolver)
|
||||
{
|
||||
Operations = operations ?? throw new ArgumentNullException(nameof(operations));
|
||||
ContractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add operation. Will result in, for example,
|
||||
/// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp">value type</typeparam>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Add<TProp>(Expression<Func<TModel, TProp>> path, TProp value)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"add",
|
||||
GetPath(path, null),
|
||||
from: null,
|
||||
value: value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add value to list at given position
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp">value type</typeparam>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <param name="position">position</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Add<TProp>(
|
||||
Expression<Func<TModel, IList<TProp>>> path,
|
||||
TProp value,
|
||||
int position)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"add",
|
||||
GetPath(path, position.ToString()),
|
||||
from: null,
|
||||
value: value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add value to the end of the list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp">value type</typeparam>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Add<TProp>(Expression<Func<TModel, IList<TProp>>> path, TProp value)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"add",
|
||||
GetPath(path, "-"),
|
||||
from: null,
|
||||
value: value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove value at target location. Will result in, for example,
|
||||
/// { "op": "remove", "path": "/a/b/c" }
|
||||
/// </summary>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Remove<TProp>(Expression<Func<TModel, TProp>> path)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>("remove", GetPath(path, null), from: null));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove value from list at given position
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp">value type</typeparam>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="position">position</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Remove<TProp>(Expression<Func<TModel, IList<TProp>>> path, int position)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"remove",
|
||||
GetPath(path, position.ToString()),
|
||||
from: null));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove value from end of list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp">value type</typeparam>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Remove<TProp>(Expression<Func<TModel, IList<TProp>>> path)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"remove",
|
||||
GetPath(path, "-"),
|
||||
from: null));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace value. Will result in, for example,
|
||||
/// { "op": "replace", "path": "/a/b/c", "value": 42 }
|
||||
/// </summary>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Replace<TProp>(Expression<Func<TModel, TProp>> path, TProp value)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"replace",
|
||||
GetPath(path, null),
|
||||
from: null,
|
||||
value: value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace value in a list at given position
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp">value type</typeparam>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <param name="position">position</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Replace<TProp>(Expression<Func<TModel, IList<TProp>>> path,
|
||||
TProp value, int position)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"replace",
|
||||
GetPath(path, position.ToString()),
|
||||
from: null,
|
||||
value: value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace value at end of a list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp">value type</typeparam>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Replace<TProp>(Expression<Func<TModel, IList<TProp>>> path, TProp value)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"replace",
|
||||
GetPath(path, "-"),
|
||||
from: null,
|
||||
value: value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test value. Will result in, for example,
|
||||
/// { "op": "test", "path": "/a/b/c", "value": 42 }
|
||||
/// </summary>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Test<TProp>(Expression<Func<TModel, TProp>> path, TProp value)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"test",
|
||||
GetPath(path, null),
|
||||
from: null,
|
||||
value: value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test value in a list at given position
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp">value type</typeparam>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <param name="position">position</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Test<TProp>(Expression<Func<TModel, IList<TProp>>> path,
|
||||
TProp value, int position)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"test",
|
||||
GetPath(path, position.ToString()),
|
||||
from: null,
|
||||
value: value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test value at end of a list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp">value type</typeparam>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="value">value</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Test<TProp>(Expression<Func<TModel, IList<TProp>>> path, TProp value)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"test",
|
||||
GetPath(path, "-"),
|
||||
from: null,
|
||||
value: value));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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" }
|
||||
/// </summary>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Move<TProp>(
|
||||
Expression<Func<TModel, TProp>> from,
|
||||
Expression<Func<TModel, TProp>> path)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"move",
|
||||
GetPath(path, null),
|
||||
GetPath(from, null)));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move from a position in a list to a new location
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="positionFrom">position</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Move<TProp>(
|
||||
Expression<Func<TModel, IList<TProp>>> from,
|
||||
int positionFrom,
|
||||
Expression<Func<TModel, TProp>> path)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"move",
|
||||
GetPath(path, null),
|
||||
GetPath(from, positionFrom.ToString())));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move from a property to a location in a list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="positionTo">position</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Move<TProp>(
|
||||
Expression<Func<TModel, TProp>> from,
|
||||
Expression<Func<TModel, IList<TProp>>> path,
|
||||
int positionTo)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"move",
|
||||
GetPath(path, positionTo.ToString()),
|
||||
GetPath(from, null)));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move from a position in a list to another location in a list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="positionFrom">position (source)</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="positionTo">position (target)</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Move<TProp>(
|
||||
Expression<Func<TModel, IList<TProp>>> from,
|
||||
int positionFrom,
|
||||
Expression<Func<TModel, IList<TProp>>> path,
|
||||
int positionTo)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"move",
|
||||
GetPath(path, positionTo.ToString()),
|
||||
GetPath(from, positionFrom.ToString())));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move from a position in a list to the end of another list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="positionFrom">position</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Move<TProp>(
|
||||
Expression<Func<TModel, IList<TProp>>> from,
|
||||
int positionFrom,
|
||||
Expression<Func<TModel, IList<TProp>>> path)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"move",
|
||||
GetPath(path, "-"),
|
||||
GetPath(from, positionFrom.ToString())));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move to the end of a list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Move<TProp>(
|
||||
Expression<Func<TModel, TProp>> from,
|
||||
Expression<Func<TModel, IList<TProp>>> path)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"move",
|
||||
GetPath(path, "-"),
|
||||
GetPath(from, null)));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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" }
|
||||
/// </summary>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Copy<TProp>(
|
||||
Expression<Func<TModel, TProp>> from,
|
||||
Expression<Func<TModel, TProp>> path)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"copy",
|
||||
GetPath(path, null),
|
||||
GetPath(from, null)));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy from a position in a list to a new location
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="positionFrom">position</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Copy<TProp>(
|
||||
Expression<Func<TModel, IList<TProp>>> from,
|
||||
int positionFrom,
|
||||
Expression<Func<TModel, TProp>> path)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"copy",
|
||||
GetPath(path, null),
|
||||
GetPath(from, positionFrom.ToString())));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy from a property to a location in a list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="positionTo">position</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Copy<TProp>(
|
||||
Expression<Func<TModel, TProp>> from,
|
||||
Expression<Func<TModel, IList<TProp>>> path,
|
||||
int positionTo)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"copy",
|
||||
GetPath(path, positionTo.ToString()),
|
||||
GetPath(from, null)));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy from a position in a list to a new location in a list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="positionFrom">position (source)</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <param name="positionTo">position (target)</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Copy<TProp>(
|
||||
Expression<Func<TModel, IList<TProp>>> from,
|
||||
int positionFrom,
|
||||
Expression<Func<TModel, IList<TProp>>> path,
|
||||
int positionTo)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"copy",
|
||||
GetPath(path, positionTo.ToString()),
|
||||
GetPath(from, positionFrom.ToString())));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy from a position in a list to the end of another list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="positionFrom">position</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Copy<TProp>(
|
||||
Expression<Func<TModel, IList<TProp>>> from,
|
||||
int positionFrom,
|
||||
Expression<Func<TModel, IList<TProp>>> path)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"copy",
|
||||
GetPath(path, "-"),
|
||||
GetPath(from, positionFrom.ToString())));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy to the end of a list
|
||||
/// </summary>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <param name="from">source location</param>
|
||||
/// <param name="path">target location</param>
|
||||
/// <returns></returns>
|
||||
public JsonPatchDocument<TModel> Copy<TProp>(
|
||||
Expression<Func<TModel, TProp>> from,
|
||||
Expression<Func<TModel, IList<TProp>>> path)
|
||||
{
|
||||
if (from == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(from));
|
||||
}
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
Operations.Add(new Operation<TModel>(
|
||||
"copy",
|
||||
GetPath(path, "-"),
|
||||
GetPath(from, null)));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply this JsonPatchDocument
|
||||
/// </summary>
|
||||
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
|
||||
public void ApplyTo(TModel objectToApplyTo)
|
||||
{
|
||||
if (objectToApplyTo == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(objectToApplyTo));
|
||||
}
|
||||
|
||||
ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply this JsonPatchDocument
|
||||
/// </summary>
|
||||
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
|
||||
/// <param name="logErrorAction">Action to log errors</param>
|
||||
public void ApplyTo(TModel objectToApplyTo, Action<JsonPatchError> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply this JsonPatchDocument
|
||||
/// </summary>
|
||||
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
|
||||
/// <param name="adapter">IObjectAdapter instance to use when applying</param>
|
||||
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<Operation> IJsonPatchDocument.GetOperations()
|
||||
{
|
||||
var allOps = new List<Operation>();
|
||||
|
||||
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<TProp>(Expression<Func<TModel, TProp>> 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<string> GetPathSegments(Expression expr)
|
||||
{
|
||||
var listOfSegments = new List<string>();
|
||||
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<Func<object, object>>(converted, fakeParameter);
|
||||
var func = lambda.Compile();
|
||||
|
||||
return Convert.ToString(func(null), CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Captures error message and the related entity and the operation that caused it.
|
||||
/// </summary>
|
||||
public class JsonPatchError
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="JsonPatchError"/>.
|
||||
/// </summary>
|
||||
/// <param name="affectedObject">The object that is affected by the error.</param>
|
||||
/// <param name="operation">The <see cref="Operation"/> that caused the error.</param>
|
||||
/// <param name="errorMessage">The error message.</param>
|
||||
public JsonPatchError(
|
||||
object affectedObject,
|
||||
Operation operation,
|
||||
string errorMessage)
|
||||
{
|
||||
if (errorMessage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(errorMessage));
|
||||
}
|
||||
|
||||
AffectedObject = affectedObject;
|
||||
Operation = operation;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object that is affected by the error.
|
||||
/// </summary>
|
||||
public object AffectedObject { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Operation"/> that caused the error.
|
||||
/// </summary>
|
||||
public Operation Operation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error message.
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core support for JSON PATCH.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore;json;jsonpatch</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="Newtonsoft.Json" />
|
||||
<Reference Include="Microsoft.Extensions.ClosedGenericMatcher.Sources" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TModel> : 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
@ -0,0 +1,338 @@
|
|||
// <auto-generated />
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// The property at '{0}' could not be copied.
|
||||
/// </summary>
|
||||
internal static string CannotCopyProperty
|
||||
{
|
||||
get => GetString("CannotCopyProperty");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The property at '{0}' could not be copied.
|
||||
/// </summary>
|
||||
internal static string FormatCannotCopyProperty(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotCopyProperty"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The type of the property at path '{0}' could not be determined.
|
||||
/// </summary>
|
||||
internal static string CannotDeterminePropertyType
|
||||
{
|
||||
get => GetString("CannotDeterminePropertyType");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type of the property at path '{0}' could not be determined.
|
||||
/// </summary>
|
||||
internal static string FormatCannotDeterminePropertyType(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotDeterminePropertyType"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The '{0}' operation at path '{1}' could not be performed.
|
||||
/// </summary>
|
||||
internal static string CannotPerformOperation
|
||||
{
|
||||
get => GetString("CannotPerformOperation");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The '{0}' operation at path '{1}' could not be performed.
|
||||
/// </summary>
|
||||
internal static string FormatCannotPerformOperation(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotPerformOperation"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// The property at '{0}' could not be read.
|
||||
/// </summary>
|
||||
internal static string CannotReadProperty
|
||||
{
|
||||
get => GetString("CannotReadProperty");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The property at '{0}' could not be read.
|
||||
/// </summary>
|
||||
internal static string FormatCannotReadProperty(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotReadProperty"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The property at path '{0}' could not be updated.
|
||||
/// </summary>
|
||||
internal static string CannotUpdateProperty
|
||||
{
|
||||
get => GetString("CannotUpdateProperty");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The property at path '{0}' could not be updated.
|
||||
/// </summary>
|
||||
internal static string FormatCannotUpdateProperty(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("CannotUpdateProperty"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The expression '{0}' is not supported. Supported expressions include member access and indexer expressions.
|
||||
/// </summary>
|
||||
internal static string ExpressionTypeNotSupported
|
||||
{
|
||||
get => GetString("ExpressionTypeNotSupported");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The expression '{0}' is not supported. Supported expressions include member access and indexer expressions.
|
||||
/// </summary>
|
||||
internal static string FormatExpressionTypeNotSupported(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ExpressionTypeNotSupported"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The index value provided by path segment '{0}' is out of bounds of the array size.
|
||||
/// </summary>
|
||||
internal static string IndexOutOfBounds
|
||||
{
|
||||
get => GetString("IndexOutOfBounds");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The index value provided by path segment '{0}' is out of bounds of the array size.
|
||||
/// </summary>
|
||||
internal static string FormatIndexOutOfBounds(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("IndexOutOfBounds"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The path segment '{0}' is invalid for an array index.
|
||||
/// </summary>
|
||||
internal static string InvalidIndexValue
|
||||
{
|
||||
get => GetString("InvalidIndexValue");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The path segment '{0}' is invalid for an array index.
|
||||
/// </summary>
|
||||
internal static string FormatInvalidIndexValue(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidIndexValue"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The JSON patch document was malformed and could not be parsed.
|
||||
/// </summary>
|
||||
internal static string InvalidJsonPatchDocument
|
||||
{
|
||||
get => GetString("InvalidJsonPatchDocument");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The JSON patch document was malformed and could not be parsed.
|
||||
/// </summary>
|
||||
internal static string FormatInvalidJsonPatchDocument()
|
||||
=> GetString("InvalidJsonPatchDocument");
|
||||
|
||||
/// <summary>
|
||||
/// Invalid JsonPatch operation '{0}'.
|
||||
/// </summary>
|
||||
internal static string InvalidJsonPatchOperation
|
||||
{
|
||||
get => GetString("InvalidJsonPatchOperation");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invalid JsonPatch operation '{0}'.
|
||||
/// </summary>
|
||||
internal static string FormatInvalidJsonPatchOperation(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidJsonPatchOperation"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The provided path segment '{0}' cannot be converted to the target type.
|
||||
/// </summary>
|
||||
internal static string InvalidPathSegment
|
||||
{
|
||||
get => GetString("InvalidPathSegment");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The provided path segment '{0}' cannot be converted to the target type.
|
||||
/// </summary>
|
||||
internal static string FormatInvalidPathSegment(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidPathSegment"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The provided string '{0}' is an invalid path.
|
||||
/// </summary>
|
||||
internal static string InvalidValueForPath
|
||||
{
|
||||
get => GetString("InvalidValueForPath");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The provided string '{0}' is an invalid path.
|
||||
/// </summary>
|
||||
internal static string FormatInvalidValueForPath(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForPath"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The value '{0}' is invalid for target location.
|
||||
/// </summary>
|
||||
internal static string InvalidValueForProperty
|
||||
{
|
||||
get => GetString("InvalidValueForProperty");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The value '{0}' is invalid for target location.
|
||||
/// </summary>
|
||||
internal static string FormatInvalidValueForProperty(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForProperty"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' must be of type '{1}'.
|
||||
/// </summary>
|
||||
internal static string ParameterMustMatchType
|
||||
{
|
||||
get => GetString("ParameterMustMatchType");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// '{0}' must be of type '{1}'.
|
||||
/// </summary>
|
||||
internal static string FormatParameterMustMatchType(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ParameterMustMatchType"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.
|
||||
/// </summary>
|
||||
internal static string PatchNotSupportedForArrays
|
||||
{
|
||||
get => GetString("PatchNotSupportedForArrays");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.
|
||||
/// </summary>
|
||||
internal static string FormatPatchNotSupportedForArrays(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForArrays"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.
|
||||
/// </summary>
|
||||
internal static string PatchNotSupportedForNonGenericLists
|
||||
{
|
||||
get => GetString("PatchNotSupportedForNonGenericLists");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.
|
||||
/// </summary>
|
||||
internal static string FormatPatchNotSupportedForNonGenericLists(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForNonGenericLists"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The target location specified by path segment '{0}' was not found.
|
||||
/// </summary>
|
||||
internal static string TargetLocationAtPathSegmentNotFound
|
||||
{
|
||||
get => GetString("TargetLocationAtPathSegmentNotFound");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The target location specified by path segment '{0}' was not found.
|
||||
/// </summary>
|
||||
internal static string FormatTargetLocationAtPathSegmentNotFound(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("TargetLocationAtPathSegmentNotFound"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// For operation '{0}', the target location specified by path '{1}' was not found.
|
||||
/// </summary>
|
||||
internal static string TargetLocationNotFound
|
||||
{
|
||||
get => GetString("TargetLocationNotFound");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For operation '{0}', the target location specified by path '{1}' was not found.
|
||||
/// </summary>
|
||||
internal static string FormatTargetLocationNotFound(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("TargetLocationNotFound"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// The test operation is not supported.
|
||||
/// </summary>
|
||||
internal static string TestOperationNotSupported
|
||||
{
|
||||
get => GetString("TestOperationNotSupported");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The test operation is not supported.
|
||||
/// </summary>
|
||||
internal static string FormatTestOperationNotSupported()
|
||||
=> GetString("TestOperationNotSupported");
|
||||
|
||||
/// <summary>
|
||||
/// The current value '{0}' at position '{2}' is not equal to the test value '{1}'.
|
||||
/// </summary>
|
||||
internal static string ValueAtListPositionNotEqualToTestValue
|
||||
{
|
||||
get => GetString("ValueAtListPositionNotEqualToTestValue");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current value '{0}' at position '{2}' is not equal to the test value '{1}'.
|
||||
/// </summary>
|
||||
internal static string FormatValueAtListPositionNotEqualToTestValue(object p0, object p1, object p2)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ValueAtListPositionNotEqualToTestValue"), p0, p1, p2);
|
||||
|
||||
/// <summary>
|
||||
/// The value at '{0}' cannot be null or empty to perform the test operation.
|
||||
/// </summary>
|
||||
internal static string ValueForTargetSegmentCannotBeNullOrEmpty
|
||||
{
|
||||
get => GetString("ValueForTargetSegmentCannotBeNullOrEmpty");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The value at '{0}' cannot be null or empty to perform the test operation.
|
||||
/// </summary>
|
||||
internal static string FormatValueForTargetSegmentCannotBeNullOrEmpty(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ValueForTargetSegmentCannotBeNullOrEmpty"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The current value '{0}' at path '{2}' is not equal to the test value '{1}'.
|
||||
/// </summary>
|
||||
internal static string ValueNotEqualToTestValue
|
||||
{
|
||||
get => GetString("ValueNotEqualToTestValue");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current value '{0}' at path '{2}' is not equal to the test value '{1}'.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="CannotCopyProperty" xml:space="preserve">
|
||||
<value>The property at '{0}' could not be copied.</value>
|
||||
</data>
|
||||
<data name="CannotDeterminePropertyType" xml:space="preserve">
|
||||
<value>The type of the property at path '{0}' could not be determined.</value>
|
||||
</data>
|
||||
<data name="CannotPerformOperation" xml:space="preserve">
|
||||
<value>The '{0}' operation at path '{1}' could not be performed.</value>
|
||||
</data>
|
||||
<data name="CannotReadProperty" xml:space="preserve">
|
||||
<value>The property at '{0}' could not be read.</value>
|
||||
</data>
|
||||
<data name="CannotUpdateProperty" xml:space="preserve">
|
||||
<value>The property at path '{0}' could not be updated.</value>
|
||||
</data>
|
||||
<data name="ExpressionTypeNotSupported" xml:space="preserve">
|
||||
<value>The expression '{0}' is not supported. Supported expressions include member access and indexer expressions.</value>
|
||||
</data>
|
||||
<data name="IndexOutOfBounds" xml:space="preserve">
|
||||
<value>The index value provided by path segment '{0}' is out of bounds of the array size.</value>
|
||||
</data>
|
||||
<data name="InvalidIndexValue" xml:space="preserve">
|
||||
<value>The path segment '{0}' is invalid for an array index.</value>
|
||||
</data>
|
||||
<data name="InvalidJsonPatchDocument" xml:space="preserve">
|
||||
<value>The JSON patch document was malformed and could not be parsed.</value>
|
||||
</data>
|
||||
<data name="InvalidJsonPatchOperation" xml:space="preserve">
|
||||
<value>Invalid JsonPatch operation '{0}'.</value>
|
||||
</data>
|
||||
<data name="InvalidPathSegment" xml:space="preserve">
|
||||
<value>The provided path segment '{0}' cannot be converted to the target type.</value>
|
||||
</data>
|
||||
<data name="InvalidValueForPath" xml:space="preserve">
|
||||
<value>The provided string '{0}' is an invalid path.</value>
|
||||
</data>
|
||||
<data name="InvalidValueForProperty" xml:space="preserve">
|
||||
<value>The value '{0}' is invalid for target location.</value>
|
||||
</data>
|
||||
<data name="ParameterMustMatchType" xml:space="preserve">
|
||||
<value>'{0}' must be of type '{1}'.</value>
|
||||
</data>
|
||||
<data name="PatchNotSupportedForArrays" xml:space="preserve">
|
||||
<value>The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.</value>
|
||||
</data>
|
||||
<data name="PatchNotSupportedForNonGenericLists" xml:space="preserve">
|
||||
<value>The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.</value>
|
||||
</data>
|
||||
<data name="TargetLocationAtPathSegmentNotFound" xml:space="preserve">
|
||||
<value>The target location specified by path segment '{0}' was not found.</value>
|
||||
</data>
|
||||
<data name="TargetLocationNotFound" xml:space="preserve">
|
||||
<value>For operation '{0}', the target location specified by path '{1}' was not found.</value>
|
||||
</data>
|
||||
<data name="TestOperationNotSupported" xml:space="preserve">
|
||||
<value>The test operation is not supported.</value>
|
||||
</data>
|
||||
<data name="ValueAtListPositionNotEqualToTestValue" xml:space="preserve">
|
||||
<value>The current value '{0}' at position '{2}' is not equal to the test value '{1}'.</value>
|
||||
</data>
|
||||
<data name="ValueForTargetSegmentCannotBeNullOrEmpty" xml:space="preserve">
|
||||
<value>The value at '{0}' cannot be null or empty to perform the test operation.</value>
|
||||
</data>
|
||||
<data name="ValueNotEqualToTestValue" xml:space="preserve">
|
||||
<value>The current value '{0}' at path '{2}' is not equal to the test value '{1}'.</value>
|
||||
</data>
|
||||
</root>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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<string, object>;
|
||||
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<string, int>()
|
||||
{
|
||||
{ "customTest", 1},
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument();
|
||||
patchDocument.Remove("Test");
|
||||
patchDocument.ContractResolver = contractResolver;
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
var cont = targetObject as IDictionary<string, int>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<JsonPatchException>(() =>
|
||||
{
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal("The current value 'A' at path 'StringProperty' is not equal to the test value 'B'.",
|
||||
exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, int> DictionaryOfStringToInteger { get; } = new Dictionary<string, int>();
|
||||
}
|
||||
|
||||
private class CustomerDictionary
|
||||
{
|
||||
public IDictionary<int, Customer> DictionaryOfStringToCustomer { get; } = new Dictionary<int, Customer>();
|
||||
}
|
||||
|
||||
[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<JsonPatchException>(() =>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<JsonPatchException>(() =>
|
||||
{
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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<int>() { 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<int>() { 1, 2, 3 }
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument();
|
||||
patchDocument.Test("Nested/IntegerList/0", 2);
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<JsonPatchException>(() =>
|
||||
{
|
||||
patchDocument.ApplyTo(dynamicTestObject);
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal("The current value '1' at position '0' is not equal to the test value '2'.", exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<JsonPatchException>(() =>
|
||||
{
|
||||
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<string, object>;
|
||||
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<string, object>;
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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<string, object>;
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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<string, object>;
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int>() { 1, 2, 3 }
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
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<int>() { 1, 2, 3 }
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
patchDocument.Test(o => o.SimpleObject.IntegerList, 4, -1);
|
||||
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<JsonPatchException>(() => { 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<int>() { 1, 2, 3 }
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
patchDocument.Add(o => (List<int>)o.SimpleObject.IntegerIList, 4, 0);
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, targetObject.SimpleObject.IntegerIList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddToComplextTypeList_SpecifyIndex()
|
||||
{
|
||||
// Arrange
|
||||
var targetObject = new SimpleObjectWithNestedObject()
|
||||
{
|
||||
SimpleObjectList = new List<SimpleObject>()
|
||||
{
|
||||
new SimpleObject
|
||||
{
|
||||
StringProperty = "String1"
|
||||
},
|
||||
new SimpleObject
|
||||
{
|
||||
StringProperty = "String2"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
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<int>() { 1, 2, 3 }
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
patchDocument.Add(o => o.SimpleObject.IntegerList, 4);
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new List<int>() { 1, 2, 3, 4 }, targetObject.SimpleObject.IntegerList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveFromList()
|
||||
{
|
||||
// Arrange
|
||||
var targetObject = new SimpleObject()
|
||||
{
|
||||
IntegerList = new List<int>() { 1, 2, 3 }
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument();
|
||||
patchDocument.Remove("IntegerList/2");
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new List<int>() { 1, 2 }, targetObject.IntegerList);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("3")]
|
||||
[InlineData("-1")]
|
||||
public void RemoveFromList_InvalidPosition(string position)
|
||||
{
|
||||
// Arrange
|
||||
var targetObject = new SimpleObject()
|
||||
{
|
||||
IntegerList = new List<int>() { 1, 2, 3 }
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument();
|
||||
patchDocument.Remove("IntegerList/" + position);
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<JsonPatchException>(() =>
|
||||
{
|
||||
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<int>() { 1, 2, 3 }
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
patchDocument.Remove<int>(o => o.SimpleObject.IntegerList);
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new List<int>() { 1, 2 }, targetObject.SimpleObject.IntegerList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceFullList_WithCollection()
|
||||
{
|
||||
// Arrange
|
||||
var targetObject = new SimpleObject()
|
||||
{
|
||||
IntegerList = new List<int>() { 1, 2, 3 }
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument();
|
||||
patchDocument.Replace("IntegerList", new Collection<int>() { 4, 5, 6 });
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new List<int>() { 4, 5, 6 }, targetObject.IntegerList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replace_AtEndOfList()
|
||||
{
|
||||
// Arrange
|
||||
var targetObject = new SimpleObjectWithNestedObject()
|
||||
{
|
||||
SimpleObject = new SimpleObject()
|
||||
{
|
||||
IntegerList = new List<int>() { 1, 2, 3 }
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
patchDocument.Replace(o => o.SimpleObject.IntegerList, 5);
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new List<int>() { 1, 2, 5 }, targetObject.SimpleObject.IntegerList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replace_InList_InvalidPosition()
|
||||
{
|
||||
// Arrange
|
||||
var targetObject = new SimpleObjectWithNestedObject()
|
||||
{
|
||||
SimpleObject = new SimpleObject()
|
||||
{
|
||||
IntegerList = new List<int>() { 1, 2, 3 }
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
patchDocument.Replace(o => o.SimpleObject.IntegerList, 5, -1);
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<JsonPatchException>(() => { 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<int>() { 1, 2, 3 }
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument();
|
||||
patchDocument.Copy("IntegerList/0", "IntegerList/-");
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new List<int>() { 1, 2, 3, 1 }, targetObject.IntegerList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyFromListToNonList()
|
||||
{
|
||||
// Arrange
|
||||
var targetObject = new SimpleObject()
|
||||
{
|
||||
IntegerList = new List<int>() { 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<int>() { 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<int>() { 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<SimpleObject>() {
|
||||
simpleObject1,
|
||||
simpleObject2,
|
||||
simpleObject3
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
patchDocument.Move(o => o.SimpleObjectList, 0, o => o.SimpleObjectList, 1);
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new List<SimpleObject>() { 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<int>() { 1, 2, 3 }
|
||||
}
|
||||
};
|
||||
|
||||
var patchDocument = new JsonPatchDocument<SimpleObjectWithNestedObject>();
|
||||
patchDocument.Move(o => o.SimpleObject.IntegerList, 0, o => o.IntegerValue);
|
||||
|
||||
// Act
|
||||
patchDocument.ApplyTo(targetObject);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new List<int>() { 2, 3 }, targetObject.SimpleObject.IntegerList);
|
||||
Assert.Equal(1, targetObject.IntegerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SimpleObjectWithNestedObjectWithNullCheck>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
patchDocument.Replace(o => o.NestedObject, newNested);
|
||||
|
||||
var serialized = JsonConvert.SerializeObject(patchDocument);
|
||||
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleObjectWithNestedObject>>(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<NestedObject>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
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<SimpleObjectWithNestedObject>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, int>(StringComparer.Ordinal);
|
||||
dictionary[key] = 404;
|
||||
var dictionaryAdapter = new DictionaryAdapter<string, int>();
|
||||
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<int, object>();
|
||||
dictionary[intKey] = "Mike";
|
||||
var dictionaryAdapter = new DictionaryAdapter<int, object>();
|
||||
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<int, object>();
|
||||
var resolver = new DefaultContractResolver();
|
||||
var key = 1;
|
||||
var dictionary = new Dictionary<int, object>();
|
||||
|
||||
// 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<string, object>();
|
||||
var resolver = new DefaultContractResolver();
|
||||
var nameKey = "Name";
|
||||
var dictionary = new Dictionary<string, object>(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<string, object>();
|
||||
var resolver = new DefaultContractResolver();
|
||||
var nameKey = "Name";
|
||||
var dictionary = new Dictionary<string, object>(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<string, object>(StringComparer.Ordinal);
|
||||
dictionary.Add(nameKey, "Mike");
|
||||
var dictionaryAdapter = new DictionaryAdapter<string, object>();
|
||||
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<Guid, object>();
|
||||
dictionary.Add(guidKey, "Mike");
|
||||
var dictionaryAdapter = new DictionaryAdapter<Guid, object>();
|
||||
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<Guid, int>();
|
||||
dictionary.Add(guidKey, 5);
|
||||
var dictionaryAdapter = new DictionaryAdapter<Guid, int>();
|
||||
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<string, object>(StringComparer.Ordinal);
|
||||
var dictionaryAdapter = new DictionaryAdapter<string, object>();
|
||||
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<string, object>(StringComparer.Ordinal);
|
||||
var dictionaryAdapter = new DictionaryAdapter<string, object>();
|
||||
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<string, object>(StringComparer.Ordinal);
|
||||
dictionary[nameKey] = "James";
|
||||
var dictionaryAdapter = new DictionaryAdapter<string, object>();
|
||||
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<Uri, object>();
|
||||
dictionary[uriKey] = "James";
|
||||
var dictionaryAdapter = new DictionaryAdapter<Uri, object>();
|
||||
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<string, List<object>>();
|
||||
var value = new List<object>()
|
||||
{
|
||||
"James",
|
||||
2,
|
||||
new Customer("James", 25)
|
||||
};
|
||||
dictionary[key] = value;
|
||||
var dictionaryAdapter = new DictionaryAdapter<string, List<object>>();
|
||||
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<string, object>();
|
||||
dictionary[key] = "James";
|
||||
var dictionaryAdapter = new DictionaryAdapter<string, object>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int>() { 1, 2, 3 };
|
||||
var value = new List<string>() { "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<object>()
|
||||
{
|
||||
"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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IContractResolver>(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<IContractResolver>(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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<string>() { "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<string>() { "James", "Mike", "Rob" }, targetObject);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("-1")]
|
||||
[InlineData("-2")]
|
||||
[InlineData("3")]
|
||||
public void Add_WithOutOfBoundsIndex_Fails(string position)
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<string>() { "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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<string>() { "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<int>, List<int>> AppendAtEndOfListData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<List<int>, List<int>>()
|
||||
{
|
||||
{
|
||||
new List<int>() { },
|
||||
new List<int>() { 20 }
|
||||
},
|
||||
{
|
||||
new List<int>() { 5, 10 },
|
||||
new List<int>() { 5, 10, 20 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AppendAtEndOfListData))]
|
||||
public void Add_Appends_AtTheEnd(List<int> targetObject, List<int> expected)
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new Mock<IContractResolver>(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<IContractResolver>(MockBehavior.Strict);
|
||||
var listAdapter = new ListAdapter();
|
||||
var targetObject = new List<string>() { "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<string>() { "James", "Mike", null }, targetObject);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_CompatibleTypeWorks()
|
||||
{
|
||||
// Arrange
|
||||
var sDto = new SimpleObject();
|
||||
var iDto = new InheritedObject();
|
||||
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<SimpleObject>() { 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<SimpleObject>() { sDto, iDto }, targetObject);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_NonCompatibleType_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>() { 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<IList, object, string, IList> AddingDifferentComplexTypeWorksData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<IList, object, string, IList>()
|
||||
{
|
||||
{
|
||||
new List<string>() { },
|
||||
"a",
|
||||
"-",
|
||||
new List<string>() { "a" }
|
||||
},
|
||||
{
|
||||
new List<string>() { "a", "b" },
|
||||
"c",
|
||||
"-",
|
||||
new List<string>() { "a", "b", "c" }
|
||||
},
|
||||
{
|
||||
new List<string>() { "a", "b" },
|
||||
"c",
|
||||
"0",
|
||||
new List<string>() { "c", "a", "b" }
|
||||
},
|
||||
{
|
||||
new List<string>() { "a", "b" },
|
||||
"c",
|
||||
"1",
|
||||
new List<string>() { "a", "c", "b" }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AddingDifferentComplexTypeWorksData))]
|
||||
public void Add_DifferentComplexTypeWorks(IList targetObject, object value, string position, IList expected)
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new Mock<IContractResolver>(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<IList, object, string, IList> AddingKeepsObjectReferenceData
|
||||
{
|
||||
get
|
||||
{
|
||||
var sDto1 = new SimpleObject();
|
||||
var sDto2 = new SimpleObject();
|
||||
var sDto3 = new SimpleObject();
|
||||
return new TheoryData<IList, object, string, IList>()
|
||||
{
|
||||
{
|
||||
new List<SimpleObject>() { },
|
||||
sDto1,
|
||||
"-",
|
||||
new List<SimpleObject>() { sDto1 }
|
||||
},
|
||||
{
|
||||
new List<SimpleObject>() { sDto1, sDto2 },
|
||||
sDto3,
|
||||
"-",
|
||||
new List<SimpleObject>() { sDto1, sDto2, sDto3 }
|
||||
},
|
||||
{
|
||||
new List<SimpleObject>() { sDto1, sDto2 },
|
||||
sDto3,
|
||||
"0",
|
||||
new List<SimpleObject>() { sDto3, sDto1, sDto2 }
|
||||
},
|
||||
{
|
||||
new List<SimpleObject>() { sDto1, sDto2 },
|
||||
sDto3,
|
||||
"1",
|
||||
new List<SimpleObject>() { sDto1, sDto3, sDto2 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AddingKeepsObjectReferenceData))]
|
||||
public void Add_KeepsObjectReference(IList targetObject, object value, string position, IList expected)
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new Mock<IContractResolver>(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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>(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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>(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<int>(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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>(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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>(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<int>(expected), targetObject);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Replace_NonCompatibleType_Fails()
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>() { 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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>() { 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<int>() { 10, 30 }, targetObject);
|
||||
}
|
||||
|
||||
public static TheoryData<string, List<int>> ReplacesValuesAtPositionData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<string, List<int>>()
|
||||
{
|
||||
{
|
||||
"0",
|
||||
new List<int>() { 30, 20 }
|
||||
},
|
||||
{
|
||||
"1",
|
||||
new List<int>() { 10, 30 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ReplacesValuesAtPositionData))]
|
||||
public void Replace_ReplacesValue_AtGivenPosition(string position, List<int> expected)
|
||||
{
|
||||
// Arrange
|
||||
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>() { 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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>() { 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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>() { 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<IContractResolver>(MockBehavior.Strict);
|
||||
var targetObject = new List<int>() { 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> States { get; set; } = new List<string>();
|
||||
public IDictionary<string, string> Countries = new Dictionary<string, string>();
|
||||
public dynamic Items { get; set; } = new ExpandoObject();
|
||||
}
|
||||
|
||||
private class Class1Nested
|
||||
{
|
||||
public List<Class1> Customers { get; set; } = new List<Class1>();
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> 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<ListAdapter>(adapter);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> 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<string, string>), adapter.GetType());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> 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<string, object>), adapter.GetType());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> 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<PocoAdapter>(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<PocoAdapter>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<JsonPatchException>(() =>
|
||||
{
|
||||
var parsedPath = new ParsedPath(path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SimpleObjectWithNestedObject>();
|
||||
|
||||
// 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<int[]>();
|
||||
|
||||
// Act
|
||||
var path = patchDocument.GetPath(p => p[3], null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/3", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpressionType_Call()
|
||||
{
|
||||
// Arrange
|
||||
var patchDocument = new JsonPatchDocument<Dictionary<string, int>>();
|
||||
|
||||
// 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<SimpleObject>();
|
||||
|
||||
// Act
|
||||
var path = patchDocument.GetPath(p => p, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpressionType_Parameter_WithPosition()
|
||||
{
|
||||
// Arrange
|
||||
var patchDocument = new JsonPatchDocument<SimpleObject>();
|
||||
|
||||
// Act
|
||||
var path = patchDocument.GetPath(p => p, "-");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("/-", path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpressionType_Convert()
|
||||
{
|
||||
// Arrange
|
||||
var patchDocument = new JsonPatchDocument<NestedObjectWithDerivedClass>();
|
||||
|
||||
// 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<SimpleObject>();
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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<JsonPropertyObject>();
|
||||
|
||||
// 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<JsonPropertyWithNoPropertyName>();
|
||||
|
||||
// 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<JsonPatchException>(() =>
|
||||
{
|
||||
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<JsonPatchException>(() =>
|
||||
{
|
||||
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<JsonPatchDocument<SimpleObject>>(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<SimpleObject>();
|
||||
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<JsonPatchDocument>(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<SimpleObject>();
|
||||
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<JsonPatchDocument<SimpleObject>>(serialized);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<JsonPatchDocument<SimpleObject>>(deserialized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialization_Fails_ForInvalidJsonPatchDocument()
|
||||
{
|
||||
// Arrange
|
||||
var serialized = "{\"Operations\": [{ \"op\": \"replace\", \"path\": \"/title\", \"value\": \"New Title\"}]}";
|
||||
|
||||
// Act
|
||||
var exception = Assert.Throws<JsonSerializationException>(() =>
|
||||
{
|
||||
var deserialized
|
||||
= JsonConvert.DeserializeObject<JsonPatchDocument>(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<JsonSerializationException>(() =>
|
||||
{
|
||||
var deserialized
|
||||
= JsonConvert.DeserializeObject<JsonPatchDocument<SimpleObject>>(serialized);
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal("The JSON patch document was malformed and could not be parsed.", exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.JsonPatch" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T> where T : class
|
||||
{
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public void LogErrorMessage(JsonPatchError patchError)
|
||||
{
|
||||
ErrorMessage = patchError.ErrorMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, object> _dictionary = new Dictionary<string, object>();
|
||||
|
||||
public object this[string key] { get => ((IDictionary<string, object>)_dictionary)[key]; set => ((IDictionary<string, object>)_dictionary)[key] = value; }
|
||||
|
||||
public ICollection<string> Keys => ((IDictionary<string, object>)_dictionary).Keys;
|
||||
|
||||
public ICollection<object> Values => ((IDictionary<string, object>)_dictionary).Values;
|
||||
|
||||
public int Count => ((IDictionary<string, object>)_dictionary).Count;
|
||||
|
||||
public bool IsReadOnly => ((IDictionary<string, object>)_dictionary).IsReadOnly;
|
||||
|
||||
public void Add(string key, object value)
|
||||
{
|
||||
((IDictionary<string, object>)_dictionary).Add(key, value);
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<string, object> item)
|
||||
{
|
||||
((IDictionary<string, object>)_dictionary).Add(item);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
((IDictionary<string, object>)_dictionary).Clear();
|
||||
}
|
||||
|
||||
public bool Contains(KeyValuePair<string, object> item)
|
||||
{
|
||||
return ((IDictionary<string, object>)_dictionary).Contains(item);
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
return ((IDictionary<string, object>)_dictionary).ContainsKey(key);
|
||||
}
|
||||
|
||||
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
|
||||
{
|
||||
((IDictionary<string, object>)_dictionary).CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
||||
{
|
||||
return ((IDictionary<string, object>)_dictionary).GetEnumerator();
|
||||
}
|
||||
|
||||
public bool Remove(string key)
|
||||
{
|
||||
return ((IDictionary<string, object>)_dictionary).Remove(key);
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<string, object> item)
|
||||
{
|
||||
return ((IDictionary<string, object>)_dictionary).Remove(item);
|
||||
}
|
||||
|
||||
public bool TryGetValue(string key, out object value)
|
||||
{
|
||||
return ((IDictionary<string, object>)_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SimpleObject> SimpleObjectList { get; set; }
|
||||
public List<int> IntegerList { get; set; }
|
||||
public IList<int> 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SimpleObject> SimpleObjectList { get; set; }
|
||||
|
||||
public IList<SimpleObject> SimpleObjectIList { get; set; }
|
||||
|
||||
public SimpleObjectWithNestedObject()
|
||||
{
|
||||
NestedObject = new NestedObject();
|
||||
SimpleObject = new SimpleObject();
|
||||
InheritedObject = new InheritedObject();
|
||||
SimpleObjectList = new List<SimpleObject>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue