[Fixes #33] Dictionary operations fail due to contract issues

This commit is contained in:
Kiran Challa 2016-09-21 22:56:55 -07:00
parent 9e8aee2478
commit 393c25988a
38 changed files with 2796 additions and 1358 deletions

View File

@ -1,14 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Microsoft.AspNetCore.JsonPatch.Helpers;
using Microsoft.AspNetCore.JsonPatch.Internal;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Adapters
@ -127,7 +124,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters
string path,
object value,
object objectToApplyTo,
Operation operationToReport)
Operation operation)
{
if (path == null)
{
@ -139,228 +136,30 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters
throw new ArgumentNullException(nameof(objectToApplyTo));
}
if (operationToReport == null)
if (operation == null)
{
throw new ArgumentNullException(nameof(operationToReport));
throw new ArgumentNullException(nameof(operation));
}
// first up: if the path ends in a numeric value, we're inserting in a list and
// that value represents the position; if the path ends in "-", we're appending
// to the list.
var parsedPath = new ParsedPath(path);
var visitor = new ObjectVisitor(parsedPath, ContractResolver);
// get path result
var pathResult = GetActualPropertyPath(
path,
objectToApplyTo,
operationToReport);
if (pathResult == null)
IAdapter adapter;
var target = objectToApplyTo;
string errorMessage;
if (!visitor.TryVisit(ref target, out adapter, out errorMessage))
{
var error = CreatePathNotFoundError(objectToApplyTo, path, operation, errorMessage);
ReportError(error);
return;
}
var appendList = pathResult.ExecuteAtEnd;
var positionAsInteger = pathResult.NumericEnd;
var actualPathToProperty = pathResult.PathToProperty;
var treeAnalysisResult = new ObjectTreeAnalysisResult(
objectToApplyTo,
actualPathToProperty,
ContractResolver);
if (!treeAnalysisResult.IsValidPathForAdd)
if (!adapter.TryAdd(target, parsedPath.LastSegment, ContractResolver, value, out errorMessage))
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatPropertyCannotBeAdded(path)));
var error = CreateOperationFailedError(objectToApplyTo, path, operation, errorMessage);
ReportError(error);
return;
}
if (treeAnalysisResult.UseDynamicLogic)
{
var container = treeAnalysisResult.Container;
if (container.ContainsCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent))
{
// Existing property.
// If it's not an array, we need to check if the value fits the property type
//
// If it's an array, we need to check if the value fits in that array type,
// and add it at the correct position (if allowed).
if (appendList || positionAsInteger > -1)
{
// get the actual type
var propertyValue = container.GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent);
var typeOfPathProperty = propertyValue.GetType();
if (!IsNonStringArray(typeOfPathProperty))
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path)));
return;
}
// now, get the generic type of the enumerable
var genericTypeOfArray = GetIListType(typeOfPathProperty);
var conversionResult = ConvertToActualType(genericTypeOfArray, value);
if (!conversionResult.CanBeConverted)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidValueForProperty(value, path)));
return;
}
// get value (it can be cast, we just checked that)
var array = treeAnalysisResult.Container.GetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent) as IList;
if (appendList)
{
array.Add(conversionResult.ConvertedInstance);
treeAnalysisResult.Container.SetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent, array);
}
else
{
// specified index must not be greater than
// the amount of items in the array
if (positionAsInteger > array.Count)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(
operationToReport.op,
path)));
return;
}
array.Insert(positionAsInteger, conversionResult.ConvertedInstance);
treeAnalysisResult.Container.SetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent, array);
}
}
else
{
// get the actual type
var typeOfPathProperty = treeAnalysisResult.Container
.GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent).GetType();
// can the value be converted to the actual type?
var conversionResult = ConvertToActualType(typeOfPathProperty, value);
if (conversionResult.CanBeConverted)
{
treeAnalysisResult.Container.SetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent,
conversionResult.ConvertedInstance);
}
else
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidValueForProperty(conversionResult.ConvertedInstance, path)));
return;
}
}
}
else
{
// New property - add it.
treeAnalysisResult.Container.Add(treeAnalysisResult.PropertyPathInParent, value);
}
}
else
{
// If it's an array, add to that array. If it's not, we replace.
// is the path an array (but not a string (= char[]))? In this case,
// the path must end with "/position" or "/-", which we already determined before.
var patchProperty = treeAnalysisResult.JsonPatchProperty;
if (appendList || positionAsInteger > -1)
{
if (!IsNonStringArray(patchProperty.Property.PropertyType))
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path)));
return;
}
// now, get the generic type of the IList<> from Property type.
var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType);
var conversionResult = ConvertToActualType(genericTypeOfArray, value);
if (!conversionResult.CanBeConverted)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidValueForProperty(conversionResult.ConvertedInstance, path)));
return;
}
if (!patchProperty.Property.Readable)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatCannotReadProperty(path)));
return;
}
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
if (appendList)
{
array.Add(conversionResult.ConvertedInstance);
}
else if (positionAsInteger <= array.Count)
{
array.Insert(positionAsInteger, conversionResult.ConvertedInstance);
}
else
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path)));
return;
}
}
else
{
var conversionResultTuple = ConvertToActualType(
patchProperty.Property.PropertyType,
value);
if (!conversionResultTuple.CanBeConverted)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidValueForProperty(value, path)));
return;
}
if (!patchProperty.Property.Writable)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatCannotUpdateProperty(path)));
return;
}
patchProperty.Property.ValueProvider.SetValue(
patchProperty.Parent,
conversionResultTuple.ConvertedInstance);
}
}
}
/// <summary>
@ -398,30 +197,19 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters
throw new ArgumentNullException(nameof(objectToApplyTo));
}
var valueAtFromLocationResult = GetValueAtLocation(operation.from, objectToApplyTo, operation);
if (valueAtFromLocationResult.HasError)
object propertyValue;
// Get value at 'from' location and add that value to the 'path' location
if (TryGetValue(operation.from, objectToApplyTo, operation, out propertyValue))
{
// Error has already been logged in GetValueAtLocation. We
// must return, because remove / add should not be allowed to continue
return;
// remove that value
Remove(operation.from, objectToApplyTo, operation);
// add that value to the path location
Add(operation.path,
propertyValue,
objectToApplyTo,
operation);
}
// remove that value
var removeResult = Remove(operation.from, objectToApplyTo, operation);
if (removeResult.HasError)
{
// Return => error has already been logged in remove method. We must
// return, because add should not be allowed to continue
return;
}
// add that value to the path location
Add(operation.path,
valueAtFromLocationResult.PropertyValue,
objectToApplyTo,
operation);
}
/// <summary>
@ -453,240 +241,33 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters
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
/// 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 RemovedPropertyTypeResult Remove(string path, object objectToApplyTo, Operation operationToReport)
private void Remove(string path, object objectToApplyTo, Operation operationToReport)
{
// get path result
var pathResult = GetActualPropertyPath(
path,
objectToApplyTo,
operationToReport);
var parsedPath = new ParsedPath(path);
var visitor = new ObjectVisitor(parsedPath, ContractResolver);
if (pathResult == null)
IAdapter adapter;
var target = objectToApplyTo;
string errorMessage;
if (!visitor.TryVisit(ref target, out adapter, out errorMessage))
{
return new RemovedPropertyTypeResult(null, true);
var error = CreatePathNotFoundError(objectToApplyTo, path, operationToReport, errorMessage);
ReportError(error);
return;
}
var removeFromList = pathResult.ExecuteAtEnd;
var positionAsInteger = pathResult.NumericEnd;
var actualPathToProperty = pathResult.PathToProperty;
var treeAnalysisResult = new ObjectTreeAnalysisResult(
objectToApplyTo,
actualPathToProperty,
ContractResolver);
if (!treeAnalysisResult.IsValidPathForRemove)
if (!adapter.TryRemove(target, parsedPath.LastSegment, ContractResolver, out errorMessage))
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatPropertyCannotBeRemoved(path)));
return new RemovedPropertyTypeResult(null, true);
}
if (treeAnalysisResult.UseDynamicLogic)
{
// if it's not an array, we can remove the property from
// the dictionary. If it's an array, we need to check the position first.
if (removeFromList || positionAsInteger > -1)
{
var propertyValue = treeAnalysisResult.Container
.GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent);
// we cannot continue when the value is null, because to be able to
// continue we need to be able to check if the array is a non-string array
if (propertyValue == null)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatCannotDeterminePropertyType(path)));
return new RemovedPropertyTypeResult(null, true);
}
var typeOfPathProperty = propertyValue.GetType();
if (!IsNonStringArray(typeOfPathProperty))
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path)));
return new RemovedPropertyTypeResult(null, true);
}
// now, get the generic type of the enumerable (we'll return this type)
var genericTypeOfArray = GetIListType(typeOfPathProperty);
// get the array
var array = (IList)treeAnalysisResult.Container.GetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent);
if (array.Count == 0)
{
// if the array is empty, we should throw an error
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(
operationToReport.op,
path)));
return new RemovedPropertyTypeResult(null, true);
}
if (removeFromList)
{
array.RemoveAt(array.Count - 1);
treeAnalysisResult.Container.SetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent, array);
// return the type of the value that has been removed.
return new RemovedPropertyTypeResult(genericTypeOfArray, false);
}
else
{
if (positionAsInteger >= array.Count)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(
operationToReport.op,
path)));
return new RemovedPropertyTypeResult(null, true);
}
array.RemoveAt(positionAsInteger);
treeAnalysisResult.Container.SetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent, array);
// return the type of the value that has been removed.
return new RemovedPropertyTypeResult(genericTypeOfArray, false);
}
}
else
{
// get the property
var getResult = treeAnalysisResult.Container.GetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent);
// remove the property
treeAnalysisResult.Container.RemoveValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent);
// value is not null, we can determine the type
if (getResult != null)
{
var actualType = getResult.GetType();
return new RemovedPropertyTypeResult(actualType, false);
}
else
{
return new RemovedPropertyTypeResult(null, false);
}
}
}
else
{
// not dynamic
var patchProperty = treeAnalysisResult.JsonPatchProperty;
if (removeFromList || positionAsInteger > -1)
{
if (!IsNonStringArray(patchProperty.Property.PropertyType))
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path)));
return new RemovedPropertyTypeResult(null, true);
}
// now, get the generic type of the IList<> from Property type.
var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType);
if (!patchProperty.Property.Readable)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatCannotReadProperty(path)));
return new RemovedPropertyTypeResult(null, true);
}
var array = (IList)patchProperty.Property.ValueProvider
.GetValue(patchProperty.Parent);
if (array.Count == 0)
{
// if the array is empty, we should throw an error
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(
operationToReport.op,
path)));
return new RemovedPropertyTypeResult(null, true);
}
if (removeFromList)
{
array.RemoveAt(array.Count - 1);
// return the type of the value that has been removed
return new RemovedPropertyTypeResult(genericTypeOfArray, false);
}
else
{
if (positionAsInteger >= array.Count)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(
operationToReport.op,
path)));
return new RemovedPropertyTypeResult(null, true);
}
array.RemoveAt(positionAsInteger);
// return the type of the value that has been removed
return new RemovedPropertyTypeResult(genericTypeOfArray, false);
}
}
else
{
if (!patchProperty.Property.Writable)
{
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatCannotUpdateProperty(path)));
return new RemovedPropertyTypeResult(null, true);
}
// setting the value to "null" will use the default value in case of value types, and
// null in case of reference types
object value = null;
if (patchProperty.Property.PropertyType.GetTypeInfo().IsValueType
&& Nullable.GetUnderlyingType(patchProperty.Property.PropertyType) == null)
{
value = Activator.CreateInstance(patchProperty.Property.PropertyType);
}
patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, value);
return new RemovedPropertyTypeResult(patchProperty.Property.PropertyType, false);
}
var error = CreateOperationFailedError(objectToApplyTo, path, operationToReport, errorMessage);
ReportError(error);
return;
}
}
@ -722,37 +303,25 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters
throw new ArgumentNullException(nameof(objectToApplyTo));
}
var removeResult = Remove(operation.path, objectToApplyTo, operation);
var parsedPath = new ParsedPath(operation.path);
var visitor = new ObjectVisitor(parsedPath, ContractResolver);
if (removeResult.HasError)
IAdapter adapter;
var target = objectToApplyTo;
string errorMessage;
if (!visitor.TryVisit(ref target, out adapter, out errorMessage))
{
// return => error has already been logged in remove method
var error = CreatePathNotFoundError(objectToApplyTo, operation.path, operation, errorMessage);
ReportError(error);
return;
}
if (!removeResult.HasError && removeResult.ActualType == null)
if (!adapter.TryReplace(target, parsedPath.LastSegment, ContractResolver, operation.value, out errorMessage))
{
// the remove operation completed succesfully, but we could not determine the type.
LogError(new JsonPatchError(
objectToApplyTo,
operation,
Resources.FormatCannotDeterminePropertyType(operation.from)));
var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, errorMessage);
ReportError(error);
return;
}
var conversionResult = ConvertToActualType(removeResult.ActualType, operation.value);
if (!conversionResult.CanBeConverted)
{
// invalid value for path
LogError(new JsonPatchError(
objectToApplyTo,
operation,
Resources.FormatInvalidValueForProperty(operation.value, operation.path)));
return;
}
Add(operation.path, conversionResult.ConvertedInstance, objectToApplyTo, operation);
}
/// <summary>
@ -789,36 +358,26 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters
throw new ArgumentNullException(nameof(objectToApplyTo));
}
// get value at from location and add that value to the path location
var valueAtFromLocationResult = GetValueAtLocation(operation.from, objectToApplyTo, operation);
if (valueAtFromLocationResult.HasError)
object propertyValue;
// Get value at 'from' location and add that value to the 'path' location
if (TryGetValue(operation.from, objectToApplyTo, operation, out propertyValue))
{
// Return, error has already been logged in GetValueAtLocation
return;
Add(operation.path,
propertyValue,
objectToApplyTo,
operation);
}
Add(operation.path,
valueAtFromLocationResult.PropertyValue,
objectToApplyTo,
operation);
}
/// <summary>
/// Method is used by Copy and Move to avoid duplicate code
/// </summary>
/// <param name="location">Location where value should be</param>
/// <param name="objectToGetValueFrom">Object to inspect for the desired value</param>
/// <param name="operationToReport">Operation to report in case of an error</param>
/// <returns>GetValueResult containing value and a bool signifying a possible error</returns>
private GetValueResult GetValueAtLocation(
string location,
private bool TryGetValue(
string fromLocation,
object objectToGetValueFrom,
Operation operationToReport)
Operation operation,
out object propertyValue)
{
if (location == null)
if (fromLocation == null)
{
throw new ArgumentNullException(nameof(location));
throw new ArgumentNullException(nameof(fromLocation));
}
if (objectToGetValueFrom == null)
@ -826,280 +385,62 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters
throw new ArgumentNullException(nameof(objectToGetValueFrom));
}
if (operationToReport == null)
if (operation == null)
{
throw new ArgumentNullException(nameof(operationToReport));
throw new ArgumentNullException(nameof(operation));
}
// get path result
var pathResult = GetActualPropertyPath(
location,
objectToGetValueFrom,
operationToReport);
propertyValue = null;
if (pathResult == null)
var parsedPath = new ParsedPath(fromLocation);
var visitor = new ObjectVisitor(parsedPath, ContractResolver);
IAdapter adapter;
var target = objectToGetValueFrom;
string errorMessage;
if (!visitor.TryVisit(ref target, out adapter, out errorMessage))
{
return new GetValueResult(null, true);
var error = CreatePathNotFoundError(objectToGetValueFrom, fromLocation, operation, errorMessage);
ReportError(error);
return false;
}
var getAtEndOfList = pathResult.ExecuteAtEnd;
var positionAsInteger = pathResult.NumericEnd;
var actualPathToProperty = pathResult.PathToProperty;
var treeAnalysisResult = new ObjectTreeAnalysisResult(
objectToGetValueFrom,
actualPathToProperty,
ContractResolver);
if (treeAnalysisResult.UseDynamicLogic)
if (!adapter.TryGet(target, parsedPath.LastSegment, ContractResolver, out propertyValue, out errorMessage))
{
// if it's not an array, we can remove the property from
// the dictionary. If it's an array, we need to check the position first.
if (getAtEndOfList || positionAsInteger > -1)
{
var propertyValue = treeAnalysisResult.Container
.GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent);
// we cannot continue when the value is null, because to be able to
// continue we need to be able to check if the array is a non-string array
if (propertyValue == null)
{
LogError(new JsonPatchError(
objectToGetValueFrom,
operationToReport,
Resources.FormatCannotDeterminePropertyType(location)));
return new GetValueResult(null, true);
}
var typeOfPathProperty = propertyValue.GetType();
if (!IsNonStringArray(typeOfPathProperty))
{
LogError(new JsonPatchError(
objectToGetValueFrom,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, location)));
return new GetValueResult(null, true);
}
// get the array
var array = (IList)treeAnalysisResult.Container.GetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent);
if (positionAsInteger >= array.Count)
{
LogError(new JsonPatchError(
objectToGetValueFrom,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(
operationToReport.op,
location)));
return new GetValueResult(null, true);
}
if (getAtEndOfList)
{
return new GetValueResult(array[array.Count - 1], false);
}
else
{
return new GetValueResult(array[positionAsInteger], false);
}
}
else
{
// get the property
var propertyValueAtLocation = treeAnalysisResult.Container.GetValueForCaseInsensitiveKey(
treeAnalysisResult.PropertyPathInParent);
return new GetValueResult(propertyValueAtLocation, false);
}
var error = CreateOperationFailedError(objectToGetValueFrom, fromLocation, operation, errorMessage);
ReportError(error);
return false;
}
else
{
// not dynamic
var patchProperty = treeAnalysisResult.JsonPatchProperty;
if (getAtEndOfList || positionAsInteger > -1)
{
if (!IsNonStringArray(patchProperty.Property.PropertyType))
{
LogError(new JsonPatchError(
objectToGetValueFrom,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, location)));
return new GetValueResult(null, true);
}
if (!patchProperty.Property.Readable)
{
LogError(new JsonPatchError(
objectToGetValueFrom,
operationToReport,
Resources.FormatCannotReadProperty(location)));
return new GetValueResult(null, true);
}
var array = (IList)patchProperty.Property.ValueProvider
.GetValue(patchProperty.Parent);
if (positionAsInteger >= array.Count)
{
LogError(new JsonPatchError(
objectToGetValueFrom,
operationToReport,
Resources.FormatInvalidIndexForArrayProperty(
operationToReport.op,
location)));
return new GetValueResult(null, true);
}
if (getAtEndOfList)
{
return new GetValueResult(array[array.Count - 1], false);
}
else
{
return new GetValueResult(array[positionAsInteger], false);
}
}
else
{
if (!patchProperty.Property.Readable)
{
LogError(new JsonPatchError(
objectToGetValueFrom,
operationToReport,
Resources.FormatCannotReadProperty(
location)));
return new GetValueResult(null, true);
}
var propertyValueAtLocation = patchProperty.Property.ValueProvider
.GetValue(patchProperty.Parent);
return new GetValueResult(propertyValueAtLocation, false);
}
}
return true;
}
private bool IsNonStringArray(Type type)
{
if (GetIListType(type) != null)
{
return true;
}
return (!(type == typeof(string)) && typeof(IList).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()));
}
private void LogError(JsonPatchError jsonPatchError)
private void ReportError(JsonPatchError error)
{
if (LogErrorAction != null)
{
LogErrorAction(jsonPatchError);
LogErrorAction(error);
}
else
{
throw new JsonPatchException(jsonPatchError);
throw new JsonPatchException(error);
}
}
private ConversionResult ConvertToActualType(Type propertyType, object value)
private JsonPatchError CreateOperationFailedError(object target, string path, Operation operation, string errorMessage)
{
try
{
var o = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), propertyType);
return new ConversionResult(true, o);
}
catch (Exception)
{
return new ConversionResult(false, null);
}
return new JsonPatchError(
target,
operation,
errorMessage ?? Resources.FormatCannotPerformOperation(operation.op, path));
}
private Type GetIListType(Type type)
private JsonPatchError CreatePathNotFoundError(object target, string path, Operation operation, string errorMessage)
{
if (IsGenericListType(type))
{
return type.GetTypeInfo().GenericTypeArguments[0];
}
foreach (Type interfaceType in type.GetTypeInfo().ImplementedInterfaces)
{
if (IsGenericListType(interfaceType))
{
return interfaceType.GetTypeInfo().GenericTypeArguments[0];
}
}
return null;
}
private bool IsGenericListType(Type type)
{
if (type.GetTypeInfo().IsGenericType &&
type.GetGenericTypeDefinition() == typeof(IList<>))
{
return true;
}
return false;
}
private ActualPropertyPathResult GetActualPropertyPath(
string propertyPath,
object objectToApplyTo,
Operation operationToReport)
{
if (propertyPath == null)
{
throw new ArgumentNullException(nameof(propertyPath));
}
if (objectToApplyTo == null)
{
throw new ArgumentNullException(nameof(objectToApplyTo));
}
if (operationToReport == null)
{
throw new ArgumentNullException(nameof(operationToReport));
}
if (propertyPath.EndsWith("/-"))
{
return new ActualPropertyPathResult(-1, propertyPath.Substring(0, propertyPath.Length - 2), true);
}
else
{
var possibleIndex = propertyPath.Substring(propertyPath.LastIndexOf("/") + 1);
int castedIndex = -1;
if (int.TryParse(possibleIndex, out castedIndex))
{
// has numeric end.
if (castedIndex > -1)
{
var pathToProperty = propertyPath.Substring(
0,
propertyPath.LastIndexOf('/' + castedIndex.ToString()));
return new ActualPropertyPathResult(castedIndex, pathToProperty, false);
}
else
{
// negative position - invalid path
LogError(new JsonPatchError(
objectToApplyTo,
operationToReport,
Resources.FormatNegativeIndexForArrayProperty(operationToReport.op, propertyPath)));
return null;
}
}
return new ActualPropertyPathResult(-1, propertyPath, false);
}
return new JsonPatchError(
target,
operation,
errorMessage ?? Resources.FormatTargetLocationNotFound(operation.op, path));
}
}
}

