Removed reflection code; used JsonContract instead and added new JsonPatchInputFormatter

This commit is contained in:
Kirthi Krishnamraju 2015-02-18 20:34:00 +01:00
parent f1e1d8f4df
commit a5da5b3acd
17 changed files with 1093 additions and 807 deletions

View File

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.JsonPatch;
using Microsoft.AspNet.Mvc;
namespace MvcSample.Web.Controllers
{
[Route("api/[controller]")]
public class JsonPatchController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPatch]
public IActionResult Patch([FromBody] JsonPatchDocument<Customer> patchDoc)
{
var customer = new Customer
{
Name = "John",
Orders = new List<Order>()
{
new Order
{
OrderName = "Order1"
},
new Order
{
OrderName = "Order2"
}
}
};
patchDoc.ApplyTo(customer);
return new ObjectResult(customer);
}
public class Customer
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
}
public class Order
{
public string OrderName { get; set; }
}
}
}

View File

@ -0,0 +1,114 @@
<head>
<script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.0.min.js"></script>
<script>
$(function () {
var currentObj = {
"Name": "John",
"Orders": [{"OrderName": "Order1"},
{"OrderName": "Order2"}]
};
$('#currentlabel').text(JSON.stringify(currentObj))
$('#addorder').on("click", function () {
var obj = [{
"op": "Add",
"path": "Customer/Orders/2",
"value": { "OrderName": "Name3" }
}]
$.ajax({
url: 'jsonpatch',
type: 'PATCH',
data: JSON.stringify(obj),
dataType: 'json',
contentType: 'application/json-patch+json',
success: function (data) {
$('#addlabel').text(JSON.stringify(data))
}
});
});
$('#removeorder').on("click", function () {
var obj = [{
"op": "Remove",
"path": "Customer/Name"
}]
$.ajax({
url: 'jsonpatch',
type: 'PATCH',
data: JSON.stringify(obj),
dataType: 'json',
contentType: 'application/json-patch+json',
success: function (data) {
$('#removelabel').text(JSON.stringify(data))
}
});
});
$('#moveorder').on("click", function () {
var obj = [{
"op": "Move",
"from": "Customer/Orders/0",
"path": "Customer/Orders/1"
}]
$.ajax({
url: 'jsonpatch',
type: 'PATCH',
data: JSON.stringify(obj),
dataType: 'json',
contentType: 'application/json-patch+json',
success: function (data) {
$('#movelabel').text(JSON.stringify(data))
}
});
});
$('#replaceorder').on("click", function () {
var obj = [{
"op": "Replace",
"path": "Customer/Name",
"value": "ReplacedName"
}]
$.ajax({
url: 'jsonpatch',
type: 'PATCH',
data: JSON.stringify(obj),
dataType: 'json',
contentType: 'application/json-patch+json',
success: function (data) {
$('#replacelabel').text(JSON.stringify(data))
}
});
});
});
</script>
</head>
<body>
<div>
<label>Current Customer</label>
<br />
<label id="currentlabel" name="currentlabel"></label>
<br />
</div>
<div>
<button id="addorder" type="submit" class="btn btn-primary"> Add </button>
<br />
<label id="addlabel" name="addlabel"></label>
<br />
</div>
<div>
<button id="removeorder" type="submit" class="btn btn-primary"> Remove </button>
<br />
<label id="removelabel" name="removelabel"></label>
</div>
<div>
<button id="moveorder" type="submit" class="btn btn-primary"> Move </button>
<br />
<label id="movelabel" name="movelabel"></label>
<br />
</div>
<div>
<button id="replaceorder" type="submit" class="btn btn-primary"> Replace </button>
<br />
<label id="replacelabel" name="replacelabel"></label>
</div>
</body>

View File

