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; // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
using Microsoft.AspNet.JsonPatch.Operations; // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.JsonPatch.Helpers;
using Microsoft.AspNet.JsonPatch.Exceptions; using System;
using System.Reflection;
using System.Collections; 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;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
@ -11,6 +14,13 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
{ {
public class SimpleObjectAdapter<T> : IObjectAdapter<T> where T : class public class SimpleObjectAdapter<T> : IObjectAdapter<T> where T : class
{ {
public IContractResolver ContractResolver { get; set; }
public SimpleObjectAdapter(IContractResolver contractResolver)
{
ContractResolver = contractResolver;
}
/// <summary> /// <summary>
/// The "add" operation performs one of the following functions, /// The "add" operation performs one of the following functions,
/// depending upon what the target location references: /// 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 // 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 // that value represents the position; if the path ends in "-", we're appending
// to the list. // to the list.
var appendList = false; var appendList = false;
var positionAsInteger = -1; var positionAsInteger = -1;
var actualPathToProperty = path; var actualPathToProperty = path;
@ -110,47 +119,30 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
} }
} }
var pathProperty = PropertyHelpers var patchProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualPathToProperty); .FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver);
// does property at path exist? // does property at path exist?
if (pathProperty == null) CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path);
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: property at location path: {0} does not exist", path),
objectToApplyTo);
}
// it exists. If it' an array, add to that array. If it's not, we replace. // 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, // 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. // the path must end with "/position" or "/-", which we already determined before.
if (appendList || positionAsInteger > -1) 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?? // what if it's an array but there's no position??
if (isNonStringArray) if (IsNonStringArray(patchProperty))
{ {
// now, get the generic type of the enumerable // 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); var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value);
if (!conversionResult.CanBeConverted) CheckIfPropertyCanBeSet(conversionResult, objectToApplyTo, operationToReport, path);
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided value is invalid for array property type at location path: {0}",
path),
objectToApplyTo);
}
// get value (it can be cast, we just checked that) // 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) if (appendList)
{ {
@ -167,40 +159,33 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
{ {
throw new JsonPatchException<T>(operationToReport, throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided path is invalid for array property type at " + 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), path),
objectToApplyTo); objectToApplyTo);
} }
} }
} }
else else
{ {
throw new JsonPatchException<T>(operationToReport, throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", string.Format("Patch failed: provided path is invalid for array property type at location " +
path), "path: {0}: expected array",
path),
objectToApplyTo); objectToApplyTo);
} }
} }
else else
{ {
var conversionResultTuple = PropertyHelpers.ConvertToActualType(pathProperty.PropertyType, value); var conversionResultTuple = PropertyHelpers.ConvertToActualType(
patchProperty.Property.PropertyType,
value);
// conversion successful // Is conversion successful
if (conversionResultTuple.CanBeConverted) CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operationToReport, path);
{
PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty, patchProperty.Property.ValueProvider.SetValue(
patchProperty.Parent,
conversionResultTuple.ConvertedInstance); 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> /// <param name="objectApplyTo">Object to apply the operation to</param>
public void Move(Operation<T> operation, T objectToApplyTo) public void Move(Operation<T> operation, T objectToApplyTo)
{ {
// get value at from location // get value at from location
object valueAtFromLocation = null; object valueAtFromLocation = null;
var positionAsInteger = -1; var positionAsInteger = -1;
var actualFromProperty = operation.from; var actualFromProperty = operation.from;
positionAsInteger = PropertyHelpers.GetNumericEnd(operation.from); positionAsInteger = PropertyHelpers.GetNumericEnd(operation.from);
if (positionAsInteger > -1) if (positionAsInteger > -1)
@ -244,74 +227,56 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
operation.from.IndexOf('/' + positionAsInteger.ToString())); operation.from.IndexOf('/' + positionAsInteger.ToString()));
} }
var fromProperty = PropertyHelpers var patchProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualFromProperty); .FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver);
// does property at from exist? // does property at from exist?
if (fromProperty == null) CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from);
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: property at location from: {0} does not exist", operation.from),
objectToApplyTo);
}
// is the path an array (but not a string (= char[]))? In this case, // 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. // the path must end with "/position" or "/-", which we already determined before.
if (positionAsInteger > -1) if (positionAsInteger > -1)
{ {
if (IsNonStringArray(patchProperty))
var isNonStringArray = !(fromProperty.PropertyType == typeof(string))
&& typeof(IList).IsAssignableFrom(fromProperty.PropertyType);
if (isNonStringArray)
{ {
// now, get the generic type of the enumerable // 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) // 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) if (array.Count <= positionAsInteger)
{ {
throw new JsonPatchException<T>(operation, throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: invalid position", string.Format("Patch failed: provided from path is invalid for array property type at " +
operation.from), "location from: {0}: invalid position",
objectToApplyTo); operation.from),
objectToApplyTo);
} }
valueAtFromLocation = array[positionAsInteger]; valueAtFromLocation = array[positionAsInteger];
} }
else else
{ {
throw new JsonPatchException<T>(operation, throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: expected array", string.Format("Patch failed: provided from path is invalid for array property type at " +
operation.from), "location from: {0}: expected array",
operation.from),
objectToApplyTo); objectToApplyTo);
} }
} }
else else
{ {
// no list, just get the value // no list, just get the value
// set the new value // set the new value
valueAtFromLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty);
} }
// remove that value // remove that value
Remove(operation.from, objectToApplyTo, operation); Remove(operation.from, objectToApplyTo, operation);
// add that value to the path location // add that value to the path location
Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); Add(operation.path, valueAtFromLocation, objectToApplyTo, operation);
} }
/// <summary> /// <summary>
@ -359,35 +324,25 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
} }
} }
var pathProperty = PropertyHelpers var patchProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualPathToProperty); .FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver);
// does the target location exist? // does the target location exist?
if (pathProperty == null) CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path);
{
throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: property at location path: {0} does not exist", path),
objectToApplyTo);
}
// get the property, and remove it - in this case, for DTO's, that means setting // 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 // it to null or its default value; in case of an array, remove at provided index
// or at the end. // or at the end.
if (removeFromList || positionAsInteger > -1) 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?? // what if it's an array but there's no position??
if (isNonStringArray) if (IsNonStringArray(patchProperty))
{ {
// now, get the generic type of the enumerable // 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) // 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) if (removeFromList)
{ {
@ -402,54 +357,58 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
else else
{ {
throw new JsonPatchException<T>(operationToReport, 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", string.Format("Patch failed: provided path is invalid for array property type at " +
path), "location path: {0}: position larger than array size",
objectToApplyTo); path),
objectToApplyTo);
} }
} }
} }
else else
{ {
throw new JsonPatchException<T>(operationToReport, throw new JsonPatchException<T>(operationToReport,
string.Format("Patch failed: provided path is invalid for array property type at location path: {0}: expected array", string.Format("Patch failed: provided path is invalid for array property type at " +
path), "location path: {0}: expected array",
path),
objectToApplyTo); objectToApplyTo);
} }
} }
else else
{ {
// setting the value to "null" will use the default value in case of value types, and // setting the value to "null" will use the default value in case of value types, and
// null in case of reference types // 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> /// <summary>
/// The "test" operation tests that a value at the target location is /// The "test" operation tests that a value at the target location is
/// equal to a specified value. /// equal to a specified value.
/// ///
/// The operation object MUST contain a "value" member that conveys the /// The operation object MUST contain a "value" member that conveys the
/// value to be compared to the target location's value. /// value to be compared to the target location's value.
/// ///
/// The target location MUST be equal to the "value" value for the /// The target location MUST be equal to the "value" value for the
/// operation to be considered successful. /// operation to be considered successful.
/// ///
/// Here, "equal" means that the value at the target location and the /// 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 /// value conveyed by "value" are of the same JSON type, and that they
/// are considered equal by the following rules for that type: /// are considered equal by the following rules for that type:
/// ///
/// o strings: are considered equal if they contain the same number of /// o strings: are considered equal if they contain the same number of
/// Unicode characters and their code points are byte-by-byte equal. /// Unicode characters and their code points are byte-by-byte equal.
/// ///
/// o numbers: are considered equal if their values are numerically /// o numbers: are considered equal if their values are numerically
/// equal. /// equal.
/// ///
/// o arrays: are considered equal if they contain the same number of /// 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 /// values, and if each value can be considered equal to the value at
/// the corresponding position in the other array, using this list of /// 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 /// Also, note that ordering of the serialization of object members is
/// not significant. /// not significant.
/// ///
/// Note that we divert from the rules here - we use .NET's comparison, /// 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 /// not the one above. In a future version, a "strict" setting might
/// be added (configurable), that takes into account above rules. /// 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) public void Test(Operation<T> operation, T objectToApplyTo)
{ {
// get value at path location // get value at path location
object valueAtPathLocation = null; object valueAtPathLocation = null;
var positionInPathAsInteger = -1; var positionInPathAsInteger = -1;
var actualPathProperty = operation.path; var actualPathProperty = operation.path;
positionInPathAsInteger = PropertyHelpers.GetNumericEnd(operation.path); positionInPathAsInteger = PropertyHelpers.GetNumericEnd(operation.path);
if (positionInPathAsInteger > -1) if (positionInPathAsInteger > -1)
@ -496,146 +453,120 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
operation.path.IndexOf('/' + positionInPathAsInteger.ToString())); operation.path.IndexOf('/' + positionInPathAsInteger.ToString()));
} }
var pathProperty = PropertyHelpers var patchProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualPathProperty); .FindPropertyAndParent(objectToApplyTo, actualPathProperty, ContractResolver);
// does property at path exist? // does property at path exist?
if (pathProperty == null) CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.path);
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: property at location path: {0} does not exist", operation.path),
objectToApplyTo);
}
// get the property path // get the property path
Type typeOfFinalPropertyAtPathLocation; Type typeOfFinalPropertyAtPathLocation;
// is the path an array (but not a string (= char[]))? In this case, // 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. // the path must end with "/position" or "/-", which we already determined before.
if (positionInPathAsInteger > -1) if (positionInPathAsInteger > -1)
{ {
if (IsNonStringArray(patchProperty))
var isNonStringArray = !(pathProperty.PropertyType == typeof(string))
&& typeof(IList).IsAssignableFrom(pathProperty.PropertyType);
if (isNonStringArray)
{ {
// now, get the generic type of the enumerable // 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) // 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) if (array.Count <= positionInPathAsInteger)
{ {
throw new JsonPatchException<T>(operation, throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location path: {0}: invalid position", string.Format("Patch failed: provided from path is invalid for array property type at " +
operation.path), "location path: {0}: invalid position",
objectToApplyTo); operation.path),
objectToApplyTo);
} }
valueAtPathLocation = array[positionInPathAsInteger]; valueAtPathLocation = array[positionInPathAsInteger];
} }
else else
{ {
throw new JsonPatchException<T>(operation, throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location path: {0}: expected array", string.Format("Patch failed: provided from path is invalid for array property type at " +
operation.path), "location path: {0}: expected array",
operation.path),
objectToApplyTo); objectToApplyTo);
} }
} }
else else
{ {
// no list, just get the value // no list, just get the value
valueAtPathLocation = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathProperty); valueAtPathLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
typeOfFinalPropertyAtPathLocation = pathProperty.PropertyType; typeOfFinalPropertyAtPathLocation = patchProperty.Property.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);
} }
var conversionResultTuple = PropertyHelpers.ConvertToActualType(
typeOfFinalPropertyAtPathLocation,
operation.value);
// Is conversion successful
CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operation, operation.path);
//Compare
} }
/// <summary> /// <summary>
/// The "replace" operation replaces the value at the target location /// The "replace" operation replaces the value at the target location
/// with a new value. The operation object MUST contain a "value" member /// with a new value. The operation object MUST contain a "value" member
/// whose content specifies the replacement value. /// whose content specifies the replacement value.
/// ///
/// The target location MUST exist for the operation to be successful. /// The target location MUST exist for the operation to be successful.
/// ///
/// For example: /// For example:
/// ///
/// { "op": "replace", "path": "/a/b/c", "value": 42 } /// { "op": "replace", "path": "/a/b/c", "value": 42 }
/// ///
/// This operation is functionally identical to a "remove" operation for /// This operation is functionally identical to a "remove" operation for
/// a value, followed immediately by an "add" operation at the same /// a value, followed immediately by an "add" operation at the same
/// location with the replacement value. /// 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). /// for performance reasons (multiple checks of same requirements).
/// </summary> /// </summary>
/// <param name="operation">The replace operation</param> /// <param name="operation">The replace operation</param>
/// <param name="objectApplyTo">Object to apply the operation to</param> /// <param name="objectApplyTo">Object to apply the operation to</param>
public void Replace(Operation<T> operation, T objectToApplyTo) public void Replace(Operation<T> operation, T objectToApplyTo)
{ {
Remove(operation.path, objectToApplyTo, operation); Remove(operation.path, objectToApplyTo, operation);
Add(operation.path, operation.value, objectToApplyTo, operation); Add(operation.path, operation.value, objectToApplyTo, operation);
} }
/// <summary> /// <summary>
/// The "copy" operation copies the value at a specified location to the /// The "copy" operation copies the value at a specified location to the
/// target location. /// target location.
/// ///
/// The operation object MUST contain a "from" member, which is a string /// The operation object MUST contain a "from" member, which is a string
/// containing a JSON Pointer value that references the location in the /// containing a JSON Pointer value that references the location in the
/// target document to copy the value from. /// target document to copy the value from.
/// ///
/// The "from" location MUST exist for the operation to be successful. /// The "from" location MUST exist for the operation to be successful.
/// ///
/// For example: /// For example:
/// ///
/// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }
/// ///
/// This operation is functionally identical to an "add" operation at the /// This operation is functionally identical to an "add" operation at the
/// target location using the value specified in the "from" member. /// 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). /// the value specified in from for performance reasons (multiple checks of same requirements).
/// </summary> /// </summary>
/// <param name="operation">The copy operation</param> /// <param name="operation">The copy operation</param>
/// <param name="objectApplyTo">Object to apply the operation to</param> /// <param name="objectApplyTo">Object to apply the operation to</param>
public void Copy(Operation<T> operation, T objectToApplyTo) public void Copy(Operation<T> operation, T objectToApplyTo)
{ {
// get value at from location // get value at from location
object valueAtFromLocation = null; object valueAtFromLocation = null;
var positionAsInteger = -1; var positionAsInteger = -1;
var actualFromProperty = operation.from; var actualFromProperty = operation.from;
positionAsInteger = PropertyHelpers.GetNumericEnd(operation.from); positionAsInteger = PropertyHelpers.GetNumericEnd(operation.from);
if (positionAsInteger > -1) if (positionAsInteger > -1)
@ -644,70 +575,96 @@ namespace Microsoft.AspNet.JsonPatch.Adapters
operation.from.IndexOf('/' + positionAsInteger.ToString())); operation.from.IndexOf('/' + positionAsInteger.ToString()));
} }
var patchProperty = PropertyHelpers
var fromProperty = PropertyHelpers .FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver);
.FindProperty(objectToApplyTo, actualFromProperty);
// does property at from exist? // does property at from exist?
if (fromProperty == null) CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from);
{
throw new JsonPatchException<T>(operation,
string.Format("Patch failed: property at location from: {0} does not exist", operation.from),
objectToApplyTo);
}
// get the property path // get the property path
// is the path an array (but not a string (= char[]))? In this case, // 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. // the path must end with "/position" or "/-", which we already determined before.
if (positionAsInteger > -1) if (positionAsInteger > -1)
{ {
if (IsNonStringArray(patchProperty))
var isNonStringArray = !(fromProperty.PropertyType == typeof(string))
&& typeof(IList).IsAssignableFrom(fromProperty.PropertyType);
if (isNonStringArray)
{ {
// now, get the generic type of the enumerable // 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) // 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) if (array.Count <= positionAsInteger)
{ {
throw new JsonPatchException<T>(operation, throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: invalid position", string.Format("Patch failed: provided from path is invalid for array property type at " +
operation.from), "location from: {0}: invalid position",
objectToApplyTo); operation.from),
objectToApplyTo);
} }
valueAtFromLocation = array[positionAsInteger]; valueAtFromLocation = array[positionAsInteger];
} }
else else
{ {
throw new JsonPatchException<T>(operation, throw new JsonPatchException<T>(operation,
string.Format("Patch failed: provided from path is invalid for array property type at location from: {0}: expected array", string.Format("Patch failed: provided from path is invalid for array property type at " +
operation.from), "location from: {0}: expected array",
operation.from),
objectToApplyTo); objectToApplyTo);
} }
} }
else else
{ {
// no list, just get the value // no list, just get the value
// set the new value // set the new value
valueAtFromLocation = patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent);
valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty);
} }
// add operation to target location with that value. // add operation to target location with that value.
Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); 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; // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
using Microsoft.AspNet.JsonPatch.Operations; // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; 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 namespace Microsoft.AspNet.JsonPatch.Converters
{ {
public class TypedJsonPatchDocumentConverter : JsonConverter public class TypedJsonPatchDocumentConverter : JsonConverter
{ {
public override bool CanConvert(Type objectType) 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, var genericType = objectType.GetGenericArguments()[0];
JsonSerializer serializer)
{
try
{
if (reader.TokenType == JsonToken.Null)
return null;
var genericType = objectType.GetGenericArguments()[0]; // load jObject
var jObject = JArray.Load(reader);
// load jObject // Create target object for Json => list of operations, typed to genericType
var jObject = JArray.Load(reader); 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 targetOperations = Activator.CreateInstance(concreteList);
var concreteOperationType = genericOperation.MakeGenericType(genericType);
var genericList = typeof(List<>); //Create a new reader for this jObject, and set all properties to match the original reader.
var concreteList = genericList.MakeGenericType(concreteOperationType); JsonReader jObjectReader = jObject.CreateReader();
jObjectReader.Culture = reader.Culture;
var targetOperations = Activator.CreateInstance(concreteList); 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 // Populate the object properties
serializer.Populate(jObjectReader, targetOperations); serializer.Populate(jObjectReader, targetOperations);
// container target: the typed JsonPatchDocument. // container target: the typed JsonPatchDocument.
var container = Activator.CreateInstance(objectType, targetOperations); 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);
}
}
} public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
catch (Exception ex) {
{ if (value is IJsonPatchDocument)
throw new JsonPatchException("The JsonPatchDocument was malformed and could not be parsed.", ex); {
} var jsonPatchDoc = (IJsonPatchDocument)value;
var lst = jsonPatchDoc.GetOperations();
} // write out the operations, no envelope
serializer.Serialize(writer, lst);
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);
}
}
}
} }

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;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.JsonPatch.Helpers namespace Microsoft.AspNet.JsonPatch.Helpers
{ {
internal static class PropertyHelpers internal static class PropertyHelpers
{ {
public static object GetValue(PropertyInfo propertyToGet, object targetObject, string pathToProperty) public static JsonPatchProperty FindPropertyAndParent(
{ object targetObject,
// it is possible the path refers to a nested property. In that case, we need to string propertyPath,
// get from a different target object: the nested object. IContractResolver contractResolver)
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)
{ {
try try
{ {
@ -62,17 +24,37 @@ namespace Microsoft.AspNet.JsonPatch.Helpers
// skip the first one if it's empty // skip the first one if it's empty
var startIndex = (string.IsNullOrWhiteSpace(splitPath[0]) ? 1 : 0); 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] var jsonContract = (JsonObjectContract)contractResolver.ResolveContract(targetObject.GetType());
, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
targetObject = propertyInfoToGet.GetValue(targetObject, null); 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(), return null;
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
return propertyToFind;
} }
catch (Exception) catch (Exception)
{ {
@ -100,20 +82,11 @@ namespace Microsoft.AspNet.JsonPatch.Helpers
if (type == null) throw new ArgumentNullException(); if (type == null) throw new ArgumentNullException();
foreach (Type interfaceType in type.GetInterfaces()) 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 && if (interfaceType.GetTypeInfo().IsGenericType &&
interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{ {
return interfaceType.GetGenericArguments()[0]; return interfaceType.GetGenericArguments()[0];
} }
#endif
} }
return null; return null;
} }
@ -130,11 +103,5 @@ namespace Microsoft.AspNet.JsonPatch.Helpers
return -1; 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 Microsoft.AspNet.JsonPatch.Operations;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.JsonPatch namespace Microsoft.AspNet.JsonPatch
{ {
public interface IJsonPatchDocument public interface IJsonPatchDocument
{ {
IContractResolver ContractResolver { get; set; }
List<Operation> GetOperations(); 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.Converters;
using Microsoft.AspNet.JsonPatch.Helpers; using Microsoft.AspNet.JsonPatch.Helpers;
using Microsoft.AspNet.JsonPatch.Operations; using Microsoft.AspNet.JsonPatch.Operations;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using Newtonsoft.Json.Serialization;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Microsoft.AspNet.JsonPatch namespace Microsoft.AspNet.JsonPatch
{ {
// Implementation details: the purpose of this type of patch document is to ensure we can do type-checking // 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 // 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 // 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. // not according to RFC 6902, and would thus break cross-platform compatibility.
[JsonConverter(typeof(TypedJsonPatchDocumentConverter))] [JsonConverter(typeof(TypedJsonPatchDocumentConverter))]
public class JsonPatchDocument<T> : IJsonPatchDocument where T : class public class JsonPatchDocument<T> : IJsonPatchDocument where T : class
{ {
public List<Operation<T>> Operations { get; private set; } public List<Operation<T>> Operations { get; private set; }
public JsonPatchDocument() [JsonIgnore]
{ public IContractResolver ContractResolver { get; set; }
Operations = new List<Operation<T>>();
}
// Create from list of operations public JsonPatchDocument()
public JsonPatchDocument(List<Operation<T>> operations) {
{ Operations = new List<Operation<T>>();
Operations = operations; ContractResolver = new DefaultContractResolver();
} }
/// <summary> // Create from list of operations
/// Add operation. Will result in, for example, public JsonPatchDocument(List<Operation<T>> operations, IContractResolver contractResolver)
/// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } {
/// </summary> Operations = operations;
/// <typeparam name="TProp">value type</typeparam> ContractResolver = contractResolver;
/// <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;
}
/// <summary> /// <summary>
/// Add value to list at given position /// Add operation. Will result in, for example,
/// </summary> /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
/// <typeparam name="TProp">value type</typeparam> /// </summary>
/// <param name="path">path</param> /// <typeparam name="TProp">value type</typeparam>
/// <param name="value">value</param> /// <param name="path">path</param>
/// <param name="position">position</param> /// <param name="value">value</param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value, int position) public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, TProp>> path, TProp value)
{ {
Operations.Add(new Operation<T>("add", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null, value)); Operations.Add(new Operation<T>(
return this; "add",
} ExpressionHelpers.GetPath<T, TProp>(path).ToLower(),
from: null,
value: value));
return this;
}
/// <summary> /// <summary>
/// At value at end of list /// Add value to list at given position
/// </summary> /// </summary>
/// <typeparam name="TProp">value type</typeparam> /// <typeparam name="TProp">value type</typeparam>
/// <param name="path">path</param> /// <param name="path">path</param>
/// <param name="value">value</param> /// <param name="value">value</param>
/// <returns></returns> /// <param name="position">position</param>
public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value) /// <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() + "/-", null, value)); {
return this; Operations.Add(new Operation<T>(
} "add",
ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position,
null, value));
return this;
}
/// <summary> /// <summary>
/// Remove value at target location. Will result in, for example, /// At value at end of list
/// { "op": "remove", "path": "/a/b/c" } /// </summary>
/// </summary> /// <typeparam name="TProp">value type</typeparam>
/// <param name="remove"></param> /// <param name="path">path</param>
/// <param name="path"></param> /// <param name="value">value</param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, TProp>> path) public JsonPatchDocument<T> Add<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value)
{ {
Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, TProp>(path).ToLower(), null)); Operations.Add(new Operation<T>("add", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-", null, value));
return this; return this;
} }
/// <summary> /// <summary>
/// Remove value from list at given position /// Remove value at target location. Will result in, for example,
/// </summary> /// { "op": "remove", "path": "/a/b/c" }
/// <typeparam name="TProp">value type</typeparam> /// </summary>
/// <param name="path">target location</param> /// <param name="remove"></param>
/// <param name="position">position</param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, IList<TProp>>> path, int position) public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, TProp>> path)
{ {
Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null)); Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, TProp>(path).ToLower(), null));
return this; return this;
} }
/// <summary> /// <summary>
/// Remove value from end of list /// Remove value from list at given position
/// </summary> /// </summary>
/// <typeparam name="TProp">value type</typeparam> /// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param> /// <param name="path">target location</param>
/// <returns></returns> /// <param name="position">position</param>
public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, IList<TProp>>> path) /// <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() + "/-", null)); {
return this; Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null));
} return this;
}
/// <summary> /// <summary>
/// Replace value. Will result in, for example, /// Remove value from end of list
/// { "op": "replace", "path": "/a/b/c", "value": 42 } /// </summary>
/// </summary> /// <typeparam name="TProp">value type</typeparam>
/// <param name="path"></param> /// <param name="path">target location</param>
/// <param name="value"></param> /// <returns></returns>
/// <returns></returns> public JsonPatchDocument<T> Remove<TProp>(Expression<Func<T, IList<TProp>>> path)
public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, TProp>> path, TProp value) {
{ Operations.Add(new Operation<T>("remove", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-", null));
Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, TProp>(path).ToLower(), null, value)); return this;
return this; }
}
/// <summary> /// <summary>
/// Replace value in a list at given position /// Replace value. Will result in, for example,
/// </summary> /// { "op": "replace", "path": "/a/b/c", "value": 42 }
/// <typeparam name="TProp">value type</typeparam> /// </summary>
/// <param name="path">target location</param> /// <param name="path"></param>
/// <param name="position">position</param> /// <param name="value"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value, int position) public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, TProp>> path, TProp value)
{ {
Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null, value)); Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, TProp>(path).ToLower(), null, value));
return this; return this;
} }
/// <summary> /// <summary>
/// Replace value at end of a list /// Replace value in a list at given position
/// </summary> /// </summary>
/// <typeparam name="TProp">value type</typeparam> /// <typeparam name="TProp">value type</typeparam>
/// <param name="path">target location</param> /// <param name="path">target location</param>
/// <returns></returns> /// <param name="position">position</param>
public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value) /// <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() + "/-", null, value)); {
return this; Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/" + position, null, value));
} return this;
}
/// <summary> /// <summary>
/// Removes value at specified location and add it to the target location. Will result in, for example: /// Replace value at end of a list
/// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } /// </summary>
/// </summary> /// <typeparam name="TProp">value type</typeparam>
/// <param name="from"></param> /// <param name="path">target location</param>
/// <param name="path"></param> /// <returns></returns>
/// <returns></returns> public JsonPatchDocument<T> Replace<TProp>(Expression<Func<T, IList<TProp>>> path, TProp value)
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, TProp>> path) {
{ Operations.Add(new Operation<T>("replace", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-", null, value));
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, TProp>(path).ToLower() return this;
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower())); }
return this;
}
/// <summary> /// <summary>
/// Move from a position in a list to a new location /// Removes value at specified location and add it to the target location. Will result in, for example:
/// </summary> /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }
/// <typeparam name="TProp"></typeparam> /// </summary>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="path"></param>
/// <param name="path"></param> /// <returns></returns>
/// <returns></returns> public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, TProp>> path)
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()
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, TProp>(path).ToLower() , ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom)); return this;
return this; }
}
/// <summary> /// <summary>
/// Move from a property to a location in a list /// Move from a position in a list to a new location
/// </summary> /// </summary>
/// <typeparam name="TProp"></typeparam> /// <typeparam name="TProp"></typeparam>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="positionFrom"></param>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from, public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, Expression<Func<T, TProp>> path)
Expression<Func<T, IList<TProp>>> path, int positionTo) {
{ Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() , ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
+ "/" + positionTo return this;
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower())); }
return this;
}
/// <summary> /// <summary>
/// Move from a position in a list to another location in a list /// Move from a property to a location in a list
/// </summary> /// </summary>
/// <typeparam name="TProp"></typeparam> /// <typeparam name="TProp"></typeparam>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="positionFrom"></param>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from,
Expression<Func<T, IList<TProp>>> path, int positionTo) Expression<Func<T, IList<TProp>>> path, int positionTo)
{ {
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo + "/" + positionTo
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom)); , ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this; return this;
} }
/// <summary> /// <summary>
/// Move from a position in a list to the end of another list /// Move from a position in a list to another location in a list
/// </summary> /// </summary>
/// <typeparam name="TProp"></typeparam> /// <typeparam name="TProp"></typeparam>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="positionFrom"></param>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path) Expression<Func<T, IList<TProp>>> path, int positionTo)
{ {
Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/-" + "/" + positionTo
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom)); , ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this; return this;
} }
/// <summary> /// <summary>
/// Move to the end of a list /// Move from a position in a list to the end of another list
/// </summary> /// </summary>
/// <typeparam name="TProp"></typeparam> /// <typeparam name="TProp"></typeparam>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="positionFrom"></param>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, IList<TProp>>> path) 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, TProp>(from).ToLower())); Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
return this; + "/-"
} , ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
/// <summary> /// <summary>
/// Copy the value at specified location to the target location. Willr esult in, for example: /// Move to the end of a list
/// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } /// </summary>
/// </summary> /// <typeparam name="TProp"></typeparam>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="path"></param> /// <param name="positionFrom"></param>
/// <returns></returns> /// <param name="path"></param>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, TProp>> path) /// <returns></returns>
{ public JsonPatchDocument<T> Move<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, IList<TProp>>> path)
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, TProp>(path).ToLower() {
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower())); Operations.Add(new Operation<T>("move", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() + "/-"
return this; , ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
} return this;
}
/// <summary> /// <summary>
/// Copy from a position in a list to a new location /// Copy the value at specified location to the target location. Willr esult in, for example:
/// </summary> /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" }
/// <typeparam name="TProp"></typeparam> /// </summary>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="path"></param>
/// <param name="path"></param> /// <returns></returns>
/// <returns></returns> public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, TProp>> path)
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()
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, TProp>(path).ToLower() , ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom)); return this;
return this; }
}
/// <summary> /// <summary>
/// Copy from a property to a location in a list /// Copy from a position in a list to a new location
/// </summary> /// </summary>
/// <typeparam name="TProp"></typeparam> /// <typeparam name="TProp"></typeparam>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="positionFrom"></param>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from, public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, Expression<Func<T, TProp>> path)
Expression<Func<T, IList<TProp>>> path, int positionTo) {
{ Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, TProp>(path).ToLower()
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() , ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
+ "/" + positionTo return this;
, ExpressionHelpers.GetPath<T, TProp>(from).ToLower())); }
return this;
}
/// <summary> /// <summary>
/// Copy from a position in a list to a new location in a list /// Copy from a property to a location in a list
/// </summary> /// </summary>
/// <typeparam name="TProp"></typeparam> /// <typeparam name="TProp"></typeparam>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="positionFrom"></param>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from,
Expression<Func<T, IList<TProp>>> path, int positionTo) Expression<Func<T, IList<TProp>>> path, int positionTo)
{ {
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/" + positionTo + "/" + positionTo
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom)); , ExpressionHelpers.GetPath<T, TProp>(from).ToLower()));
return this; return this;
} }
/// <summary> /// <summary>
/// Copy from a position in a list to the end of another list /// Copy from a position in a list to a new location in a list
/// </summary> /// </summary>
/// <typeparam name="TProp"></typeparam> /// <typeparam name="TProp"></typeparam>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="positionFrom"></param>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom, public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, IList<TProp>>> from, int positionFrom,
Expression<Func<T, IList<TProp>>> path) Expression<Func<T, IList<TProp>>> path, int positionTo)
{ {
Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower() Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
+ "/-" + "/" + positionTo
, ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom)); , ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this; return this;
} }
/// <summary> /// <summary>
/// Copy to the end of a list /// Copy from a position in a list to the end of another list
/// </summary> /// </summary>
/// <typeparam name="TProp"></typeparam> /// <typeparam name="TProp"></typeparam>
/// <param name="from"></param> /// <param name="from"></param>
/// <param name="positionFrom"></param> /// <param name="positionFrom"></param>
/// <param name="path"></param> /// <param name="path"></param>
/// <returns></returns> /// <returns></returns>
public JsonPatchDocument<T> Copy<TProp>(Expression<Func<T, TProp>> from, Expression<Func<T, IList<TProp>>> path) 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, TProp>(from).ToLower())); Operations.Add(new Operation<T>("copy", ExpressionHelpers.GetPath<T, IList<TProp>>(path).ToLower()
return this; + "/-"
} , ExpressionHelpers.GetPath<T, IList<TProp>>(from).ToLower() + "/" + positionFrom));
return this;
}
public void ApplyTo(T objectToApplyTo) /// <summary>
{ /// Copy to the end of a list
ApplyTo(objectToApplyTo, new SimpleObjectAdapter<T>()); /// </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 public void ApplyTo(T objectToApplyTo, IObjectAdapter<T> adapter)
foreach (var op in Operations) {
{
op.Apply(objectToApplyTo, adapter);
}
}
public List<Operation> GetOperations() // apply each operation in order
{ foreach (var op in Operations)
var allOps = new List<Operation>(); {
op.Apply(objectToApplyTo, adapter);
}
}
if (Operations != null) public List<Operation> GetOperations()
{ {
foreach (var op in Operations) var allOps = new List<Operation>();
{
var untypedOp = new Operation();
untypedOp.op = op.op; if (Operations != null)
untypedOp.value = op.value; {
untypedOp.path = op.path; foreach (var op in Operations)
untypedOp.from = op.from; {
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": "4.0.10-beta-*",
"System.Collections.Concurrent": "4.0.10-beta-*", "System.Collections.Concurrent": "4.0.10-beta-*",
"System.Globalization": "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; // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
using System.IO; // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.JsonPatch;
using Microsoft.Framework.Internal; using Microsoft.Framework.Internal;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
public class JsonPatchInputFormatter : InputFormatter public class JsonPatchInputFormatter : JsonInputFormatter
{ {
private const int DefaultMaxDepth = 32;
private JsonSerializerSettings _jsonSerializerSettings;
public JsonPatchInputFormatter() public JsonPatchInputFormatter()
{ {
SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM); // Clear all values and only include json-patch+json value.
SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian); SupportedMediaTypes.Clear();
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/jsonpatch")); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json-patch+json"));
_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;
}
} }
/// <inheritdoc /> /// <inheritdoc />
public override Task<object> ReadRequestBodyAsync([NotNull] InputFormatterContext context) public async override Task<object> ReadRequestBodyAsync([NotNull] InputFormatterContext context)
{ {
var type = context.ModelType; var jsonPatchDocument = (IJsonPatchDocument)(await base.ReadRequestBodyAsync(context));
var request = context.ActionContext.HttpContext.Request; if (jsonPatchDocument != null)
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))
{ {
jsonReader.CloseInput = false; jsonPatchDocument.ContractResolver = SerializerSettings.ContractResolver;
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;
}
}
} }
return (object)jsonPatchDocument;
} }
/// <summary> /// <inheritdoc />
/// Called during deserialization to get the <see cref="JsonReader"/>. public override bool CanRead(InputFormatterContext context)
/// </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)
{ {
return new JsonTextReader(new StreamReader(readStream, effectiveEncoding)); if (!typeof(IJsonPatchDocument).IsAssignableFrom(context.ModelType) ||
} !context.ModelType.IsGenericType())
{
return false;
}
/// <summary> return base.CanRead(context);
/// 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);
} }
} }
} }