View File

@ -1,22 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch.Helpers
{
internal class ActualPropertyPathResult
{
public int NumericEnd { get; private set; }
public string PathToProperty { get; set; }
public bool ExecuteAtEnd { get; set; }
public ActualPropertyPathResult(
int numericEnd,
string pathToProperty,
bool executeAtEnd)
{
NumericEnd = numericEnd;
PathToProperty = pathToProperty;
ExecuteAtEnd = executeAtEnd;
}
}
}

View File

@ -1,77 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.JsonPatch.Helpers
{
// Helper methods to allow case-insensitive key search
internal static class ExpandoObjectDictionaryExtensions
{
internal static void SetValueForCaseInsensitiveKey(
this IDictionary<string, object> propertyDictionary,
string key,
object value)
{
foreach (KeyValuePair<string, object> kvp in propertyDictionary)
{
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
{
propertyDictionary[kvp.Key] = value;
break;
}
}
}
internal static void RemoveValueForCaseInsensitiveKey(
this IDictionary<string, object> propertyDictionary,
string key)
{
string realKey = null;
foreach (KeyValuePair<string, object> kvp in propertyDictionary)
{
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
{
realKey = kvp.Key;
break;
}
}
if (realKey != null)
{
propertyDictionary.Remove(realKey);
}
}
internal static object GetValueForCaseInsensitiveKey(
this IDictionary<string, object> propertyDictionary,
string key)
{
foreach (KeyValuePair<string, object> kvp in propertyDictionary)
{
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
{
return kvp.Value;
}
}
throw new ArgumentException(Resources.FormatDictionaryKeyNotFound(key));
}
internal static bool ContainsCaseInsensitiveKey(
this IDictionary<string, object> propertyDictionary,
string key)
{
foreach (KeyValuePair<string, object> kvp in propertyDictionary)
{
if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}
}

View File

@ -1,209 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Helpers
{
internal class ObjectTreeAnalysisResult
{
// either the property is part of the container dictionary,
// or we have a direct reference to a JsonPatchProperty instance
public bool UseDynamicLogic { get; private set; }
public bool IsValidPathForAdd { get; private set; }
public bool IsValidPathForRemove { get; private set; }
public IDictionary<string, object> Container { get; private set; }
public string PropertyPathInParent { get; private set; }
public JsonPatchProperty JsonPatchProperty { get; private set; }
public ObjectTreeAnalysisResult(
object objectToSearch,
string propertyPath,
IContractResolver contractResolver)
{
// construct the analysis result.
// split the propertypath, and if necessary, remove the first
// empty item (that's the case when it starts with a "/")
var propertyPathTree = propertyPath.Split(
new char[] { '/' },
StringSplitOptions.RemoveEmptyEntries);
// we've now got a split up property tree "base/property/otherproperty/..."
int lastPosition = 0;
object targetObject = objectToSearch;
for (int i = 0; i < propertyPathTree.Length; i++)
{
lastPosition = i;
// if the current target object is an ExpandoObject (IDictionary<string, object>),
// we cannot use the ContractResolver.
var dictionary = targetObject as IDictionary<string, object>;
if (dictionary != null)
{
// find the value in the dictionary
if (dictionary.ContainsCaseInsensitiveKey(propertyPathTree[i]))
{
var possibleNewTargetObject = dictionary.GetValueForCaseInsensitiveKey(propertyPathTree[i]);
// unless we're at the last item, we should set the targetobject
// to the new object. If we're at the last item, we need to stop
if (i != propertyPathTree.Length - 1)
{
targetObject = possibleNewTargetObject;
}
}
else
{
break;
}
}
else
{
// if the current part of the path is numeric, this means we're trying
// to get the propertyInfo of a specific object in an array. To allow
// for this, the previous value (targetObject) must be an IEnumerable, and
// the position must exist.
int numericValue = -1;
if (int.TryParse(propertyPathTree[i], out numericValue))
{
var element = GetElementAtFromObject(targetObject, numericValue);
if (element != null)
{
targetObject = element;
}
else
{
break;
}
}
else
{
var jsonContract = (JsonObjectContract)contractResolver.ResolveContract(targetObject.GetType());
// does the property exist?
var attemptedProperty = jsonContract
.Properties
.FirstOrDefault(p => string.Equals(p.PropertyName, propertyPathTree[i], StringComparison.OrdinalIgnoreCase));
if (attemptedProperty != null)
{
// unless we're at the last item, we should continue searching.
// If we're at the last item, we need to stop
if ((i != propertyPathTree.Length - 1))
{
targetObject = attemptedProperty.ValueProvider.GetValue(targetObject);
}
}
else
{
// property cannot be found, and we're not working with dynamics.
// Stop, and return invalid path.
break;
}
}
}
}
if (propertyPathTree.Length - lastPosition != 1)
{
IsValidPathForAdd = false;
IsValidPathForRemove = false;
return;
}
// two things can happen now. The targetproperty can be an IDictionary - in that
// case, it's valid for add if there's 1 item left in the propertyPathTree.
//
// it can also be a property info. In that case, if there's nothing left in the path
// tree we're at the end, if there's one left we can try and set that.
if (targetObject is IDictionary<string, object>)
{
UseDynamicLogic = true;
Container = (IDictionary<string, object>)targetObject;
IsValidPathForAdd = true;
PropertyPathInParent = propertyPathTree[propertyPathTree.Length - 1];
// to be able to remove this property, it must exist
IsValidPathForRemove = Container.ContainsCaseInsensitiveKey(PropertyPathInParent);
}
else if (targetObject is IList)
{
UseDynamicLogic = false;
int index;
if (!Int32.TryParse(propertyPathTree[propertyPathTree.Length - 1], out index))
{
// We only support indexing into a list
IsValidPathForAdd = false;
IsValidPathForRemove = false;
return;
}
IsValidPathForAdd = true;
IsValidPathForRemove = ((IList)targetObject).Count > index;
PropertyPathInParent = propertyPathTree[propertyPathTree.Length - 1];
}
else
{
UseDynamicLogic = false;
var property = propertyPathTree[propertyPathTree.Length - 1];
var jsonContract = (JsonObjectContract)contractResolver.ResolveContract(targetObject.GetType());
var attemptedProperty = jsonContract
.Properties
.FirstOrDefault(p => string.Equals(p.PropertyName, property, StringComparison.OrdinalIgnoreCase));
if (attemptedProperty == null)
{
IsValidPathForAdd = false;
IsValidPathForRemove = false;
}
else
{
IsValidPathForAdd = true;
IsValidPathForRemove = true;
JsonPatchProperty = new JsonPatchProperty(attemptedProperty, targetObject);
PropertyPathInParent = property;
}
}
}
private object GetElementAtFromObject(object targetObject, int numericValue)
{
if (numericValue > -1)
{
// Check if the targetobject is an IEnumerable,
// and if the position is valid.
if (targetObject is IEnumerable)
{
var indexable = ((IEnumerable)targetObject).Cast<object>();
if (indexable.Count() >= numericValue)
{
return indexable.ElementAt(numericValue);
}
else { return null; }
}
else { return null; }
}
else
{
return null;
}
}
}
}

View File

@ -1,38 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.JsonPatch.Helpers
{
/// <summary>
/// Return value for Remove operation. The combination tells us what to do next (if this operation
/// is called from inside another operation, eg: Replace, Copy.
///
/// Possible combo:
/// - ActualType contains type: operation succesfully completed, can continue when called from inside
/// another operation
/// - ActualType null and HasError true: operation not completed succesfully, should not be allowed to continue
/// - ActualType null and HasError false: operation completed succesfully, but we should not be allowed to
/// continue when called from inside another method as we could not verify the type of the removed property.
/// This happens when the value of an item in an ExpandoObject dictionary is null.
/// </summary>
internal class RemovedPropertyTypeResult
{
/// <summary>
/// The type of the removed property (value)
/// </summary>
public Type ActualType { get; private set; }
/// <summary>
/// HasError: true when an error occurred, the operation didn't complete succesfully
/// </summary>
public bool HasError { get; set; }
public RemovedPropertyTypeResult(Type actualType, bool hasError)
{
ActualType = actualType;
HasError = hasError;
}
}
}

View File

@ -1,17 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch.Helpers
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
internal class ConversionResult
public class ConversionResult
{
public bool CanBeConverted { get; private set; }
public object ConvertedInstance { get; private set; }
public ConversionResult(bool canBeConverted, object convertedInstance)
{
CanBeConverted = canBeConverted;
ConvertedInstance = convertedInstance;
}
public bool CanBeConverted { get; }
public object ConvertedInstance { get; }
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public static class ConversionResultProvider
{
public static ConversionResult ConvertTo(object value, Type typeToConvertTo)
{
try
{
var deserialized = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), typeToConvertTo);
return new ConversionResult(true, deserialized);
}
catch
{
return new ConversionResult(canBeConverted: false, convertedInstance: null);
}
}
}
}