@ -1,9 +1,12 @@
using System;
using Microsoft.AspNet.JsonPatch.Operations;
using Microsoft.AspNet.JsonPatch.Helpers;
using Microsoft.AspNet.JsonPatch.Exceptions;
using System.Reflection;
// Copyright (c) Microsoft Open Technologies, Inc. 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.Reflection;
using Microsoft.AspNet.JsonPatch.Exceptions;
using Microsoft.AspNet.JsonPatch.Helpers;
using Microsoft.AspNet.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
@ -11,6 +14,13 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
{
public class SimpleObjectAdapter<T> : IObjectAdapter<T> where T : class
{
public IContractResolver ContractResolver { get; set; }
public SimpleObjectAdapter(IContractResolver contractResolver)
{
ContractResolver = contractResolver;
}
/// <summary>
/// The "add" operation performs one of the following functions,
/// depending upon what the target location references:
@ -89,7 +99,6 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
// 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 appendList = false;
var positionAsInteger = -1;
var actualPathToProperty = path;
@ -110,47 +119,30 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
}
}
var pathProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualPathToProperty);
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver);
// does property at path exist?
if (pathProperty == null)
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: property at location path: {0} does not exist", path),
objectToApplyTo);
}
CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path);
// it exists. If it' 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.
if (appendList || positionAsInteger > -1)
{
var isNonStringArray = !(pathProperty.PropertyType == typeof(string))
&& typeof(IList).IsAssignableFrom(pathProperty.PropertyType);
// what if it's an array but there's no position??
if (isNonStringArray)
if (IsNonStringArray(patchProperty))
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType);
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(
patchProperty.Property.PropertyType);
var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value);
if (!conversionResult.CanBeConverted)
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided value is invalid for array property type at location path: {0}",
path),
objectToApplyTo);
}
CheckIfPropertyCanBeSet(conversionResult, objectToApplyTo, operationToReport, path);
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList;
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
if (appendList)
{
@ -167,40 +159,33 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided path is invalid for array property type at " +
"location path: {0}: position doesn't exist in array",
"location path: {0}: position larger than array size",
path),
objectToApplyTo);
}
}
}
else
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array",
path),
string.Format("Patch failed: provided path is invalid for array property type at location " +
"path: {0}: expected array",
path),
objectToApplyTo);
}
}
else
{
var conversionResultTuple = PropertyHelpers.ConvertToActualType(pathProperty.PropertyType, value);
var conversionResultTuple = PropertyHelpers.ConvertToActualType(
patchProperty.Property.PropertyType,
value);
// conversion successful
if (conversionResultTuple.CanBeConverted)
{
PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty,
// Is conversion successful
CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operationToReport, path);
patchProperty.Property.ValueProvider.SetValue(
patchProperty.Parent,
conversionResultTuple.ConvertedInstance);
}
else
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided value is invalid for property type at location path: {0}",
path),
objectToApplyTo);
}
}
}
@ -229,13 +214,11 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
/// <param name="objectApplyTo">Object to apply the operation to</param>
public void Move(Operation<T> operation, T objectToApplyTo)
{
// get value at from location
object valueAtFromLocation = null;
var positionAsInteger = -1;
var actualFromProperty = operation.from;
positionAsInteger = PropertyHelpers.GetNumericEnd(operation.from);
if (positionAsInteger > -1)
@ -244,74 +227,56 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
operation.from.IndexOf('/' + positionAsInteger.ToString()));
}
var fromProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualFromProperty);
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver);
// does property at from exist?
if (fromProperty == null)
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: property at location from: {0} does not exist", operation.from),
objectToApplyTo);
}
CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from);
// 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.
if (positionAsInteger > -1)
{
var isNonStringArray = !(fromProperty.PropertyType == typeof(string))
&& typeof(IList).IsAssignableFrom(fromProperty.PropertyType);
if (isNonStringArray)
if (IsNonStringArray(patchProperty))
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType);
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType);
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty) as IList;
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
if (array.Count <= positionAsInteger)
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: invalid position",
operation.from),
objectToApplyTo);
string.Format("Patch failed: provided from path is invalid for array property type at " +
"location from: {0}: invalid position",
operation.from),
objectToApplyTo);
}
valueAtFromLocation = array[positionAsInteger];
}
else
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: expected array",
operation.from),
string.Format("Patch failed: provided from path is invalid for array property type at " +
"location from: {0}: expected array",
operation.from),
objectToApplyTo);
}
}
else
{
// no list, just get the value
// set the new value
valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty);
valueAtFromLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
}
// remove that value
Remove(operation.from, objectToApplyTo, operation);
// add that value to the path location
Add(operation.path, valueAtFromLocation, objectToApplyTo, operation);
}
/// <summary>
@ -359,35 +324,25 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
}
}
var pathProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualPathToProperty);
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver);
// does the target location exist?
if (pathProperty == null)
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: property at location path: {0} does not exist", path),
objectToApplyTo);
}
CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path);
// get the property, and remove it - in this case, for DTO's, that means setting
// it to null or its default value; in case of an array, remove at provided index
// or at the end.
if (removeFromList || positionAsInteger > -1)
{
var isNonStringArray = !(pathProperty.PropertyType == typeof(string))
&& typeof(IList).IsAssignableFrom(pathProperty.PropertyType);
// what if it's an array but there's no position??
if (isNonStringArray)
if (IsNonStringArray(patchProperty))
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType);
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType);
// TODO: nested!
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList;
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
if (removeFromList)
{
@ -402,54 +357,58 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
else
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: position larger than array size",
path),
objectToApplyTo);
string.Format("Patch failed: provided path is invalid for array property type at " +
"location path: {0}: position larger than array size",
path),
objectToApplyTo);
}
}
}
else
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array",
path),
string.Format("Patch failed: provided path is invalid for array property type at " +
"location path: {0}: expected array",
path),
objectToApplyTo);
}
}
else
{
// setting the value to "null" will use the default value in case of value types, and
// null in case of reference types
PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty, null);
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);
}
}
/// <summary>
/// The "test" operation tests that a value at the target location is
/// equal to a specified value.
///
///
/// The operation object MUST contain a "value" member that conveys the
/// value to be compared to the target location's value.
///
///
/// The target location MUST be equal to the "value" value for the
/// operation to be considered successful.
///
///
/// Here, "equal" means that the value at the target location and the
/// value conveyed by "value" are of the same JSON type, and that they
/// are considered equal by the following rules for that type:
///
///
/// o strings: are considered equal if they contain the same number of
/// Unicode characters and their code points are byte-by-byte equal.
///
///
/// o numbers: are considered equal if their values are numerically
/// equal.
///
///
/// o arrays: are considered equal if they contain the same number of
/// values, and if each value can be considered equal to the value at
/// the corresponding position in the other array, using this list of
@ -468,7 +427,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
///
/// Also, note that ordering of the serialization of object members is
/// not significant.
///
///
/// Note that we divert from the rules here - we use .NET's comparison,
/// not the one above. In a future version, a "strict" setting might
/// be added (configurable), that takes into account above rules.
@ -482,12 +441,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
public void Test(Operation<T> operation, T objectToApplyTo)
{
// get value at path location
object valueAtPathLocation = null;
var positionInPathAsInteger = -1;
var actualPathProperty = operation.path;
positionInPathAsInteger = PropertyHelpers.GetNumericEnd(operation.path);
if (positionInPathAsInteger > -1)
@ -496,146 +453,120 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
operation.path.IndexOf('/' + positionInPathAsInteger.ToString()));
}
var pathProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualPathProperty);
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualPathProperty, ContractResolver);
// does property at path exist?
if (pathProperty == null)
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: property at location path: {0} does not exist", operation.path),
objectToApplyTo);
}
CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.path);
// get the property path
Type typeOfFinalPropertyAtPathLocation;
// 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.
if (positionInPathAsInteger > -1)
{
var isNonStringArray = !(pathProperty.PropertyType == typeof(string))
&& typeof(IList).IsAssignableFrom(pathProperty.PropertyType);
if (isNonStringArray)
if (IsNonStringArray(patchProperty))
{
// now, get the generic type of the enumerable
typeOfFinalPropertyAtPathLocation = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType);
typeOfFinalPropertyAtPathLocation = PropertyHelpers
.GetEnumerableType(patchProperty.Property.PropertyType);
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathProperty) as IList;
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
if (array.Count <= positionInPathAsInteger)
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location path: {0}: invalid position",
operation.path),
objectToApplyTo);
string.Format("Patch failed: provided from path is invalid for array property type at " +
"location path: {0}: invalid position",
operation.path),
objectToApplyTo);
}
valueAtPathLocation = array[positionInPathAsInteger];
}
else
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location path: {0}: expected array",
operation.path),
string.Format("Patch failed: provided from path is invalid for array property type at " +
"location path: {0}: expected array",
operation.path),
objectToApplyTo);
}
}
else
{
// no list, just get the value
valueAtPathLocation = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathProperty);
typeOfFinalPropertyAtPathLocation = pathProperty.PropertyType;
}
var conversionResultTuple = PropertyHelpers.ConvertToActualType(typeOfFinalPropertyAtPathLocation, operation.value);
// conversion successful
if (conversionResultTuple.CanBeConverted)
{
// COMPARE - TODO
}
else
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided value is invalid for property type at location path: {0}",
operation.path),
objectToApplyTo);
valueAtPathLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
typeOfFinalPropertyAtPathLocation = patchProperty.Property.PropertyType;
}
var conversionResultTuple = PropertyHelpers.ConvertToActualType(
typeOfFinalPropertyAtPathLocation,
operation.value);
// Is conversion successful
CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operation, operation.path);
//Compare
}
/// <summary>
/// The "replace" operation replaces the value at the target location
/// with a new value. The operation object MUST contain a "value" member
/// whose content specifies the replacement value.
///
///
/// The target location MUST exist for the operation to be successful.
///
///
/// For example:
///
///
/// { "op": "replace", "path": "/a/b/c", "value": 42 }
///
///
/// This operation is functionally identical to a "remove" operation for
/// a value, followed immediately by an "add" operation at the same
/// location with the replacement value.
///
/// Note: even though it's the same functionally, we do not call remove + add
///
/// Note: even though it's the same functionally, we do not call remove + add
/// for performance reasons (multiple checks of same requirements).
/// </summary>
/// <param name="operation">The replace operation</param>
/// <param name="objectApplyTo">Object to apply the operation to</param>
public void Replace(Operation<T> operation, T objectToApplyTo)
{
Remove(operation.path, objectToApplyTo, operation);
Add(operation.path, operation.value, objectToApplyTo, operation);
}
/// <summary>
/// The "copy" operation copies the value at a specified location to the
/// target location.
///
///
/// The operation object MUST contain a "from" member, which is a string
/// containing a JSON Pointer value that references the location in the
/// target document to copy the value from.
///
///
/// The "from" location MUST exist for the operation to be successful.
///
///
/// For example:
///
///
/// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }
///
///
/// This operation is functionally identical to an "add" operation at the
/// target location using the value specified in the "from" member.
///
/// Note: even though it's the same functionally, we do not call add with
///
/// Note: even though it's the same functionally, we do not call add with
/// the value specified in from for performance reasons (multiple checks of same requirements).
/// </summary>
/// <param name="operation">The copy operation</param>
/// <param name="objectApplyTo">Object to apply the operation to</param>
public void Copy(Operation<T> operation, T objectToApplyTo)
{
// get value at from location
object valueAtFromLocation = null;
var positionAsInteger = -1;
var actualFromProperty = operation.from;
positionAsInteger = PropertyHelpers.GetNumericEnd(operation.from);
if (positionAsInteger > -1)
@ -644,70 +575,96 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
operation.from.IndexOf('/' + positionAsInteger.ToString()));
}
var fromProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualFromProperty);
var patchProperty = PropertyHelpers
.FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver);
// does property at from exist?
if (fromProperty == null)
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: property at location from: {0} does not exist", operation.from),
objectToApplyTo);
}
CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from);
// get the property path
// 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.
if (positionAsInteger > -1)
{
var isNonStringArray = !(fromProperty.PropertyType == typeof(string))
&& typeof(IList).IsAssignableFrom(fromProperty.PropertyType);
if (isNonStringArray)
if (IsNonStringArray(patchProperty))
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType);
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(patchProperty.Property.PropertyType);
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty) as IList;
var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
if (array.Count <= positionAsInteger)
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: invalid position",
operation.from),
objectToApplyTo);
string.Format("Patch failed: provided from path is invalid for array property type at " +
"location from: {0}: invalid position",
operation.from),
objectToApplyTo);
}
valueAtFromLocation = array[positionAsInteger];
}
else
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: expected array",
operation.from),
string.Format("Patch failed: provided from path is invalid for array property type at " +
"location from: {0}: expected array",
operation.from),
objectToApplyTo);
}
}
else
{
// no list, just get the value
// set the new value
valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty);
valueAtFromLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
}
// add operation to target location with that value.
Add(operation.path, valueAtFromLocation, objectToApplyTo, operation);
}
private void CheckIfPropertyExists(
JsonPatchProperty patchProperty,
T objectToApplyTo,
Operation<T> operation,
string propertyPath)
{
if (patchProperty == null)
{
throw new JsonPatchException<T>(
operation,
string.Format("Patch failed: property at location {0} does not exist", propertyPath),
objectToApplyTo);
}
if (patchProperty.Property.Ignored)
{
throw new JsonPatchException<T>(
operation,
string.Format("Patch failed: cannot update property at location {0}", propertyPath),
objectToApplyTo);
}
}
private bool IsNonStringArray(JsonPatchProperty patchProperty)
{
return !(patchProperty.Property.PropertyType == typeof(string))
&& typeof(IList).GetTypeInfo().IsAssignableFrom(
patchProperty.Property.PropertyType.GetTypeInfo());
}
private void CheckIfPropertyCanBeSet(
ConversionResult result,
T objectToApplyTo,
Operation<T> operation,
string path)
{
var errorMessage = "Patch failed: provided value is invalid for property type at location path: ";
if (!result.CanBeConverted)
{
throw new JsonPatchException<T>(operation, string.Format(errorMessage + "{0}", path), objectToApplyTo);
}
}
}
}
}