View File

@ -201,6 +201,69 @@ namespace Microsoft.AspNet.JsonPatch.Test
Assert.Equal(new List<int>() { 4, 1, 2, 3 }, doc.SimpleDTO.IntegerList); 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] [Fact]
public void AddToListInvalidPositionTooLarge() 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; using System.Collections.Generic;
namespace Microsoft.AspNet.JsonPatch.Test namespace Microsoft.AspNet.JsonPatch.Test
{ {
public class SimpleDTO public class SimpleDTO
{ {
public List<int> IntegerList { get; set; } public List<int> IntegerList { get; set; }
public int IntegerValue { get; set; } public int IntegerValue { get; set; }
public string StringProperty { get; set; } public string StringProperty { get; set; }
public string AnotherStringProperty { get; set; } public string AnotherStringProperty { get; set; }
public decimal DecimalValue { get; set; } public decimal DecimalValue { get; set; }
public double DoubleValue { get; set; }
public double DoubleValue { get; set; } public float FloatValue { get; set; }
public Guid GuidValue { 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. // 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. // 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 namespace Microsoft.AspNet.JsonPatch.Test
{ {
public class SimpleDTOWithNestedDTO public class SimpleDTOWithNestedDTO
{ {
public int IntegerValue { get; set; } public int IntegerValue { get; set; }
@ -12,10 +13,13 @@ namespace Microsoft.AspNet.JsonPatch.Test
public SimpleDTO SimpleDTO { get; set; } public SimpleDTO SimpleDTO { get; set; }
public List<SimpleDTO> SimpleDTOList { get; set; }
public SimpleDTOWithNestedDTO() public SimpleDTOWithNestedDTO()
{ {
this.NestedDTO = new NestedDTO(); this.NestedDTO = new NestedDTO();
this.SimpleDTO = new SimpleDTO(); this.SimpleDTO = new SimpleDTO();
this.SimpleDTOList = new List<SimpleDTO>();
} }
} }
} }

View File

@ -3,12 +3,12 @@
"warningsAsErrors": true "warningsAsErrors": true
}, },
"dependencies": { "dependencies": {
"Microsoft.AspNet.Http.Extensions": "1.0.0-*", "Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.AspNet.JsonPatch": "1.0.0-*", "Microsoft.AspNet.JsonPatch": "1.0.0-*",
"Microsoft.AspNet.Mvc.Core": "6.0.0-*", "Microsoft.AspNet.Mvc.Core": "6.0.0-*",
"Microsoft.AspNet.Testing": "1.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*",
"Moq": "4.2.1312.1622", "Moq": "4.2.1312.1622",
"Newtonsoft.Json": "6.0.6", "Newtonsoft.Json": "6.0.6",
"xunit.runner.aspnet": "2.0.0-aspnet-*" "xunit.runner.aspnet": "2.0.0-aspnet-*"
}, },
"commands": { "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); setup.Configure(mvcOptions);
// Assert // Assert
Assert.Equal(1, mvcOptions.InputFormatters.Count); Assert.Equal(2, mvcOptions.InputFormatters.Count);
Assert.IsType<JsonInputFormatter>(mvcOptions.InputFormatters[0].Instance); Assert.IsType<JsonInputFormatter>(mvcOptions.InputFormatters[0].Instance);
Assert.IsType<JsonPatchInputFormatter>(mvcOptions.InputFormatters[1].Instance);
} }
[Fact] [Fact]

View File

@ -25,7 +25,10 @@ namespace FiltersWebSite.Controllers
public void OnResourceExecuting(ResourceExecutingContext context) 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.Clear();
context.InputFormatters.Add(jsonFormatter); context.InputFormatters.Add(jsonFormatter);