View File

@ -0,0 +1,111 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class DictionaryAdapter : IAdapter
{
public bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var dictionary = (IDictionary)target;
// As per JsonPatch spec, if a key already exists, adding should replace the existing value
dictionary[segment] = value;
errorMessage = null;
return true;
}
public bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
var dictionary = (IDictionary)target;
value = dictionary[segment];
errorMessage = null;
return true;
}
public bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage)
{
var dictionary = (IDictionary)target;
// As per JsonPatch spec, the target location must exist for remove to be successful
if (!dictionary.Contains(segment))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
dictionary.Remove(segment);
errorMessage = null;
return true;
}
public bool TryReplace(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var dictionary = (IDictionary)target;
// As per JsonPatch spec, the target location must exist for remove to be successful
if (!dictionary.Contains(segment))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
dictionary[segment] = value;
errorMessage = null;
return true;
}
public bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object nextTarget,
out string errorMessage)
{
var dictionary = target as IDictionary;
if (dictionary == null)
{
nextTarget = null;
errorMessage = null;
return false;
}
if (dictionary.Contains(segment))
{
nextTarget = dictionary[segment];
errorMessage = null;
return true;
}
else
{
nextTarget = null;
errorMessage = null;
return false;
}
}
}
}

View File

@ -0,0 +1,143 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Dynamic;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ExpandoObjectAdapter : IAdapter
{
public bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var dictionary = (IDictionary<string, object>)target;
var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment);
// As per JsonPatch spec, if a key already exists, adding should replace the existing value
dictionary[key] = ConvertValue(dictionary, key, value);
errorMessage = null;
return true;
}
public bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
var dictionary = (IDictionary<string, object>)target;
var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment);
value = dictionary[key];
errorMessage = null;
return true;
}
public bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage)
{
var dictionary = (IDictionary<string, object>)target;
var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment);
// As per JsonPatch spec, the target location must exist for remove to be successful
if (!dictionary.ContainsKey(key))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
dictionary.Remove(key);
errorMessage = null;
return true;
}
public bool TryReplace(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var dictionary = (IDictionary<string, object>)target;
var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment);
// As per JsonPatch spec, the target location must exist for remove to be successful
if (!dictionary.ContainsKey(key))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
dictionary[key] = ConvertValue(dictionary, key, value);
errorMessage = null;
return true;
}
public bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object nextTarget,
out string errorMessage)
{
var expandoObject = target as ExpandoObject;
if (expandoObject == null)
{
errorMessage = null;
nextTarget = null;
return false;
}
var dictionary = (IDictionary<string, object>)expandoObject;
var key = dictionary.GetKeyUsingCaseInsensitiveSearch(segment);
if (dictionary.ContainsKey(key))
{
nextTarget = dictionary[key];
errorMessage = null;
return true;
}
else
{
nextTarget = null;
errorMessage = null;
return false;
}
}
private object ConvertValue(IDictionary<string, object> dictionary, string key, object newValue)
{
object existingValue = null;
if (dictionary.TryGetValue(key, out existingValue))
{
if (existingValue != null)
{
var conversionResult = ConversionResultProvider.ConvertTo(newValue, existingValue.GetType());
if (conversionResult.CanBeConverted)
{
return conversionResult.ConvertedInstance;
}
}
}
return newValue;
}
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
// Helper methods to allow case-insensitive key search
public static class ExpandoObjectDictionaryExtensions
{
internal static string GetKeyUsingCaseInsensitiveSearch(
this IDictionary<string, object> propertyDictionary,
string key)
{
foreach (var keyInDictionary in propertyDictionary.Keys)
{
if (string.Equals(key, keyInDictionary, StringComparison.OrdinalIgnoreCase))
{
return keyInDictionary;
}
}
return key;
}
}
}

View File

@ -1,16 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Newtonsoft.Json;
using System;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.JsonPatch.Helpers
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
internal static class ExpressionHelpers
public static class ExpressionHelpers
{
public static string GetPath<TModel, TProp>(Expression<Func<TModel, TProp>> expr) where TModel : class
{
@ -59,7 +58,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Helpers
}
else
{
// Get property name, respecting JsonProperty attribute
// Get property name, respecting JsonProperty attribute
return GetPropertyNameFromMemberExpression(memberExpression);
}
case ExpressionType.Parameter:
@ -73,7 +72,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Helpers
private static string GetPropertyNameFromMemberExpression(MemberExpression memberExpression)
{
// if there's a JsonProperty attribute, we must return the PropertyName
// from the attribute rather than the member name
// from the attribute rather than the member name
var jsonPropertyAttribute =
memberExpression.Member.GetCustomAttribute(
typeof(JsonPropertyAttribute), true);

View File

@ -0,0 +1,44 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public interface IAdapter
{
bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object nextTarget,
out string errorMessage);
bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage);
bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage);
bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage);
bool TryReplace(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage);
}
}