View File

@ -1,79 +1,79 @@
using Microsoft.AspNet.JsonPatch.Exceptions;
using Microsoft.AspNet.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.AspNet.JsonPatch.Exceptions;
using Microsoft.AspNet.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.JsonPatch.Converters
{
public class TypedJsonPatchDocumentConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
public class TypedJsonPatchDocumentConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
try
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
try
{
if (reader.TokenType == JsonToken.Null)
return null;
var genericType = objectType.GetGenericArguments()[0];
var genericType = objectType.GetGenericArguments()[0];
// load jObject
var jObject = JArray.Load(reader);
// load jObject
var jObject = JArray.Load(reader);
// Create target object for Json => list of operations, typed to genericType
var genericOperation = typeof(Operation<>);
var concreteOperationType = genericOperation.MakeGenericType(genericType);
// Create target object for Json => list of operations, typed to genericType
var genericList = typeof(List<>);
var concreteList = genericList.MakeGenericType(concreteOperationType);
var genericOperation = typeof(Operation<>);
var concreteOperationType = genericOperation.MakeGenericType(genericType);
var targetOperations = Activator.CreateInstance(concreteList);
var genericList = typeof(List<>);
var concreteList = genericList.MakeGenericType(concreteOperationType);
var targetOperations = Activator.CreateInstance(concreteList);
//Create a new reader for this jObject, and set all properties to match the original reader.
JsonReader jObjectReader = jObject.CreateReader();
jObjectReader.Culture = reader.Culture;
jObjectReader.DateParseHandling = reader.DateParseHandling;
jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
jObjectReader.FloatParseHandling = reader.FloatParseHandling;
//Create a new reader for this jObject, and set all properties to match the original reader.
JsonReader jObjectReader = jObject.CreateReader();
jObjectReader.Culture = reader.Culture;
jObjectReader.DateParseHandling = reader.DateParseHandling;
jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
jObjectReader.FloatParseHandling = reader.FloatParseHandling;
// Populate the object properties
serializer.Populate(jObjectReader, targetOperations);
serializer.Populate(jObjectReader, targetOperations);
// container target: the typed JsonPatchDocument.
var container = Activator.CreateInstance(objectType, targetOperations);
// container target: the typed JsonPatchDocument.
var container = Activator.CreateInstance(objectType, targetOperations, new DefaultContractResolver());
return container;
return container;
}
catch (Exception ex)
{
throw new JsonPatchException("The JsonPatchDocument was malformed and could not be parsed.", ex);
}
}
}
catch (Exception ex)
{
throw new JsonPatchException("The JsonPatchDocument was malformed and could not be parsed.", ex);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is IJsonPatchDocument)
{
var jsonPatchDoc = (IJsonPatchDocument)value;
var lst = jsonPatchDoc.GetOperations();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is IJsonPatchDocument)
{
var jsonPatchDoc = (IJsonPatchDocument)value;
var lst = jsonPatchDoc.GetOperations();
// write out the operations, no envelope
serializer.Serialize(writer, lst);
}
}
}
// write out the operations, no envelope
serializer.Serialize(writer, lst);
}
}
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.JsonPatch
{
/// <summary>
/// Metadata for JsonProperty.
/// </summary>
public class JsonPatchProperty
{
/// <summary>
/// Initializes a new instance.
/// </summary>
public JsonPatchProperty(JsonProperty property, object parent)
{
Property = property;
Parent = parent;
}
/// <summary>
/// Gets or sets JsonProperty.
/// </summary>
public JsonProperty Property { get; set; }
/// <summary>
/// Gets or sets Parent.
/// </summary>
public object Parent { get; set; }
}
}