View File

@ -0,0 +1,299 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Extensions.Internal;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ListAdapter : IAdapter
{
public bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var list = (IList)target;
Type typeArgument = null;
if (!TryGetListTypeArgument(list, out typeArgument, out errorMessage))
{
return false;
}
PositionInfo positionInfo;
if (!TryGetPositionInfo(list, segment, out positionInfo, out errorMessage))
{
return false;
}
object convertedValue = null;
if (!TryConvertValue(value, typeArgument, segment, out convertedValue, out errorMessage))
{
return false;
}
if (positionInfo.Type == PositionType.EndOfList)
{
list.Add(convertedValue);
}
else
{
list.Insert(positionInfo.Index, convertedValue);
}
errorMessage = null;
return true;
}
public bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
var list = (IList)target;
Type typeArgument = null;
if (!TryGetListTypeArgument(list, out typeArgument, out errorMessage))
{
value = null;
return false;
}
PositionInfo positionInfo;
if (!TryGetPositionInfo(list, segment, out positionInfo, out errorMessage))
{
value = null;
return false;
}
if (positionInfo.Type == PositionType.EndOfList)
{
value = list[list.Count - 1];
}
else
{
value = list[positionInfo.Index];
}
errorMessage = null;
return true;
}
public bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage)
{
var list = (IList)target;
Type typeArgument = null;
if (!TryGetListTypeArgument(list, out typeArgument, out errorMessage))
{
return false;
}
PositionInfo positionInfo;
if (!TryGetPositionInfo(list, segment, out positionInfo, out errorMessage))
{
return false;
}
if (positionInfo.Type == PositionType.EndOfList)
{
list.RemoveAt(list.Count - 1);
}
else
{
list.RemoveAt(positionInfo.Index);
}
errorMessage = null;
return true;
}
public bool TryReplace(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
var list = (IList)target;
Type typeArgument = null;
if (!TryGetListTypeArgument(list, out typeArgument, out errorMessage))
{
return false;
}
PositionInfo positionInfo;
if (!TryGetPositionInfo(list, segment, out positionInfo, out errorMessage))
{
return false;
}
object convertedValue = null;
if (!TryConvertValue(value, typeArgument, segment, out convertedValue, out errorMessage))
{
return false;
}
if (positionInfo.Type == PositionType.EndOfList)
{
list[list.Count - 1] = convertedValue;
}
else
{
list[positionInfo.Index] = convertedValue;
}
errorMessage = null;
return true;
}
public bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
var list = target as IList;
if (list == null)
{
value = null;
errorMessage = null;
return false;
}
int index = -1;
if (!int.TryParse(segment, out index))
{
value = null;
errorMessage = Resources.FormatInvalidIndexValue(segment);
return false;
}
if (index < 0 || index >= list.Count)
{
value = null;
errorMessage = Resources.FormatIndexOutOfBounds(segment);
return false;
}
value = list[index];
errorMessage = null;
return true;
}
private bool TryConvertValue(
object originalValue,
Type listTypeArgument,
string segment,
out object convertedValue,
out string errorMessage)
{
var conversionResult = ConversionResultProvider.ConvertTo(originalValue, listTypeArgument);
if (!conversionResult.CanBeConverted)
{
convertedValue = null;
errorMessage = Resources.FormatInvalidValueForProperty(originalValue);
return false;
}
convertedValue = conversionResult.ConvertedInstance;
errorMessage = null;
return true;
}
private bool TryGetListTypeArgument(IList list, out Type listTypeArgument, out string errorMessage)
{
// Arrays are not supported as they have fixed size and operations like Add, Insert do not make sense
var listType = list.GetType();
if (listType.IsArray)
{
errorMessage = Resources.FormatPatchNotSupportedForArrays(listType.FullName);
listTypeArgument = null;
return false;
}
else
{
var genericList = ClosedGenericMatcher.ExtractGenericInterface(listType, typeof(IList<>));
if (genericList == null)
{
errorMessage = Resources.FormatPatchNotSupportedForNonGenericLists(listType.FullName);
listTypeArgument = null;
return false;
}
else
{
listTypeArgument = genericList.GenericTypeArguments[0];
errorMessage = null;
return true;
}
}
}
private bool TryGetPositionInfo(IList list, string segment, out PositionInfo positionInfo, out string errorMessage)
{
if (segment == "-")
{
positionInfo = new PositionInfo(PositionType.EndOfList, -1);
errorMessage = null;
return true;
}
int position = -1;
if (int.TryParse(segment, out position))
{
if (position >= 0 && position < list.Count)
{
positionInfo = new PositionInfo(PositionType.Index, position);
errorMessage = null;
return true;
}
else
{
positionInfo = default(PositionInfo);
errorMessage = Resources.FormatIndexOutOfBounds(segment);
return false;
}
}
else
{
positionInfo = default(PositionInfo);
errorMessage = Resources.FormatInvalidIndexValue(segment);
return false;
}
}
private struct PositionInfo
{
public PositionInfo(PositionType type, int index)
{
Type = type;
Index = index;
}
public PositionType Type { get; }
public int Index { get; }
}
private enum PositionType
{
Index, // valid index
EndOfList, // '-'
Invalid, // Ex: not an integer
OutOfBounds
}
}
}

View File

@ -0,0 +1,76 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Dynamic;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ObjectVisitor
{
private readonly IContractResolver _contractResolver;
private readonly ParsedPath _path;
public ObjectVisitor(ParsedPath path, IContractResolver contractResolver)
{
if (contractResolver == null)
{
throw new ArgumentNullException(nameof(contractResolver));
}
_path = path;
_contractResolver = contractResolver;
}
public bool TryVisit(ref object target, out IAdapter adapter, out string errorMessage)
{
if (target == null)
{
adapter = null;
errorMessage = null;
return false;
}
adapter = SelectAdapater(target);
// Traverse until the penultimate segment to get the target object and adapter
for (var i = 0; i < _path.Segments.Count - 1; i++)
{
object next;
if (!adapter.TryTraverse(target, _path.Segments[i], _contractResolver, out next, out errorMessage))
{
adapter = null;
return false;
}
target = next;
adapter = SelectAdapater(target);
}
errorMessage = null;
return true;
}
private IAdapter SelectAdapater(object targetObject)
{
if (targetObject is ExpandoObject)
{
return new ExpandoObjectAdapter();
}
else if (targetObject is IDictionary)
{
return new DictionaryAdapter();
}
else if (targetObject is IList)
{
return new ListAdapter();
}
else
{
return new PocoAdapter();
}
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public struct ParsedPath
{
private static readonly string[] Empty = null;
private readonly string[] _segments;
public ParsedPath(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
_segments = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
}
public string LastSegment
{
get
{
if (_segments == null || _segments.Length == 0)
{
return null;
}
return _segments[_segments.Length - 1];
}
}
public IReadOnlyList<string> Segments => _segments ?? Empty;
}
}

View File

@ -3,7 +3,7 @@
using Microsoft.AspNetCore.JsonPatch.Exceptions;
namespace Microsoft.AspNetCore.JsonPatch.Helpers
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
internal static class PathHelpers
{

View File

@ -0,0 +1,205 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class PocoAdapter : IAdapter
{
public bool TryAdd(
object target,
string segment,
IContractResolver contractResolver,
object value,
out string errorMessage)
{
JsonProperty jsonProperty = null;
if (!TryGetJsonProperty(target, contractResolver, segment, out jsonProperty))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
if (!jsonProperty.Writable)
{
errorMessage = Resources.FormatCannotUpdateProperty(segment);
return false;
}
object convertedValue = null;
if (!TryConvertValue(value, jsonProperty.PropertyType, out convertedValue))
{
errorMessage = Resources.FormatInvalidValueForProperty(value);
return false;
}
jsonProperty.ValueProvider.SetValue(target, convertedValue);
errorMessage = null;
return true;
}
public bool TryGet(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
JsonProperty jsonProperty = null;
if (!TryGetJsonProperty(target, contractResolver, segment, out jsonProperty))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
value = null;
return false;
}
if (!jsonProperty.Readable)
{
errorMessage = Resources.FormatCannotReadProperty(segment);
value = null;
return false;
}
value = jsonProperty.ValueProvider.GetValue(target);
errorMessage = null;
return true;
}
public bool TryRemove(
object target,
string segment,
IContractResolver contractResolver,
out string errorMessage)
{
JsonProperty jsonProperty = null;
if (!TryGetJsonProperty(target, contractResolver, segment, out jsonProperty))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
if (!jsonProperty.Writable)
{
errorMessage = Resources.FormatCannotUpdateProperty(segment);
return false;
}
// Setting the value to "null" will use the default value in case of value types, and
// null in case of reference types
object value = null;
if (jsonProperty.PropertyType.GetTypeInfo().IsValueType
&& Nullable.GetUnderlyingType(jsonProperty.PropertyType) == null)
{
value = Activator.CreateInstance(jsonProperty.PropertyType);
}
jsonProperty.ValueProvider.SetValue(target, value);
errorMessage = null;
return true;
}
public bool TryReplace(
object target,
string segment,
IContractResolver
contractResolver,
object value,
out string errorMessage)
{
JsonProperty jsonProperty = null;
if (!TryGetJsonProperty(target, contractResolver, segment, out jsonProperty))
{
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
if (!jsonProperty.Writable)
{
errorMessage = Resources.FormatCannotUpdateProperty(segment);
return false;
}
object convertedValue = null;
if (!TryConvertValue(value, jsonProperty.PropertyType, out convertedValue))
{
errorMessage = Resources.FormatInvalidValueForProperty(value);
return false;
}
jsonProperty.ValueProvider.SetValue(target, convertedValue);
errorMessage = null;
return true;
}
public bool TryTraverse(
object target,
string segment,
IContractResolver contractResolver,
out object value,
out string errorMessage)
{
if (target == null)
{
value = null;
errorMessage = null;
return false;
}
JsonProperty jsonProperty = null;
if (TryGetJsonProperty(target, contractResolver, segment, out jsonProperty))
{
value = jsonProperty.ValueProvider.GetValue(target);
errorMessage = null;
return true;
}
value = null;
errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment);
return false;
}
private bool TryGetJsonProperty(
object target,
IContractResolver contractResolver,
string segment,
out JsonProperty jsonProperty)
{
var jsonObjectContract = contractResolver.ResolveContract(target.GetType()) as JsonObjectContract;
if (jsonObjectContract != null)
{
var pocoProperty = jsonObjectContract
.Properties
.FirstOrDefault(p => string.Equals(p.PropertyName, segment, StringComparison.OrdinalIgnoreCase));
if (pocoProperty != null)
{
jsonProperty = pocoProperty;
return true;
}
}
jsonProperty = null;
return false;
}
private bool TryConvertValue(object value, Type propertyType, out object convertedValue)
{
var conversionResult = ConversionResultProvider.ConvertTo(value, propertyType);
if (!conversionResult.CanBeConverted)
{
convertedValue = null;
return false;
}
convertedValue = conversionResult.ConvertedInstance;
return true;
}
}
}

View File

@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.JsonPatch.Adapters;
using Microsoft.AspNetCore.JsonPatch.Converters;
using Microsoft.AspNetCore.JsonPatch.Helpers;
using Microsoft.AspNetCore.JsonPatch.Internal;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
@ -13,7 +13,7 @@ using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNetCore.JsonPatch
{
// Implementation details: the purpose of this type of patch document is to allow creation of such
// documents for cases where there's no class/DTO to work on. Typical use case: backend not built in
// documents for cases where there's no class/DTO to work on. Typical use case: backend not built in
// .NET or architecture doesn't contain a shared DTO layer.
[JsonConverter(typeof(JsonPatchDocumentConverter))]
public class JsonPatchDocument : IJsonPatchDocument
@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// Apply this JsonPatchDocument
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
public void ApplyTo(object objectToApplyTo)
@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// Apply this JsonPatchDocument
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
/// <param name="logErrorAction">Action to log errors</param>
@ -174,7 +174,7 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// Apply this JsonPatchDocument
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
/// <param name="adapter">IObjectAdapter instance to use when applying</param>

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Linq.Expressions;
using Microsoft.AspNetCore.JsonPatch.Adapters;
using Microsoft.AspNetCore.JsonPatch.Converters;
using Microsoft.AspNetCore.JsonPatch.Helpers;
using Microsoft.AspNetCore.JsonPatch.Internal;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
@ -502,7 +502,7 @@ namespace Microsoft.AspNetCore.JsonPatch
/// Copy from a property to a location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from">source location</param>
/// <param name="from">source location</param>
/// <param name="path">target location</param>
/// <param name="positionTo">position</param>
/// <returns></returns>
@ -623,7 +623,7 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// Apply this JsonPatchDocument
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
public void ApplyTo(TModel objectToApplyTo)
@ -637,7 +637,7 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// Apply this JsonPatchDocument
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
/// <param name="logErrorAction">Action to log errors</param>
@ -652,7 +652,7 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// Apply this JsonPatchDocument
/// Apply this JsonPatchDocument
/// </summary>
/// <param name="objectToApplyTo">Object to apply the JsonPatchDocument to</param>
/// <param name="adapter">IObjectAdapter instance to use when applying</param>

View File

@ -26,6 +26,22 @@ namespace Microsoft.AspNetCore.JsonPatch
return string.Format(CultureInfo.CurrentCulture, GetString("CannotDeterminePropertyType"), p0);
}
/// <summary>
/// The '{0}' operation at path '{1}' could not be performed.
/// </summary>
internal static string CannotPerformOperation
{
get { return GetString("CannotPerformOperation"); }
}
/// <summary>
/// The '{0}' operation at path '{1}' could not be performed.
/// </summary>
internal static string FormatCannotPerformOperation(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("CannotPerformOperation"), p0, p1);
}
/// <summary>
/// The property at '{0}' could not be read.
/// </summary>
@ -59,35 +75,35 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// The key '{0}' was not found.
/// The index value provided by path segment '{0}' is out of bounds of the array size.
/// </summary>
internal static string DictionaryKeyNotFound
internal static string IndexOutOfBounds
{
get { return GetString("DictionaryKeyNotFound"); }
get { return GetString("IndexOutOfBounds"); }
}
/// <summary>
/// The key '{0}' was not found.
/// The index value provided by path segment '{0}' is out of bounds of the array size.
/// </summary>
internal static string FormatDictionaryKeyNotFound(object p0)
internal static string FormatIndexOutOfBounds(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("DictionaryKeyNotFound"), p0);
return string.Format(CultureInfo.CurrentCulture, GetString("IndexOutOfBounds"), p0);
}
/// <summary>
/// For operation '{0}' on array property at path '{1}', the index is larger than the array size.
/// The path segment '{0}' is invalid for an array index.
/// </summary>
internal static string InvalidIndexForArrayProperty
internal static string InvalidIndexValue
{
get { return GetString("InvalidIndexForArrayProperty"); }
get { return GetString("InvalidIndexValue"); }
}
/// <summary>
/// For operation '{0}' on array property at path '{1}', the index is larger than the array size.
/// The path segment '{0}' is invalid for an array index.
/// </summary>
internal static string FormatInvalidIndexForArrayProperty(object p0, object p1)
internal static string FormatInvalidIndexValue(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("InvalidIndexForArrayProperty"), p0, p1);
return string.Format(CultureInfo.CurrentCulture, GetString("InvalidIndexValue"), p0);
}
/// <summary>
@ -106,22 +122,6 @@ namespace Microsoft.AspNetCore.JsonPatch
return string.Format(CultureInfo.CurrentCulture, GetString("InvalidJsonPatchDocument"), p0);
}
/// <summary>
/// For operation '{0}', the provided path is invalid for array property at path '{1}'.
/// </summary>
internal static string InvalidPathForArrayProperty
{
get { return GetString("InvalidPathForArrayProperty"); }
}
/// <summary>
/// For operation '{0}', the provided path is invalid for array property at path '{1}'.
/// </summary>
internal static string FormatInvalidPathForArrayProperty(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("InvalidPathForArrayProperty"), p0, p1);
}
/// <summary>
/// The provided string '{0}' is an invalid path.
/// </summary>
@ -139,7 +139,7 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// The value '{0}' is invalid for property at path '{1}'.
/// The value '{0}' is invalid for target location.
/// </summary>
internal static string InvalidValueForProperty
{
@ -147,27 +147,11 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// The value '{0}' is invalid for property at path '{1}'.
/// The value '{0}' is invalid for target location.
/// </summary>
internal static string FormatInvalidValueForProperty(object p0, object p1)
internal static string FormatInvalidValueForProperty(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForProperty"), p0, p1);
}
/// <summary>
/// For operation '{0}' on array property at path '{1}', the index is negative.
/// </summary>
internal static string NegativeIndexForArrayProperty
{
get { return GetString("NegativeIndexForArrayProperty"); }
}
/// <summary>
/// For operation '{0}' on array property at path '{1}', the index is negative.
/// </summary>
internal static string FormatNegativeIndexForArrayProperty(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("NegativeIndexForArrayProperty"), p0, p1);
return string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForProperty"), p0);
}
/// <summary>
@ -187,51 +171,67 @@ namespace Microsoft.AspNetCore.JsonPatch
}
/// <summary>
/// The property at path '{0}' could not be added.
/// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.
/// </summary>
internal static string PropertyCannotBeAdded
internal static string PatchNotSupportedForArrays
{
get { return GetString("PropertyCannotBeAdded"); }
get { return GetString("PatchNotSupportedForArrays"); }
}
/// <summary>
/// The property at path '{0}' could not be added.
/// The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.
/// </summary>
internal static string FormatPropertyCannotBeAdded(object p0)
internal static string FormatPatchNotSupportedForArrays(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PropertyCannotBeAdded"), p0);
return string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForArrays"), p0);
}
/// <summary>
/// The property at path '{0}' could not be removed.
/// The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.
/// </summary>
internal static string PropertyCannotBeRemoved
internal static string PatchNotSupportedForNonGenericLists
{
get { return GetString("PropertyCannotBeRemoved"); }
get { return GetString("PatchNotSupportedForNonGenericLists"); }
}
/// <summary>
/// The property at path '{0}' could not be removed.
/// The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.
/// </summary>
internal static string FormatPropertyCannotBeRemoved(object p0)
internal static string FormatPatchNotSupportedForNonGenericLists(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PropertyCannotBeRemoved"), p0);
return string.Format(CultureInfo.CurrentCulture, GetString("PatchNotSupportedForNonGenericLists"), p0);
}
/// <summary>
/// Property does not exist at path '{0}'.
/// The target location specified by path segment '{0}' was not found.
/// </summary>
internal static string PropertyDoesNotExist
internal static string TargetLocationAtPathSegmentNotFound
{
get { return GetString("PropertyDoesNotExist"); }
get { return GetString("TargetLocationAtPathSegmentNotFound"); }
}
/// <summary>
/// Property does not exist at path '{0}'.
/// The target location specified by path segment '{0}' was not found.
/// </summary>
internal static string FormatPropertyDoesNotExist(object p0)
internal static string FormatTargetLocationAtPathSegmentNotFound(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PropertyDoesNotExist"), p0);
return string.Format(CultureInfo.CurrentCulture, GetString("TargetLocationAtPathSegmentNotFound"), p0);
}
/// <summary>
/// For operation '{0}', the target location specified by path '{1}' was not found.
/// </summary>
internal static string TargetLocationNotFound
{
get { return 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)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TargetLocationNotFound"), p0, p1);
}
/// <summary>

View File

@ -120,44 +120,44 @@
<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="DictionaryKeyNotFound" xml:space="preserve">
<value>The key '{0}' was not found.</value>
<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="InvalidIndexForArrayProperty" xml:space="preserve">
<value>For operation '{0}' on array property at path '{1}', the index is larger than the array size.</value>
<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 type '{0}' was malformed and could not be parsed.</value>
</data>
<data name="InvalidPathForArrayProperty" xml:space="preserve">
<value>For operation '{0}', the provided path is invalid for array property at path '{1}'.</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 property at path '{1}'.</value>
</data>
<data name="NegativeIndexForArrayProperty" xml:space="preserve">
<value>For operation '{0}' on array property at path '{1}', the index is negative.</value>
<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="PropertyCannotBeAdded" xml:space="preserve">
<value>The property at path '{0}' could not be added.</value>
<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="PropertyCannotBeRemoved" xml:space="preserve">
<value>The property at path '{0}' could not be removed.</value>
<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="PropertyDoesNotExist" xml:space="preserve">
<value>Property does not exist at path '{0}'.</value>
<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>

View File