View File

@ -1,59 +1,21 @@
using Newtonsoft.Json;
// Copyright (c) Microsoft Open Technologies, Inc. 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.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.JsonPatch.Helpers
{
internal static class PropertyHelpers
{
public static object GetValue(PropertyInfo propertyToGet, object targetObject, string pathToProperty)
{
// it is possible the path refers to a nested property. In that case, we need to
// get from a different target object: the nested object.
var splitPath = pathToProperty.Split('/');
// skip the first one if it's empty
var startIndex = (string.IsNullOrWhiteSpace(splitPath[0]) ? 1 : 0);
for (int i = startIndex; i < splitPath.Length - 1; i++)
{
var propertyInfoToGet = GetPropertyInfo(targetObject, splitPath[i]
, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
targetObject = propertyInfoToGet.GetValue(targetObject, null);
}
return propertyToGet.GetValue(targetObject, null);
}
public static bool SetValue(PropertyInfo propertyToSet, object targetObject, string pathToProperty, object value)
{
// it is possible the path refers to a nested property. In that case, we need to
// set on a different target object: the nested object.
var splitPath = pathToProperty.Split('/');
// skip the first one if it's empty
var startIndex = (string.IsNullOrWhiteSpace(splitPath[0]) ? 1 : 0);
for (int i = startIndex; i < splitPath.Length - 1; i++)
{
var propertyInfoToGet = GetPropertyInfo(targetObject, splitPath[i]
, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
targetObject = propertyInfoToGet.GetValue(targetObject, null);
}
propertyToSet.SetValue(targetObject, value, null);
return true;
}
public static PropertyInfo FindProperty(object targetObject, string propertyPath)
public static JsonPatchProperty FindPropertyAndParent(
object targetObject,
string propertyPath,
IContractResolver contractResolver)
{
try
{
@ -62,17 +24,37 @@ namespace Microsoft.AspNet.JsonPatch.Helpers
// skip the first one if it's empty
var startIndex = (string.IsNullOrWhiteSpace(splitPath[0]) ? 1 : 0);
for (int i = startIndex; i < splitPath.Length - 1; i++)
for (int i = startIndex; i < splitPath.Length; i++)
{
var propertyInfoToGet = GetPropertyInfo(targetObject, splitPath[i]
, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
targetObject = propertyInfoToGet.GetValue(targetObject, null);
var jsonContract = (JsonObjectContract)contractResolver.ResolveContract(targetObject.GetType());
foreach (var property in jsonContract.Properties)
{
if (string.Equals(property.PropertyName, splitPath[i], StringComparison.OrdinalIgnoreCase))
{
if (i == (splitPath.Length - 1))
{
return new JsonPatchProperty(property, targetObject);
}
else
{
targetObject = property.ValueProvider.GetValue(targetObject);
// if property is of IList type then get the array index from splitPath and get the
// object at the indexed position from the list.
if (typeof(IList).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo()))
{
var index = int.Parse(splitPath[++i]);
targetObject = ((IList)targetObject)[index];
}
}
break;
}
}
}
var propertyToFind = targetObject.GetType().GetProperty(splitPath.Last(),
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
return propertyToFind;
return null;
}
catch (Exception)
{
@ -100,20 +82,11 @@ namespace Microsoft.AspNet.JsonPatch.Helpers
if (type == null) throw new ArgumentNullException();
foreach (Type interfaceType in type.GetInterfaces())
{
#if NETFX_CORE || ASPNETCORE50
if (interfaceType.GetTypeInfo().IsGenericType &&
interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return interfaceType.GetGenericArguments()[0];
}
#else
if (interfaceType.GetTypeInfo().IsGenericType &&
interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return interfaceType.GetGenericArguments()[0];
}
#endif
}
return null;
}
@ -130,11 +103,5 @@ namespace Microsoft.AspNet.JsonPatch.Helpers
return -1;
}
private static PropertyInfo GetPropertyInfo(object targetObject, string propertyName,
BindingFlags bindingFlags)
{
return targetObject.GetType().GetProperty(propertyName, bindingFlags);
}
}
}

View File

@ -3,11 +3,14 @@
using Microsoft.AspNet.JsonPatch.Operations;
using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.JsonPatch
{
public interface IJsonPatchDocument
{
IContractResolver ContractResolver { get; set; }
List<Operation> GetOperations();
}
}

View File

@ -1,377 +1,392 @@
using Microsoft.AspNet.JsonPatch.Adapters;
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using Microsoft.AspNet.JsonPatch.Adapters;
using Microsoft.AspNet.JsonPatch.Converters;
using Microsoft.AspNet.JsonPatch.Helpers;
using Microsoft.AspNet.JsonPatch.Operations;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.JsonPatch
{
// Implementation details: the purpose of this type of patch document is to ensure we can do type-checking
// when producing a JsonPatchDocument. However, we cannot send this "typed" over the wire, as that would require
// including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's
// not according to RFC 6902, and would thus break cross-platform compatibility.
[JsonConverter(typeof(TypedJsonPatchDocumentConverter))]
public class JsonPatchDocument<T> : IJsonPatchDocument where T : class
{
public List<Operation<T>> Operations { get; private set; }
// Implementation details: the purpose of this type of patch document is to ensure we can do type-checking
// when producing a JsonPatchDocument. However, we cannot send this "typed" over the wire, as that would require
// including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's
// not according to RFC 6902, and would thus break cross-platform compatibility.
[JsonConverter(typeof(TypedJsonPatchDocumentConverter))]
public class JsonPatchDocument<T> : IJsonPatchDocument where T : class
{
public List<Operation<T>> Operations { get; private set; }
public JsonPatchDocument()
{
Operations = new List<Operation<T>>();
}
[JsonIgnore]
public IContractResolver ContractResolver { get; set; }
// Create from list of operations
public JsonPatchDocument(List<Operation<T>> operations)
{
Operations = operations;
}
public JsonPatchDocument()
{
Operations = new List<Operation<T>>();
ContractResolver = new DefaultContractResolver();
}
/// <summary>
/// Add operation. Will result in, for example,
/// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">path</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, TProp>> path, TProp value)
{
Operations.Add(new Operation<T>("add", ExpressionHelpers.GetPath<T, TProp>(path).ToLower(), null, value));
return this;
}
// Create from list of operations
public JsonPatchDocument(List<Operation<T>> operations, IContractResolver contractResolver)
{
Operations = operations;
ContractResolver = contractResolver;
}
/// <summary>
/// Add value to list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">path</param>
/// <param name="value">value</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value, int position)
{
Operations.Add(new Operation<T>("add", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null, value));
return this;
}
/// <summary>
/// Add operation. Will result in, for example,
/// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">path</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, TProp>> path, TProp value)
{
Operations.Add(new Operation<T>(
"add",
ExpressionHelpers.GetPath<T, TProp>(path).ToLower(),
from: null,
value: value));
return this;
}
/// <summary>
/// At value at end of list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">path</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value)
{
Operations.Add(new Operation<T>("add", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-", null, value));
return this;
}
/// <summary>
/// Add value to list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">path</param>
/// <param name="value">value</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value, int position)
{
Operations.Add(new Operation<T>(
"add",
ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position,
null, value));
return this;
}
/// <summary>
/// Remove value at target location. Will result in, for example,
/// { "op": "remove", "path": "/a/b/c" }
/// </summary>
/// <param name="remove"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, TProp>(path).ToLower(), null));
return this;
}
/// <summary>
/// At value at end of list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">path</param>
/// <param name="value">value</param>
/// <returns></returns>
public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value)
{
Operations.Add(new Operation<T>("add", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-", null, value));
return this;
}
/// <summary>
/// Remove value from list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, IList<TProp>>> path, int position)
{
Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null));
return this;
}
/// <summary>
/// Remove value at target location. Will result in, for example,
/// { "op": "remove", "path": "/a/b/c" }
/// </summary>
/// <param name="remove"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, TProp>(path).ToLower(), null));
return this;
}
/// <summary>
/// Remove value from end of list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-", null));
return this;
}
/// <summary>
/// Remove value from list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, IList<TProp>>> path, int position)
{
Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null));
return this;
}
/// <summary>
/// Replace value. Will result in, for example,
/// { "op": "replace", "path": "/a/b/c", "value": 42 }
/// </summary>
/// <param name="path"></param>
/// <param name="value"></param>
/// <returns></returns>
public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, TProp>> path, TProp value)
{
Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, TProp>(path).ToLower(), null, value));
return this;
}
/// <summary>
/// Remove value from end of list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-", null));
return this;
}
/// <summary>
/// Replace value in a list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value, int position)
{
Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null, value));
return this;
}
/// <summary>
/// Replace value. Will result in, for example,
/// { "op": "replace", "path": "/a/b/c", "value": 42 }
/// </summary>
/// <param name="path"></param>
/// <param name="value"></param>
/// <returns></returns>
public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, TProp>> path, TProp value)
{
Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, TProp>(path).ToLower(), null, value));
return this;
}
/// <summary>
/// Replace value at end of a list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value)
{
Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-", null, value));
return this;
}
/// <summary>
/// Replace value in a list at given position
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <param name="position">position</param>
/// <returns></returns>
public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value, int position)
{
Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null, value));
return this;
}
/// <summary>
/// Removes value at specified location and add it to the target location. Will result in, for example:
/// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }
/// </summary>
/// <param name="from"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Replace value at end of a list
/// </summary>
/// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param>
/// <returns></returns>
public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value)
{
Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-", null, value));
return this;
}
/// <summary>
/// Move from a position in a list to a new location
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Removes value at specified location and add it to the target location. Will result in, for example:
/// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }
/// </summary>
/// <param name="from"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Move from a property to a location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from,
Expression<Func<T, IList<TProp>>> path, int positionTo)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Move from a position in a list to a new location
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Move from a position in a list to another location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path, int positionTo)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Move from a property to a location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from,
Expression<Func<T, IList<TProp>>> path, int positionTo)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Move from a position in a list to the end of another list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/-"
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Move from a position in a list to another location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path, int positionTo)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Move to the end of a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-"
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Move from a position in a list to the end of another list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/-"
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Copy the value at specified location to the target location. Willr esult in, for example:
/// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }
/// </summary>
/// <param name="from"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Move to the end of a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-"
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Copy from a position in a list to a new location
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Copy the value at specified location to the target location. Willr esult in, for example:
/// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }
/// </summary>
/// <param name="from"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Copy from a property to a location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from,
Expression<Func<T, IList<TProp>>> path, int positionTo)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Copy from a position in a list to a new location
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, Expression<Func<T, TProp>> path)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Copy from a position in a list to a new location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path, int positionTo)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Copy from a property to a location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from,
Expression<Func<T, IList<TProp>>> path, int positionTo)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Copy from a position in a list to the end of another list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/-"
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Copy from a position in a list to a new location in a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path, int positionTo)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary>
/// Copy to the end of a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-"
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
/// <summary>
/// Copy from a position in a list to the end of another list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/-"
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
public void ApplyTo(T objectToApplyTo)
{
ApplyTo(objectToApplyTo, new SimpleObjectAdapter<T>());
}
/// <summary>
/// Copy to the end of a list
/// </summary>
/// <typeparam name="TProp"></typeparam>
/// <param name="from"></param>
/// <param name="positionFrom"></param>
/// <param name="path"></param>
/// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, IList<TProp>>> path)
{
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-"
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this;
}
public void ApplyTo(T objectToApplyTo, IObjectAdapter<T> adapter)
{
public void ApplyTo(T objectToApplyTo)
{
ApplyTo(objectToApplyTo, new SimpleObjectAdapter<T>(ContractResolver));
}
// apply each operation in order
foreach (var op in Operations)
{
op.Apply(objectToApplyTo, adapter);
}
}
public void ApplyTo(T objectToApplyTo, IObjectAdapter<T> adapter)
{
public List<Operation> GetOperations()
{
var allOps = new List<Operation>();
// apply each operation in order
foreach (var op in Operations)
{
op.Apply(objectToApplyTo, adapter);
}
}
if (Operations != null)
{
foreach (var op in Operations)
{
var untypedOp = new Operation();
public List<Operation> GetOperations()
{
var allOps = new List<Operation>();
untypedOp.op = op.op;
untypedOp.value = op.value;
untypedOp.path = op.path;
untypedOp.from = op.from;
if (Operations != null)
{
foreach (var op in Operations)
{
var untypedOp = new Operation();
allOps.Add(untypedOp);
}
}
untypedOp.op = op.op;
untypedOp.value = op.value;
untypedOp.path = op.path;
untypedOp.from = op.from;
return allOps;
}
}
allOps.Add(untypedOp);
}
}
return allOps;
}
}
}

View File

@ -15,7 +15,7 @@
"System.Collections": "4.0.10-beta-*",
"System.Collections.Concurrent": "4.0.10-beta-*",
"System.Globalization": "4.0.10-beta-*",
"System.Runtime.Extensions": "4.0.10-beta-*"
"System.Runtime.Extensions": "4.0.10-beta-*"
}
}
}

View File

@ -1,126 +1,45 @@
using System;
using System.IO;
using System.Text;
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.JsonPatch;
using Microsoft.Framework.Internal;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
namespace Microsoft.AspNet.Mvc
{
public class JsonPatchInputFormatter : InputFormatter
public class JsonPatchInputFormatter : JsonInputFormatter
{
private const int DefaultMaxDepth = 32;
private JsonSerializerSettings _jsonSerializerSettings;
public JsonPatchInputFormatter()
{
SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM);
SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian);
// Clear all values and only include json-patch+json value.
SupportedMediaTypes.Clear();
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/jsonpatch"));
_jsonSerializerSettings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Ignore,
// Limit the object graph we'll consume to a fixed depth. This prevents stackoverflow exceptions
// from deserialization errors that might occur from deeply nested objects.
MaxDepth = DefaultMaxDepth,
// Do not change this setting
// Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types
TypeNameHandling = TypeNameHandling.None
};
_jsonSerializerSettings.ContractResolver = new JsonContractResolver();
}
/// <summary>
/// Gets or sets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>.
/// </summary>
public JsonSerializerSettings SerializerSettings
{
get { return _jsonSerializerSettings; }
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_jsonSerializerSettings = value;
}
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json-patch+json"));
}
/// <inheritdoc />
public override Task<object> ReadRequestBodyAsync([NotNull] InputFormatterContext context)
public async override Task<object> ReadRequestBodyAsync([NotNull] InputFormatterContext context)
{
var type = context.ModelType;
var request = context.ActionContext.HttpContext.Request;
MediaTypeHeaderValue requestContentType = null;
MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType);
// Get the character encoding for the content
// Never non-null since SelectCharacterEncoding() throws in error / not found scenarios
var effectiveEncoding = SelectCharacterEncoding(requestContentType);
using (var jsonReader = CreateJsonReader(context, request.Body, effectiveEncoding))
var jsonPatchDocument = (IJsonPatchDocument)(await base.ReadRequestBodyAsync(context));
if (jsonPatchDocument != null)
{
jsonReader.CloseInput = false;
var jsonSerializer = CreateJsonSerializer();
EventHandler<Newtonsoft.Json.Serialization.ErrorEventArgs> errorHandler = null;
errorHandler = (sender, e) =>
{
var exception = e.ErrorContext.Error;
context.ActionContext.ModelState.TryAddModelError(e.ErrorContext.Path, e.ErrorContext.Error);
// Error must always be marked as handled
// Failure to do so can cause the exception to be rethrown at every recursive level and
// overflow the stack for x64 CLR processes
e.ErrorContext.Handled = true;
};
jsonSerializer.Error += errorHandler;
try
{
var contractObject = SerializerSettings.ContractResolver.ResolveContract(type);
return Task.FromResult(jsonSerializer.Deserialize(jsonReader, type));
}
finally
{
// Clean up the error handler in case CreateJsonSerializer() reuses a serializer
if (errorHandler != null)
{
jsonSerializer.Error -= errorHandler;
}
}
jsonPatchDocument.ContractResolver = SerializerSettings.ContractResolver;
}
return (object)jsonPatchDocument;
}
/// <summary>
/// Called during deserialization to get the <see cref="JsonReader"/>.
/// </summary>
/// <param name="context">The <see cref="InputFormatterContext"/> for the read.</param>
/// <param name="readStream">The <see cref="Stream"/> from which to read.</param>
/// <param name="effectiveEncoding">The <see cref="Encoding"/> to use when reading.</param>
/// <returns>The <see cref="JsonReader"/> used during deserialization.</returns>
public virtual JsonReader CreateJsonReader([NotNull] InputFormatterContext context,
[NotNull] Stream readStream,
[NotNull] Encoding effectiveEncoding)
/// <inheritdoc />
public override bool CanRead(InputFormatterContext context)
{
return new JsonTextReader(new StreamReader(readStream, effectiveEncoding));
}
if (!typeof(IJsonPatchDocument).IsAssignableFrom(context.ModelType) ||
!context.ModelType.IsGenericType())
{
return false;
}
/// <summary>
/// Called during deserialization to get the <see cref="JsonSerializer"/>.
/// </summary>
/// <returns>The <see cref="JsonSerializer"/> used during serialization and deserialization.</returns>
public virtual JsonSerializer CreateJsonSerializer()
{
return JsonSerializer.Create(SerializerSettings);
return base.CanRead(context);
}
}
}