@ -22,14 +22,18 @@
},
"dependencies": {
"NETStandard.Library": "1.6.1-*",
"Newtonsoft.Json": "9.0.1"
"Newtonsoft.Json": "9.0.1",
"Microsoft.Extensions.ClosedGenericMatcher.Sources": {
"type": "build",
"version": "1.1.0-*"
}
},
"frameworks": {
"netstandard1.1": {
"net451": {},
"netstandard1.3": {
"dependencies": {
"Microsoft.CSharp": "4.3.0-*",
"System.ComponentModel.TypeConverter": "4.3.0-*",
"System.Runtime.Serialization.Primitives": "4.3.0-*"
"System.Reflection.TypeExtensions": "4.1.0-*"
}
}
}

View File

@ -0,0 +1,176 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Moq;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class DictionaryAdapterTest
{
[Fact]
public void Add_KeyWhichAlreadyExists_ReplacesExistingValue()
{
// Arrange
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
dictionary[nameKey] = "Mike";
var dictionaryAdapter = new DictionaryAdapter();
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
string message = null;
// Act
var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver.Object, "James", out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(1, dictionary.Count);
Assert.Equal("James", dictionary[nameKey]);
}
[Fact]
public void Get_UsingCaseSensitiveKey_FailureScenario()
{
// Arrange
var dictionaryAdapter = new DictionaryAdapter();
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
string message = null;
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver.Object, "James", out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(1, dictionary.Count);
Assert.Equal("James", dictionary[nameKey]);
// Act
object outValue = null;
addStatus = dictionaryAdapter.TryGet(dictionary, nameKey.ToUpper(), resolver.Object, out outValue, out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Null(outValue);
}
[Fact]
public void Get_UsingCaseSensitiveKey_SuccessScenario()
{
// Arrange
var dictionaryAdapter = new DictionaryAdapter();
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
string message = null;
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var addStatus = dictionaryAdapter.TryAdd(dictionary, nameKey, resolver.Object, "James", out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(1, dictionary.Count);
Assert.Equal("James", dictionary[nameKey]);
// Act
object outValue = null;
addStatus = dictionaryAdapter.TryGet(dictionary, nameKey, resolver.Object, out outValue, out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal("James", outValue?.ToString());
}
[Fact]
public void ReplacingExistingItem()
{
// Arrange
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
dictionary.Add(nameKey, "Mike");
var dictionaryAdapter = new DictionaryAdapter();
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
string message = null;
// Act
var replaceStatus = dictionaryAdapter.TryReplace(dictionary, nameKey, resolver.Object, "James", out message);
// Assert
Assert.True(replaceStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(1, dictionary.Count);
Assert.Equal("James", dictionary[nameKey]);
}
[Fact]
public void Replace_NonExistingKey_Fails()
{
// Arrange
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
var dictionaryAdapter = new DictionaryAdapter();
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
string message = null;
// Act
var replaceStatus = dictionaryAdapter.TryReplace(dictionary, nameKey, resolver.Object, "Mike", out message);
// Assert
Assert.False(replaceStatus);
Assert.Equal(
string.Format("The target location specified by path segment '{0}' was not found.", nameKey),
message);
Assert.Equal(0, dictionary.Count);
}
[Fact]
public void Remove_NonExistingKey_Fails()
{
// Arrange
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
var dictionaryAdapter = new DictionaryAdapter();
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
string message = null;
// Act
var removeStatus = dictionaryAdapter.TryRemove(dictionary, nameKey, resolver.Object, out message);
// Assert
Assert.False(removeStatus);
Assert.Equal(
string.Format("The target location specified by path segment '{0}' was not found.", nameKey),
message);
Assert.Equal(0, dictionary.Count);
}
[Fact]
public void Remove_RemovesFromDictionary()
{
// Arrange
var nameKey = "Name";
var dictionary = new Dictionary<string, object>(StringComparer.Ordinal);
dictionary[nameKey] = "James";
var dictionaryAdapter = new DictionaryAdapter();
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
string message = null;
// Act
var removeStatus = dictionaryAdapter.TryRemove(dictionary, nameKey, resolver.Object, out message);
//Assert
Assert.True(removeStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(0, dictionary.Count);
}
}
}

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/NewInt' could not be added.",
string.Format("The target location specified by path segment '{0}' was not found.", "NewInt"),
exception.Message);
}
@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/Nested/NewInt' could not be added.",
string.Format("The target location specified by path segment '{0}' was not found.", "NewInt"),
exception.Message);
}
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/Nested/NewInt' could not be added.",
string.Format("The target location specified by path segment '{0}' was not found.", "NewInt"),
exception.Message);
}
@ -214,7 +214,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/ComplexProperty' could not be added.",
string.Format("The target location specified by path segment '{0}' was not found.", "ComplexProperty"),
exception.Message);
}
@ -238,7 +238,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/StringProperty' could not be updated.",
string.Format("The property at path '{0}' could not be updated.", "StringProperty"),
exception.Message);
}
@ -336,7 +336,10 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/DynamicProperty/OtherProperty/IntProperty' could not be added.",
string.Format(
"For operation '{0}', the target location specified by path '{1}' was not found.",
"add",
"/DynamicProperty/OtherProperty/IntProperty"),
exception.Message);
}
@ -374,7 +377,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/baz/bat' could not be added.",
string.Format("The target location specified by path segment '{0}' was not found.", "baz"),
exception.Message);
}
@ -434,7 +437,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'add' on array property at path '/IntegerList/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -478,30 +481,10 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'add' on array property at path '/IntegerList/4', the index is larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"),
exception.Message);
}
[Fact]
public void AddToListAtEndWithSerialization()
{
var doc = new
{
IntegerList = new List<int>() { 1, 2, 3 }
};
// create patch
JsonPatchDocument patchDoc = new JsonPatchDocument();
patchDoc.Add("IntegerList/3", 4);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument>(serialized);
deserialized.ApplyTo(doc);
Assert.Equal(new List<int>() { 1, 2, 3, 4 }, doc.IntegerList);
}
[Fact]
public void AddToListAtBeginning()
{
@ -542,7 +525,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'add' on array property at path '/IntegerList/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'add' on array property at path '/IntegerList/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/ListOfSimpleDTO/-1/IntegerList/0' could not be added.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/ListOfSimpleDTO/20/IntegerList/0' could not be added.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "20"),
exception.Message);
}
}

View File

@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/Test' could not be updated.",
string.Format("The property at path '{0}' could not be updated.", "Test"),
exception.Message);
}
@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"The property at path '/NonExisting' could not be removed.",
string.Format("The target location specified by path segment '{0}' was not found.", "NonExisting"),
exception.Message);
}
@ -250,7 +250,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'remove' on array property at path '/SimpleDTO/IntegerList/3', the index is larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -275,8 +275,8 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'remove' on array property at path '/SimpleDTO/IntegerList/-1', the index is negative.",
exception.Message);
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
[Fact]

View File

@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'remove' on array property at path '/IntegerList/3', the index is larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'remove' on array property at path '/IntegerList/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -187,8 +187,8 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'remove' on array property at path '/SimpleDTO/IntegerList/3', the index is larger than the array size.",
exception.Message);
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
[Fact]
@ -214,7 +214,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'remove' on array property at path '/SimpleDTO/IntegerList/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Dynamic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
@ -25,7 +26,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
JsonPatchDocument patchDoc = new JsonPatchDocument();
patchDoc.Replace("GuidValue", newGuid);
// serialize & deserialize
// serialize & deserialize
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserizalized = JsonConvert.DeserializeObject<JsonPatchDocument>(serialized);
@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
JsonPatchDocument patchDoc = new JsonPatchDocument();
patchDoc.Replace("GuidValue", newGuid);
// serialize & deserialize
// serialize & deserialize
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserizalized = JsonConvert.DeserializeObject<JsonPatchDocument>(serialized);
@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
JsonPatchDocument patchDoc = new JsonPatchDocument();
patchDoc.Replace("nestedobject/GuidValue", newGuid);
// serialize & deserialize
// serialize & deserialize
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserizalized = JsonConvert.DeserializeObject<JsonPatchDocument>(serialized);
@ -98,7 +99,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
JsonPatchDocument patchDoc = new JsonPatchDocument();
patchDoc.Replace("SimpleDTO", newDTO);
// serialize & deserialize
// serialize & deserialize
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument>(serialized);
@ -201,6 +202,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test.Dynamic
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument>(serialized);
deserialized.ApplyTo(doc);
Assert.Equal(new List<int>() { 4, 5, 6 }, doc.IntegerList);
}

View File

@ -0,0 +1,302 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections;
using System.Collections.Generic;
using Moq;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ListAdapterTest
{
[Fact]
public void Patch_OnArrayObject_Fails()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new[] { 20, 30 };
var listAdapter = new ListAdapter();
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, "0", resolver.Object, "40", out message);
// Assert
Assert.False(addStatus);
Assert.Equal(
string.Format(
"The type '{0}' which is an array is not supported for json patch operations as it has a fixed size.",
targetObject.GetType().FullName),
message);
}
[Fact]
public void Patch_OnNonGenericListObject_Fails()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new ArrayList();
targetObject.Add(20);
targetObject.Add(30);
var listAdapter = new ListAdapter();
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "40", out message);
// Assert
Assert.False(addStatus);
Assert.Equal(
string.Format(
"The type '{0}' which is a non generic list is not supported for json patch operations. Only generic list types are supported.",
targetObject.GetType().FullName),
message);
}
[Theory]
[InlineData("-1")]
[InlineData("-2")]
[InlineData("2")]
[InlineData("3")]
public void Patch_WithOutOfBoundsIndex_Fails(string position)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<string>() { "James", "Mike" };
var listAdapter = new ListAdapter();
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out message);
// Assert
Assert.False(addStatus);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", position),
message);
}
[Theory]
[InlineData("_")]
[InlineData("blah")]
public void Patch_WithInvalidPositionFormat_Fails(string position)
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<string>() { "James", "Mike" };
var listAdapter = new ListAdapter();
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, "40", out message);
// Assert
Assert.False(addStatus);
Assert.Equal(
string.Format("The path segment '{0}' is invalid for an array index.", position),
message);
}
public static TheoryData<List<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();
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "20", out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(expected.Count, targetObject.Count);
Assert.Equal(expected, targetObject);
}
[Fact]
public void Add_NullObject_ToReferenceTypeListWorks()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var listAdapter = new ListAdapter();
var targetObject = new List<string>() { "James", "Mike" };
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, value: null, errorMessage: out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(3, targetObject.Count);
Assert.Equal(new List<string>() { "James", "Mike", null }, targetObject);
}
[Fact]
public void Add_NonCompatibleType_Fails()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = (new List<int>() { 10, 20 }).AsReadOnly();
var listAdapter = new ListAdapter();
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, "-", resolver.Object, "James", out message);
// Assert
Assert.False(addStatus);
Assert.Equal(string.Format("The value '{0}' is invalid for target location.", "James"), message);
}
public static TheoryData<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();
string message = null;
// Act
var addStatus = listAdapter.TryAdd(targetObject, position, resolver.Object, value, out message);
// Assert
Assert.True(addStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(expected.Count, targetObject.Count);
Assert.Equal(expected, targetObject);
}
[Fact]
public void Replace_NonCompatibleType_Fails()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = (new List<int>() { 10, 20 }).AsReadOnly();
var listAdapter = new ListAdapter();
string message = null;
// Act
var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "James", out message);
// Assert
Assert.False(replaceStatus);
Assert.Equal(
string.Format("The value '{0}' is invalid for target location.", "James"),
message);
}
[Fact]
public void Replace_ReplacesValue_AtTheEnd()
{
// Arrange
var resolver = new Mock<IContractResolver>(MockBehavior.Strict);
var targetObject = new List<int>() { 10, 20 };
var listAdapter = new ListAdapter();
string message = null;
// Act
var replaceStatus = listAdapter.TryReplace(targetObject, "-", resolver.Object, "30", out message);
// Assert
Assert.True(replaceStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(new List<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();
string message = null;
// Act
var replaceStatus = listAdapter.TryReplace(targetObject, position, resolver.Object, "30", out message);
// Assert
Assert.True(replaceStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Equal(expected, targetObject);
}
}
}

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch.Test
namespace Microsoft.AspNetCore.JsonPatch
{
public class NestedDTO
{

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Test
namespace Microsoft.AspNetCore.JsonPatch
{
public class NestedObjectTests
{
@ -386,8 +386,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal(
"For operation 'add' on array property at path '/simpledto/integerlist/4', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"),
exception.Message);
}
@ -417,8 +416,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'add' on array property at path '/simpledto/integerlist/4', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"),
exception.Message);
}
@ -445,8 +443,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
//Assert
Assert.Equal(
"For operation 'add' on array property at path '/simpledto/integerlist/4', the index is larger than " +
"the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"),
logger.ErrorMessage);
}
@ -470,7 +467,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal(
"For operation 'add' on array property at path '/simpledto/integerlist/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -499,7 +496,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'add' on array property at path '/simpledto/integerlist/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -527,7 +524,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
//Assert
Assert.Equal(
"For operation 'add' on array property at path '/simpledto/integerlist/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
logger.ErrorMessage);
}
@ -697,8 +694,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal(
"For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -727,8 +723,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -754,8 +749,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Assert
Assert.Equal(
"For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
logger.ErrorMessage);
}
@ -777,7 +771,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", exception.Message);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
[Fact]
@ -804,7 +800,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
{
deserialized.ApplyTo(doc);
});
Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", exception.Message);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
[Fact]
@ -829,7 +827,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
patchDoc.ApplyTo(doc, logger.LogErrorMessage);
// Assert
Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", logger.ErrorMessage);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
logger.ErrorMessage);
}
[Fact]
@ -1239,8 +1239,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal(
"For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -1269,8 +1268,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -1298,8 +1296,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Assert
Assert.Equal(
"For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
logger.ErrorMessage);
}
@ -1321,7 +1318,8 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal("For operation 'replace' on array property at path '/simpledto/integerlist/-1', the index is negative.",
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -1347,7 +1345,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { deserialized.ApplyTo(doc); });
Assert.Equal(
"For operation 'replace' on array property at path '/simpledto/integerlist/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -1375,7 +1373,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Assert
Assert.Equal(
"For operation 'replace' on array property at path '/simpledto/integerlist/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
logger.ErrorMessage);
}

View File