View File

@ -201,6 +201,69 @@ namespace Microsoft.AspNet.JsonPatch.Test
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.SimpleDTO.IntegerList);
}
[Fact]
public void AddToComplextTypeListSpecifyIndex()
{
// Arrange
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTOList = new List<SimpleDTO>()
{
new SimpleDTO
{
StringProperty = "String1"
},
new SimpleDTO
{
StringProperty = "String2"
}
}
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Add<string>(o => o.SimpleDTOList[0].StringProperty, "ChangedString1");
// Act
patchDoc.ApplyTo(doc);
// Assert
Assert.Equal("ChangedString1", doc.SimpleDTOList[0].StringProperty);
}
[Fact]
public void AddToComplextTypeListSpecifyIndexWithSerialization()
{
// Arrange
var doc = new SimpleDTOWithNestedDTO()
{
SimpleDTOList = new List<SimpleDTO>()
{
new SimpleDTO
{
StringProperty = "String1"
},
new SimpleDTO
{
StringProperty = "String2"
}
}
};
// create patch
var patchDoc = new JsonPatchDocument<SimpleDTOWithNestedDTO>();
patchDoc.Add<string>(o => o.SimpleDTOList[0].StringProperty, "ChangedString1");
var serialized = JsonConvert.SerializeObject(patchDoc);
var deserialized = JsonConvert.DeserializeObject<JsonPatchDocument<SimpleDTOWithNestedDTO>>(serialized);
// Act
deserialized.ApplyTo(doc);
// Assert
Assert.Equal("ChangedString1", doc.SimpleDTOList[0].StringProperty);
}
[Fact]
public void AddToListInvalidPositionTooLarge()
{

View File

@ -1,20 +1,20 @@
using System;
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNet.JsonPatch.Test
{
public class SimpleDTO
{
public List<int> IntegerList { get; set; }
public int IntegerValue { get; set; }
public string StringProperty { get; set; }
public string AnotherStringProperty { get; set; }
public decimal DecimalValue { get; set; }
public double DoubleValue { get; set; }
public float FloatValue { get; set; }
public Guid GuidValue { get; set; }
}
public class SimpleDTO
{
public List<int> IntegerList { get; set; }
public int IntegerValue { get; set; }
public string StringProperty { get; set; }
public string AnotherStringProperty { get; set; }
public decimal DecimalValue { get; set; }
public double DoubleValue { get; set; }
public float FloatValue { get; set; }
public Guid GuidValue { get; set; }
}
}

View File

@ -1,9 +1,10 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNet.JsonPatch.Test
{
public class SimpleDTOWithNestedDTO
{
public int IntegerValue { get; set; }
@ -12,10 +13,13 @@ namespace Microsoft.AspNet.JsonPatch.Test
public SimpleDTO SimpleDTO { get; set; }
public List<SimpleDTO> SimpleDTOList { get; set; }
public SimpleDTOWithNestedDTO()
{
this.NestedDTO = new NestedDTO();
this.SimpleDTO = new SimpleDTO();
this.SimpleDTOList = new List<SimpleDTO>();
}
}
}

View File

@ -3,12 +3,12 @@
"warningsAsErrors": true
},
"dependencies": {
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.AspNet.JsonPatch": "1.0.0-*",
"Microsoft.AspNet.Mvc.Core": "6.0.0-*",
"Microsoft.AspNet.Testing": "1.0.0-*",
"Moq": "4.2.1312.1622",
"Newtonsoft.Json": "6.0.6",
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.AspNet.JsonPatch": "1.0.0-*",
"Microsoft.AspNet.Mvc.Core": "6.0.0-*",
"Microsoft.AspNet.Testing": "1.0.0-*",
"Moq": "4.2.1312.1622",
"Newtonsoft.Json": "6.0.6",
"xunit.runner.aspnet": "2.0.0-aspnet-*"
},
"commands": {

View File

@ -0,0 +1,154 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.JsonPatch;
using Microsoft.AspNet.Mvc.ModelBinding;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
public class JsonPatchInputFormatterTest
{
[Fact]
public async Task JsonPatchInputFormatter_ReadsOneOperation_Successfully()
{
// Arrange
var formatter = new JsonPatchInputFormatter();
var content = "[{\"op\":\"add\",\"path\":\"Customer/Name\",\"value\":\"John\"}]";
var contentBytes = Encoding.UTF8.GetBytes(content);
var actionContext = GetActionContext(contentBytes);
var context = new InputFormatterContext(actionContext, typeof(JsonPatchDocument<Customer>));
// Act
var model = await formatter.ReadAsync(context);
// Assert
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(model);
Assert.Equal("add", patchDoc.Operations[0].op);
Assert.Equal("Customer/Name", patchDoc.Operations[0].path);
Assert.Equal("John", patchDoc.Operations[0].value);
}
[Fact]
public async Task JsonPatchInputFormatter_ReadsMultipleOperations_Successfully()
{
// Arrange
var formatter = new JsonPatchInputFormatter();
var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}," +
"{\"op\": \"remove\", \"path\" : \"Customer/Name\"}]";
var contentBytes = Encoding.UTF8.GetBytes(content);
var actionContext = GetActionContext(contentBytes);
var context = new InputFormatterContext(actionContext, typeof(JsonPatchDocument<Customer>));
// Act
var model = await formatter.ReadAsync(context);
// Assert
var patchDoc = Assert.IsType<JsonPatchDocument<Customer>>(model);
Assert.Equal("add", patchDoc.Operations[0].op);
Assert.Equal("Customer/Name", patchDoc.Operations[0].path);
Assert.Equal("John", patchDoc.Operations[0].value);
Assert.Equal("remove", patchDoc.Operations[1].op);
Assert.Equal("Customer/Name", patchDoc.Operations[1].path);
}
[Theory]
[InlineData("application/json-patch+json", true)]
[InlineData("application/json", false)]
[InlineData("application/*", true)]
[InlineData("*/*", true)]
public void CanRead_ReturnsTrueOnlyForJsonPatchContentType(string requestContentType, bool expectedCanRead)
{
// Arrange
var formatter = new JsonPatchInputFormatter();
var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]";
var contentBytes = Encoding.UTF8.GetBytes(content);
var actionContext = GetActionContext(contentBytes, contentType: requestContentType);
var formatterContext = new InputFormatterContext(actionContext, typeof(JsonPatchDocument<Customer>));
// Act
var result = formatter.CanRead(formatterContext);
// Assert
Assert.Equal(expectedCanRead, result);
}
[Theory]
[InlineData(typeof(Customer))]
[InlineData(typeof(IJsonPatchDocument))]
public void CanRead_ReturnsFalse_NonJsonPatchContentType(Type modelType)
{
// Arrange
var formatter = new JsonPatchInputFormatter();
var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]";
var contentBytes = Encoding.UTF8.GetBytes(content);
var actionContext = GetActionContext(contentBytes, contentType: "application/json-patch+json");
var formatterContext = new InputFormatterContext(actionContext, modelType);
// Act
var result = formatter.CanRead(formatterContext);
// Assert
Assert.False(result);
}
[Fact]
public async Task JsonPatchInputFormatter_ReturnsModelStateErrors_InvalidModelType()
{
// Arrange
var exceptionMessage = "Cannot deserialize the current JSON array (e.g. [1,2,3]) into type " +
"'Microsoft.AspNet.Mvc.JsonPatchInputFormatterTest+Customer' because the type requires a JSON object ";
var formatter = new JsonPatchInputFormatter();
var content = "[{\"op\": \"add\", \"path\" : \"Customer/Name\", \"value\":\"John\"}]";
var contentBytes = Encoding.UTF8.GetBytes(content);
var actionContext = GetActionContext(contentBytes, contentType: "application/json-patch+json");
var context = new InputFormatterContext(actionContext, typeof(Customer));
// Act
var model = await formatter.ReadAsync(context);
// Assert
Assert.Contains(exceptionMessage, actionContext.ModelState[""].Errors[0].Exception.Message);
}
private static ActionContext GetActionContext(byte[] contentBytes,
string contentType = "application/json-patch+json")
{
return new ActionContext(GetHttpContext(contentBytes, contentType),
new AspNet.Routing.RouteData(),
new ActionDescriptor());
}
private static HttpContext GetHttpContext(byte[] contentBytes,
string contentType = "application/json-patch+json")
{
var request = new Mock<HttpRequest>();
var headers = new Mock<IHeaderDictionary>();
request.SetupGet(r => r.Headers).Returns(headers.Object);
request.SetupGet(f => f.Body).Returns(new MemoryStream(contentBytes));
request.SetupGet(f => f.ContentType).Returns(contentType);
var httpContext = new Mock<HttpContext>();
httpContext.SetupGet(c => c.Request).Returns(request.Object);
httpContext.SetupGet(c => c.Request).Returns(request.Object);
return httpContext.Object;
}
private class Customer
{
public string Name { get; set; }
}
}
}

View File

@ -103,8 +103,9 @@ namespace Microsoft.AspNet.Mvc
setup.Configure(mvcOptions);
// Assert
Assert.Equal(1, mvcOptions.InputFormatters.Count);
Assert.Equal(2, mvcOptions.InputFormatters.Count);
Assert.IsType<JsonInputFormatter>(mvcOptions.InputFormatters[0].Instance);
Assert.IsType<JsonPatchInputFormatter>(mvcOptions.InputFormatters[1].Instance);
}
[Fact]

View File

@ -25,7 +25,10 @@ namespace FiltersWebSite.Controllers
public void OnResourceExecuting(ResourceExecutingContext context)
{
var jsonFormatter = context.InputFormatters.OfType<JsonInputFormatter>().Single();
// InputFormatters collection contains JsonInputFormatter and JsonPatchInputFormatter. Picking
// JsonInputFormatter by matching the typename
var jsonFormatter = context.InputFormatters.OfType<JsonInputFormatter>()
.Where(t => t.GetType() == typeof(JsonInputFormatter)).FirstOrDefault();
context.InputFormatters.Clear();
context.InputFormatters.Add(jsonFormatter);