@ -8,7 +8,7 @@ using Microsoft.AspNetCore.JsonPatch.Exceptions;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Test
namespace Microsoft.AspNetCore.JsonPatch.Adapters
{
public class ObjectAdapterTests
{
@ -157,8 +157,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal(
"For operation 'add' on array property at path '/integerlist/4', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"),
exception.Message);
}
@ -181,8 +180,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { deserialized.ApplyTo(doc); });
Assert.Equal(
"For operation 'add' on array property at path '/integerlist/4', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"),
exception.Message);
}
@ -206,54 +204,10 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Assert
Assert.Equal(
"For operation 'add' on array property at path '/integerlist/4', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "4"),
logger.ErrorMessage);
}
[Fact]
public void AddToListAtEnd()
{
// Arrange
var doc = new SimpleDTO()
{
IntegerList = new List<int>() { 1, 2, 3 }
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTO>();
patchDoc.Add<int>(o => o.IntegerList, 4, 3);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal(new List<int>() { 1, 2, 3, 4 }, doc.IntegerList);
}
[Fact]
public void AddToListAtEndWithSerialization()
{
// Arrange
var doc = new SimpleDTO()
{
IntegerList = new List<int>() { 1, 2, 3 }
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTO>();
patchDoc.Add<int>(o => o.IntegerList, 4, 3);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleDTO>>(serialized);
// Act
deserialized.ApplyTo(doc);
// Assert
Assert.Equal(new List<int>() { 1, 2, 3, 4 }, doc.IntegerList);
}
[Fact]
public void AddToListAtBeginning()
{
@ -313,7 +267,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal(
"For operation 'add' on array property at path '/integerlist/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -336,7 +290,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { deserialized.ApplyTo(doc); });
Assert.Equal(
"For operation 'add' on array property at path '/integerlist/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
@ -359,7 +313,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Assert
Assert.Equal(
"For operation 'add' on array property at path '/integerlist/-1', the index is negative.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
logger.ErrorMessage);
}
@ -508,8 +462,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal(
"For operation 'remove' on array property at path '/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -532,8 +485,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { deserialized.ApplyTo(doc); });
Assert.Equal(
"For operation 'remove' on array property at path '/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -557,8 +509,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Assert
Assert.Equal(
"For operation 'remove' on array property at path '/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
logger.ErrorMessage);
}
@ -577,7 +528,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { patchDoc.ApplyTo(doc); });
Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", exception.Message);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
[Fact]
@ -598,7 +551,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => { deserialized.ApplyTo(doc); });
Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", exception.Message);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
[Fact]
@ -621,7 +576,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
// Assert
Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", logger.ErrorMessage);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
logger.ErrorMessage);
}
[Fact]
@ -1123,8 +1080,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
patchDoc.ApplyTo(doc);
});
Assert.Equal(
"For operation 'replace' on array property at path '/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -1149,8 +1105,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
deserialized.ApplyTo(doc);
});
Assert.Equal(
"For operation 'replace' on array property at path '/integerlist/3', the index is " +
"larger than the array size.",
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "3"),
exception.Message);
}
@ -1172,7 +1127,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
{
patchDoc.ApplyTo(doc);
});
Assert.Equal("For operation 'replace' on array property at path '/integerlist/-1', the index is negative.", exception.Message);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
[Fact]
@ -1196,7 +1153,9 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
{
deserialized.ApplyTo(doc);
});
Assert.Equal("For operation 'replace' on array property at path '/integerlist/-1', the index is negative.", exception.Message);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", "-1"),
exception.Message);
}
[Fact]
@ -1733,5 +1692,577 @@ namespace Microsoft.AspNetCore.JsonPatch.Test
Assert.Equal(0, doc.IntegerValue);
Assert.Equal(new List<int>() { 1, 2, 3, 5 }, doc.IntegerList);
}
private class Class6
{
public IDictionary<string, int> DictionaryOfStringToInteger { get; } = new Dictionary<string, int>();
}
[Fact]
public void Add_WhenDictionary_ValueIsNonObject_Succeeds()
{
// Arrange
var model = new Class6();
model.DictionaryOfStringToInteger["one"] = 1;
model.DictionaryOfStringToInteger["two"] = 2;
var patchDocument = new JsonPatchDocument();
patchDocument.Add("/DictionaryOfStringToInteger/three", 3);
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(3, model.DictionaryOfStringToInteger.Count);
Assert.Equal(1, model.DictionaryOfStringToInteger["one"]);
Assert.Equal(2, model.DictionaryOfStringToInteger["two"]);
Assert.Equal(3, model.DictionaryOfStringToInteger["three"]);
}
[Fact]
public void Remove_WhenDictionary_ValueIsNonObject_Succeeds()
{
// Arrange
var model = new Class6();
model.DictionaryOfStringToInteger["one"] = 1;
model.DictionaryOfStringToInteger["two"] = 2;
var patchDocument = new JsonPatchDocument();
patchDocument.Remove("/DictionaryOfStringToInteger/two");
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(1, model.DictionaryOfStringToInteger.Count);
Assert.Equal(1, model.DictionaryOfStringToInteger["one"]);
}
[Fact]
public void Replace_WhenDictionary_ValueIsNonObject_Succeeds()
{
// Arrange
var model = new Class6();
model.DictionaryOfStringToInteger["one"] = 1;
model.DictionaryOfStringToInteger["two"] = 2;
var patchDocument = new JsonPatchDocument();
patchDocument.Replace("/DictionaryOfStringToInteger/two", 20);
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToInteger.Count);
Assert.Equal(1, model.DictionaryOfStringToInteger["one"]);
Assert.Equal(20, model.DictionaryOfStringToInteger["two"]);
}
private class Customer
{
public string Name { get; set; }
public Address Address { get; set; }
}
private class Address
{
public string City { get; set; }
}
private class Class8
{
public IDictionary<string, Customer> DictionaryOfStringToCustomer { get; } = new Dictionary<string, Customer>();
}
[Fact]
public void Replace_WhenDictionary_ValueAPocoType_Succeeds()
{
// Arrange
var key1 = "key1";
var value1 = new Customer() { Name = "Jamesss" };
var key2 = "key2";
var value2 = new Customer() { Name = "Mike" };
var model = new Class8();
model.DictionaryOfStringToCustomer[key1] = value1;
model.DictionaryOfStringToCustomer[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Name", "James");
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
Assert.NotNull(actualValue1);
Assert.Equal("James", actualValue1.Name);
}
[Fact]
public void Replace_WhenDictionary_ValueAPocoType_Succeeds_WithSerialization()
{
// Arrange
var key1 = "key1";
var value1 = new Customer() { Name = "Jamesss" };
var key2 = "key2";
var value2 = new Customer() { Name = "Mike" };
var model = new Class8();
model.DictionaryOfStringToCustomer[key1] = value1;
model.DictionaryOfStringToCustomer[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Name", "James");
var serialized = JsonConvert.SerializeObject(patchDocument);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<Class8>>(serialized);
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
Assert.NotNull(actualValue1);
Assert.Equal("James", actualValue1.Name);
}
[Fact]
public void Replace_DeepNested_DictionaryValue_Succeeds()
{
// Arrange
var key1 = "key1";
var value1 = new Customer() { Name = "Jamesss" };
var key2 = "key2";
var value2 = new Customer() { Name = "Mike" };
var model = new Class8();
model.DictionaryOfStringToCustomer[key1] = value1;
model.DictionaryOfStringToCustomer[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Name", "James");
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
Assert.NotNull(actualValue1);
Assert.Equal("James", actualValue1.Name);
}
[Fact]
public void Replace_DeepNested_DictionaryValue_Succeeds_WithSerialization()
{
// Arrange
var key1 = "key1";
var value1 = new Customer() { Name = "James", Address = new Address { City = "Redmond" } };
var key2 = "key2";
var value2 = new Customer() { Name = "Mike", Address = new Address { City = "Seattle" } };
var model = new Class8();
model.DictionaryOfStringToCustomer[key1] = value1;
model.DictionaryOfStringToCustomer[key2] = value2;
var patchDocument = new JsonPatchDocument();
patchDocument.Replace($"/DictionaryOfStringToCustomer/{key1}/Address/City", "Bellevue");
var serialized = JsonConvert.SerializeObject(patchDocument);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<Class8>>(serialized);
// Act
patchDocument.ApplyTo(model);
// Assert
Assert.Equal(2, model.DictionaryOfStringToCustomer.Count);
var actualValue1 = model.DictionaryOfStringToCustomer[key1];
Assert.NotNull(actualValue1);
Assert.Equal("James", actualValue1.Name);
var address = actualValue1.Address;
Assert.NotNull(address);
Assert.Equal("Bellevue", address.City);
}
class Class9
{
public List<string> StringList { get; set; } = new List<string>();
}
[Fact]
public void AddToNonIntegerListAtEnd()
{
// Arrange
var model = new Class9()
{
StringList = new List<string>()
};
model.StringList.Add("string1");
model.StringList.Add("string2");
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/StringList/0", "string3");
// Act
patchDoc.ApplyTo(model);
// Assert
Assert.Equal(new List<string>() { "string3", "string1", "string2" }, model.StringList);
}
[Fact]
public void AddMember_OnPOCO_WithNullPropertyValue_ShouldAddPropertyValue()
{
// Arrange
var doc = new SimpleDTO()
{
StringProperty = null
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTO>();
patchDoc.Add<string>(o => o.StringProperty, "B");
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal("B", doc.StringProperty);
}
private class Class1
{
public IDictionary<string, string> USStates { get; set; } = new Dictionary<string, string>();
}
[Fact]
public void AddMember_OnDictionaryProperty_ShouldAddKeyValueMember()
{
// Arrange
var expected = "Washington";
var model = new Class1();
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/USStates/WA", expected);
// Act
patchDoc.ApplyTo(model);
// Assert
Assert.Equal(1, model.USStates.Count);
Assert.Equal(expected, model.USStates["WA"]);
}
[Fact]
public void AddMember_OnDictionaryProperty_ShouldAddKeyValueMember_WithSerialization()
{
// Arrange
var expected = "Washington";
var model = new Class1();
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/USStates/WA", expected);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<Class1>>(serialized);
// Act
deserialized.ApplyTo(model);
// Assert
Assert.Equal(1, model.USStates.Count);
Assert.Equal(expected, model.USStates["WA"]);
}
private class Class2
{
public Class1 Class1Property { get; set; } = new Class1();
}
[Fact]
public void AddMember_OnDictionaryPropertyDeeplyNested_ShouldAddKeyValueMember()
{
// Arrange
var expected = "Washington";
var model = new Class2();
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/Class1Property/USStates/WA", expected);
// Act
patchDoc.ApplyTo(model);
// Assert
Assert.Equal(1, model.Class1Property.USStates.Count);
Assert.Equal(expected, model.Class1Property.USStates["WA"]);
}
[Fact]
public void AddMember_OnDictionaryPropertyDeeplyNested_ShouldAddKeyValueMember_WithSerialization()
{
// Arrange
var expected = "Washington";
var model = new Class2();
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/Class1Property/USStates/WA", expected);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<Class2>>(serialized);
// Act
deserialized.ApplyTo(model);
// Assert
Assert.Equal(1, model.Class1Property.USStates.Count);
Assert.Equal(expected, model.Class1Property.USStates["WA"]);
}
[Fact]
public void AddMember_OnDictionaryObjectDirectly_ShouldAddKeyValueMember()
{
// Arrange
var expected = "Washington";
var model = new Dictionary<string, string>();
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/WA", expected);
// Act
patchDoc.ApplyTo(model);
// Assert
Assert.Equal(1, model.Count);
Assert.Equal(expected, model["WA"]);
}
[Fact]
public void AddMember_OnDictionaryObjectDirectly_ShouldAddKeyValueMember_WithSerialization()
{
// Arrange
var expected = "Washington";
var model = new Dictionary<string, string>();
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/WA", expected);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<Dictionary<string, string>>>(serialized);
// Act
deserialized.ApplyTo(model);
// Assert
Assert.Equal(1, model.Count);
Assert.Equal(expected, model["WA"]);
}
[Fact]
public void AddElement_ToListDirectly_ShouldAppendValue()
{
// Arrange
var model = new List<int>() { 1, 2, 3 };
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/-", value: 4);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<List<int>>>(serialized);
// Act
deserialized.ApplyTo(model);
// Assert
Assert.Equal(new List<int>() { 1, 2, 3, 4 }, model);
}
[Fact]
public void AddElement_ToListDirectly_ShouldAppendValue_WithSerialization()
{
// Arrange
var model = new List<int>() { 1, 2, 3 };
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/-", value: 4);
// Act
patchDoc.ApplyTo(model);
// Assert
Assert.Equal(new List<int>() { 1, 2, 3, 4 }, model);
}
[Fact]
public void AddElement_ToListDirectly_ShouldAddValue_AtSuppliedPosition()
{
// Arrange
var model = new List<int>() { 1, 2, 3 };
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/0", value: 4);
// Act
patchDoc.ApplyTo(model);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, model);
}
[Fact]
public void AddElement_ToListDirectly_ShouldAddValue_AtSuppliedPosition_WithSerialization()
{
// Arrange
var model = new List<int>() { 1, 2, 3 };
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/0", value: 4);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<List<int>>>(serialized);
// Act
deserialized.ApplyTo(model);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, model);
}
class ListOnDictionary
{
public IDictionary<string, List<int>> NamesAndBadgeIds { get; set; } = new Dictionary<string, List<int>>();
}
[Fact]
public void AddElement_ToList_OnDictionary_ShouldAddValue_AtSuppliedPosition()
{
// Arrange
var model = new ListOnDictionary();
model.NamesAndBadgeIds["James"] = new List<int>();
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/NamesAndBadgeIds/James/-", 200);
// Act
patchDoc.ApplyTo(model);
// Assert
var list = model.NamesAndBadgeIds["James"];
Assert.NotNull(list);
Assert.Equal(new List<int>() { 200 }, list);
}
[Fact]
public void AddElement_ToList_OnPOCO_ShouldAddValue_AtSuppliedPosition()
{
// Arrange
var doc = new SimpleDTO()
{
IntegerIList = new List<int>() { 1, 2, 3 }
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTO>();
patchDoc.Add<int>(o => o.IntegerIList, 4, 0);
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.IntegerIList);
}
class Class3
{
public SimpleDTO SimpleDTOProperty { get; set; } = new SimpleDTO();
}
[Fact]
public void AddElement_ToDeeplyNestedListProperty_OnPOCO_ShouldAddValue_AtSuppliedPosition()
{
// Arrange
var model = new Class3()
{
SimpleDTOProperty = new SimpleDTO()
{
IntegerIList = new List<int>() { 1, 2, 3 }
}
};
var patchDoc = new JsonPatchDocument<Class3>();
patchDoc.Add<int>(o => o.SimpleDTOProperty.IntegerIList, value: 4, position: 0);
// Act
patchDoc.ApplyTo(model);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, model.SimpleDTOProperty.IntegerIList);
}
[Fact]
public void AddElement_ToDeeplyNestedListProperty_OnPOCO_ShouldAddValue_AtSuppliedPosition_WithSerialization()
{
// Arrange
var model = new Class3()
{
SimpleDTOProperty = new SimpleDTO()
{
IntegerIList = new List<int>() { 1, 2, 3 }
}
};
var patchDoc = new JsonPatchDocument<Class3>();
patchDoc.Add<int>(o => o.SimpleDTOProperty.IntegerIList, value: 4, position: 0);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<Class3>>(serialized);
// Act
deserialized.ApplyTo(model);
// Assert
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, model.SimpleDTOProperty.IntegerIList);
}
class Class4
{
public int IntegerProperty { get; set; }
}
[Fact]
public void Remove_OnNonReferenceType_POCOProperty_ShouldSetDefaultValue()
{
// Arrange
var model = new Class4()
{
IntegerProperty = 10
};
var patchDoc = new JsonPatchDocument<Class4>();
patchDoc.Remove<int>(o => o.IntegerProperty);
// Act
patchDoc.ApplyTo(model);
// Assert
Assert.Equal(0, model.IntegerProperty);
}
[Fact]
public void Remove_OnNonReferenceType_POCOProperty_ShouldSetDefaultValue_WithSerialization()
{
// Arrange
var doc = new SimpleDTO()
{
StringProperty = "A"
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTO>();
patchDoc.Remove<string>(o => o.StringProperty);
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleDTO>>(serialized);
// Act
deserialized.ApplyTo(doc);
// Assert
Assert.Equal(null, doc.StringProperty);
}
class ClassWithPrivateProperties
{
public string Name { get; set; }
private int Age { get; set; } = 45;
}
[Fact]
public void Add_OnPrivateProperties_FailesWithException()
{
// Arrange
var doc = new ClassWithPrivateProperties()
{
Name = "James"
};
// create patch
var patchDoc = new JsonPatchDocument();
patchDoc.Add("/Age", 30);
// Act & Assert
var exception = Assert.Throws<JsonPatchException>(() => patchDoc.ApplyTo(doc));
Assert.Equal(
string.Format("The target location specified by path segment '{0}' was not found.", "Age"),
exception.Message);
}
}
}

View File

@ -0,0 +1,230 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Dynamic;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ObjectVisitorTest
{
private class Class1
{
public string Name { get; set; }
public IList<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());
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(expectedTargetObject, targetObject);
Assert.IsType<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());
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(expectedTargetObject, targetObject);
Assert.IsType<DictionaryAdapter>(adapter);
}
public static IEnumerable<object[]> ReturnsExpandoAdapterData
{
get
{
var model = new Class1();
yield return new object[] { model, "/Items/Name", model.Items };
yield return new object[] { model.Items, "/Name", model.Items };
var nestedModel = new Class1Nested();
nestedModel.Customers.Add(new Class1());
yield return new object[] { nestedModel, "/Customers/0/Items/Name", nestedModel.Customers[0].Items };
yield return new object[] { nestedModel.Customers, "/0/Items/Name", nestedModel.Customers[0].Items };
yield return new object[] { nestedModel.Customers[0], "/Items/Name", nestedModel.Customers[0].Items };
}
}
[Theory]
[MemberData(nameof(ReturnsExpandoAdapterData))]
public void Visit_ValidPathToExpandoObject_ReturnsExpandoAdapter(object targetObject, string path, object expectedTargetObject)
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver());
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(expectedTargetObject, targetObject);
Assert.IsType<ExpandoObjectAdapter>(adapter);
}
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());
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(expectedTargetObject, targetObject);
Assert.IsType<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;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.False(visitStatus);
Assert.Equal(
string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", position),
message);
}
[Theory]
[InlineData("-")]
[InlineData("foo")]
public void Visit_InvalidIndexFormatToArray_Fails(string position)
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{position}/States/-"), new DefaultContractResolver());
var automobileDepartment = new Class1Nested();
object targetObject = automobileDepartment;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.False(visitStatus);
Assert.Equal(string.Format(
"The path segment '{0}' is invalid for an array index.", position),
message);
}
// The adapter takes care of the responsibility of validating the final segment
[Fact]
public void Visit_DoesNotValidate_FinalPathSegment()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath($"/NonExisting"), new DefaultContractResolver());
var model = new Class1();
object targetObject = model;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.IsType<PocoAdapter>(adapter);
}
}
}

View File

@ -0,0 +1,119 @@
using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ObjectVisitorTest
{
private class Class1
{
public IList<string> States { get; set; } = new List<string>();
public IDictionary<string, string> Countries = new Dictionary<string, string>();
}
[Fact]
public void Visit_ValidPathToArray_ReturnsListAdapter()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath("/States/-"), new DefaultContractResolver());
var model = new Class1();
object targetObject = model;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(model.States, targetObject);
Assert.IsType<ListAdapter>(adapter);
}
[Fact]
public void Visit_ValidPathToDictionary_ReturnsDictionaryAdapter()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath("/Countries/USA"), new DefaultContractResolver());
var model = new Class1();
object targetObject = model;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(model.Countries, targetObject);
Assert.IsType<ListAdapter>(adapter);
}
private class AutomobileDepartment
{
public List<Class1> Customers { get; set; } = new List<Class1>();
}
[Fact]
public void Visit_ValidPathToArray_ReturnsListAdapter_ForDeepNestedPath()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath("/Customers/0/States/-"), new DefaultContractResolver());
var customer = new Class1();
var automobileDepartment = new AutomobileDepartment();
automobileDepartment.Customers.Add(customer);
object targetObject = automobileDepartment;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(customer.States, targetObject);
Assert.IsType<ListAdapter>(adapter);
}
[Fact]
public void Visit_InvalidPathToArray_Fails()
{
// Arrange
var invalidIndex = 2;
var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{invalidIndex}/States/-"), new DefaultContractResolver());
var automobileDepartment = new AutomobileDepartment();
object targetObject = automobileDepartment;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.False(visitStatus);
Assert.Equal(string.Format(ErrorMessageFormats.IndexOutOfBounds, invalidIndex), message);
}
[Fact]
public void Visit_DoesNotValidate_FinalPathSegment()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath($"/NonExisting"), new DefaultContractResolver());
var model = new Class1();
object targetObject = model;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.False(visitStatus);
Assert.Equal(string.Format(ErrorMessageFormats.TargetLocationAtPathSegmentNotFound, "NonExisting"), message);
}
}
}

View File

@ -0,0 +1,131 @@
using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Microsoft.AspNetCore.JsonPatch.Internal
{
public class ObjectVisitorTest
{
private class Class1
{
public IList<string> States { get; set; } = new List<string>();
public IDictionary<string, string> Countries = new Dictionary<string, string>();
}
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 };
}
}
[Theory]
[MemberData(nameof(ReturnsListAdapterData))]
public void Visit_ValidPathToArray_ReturnsListAdapter(object targetObject, string path, object expectedTargetObject)
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver());
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(expectedTargetObject, targetObject);
Assert.IsType<ListAdapter>(adapter);
}
[Fact]
public void Visit_ValidPathToDictionary_ReturnsDictionaryAdapter()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath("/Countries/USA"), new DefaultContractResolver());
var model = new Class1();
object targetObject = model;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(model.Countries, targetObject);
Assert.IsType<DictionaryAdapter>(adapter);
}
private class AutomobileDepartment
{
public List<Class1> Customers { get; set; } = new List<Class1>();
}
[Fact]
public void Visit_ValidPathToArray_ReturnsListAdapter_ForDeepNestedPath()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath("/Customers/0/States/-"), new DefaultContractResolver());
var customer = new Class1();
var automobileDepartment = new AutomobileDepartment();
automobileDepartment.Customers.Add(customer);
object targetObject = automobileDepartment;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.Same(customer.States, targetObject);
Assert.IsType<ListAdapter>(adapter);
}
[Fact]
public void Visit_InvalidPathToArray_Fails()
{
// Arrange
var invalidIndex = 2;
var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{invalidIndex}/States/-"), new DefaultContractResolver());
var automobileDepartment = new AutomobileDepartment();
object targetObject = automobileDepartment;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.False(visitStatus);
Assert.Equal(string.Format(ErrorMessageFormats.IndexOutOfBounds, invalidIndex), message);
}
// The adapter takes care of the responsibility of validating the final segment
[Fact]
public void Visit_DoesNotValidate_FinalPathSegment()
{
// Arrange
var visitor = new ObjectVisitor(new ParsedPath($"/NonExisting"), new DefaultContractResolver());
var model = new Class1();
object targetObject = model;
IAdapter adapter = null;
string message = null;
// Act
var visitStatus = visitor.TryVisit(ref targetObject, out adapter, out message);
// Assert
Assert.True(visitStatus);
Assert.True(string.IsNullOrEmpty(message), "Expected no error message");
Assert.IsType<PocoAdapter>(adapter);
}
}
}

View File

@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.JsonPatch.Test
namespace Microsoft.AspNetCore.JsonPatch
{
public class SimpleDTO
{

View File

@ -3,7 +3,7 @@
using System.Collections.Generic;
namespace Microsoft.AspNetCore.JsonPatch.Test
namespace Microsoft.AspNetCore.JsonPatch
{
public class SimpleDTOWithNestedDTO
{

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.JsonPatch.Test
namespace Microsoft.AspNetCore.JsonPatch
{
public class TestErrorLogger<T> where T : class
{