This commit is contained in:
Kevin Dockx 2015-02-18 20:34:00 +01:00 коммит произвёл Kirthi Krishnamraju
Родитель 5818c0b5b7
Коммит f1e1d8f4df
30 изменённых файлов: 5226 добавлений и 6 удалений

Просмотреть файл

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22609.0
VisualStudioVersion = 14.0.22710.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -60,6 +60,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Common
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.TestCommon", "test\Microsoft.AspNet.Mvc.TestCommon\Microsoft.AspNet.Mvc.TestCommon.xproj", "{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch.Test", "test\Microsoft.AspNet.JsonPatch.Test\Microsoft.AspNet.JsonPatch.Test.xproj", "{81C20848-E063-4E12-AC40-0B55A532C16C}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch", "src\Microsoft.AspNet.JsonPatch\Microsoft.AspNet.JsonPatch.xproj", "{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -318,6 +322,29 @@ Global
{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.ActiveCfg = Release|Any CPU
{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.Build.0 = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|x86.ActiveCfg = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|x86.Build.0 = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Any CPU.Build.0 = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|x86.ActiveCfg = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|x86.Build.0 = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|x86.ActiveCfg = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|x86.Build.0 = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Any CPU.Build.0 = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -346,5 +373,7 @@ Global
{4DA2D7C1-A7B6-4C01-B57D-89E6EA4609DE} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{0449D6D2-BE1B-4E29-8E1B-444420802C03} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{81C20848-E063-4E12-AC40-0B55A532C16C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
EndGlobalSection
EndGlobal

35
Mvc.sln
Просмотреть файл

@ -1,5 +1,4 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22711.0
MinimumVisualStudioVersion = 10.0.40219.1
@ -156,6 +155,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CorsWebSite", "test\WebSite
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CorsMiddlewareWebSite", "test\WebSites\CorsMiddlewareWebSite\CorsMiddlewareWebSite.xproj", "{B42D4844-FFF8-4EC2-88D1-3AE95234D9EB}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch.Test", "test\Microsoft.AspNet.JsonPatch.Test\Microsoft.AspNet.JsonPatch.Test.xproj", "{81C20848-E063-4E12-AC40-0B55A532C16C}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch", "src\Microsoft.AspNet.JsonPatch\Microsoft.AspNet.JsonPatch.xproj", "{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -898,6 +901,30 @@ Global
{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.ActiveCfg = Release|Any CPU
{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5}.Release|x86.Build.0 = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|x86.ActiveCfg = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Debug|x86.Build.0 = Debug|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Any CPU.Build.0 = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|x86.ActiveCfg = Release|Any CPU
{81C20848-E063-4E12-AC40-0B55A532C16C}.Release|x86.Build.0 = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|x86.ActiveCfg = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Debug|x86.Build.0 = Debug|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Any CPU.Build.0 = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|x86.ActiveCfg = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|x86.Build.0 = Release|Any CPU
{94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94BA134D-04B3-48AA-BA55-5A4DB8640F2D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -996,7 +1023,9 @@ Global
{BCDB13A6-7D6E-485E-8424-A156432B71AC} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{8AEB631E-AB74-4D2E-83FB-8931EE10D9D3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{81C20848-E063-4E12-AC40-0B55A532C16C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{94BA134D-04B3-48AA-BA55-5A4DB8640F2D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{B42D4844-FFF8-4EC2-88D1-3AE95234D9EB} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
EndGlobal
EndGlobal

Просмотреть файл

@ -0,0 +1,16 @@
// 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.
namespace Microsoft.AspNet.JsonPatch.Adapters
{
public interface IObjectAdapter<T>
where T : class
{
void Add(Microsoft.AspNet.JsonPatch.Operations.Operation<T> operation, T objectToApplyTo);
void Copy(Microsoft.AspNet.JsonPatch.Operations.Operation<T> operation, T objectToApplyTo);
void Move(Microsoft.AspNet.JsonPatch.Operations.Operation<T> operation, T objectToApplyTo);
void Remove(Microsoft.AspNet.JsonPatch.Operations.Operation<T> operation, T objectToApplyTo);
void Replace(Microsoft.AspNet.JsonPatch.Operations.Operation<T> operation, T objectToApplyTo);
void Test(Microsoft.AspNet.JsonPatch.Operations.Operation<T> operation, T objectToApplyTo);
}
}

Просмотреть файл

@ -0,0 +1,713 @@
using System;
using Microsoft.AspNet.JsonPatch.Operations;
using Microsoft.AspNet.JsonPatch.Helpers;
using Microsoft.AspNet.JsonPatch.Exceptions;
using System.Reflection;
using System.Collections;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.JsonPatch.Adapters
{
public class SimpleObjectAdapter<T> : IObjectAdapter<T> where T : class
{
/// <summary>
/// The "add" operation performs one of the following functions,
/// depending upon what the target location references:
///
/// o If the target location specifies an array index, a new value is
/// inserted into the array at the specified index.
///
/// o If the target location specifies an object member that does not
/// already exist, a new member is added to the object.
///
/// o If the target location specifies an object member that does exist,
/// that member's value is replaced.
///
/// The operation object MUST contain a "value" member whose content
/// specifies the value to be added.
///
/// For example:
///
/// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] }
///
/// When the operation is applied, the target location MUST reference one
/// of:
///
/// o The root of the target document - whereupon the specified value
/// becomes the entire content of the target document.
///
/// o A member to add to an existing object - whereupon the supplied
/// value is added to that object at the indicated location. If the
/// member already exists, it is replaced by the specified value.
///
/// o An element to add to an existing array - whereupon the supplied
/// value is added to the array at the indicated location. Any
/// elements at or above the specified index are shifted one position
/// to the right. The specified index MUST NOT be greater than the
/// number of elements in the array. If the "-" character is used to
/// index the end of the array (see [RFC6901]), this has the effect of
/// appending the value to the array.
///
/// Because this operation is designed to add to existing objects and
/// arrays, its target location will often not exist. Although the
/// pointer's error handling algorithm will thus be invoked, this
/// specification defines the error handling behavior for "add" pointers
/// to ignore that error and add the value as specified.
///
/// However, the object itself or an array containing it does need to
/// exist, and it remains an error for that not to be the case. For
/// example, an "add" with a target location of "/a/b" starting with this
/// document:
///
/// { "a": { "foo": 1 } }
///
/// is not an error, because "a" exists, and "b" will be added to its
/// value. It is an error in this document:
///
/// { "q": { "bar": 2 } }
///
/// because "a" does not exist.
/// </summary>
/// <param name="operation">The add operation</param>
/// <param name="objectApplyTo">Object to apply the operation to</param>
public void Add(Operation<T> operation, T objectToApplyTo)
{
Add(operation.path, operation.value, objectToApplyTo, operation);
}
/// <summary>
/// Add is used by various operations (eg: add, copy, ...), yet through different operations;
/// This method allows code reuse yet reporting the correct operation on error
/// </summary>
private void Add(string path, object value, T objectToApplyTo, Operation<T> operationToReport)
{
// add, in this implementation, does not just "add" properties - that's
// technically impossible; It can however be used to add items to arrays,
// or to replace values.
// 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;
if (path.EndsWith("/-"))
{
appendList = true;
actualPathToProperty = path.Substring(0, path.Length - 2);
}
else
{
positionAsInteger = PropertyHelpers.GetNumericEnd(path);
if (positionAsInteger > -1)
{
actualPathToProperty = path.Substring(0,
path.IndexOf('/' + positionAsInteger.ToString()));
}
}
var pathProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualPathToProperty);
// 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);
}
// 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)
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.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);
}
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList;
if (appendList)
{
array.Add(conversionResult.ConvertedInstance);
}
else
{
// specified index must not be greater than the amount of items in the array
if (positionAsInteger <= array.Count)
{
array.Insert(positionAsInteger, conversionResult.ConvertedInstance);
}
else
{
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",
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),
objectToApplyTo);
}
}
else
{
var conversionResultTuple = PropertyHelpers.ConvertToActualType(pathProperty.PropertyType, value);
// conversion successful
if (conversionResultTuple.CanBeConverted)
{
PropertyHelpers.SetValue(pathProperty, objectToApplyTo, actualPathToProperty,
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);
}
}
}
/// <summary>
/// The "move" operation removes the value at a specified location and
/// adds it 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 move the value from.
///
/// The "from" location MUST exist for the operation to be successful.
///
/// For example:
///
/// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" }
///
/// This operation is functionally identical to a "remove" operation on
/// the "from" location, followed immediately by an "add" operation at
/// the target location with the value that was just removed.
///
/// The "from" location MUST NOT be a proper prefix of the "path"
/// location; i.e., a location cannot be moved into one of its children.
/// </summary>
/// <param name="operation">The move operation</param>
/// <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)
{
actualFromProperty = operation.from.Substring(0,
operation.from.IndexOf('/' + positionAsInteger.ToString()));
}
var fromProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualFromProperty);
// 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);
}
// 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)
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType);
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty) as IList;
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);
}
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),
objectToApplyTo);
}
}
else
{
// no list, just get the value
// set the new value
valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty);
}
// remove that value
Remove(operation.from, objectToApplyTo, operation);
// add that value to the path location
Add(operation.path, valueAtFromLocation, objectToApplyTo, operation);
}
/// <summary>
/// The "remove" operation removes the value at the target location.
///
/// The target location MUST exist for the operation to be successful.
///
/// For example:
///
/// { "op": "remove", "path": "/a/b/c" }
///
/// If removing an element from an array, any elements above the
/// specified index are shifted one position to the left.
/// </summary>
/// <param name="operation">The remove operation</param>
/// <param name="objectApplyTo">Object to apply the operation to</param>
public void Remove(Operation<T> operation, T objectToApplyTo)
{
Remove(operation.path, objectToApplyTo, operation);
}
/// <summary>
/// Remove is used by various operations (eg: remove, move, ...), yet through different operations;
/// This method allows code reuse yet reporting the correct operation on error
/// </summary>
private void Remove(string path, T objectToApplyTo, Operation<T> operationToReport)
{
var removeFromList = false;
var positionAsInteger = -1;
var actualPathToProperty = path;
if (path.EndsWith("/-"))
{
removeFromList = true;
actualPathToProperty = path.Substring(0, path.Length - 2);
}
else
{
positionAsInteger = PropertyHelpers.GetNumericEnd(path);
if (positionAsInteger > -1)
{
actualPathToProperty = path.Substring(0,
path.IndexOf('/' + positionAsInteger.ToString()));
}
}
var pathProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualPathToProperty);
// 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);
}
// 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)
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType);
// TODO: nested!
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathToProperty) as IList;
if (removeFromList)
{
array.RemoveAt(array.Count - 1);
}
else
{
if (positionAsInteger < array.Count)
{
array.RemoveAt(positionAsInteger);
}
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);
}
}
}
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),
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);
}
}
/// <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
/// type-specific rules.
///
/// o objects: are considered equal if they contain the same number of
/// members, and if each member can be considered equal to a member in
/// the other object, by comparing their keys (as strings) and their
/// values (using this list of type-specific rules).
///
/// o literals (false, true, and null): are considered equal if they are
/// the same.
///
/// Note that the comparison that is done is a logical comparison; e.g.,
/// whitespace between the member values of an array is not significant.
///
/// 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.
///
/// For example:
///
/// { "op": "test", "path": "/a/b/c", "value": "foo" }
/// </summary>
/// <param name="operation">The test operation</param>
/// <param name="objectApplyTo">Object to apply the operation to</param>
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)
{
actualPathProperty = operation.path.Substring(0,
operation.path.IndexOf('/' + positionInPathAsInteger.ToString()));
}
var pathProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualPathProperty);
// 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);
}
// 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)
{
// now, get the generic type of the enumerable
typeOfFinalPropertyAtPathLocation = PropertyHelpers.GetEnumerableType(pathProperty.PropertyType);
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(pathProperty, objectToApplyTo, actualPathProperty) as IList;
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);
}
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),
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);
}
}
/// <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
/// 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
/// 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)
{
actualFromProperty = operation.from.Substring(0,
operation.from.IndexOf('/' + positionAsInteger.ToString()));
}
var fromProperty = PropertyHelpers
.FindProperty(objectToApplyTo, actualFromProperty);
// 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);
}
// 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)
{
// now, get the generic type of the enumerable
var genericTypeOfArray = PropertyHelpers.GetEnumerableType(fromProperty.PropertyType);
// get value (it can be cast, we just checked that)
var array = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty) as IList;
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);
}
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),
objectToApplyTo);
}
}
else
{
// no list, just get the value
// set the new value
valueAtFromLocation = PropertyHelpers.GetValue(fromProperty, objectToApplyTo, actualFromProperty);
}
// add operation to target location with that value.
Add(operation.path, valueAtFromLocation, objectToApplyTo, operation);
}
}
}

Просмотреть файл

@ -0,0 +1,79 @@
using Microsoft.AspNet.JsonPatch.Exceptions;
using Microsoft.AspNet.JsonPatch.Operations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Microsoft.AspNet.JsonPatch.Converters
{
public class TypedJsonPatchDocumentConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
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];
// load jObject
var jObject = JArray.Load(reader);
// Create target object for Json => list of operations, typed to genericType
var genericOperation = typeof(Operation<>);
var concreteOperationType = genericOperation.MakeGenericType(genericType);
var genericList = typeof(List<>);
var concreteList = genericList.MakeGenericType(concreteOperationType);
var targetOperations = Activator.CreateInstance(concreteList);
//Create a new reader for this jObject, and set all properties to match the original reader.
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);
// container target: the typed JsonPatchDocument.
var container = Activator.CreateInstance(objectType, targetOperations);
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)
{
if (value is IJsonPatchDocument)
{
var jsonPatchDoc = (IJsonPatchDocument)value;
var lst = jsonPatchDoc.GetOperations();
// write out the operations, no envelope
serializer.Serialize(writer, lst);
}
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
// 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;
namespace Microsoft.AspNet.JsonPatch.Exceptions
{
public class JsonPatchException : Exception
{
public new Exception InnerException { get; internal set; }
public object AffectedObject { get; private set; }
private string _message = "";
public override string Message
{
get
{
return _message;
}
}
public JsonPatchException()
{
}
public JsonPatchException(string message, Exception innerException)
{
_message = message;
InnerException = innerException;
}
}
}

Просмотреть файл

@ -0,0 +1,41 @@
// 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 Microsoft.AspNet.JsonPatch.Operations;
namespace Microsoft.AspNet.JsonPatch.Exceptions
{
public class JsonPatchException<T> : JsonPatchException where T : class
{
public Operation<T> FailedOperation { get; private set; }
public new T AffectedObject { get; private set; }
private string _message = "";
public override string Message
{
get
{
return _message;
}
}
public JsonPatchException()
{
}
public JsonPatchException(Operation<T> operation, string message, T affectedObject)
{
FailedOperation = operation;
_message = message;
AffectedObject = affectedObject;
}
public JsonPatchException(Operation<T> operation, string message, T affectedObject, Exception innerException)
: this(operation, message, affectedObject)
{
InnerException = innerException;
}
}
}

Просмотреть файл

@ -0,0 +1,17 @@
// 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.
namespace Microsoft.AspNet.JsonPatch.Helpers
{
internal class ConversionResult
{
public bool CanBeConverted { get; private set; }
public object ConvertedInstance { get; private set; }
public ConversionResult(bool canBeConverted, object convertedInstance)
{
CanBeConverted = canBeConverted;
ConvertedInstance = convertedInstance;
}
}
}

Просмотреть файл

@ -0,0 +1,99 @@
// 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.Globalization;
using System.Linq.Expressions;
namespace Microsoft.AspNet.JsonPatch.Helpers
{
internal static class ExpressionHelpers
{
public static string GetPath<T, TProp>(Expression<Func<T, TProp>> expr) where T : class
{
return "/" + GetPath(expr.Body, true);
}
private static string GetPath(Expression expr, bool firstTime)
{
switch (expr.NodeType)
{
case ExpressionType.ArrayIndex:
var binaryExpression = (BinaryExpression)expr;
if (ContinueWithSubPath(binaryExpression.Left.NodeType, false))
{
var leftFromBinaryExpression = GetPath(binaryExpression.Left, false);
return leftFromBinaryExpression + "/" + binaryExpression.Right.ToString();
}
else
{
return binaryExpression.Right.ToString();
}
case ExpressionType.Call:
var methodCallExpression = (MethodCallExpression)expr;
if (ContinueWithSubPath(methodCallExpression.Object.NodeType, false))
{
var leftFromMemberCallExpression = GetPath(methodCallExpression.Object, false);
return leftFromMemberCallExpression + "/" +
GetIndexerInvocation(methodCallExpression.Arguments[0]);
}
else
{
return GetIndexerInvocation(methodCallExpression.Arguments[0]);
}
case ExpressionType.Convert:
return GetPath(((UnaryExpression)expr).Operand, false);
case ExpressionType.MemberAccess:
var memberExpression = expr as MemberExpression;
if (ContinueWithSubPath(memberExpression.Expression.NodeType, false))
{
var left = GetPath(memberExpression.Expression, false);
return left + "/" + memberExpression.Member.Name;
}
else
{
return memberExpression.Member.Name;
}
case ExpressionType.Parameter:
// Fits "x => x" (the whole document which is "" as JSON pointer)
return firstTime ? "" : null;
default:
return "";
}
}
private static bool ContinueWithSubPath(ExpressionType expressionType, bool firstTime)
{
if (firstTime)
{
return (expressionType == ExpressionType.ArrayIndex
|| expressionType == ExpressionType.Call
|| expressionType == ExpressionType.Convert
|| expressionType == ExpressionType.MemberAccess
|| expressionType == ExpressionType.Parameter);
}
else
{
return (expressionType == ExpressionType.ArrayIndex
|| expressionType == ExpressionType.Call
|| expressionType == ExpressionType.Convert
|| expressionType == ExpressionType.MemberAccess);
}
}
private static string GetIndexerInvocation(Expression expression)
{
var converted = Expression.Convert(expression, typeof(object));
var fakeParameter = Expression.Parameter(typeof(object), null);
var lambda = Expression.Lambda<Func<object, object>>(converted, fakeParameter);
Func<object, object> func;
func = lambda.Compile();
return Convert.ToString(func(null), CultureInfo.InvariantCulture);
}
}
}

Просмотреть файл

@ -0,0 +1,140 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
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)
{
try
{
var splitPath = propertyPath.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);
}
var propertyToFind = targetObject.GetType().GetProperty(splitPath.Last(),
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
return propertyToFind;
}
catch (Exception)
{
// will result in JsonPatchException in calling class, as expected
return null;
}
}
internal static ConversionResult ConvertToActualType(Type propertyType, object value)
{
try
{
var o = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), propertyType);
return new ConversionResult(true, o);
}
catch (Exception)
{
return new ConversionResult(false, null);
}
}
internal static Type GetEnumerableType(Type type)
{
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<>))
{
return interfaceType.GetGenericArguments()[0];
}
#endif
}
return null;
}
internal static int GetNumericEnd(string path)
{
var possibleIndex = path.Substring(path.LastIndexOf("/") + 1);
var castedIndex = -1;
if (int.TryParse(possibleIndex, out castedIndex))
{
return castedIndex;
}
return -1;
}
private static PropertyInfo GetPropertyInfo(object targetObject, string propertyName,
BindingFlags bindingFlags)
{
return targetObject.GetType().GetProperty(propertyName, bindingFlags);
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
// 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 Microsoft.AspNet.JsonPatch.Operations;
using System.Collections.Generic;
namespace Microsoft.AspNet.JsonPatch
{
public interface IJsonPatchDocument
{
List<Operation> GetOperations();
}
}

Просмотреть файл

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>4d55f4d8-633b-462f-a5b1-feb84bd2d534</ProjectGuid>
<RootNamespace>Microsoft.AspNet.JsonPatch</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

Просмотреть файл

@ -0,0 +1,31 @@
// 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;
namespace Microsoft.AspNet.JsonPatch.Operations
{
public class Operation : OperationBase
{
[JsonProperty("value")]
public object value { get; set; }
public Operation()
{
}
public Operation(string op, string path, string from, object value)
: base(op, path, from)
{
this.value = value;
}
public Operation(string op, string path, string from)
: base(op, path, from)
{
}
}
}

Просмотреть файл

@ -0,0 +1,47 @@
// 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 Newtonsoft.Json;
namespace Microsoft.AspNet.JsonPatch.Operations
{
public class OperationBase
{
[JsonIgnore]
public OperationType OperationType
{
get
{
return (OperationType)Enum.Parse(typeof(OperationType), op, true);
}
}
[JsonProperty("path")]
public string path { get; set; }
[JsonProperty("op")]
public string op { get; set; }
[JsonProperty("from")]
public string from { get; set; }
public OperationBase()
{
}
public OperationBase(string op, string path, string from)
{
this.op = op;
this.path = path;
this.from = from;
}
public bool ShouldSerializefrom()
{
return (OperationType == Operations.OperationType.Move
|| OperationType == OperationType.Copy);
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// 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.
namespace Microsoft.AspNet.JsonPatch.Operations
{
public enum OperationType
{
Add,
Remove,
Replace,
Move,
Copy,
Test
}
}

Просмотреть файл

@ -0,0 +1,61 @@
// 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 Microsoft.AspNet.JsonPatch.Adapters;
namespace Microsoft.AspNet.JsonPatch.Operations
{
public class Operation<T> : Operation where T : class
{
public Operation()
{
}
public Operation(string op, string path, string from, object value)
: base(op, path, from)
{
this.value = value;
}
public Operation(string op, string path, string from)
: base(op, path, from)
{
}
public void Apply(T objectToApplyTo, IObjectAdapter<T> adapter)
{
switch (OperationType)
{
case OperationType.Add:
adapter.Add(this, objectToApplyTo);
break;
case OperationType.Remove:
adapter.Remove(this, objectToApplyTo);
break;
case OperationType.Replace:
adapter.Replace(this, objectToApplyTo);
break;
case OperationType.Move:
adapter.Move(this, objectToApplyTo);
break;
case OperationType.Copy:
adapter.Copy(this, objectToApplyTo);
break;
case OperationType.Test:
adapter.Test(this, objectToApplyTo);
break;
default:
break;
}
}
public bool ShouldSerializevalue()
{
return (OperationType == Operations.OperationType.Add
|| OperationType == OperationType.Replace
|| OperationType == OperationType.Test);
}
}
}

Просмотреть файл

@ -0,0 +1,377 @@
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;
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; }
public JsonPatchDocument()
{
Operations = new List<Operation<T>>();
}
// Create from list of operations
public JsonPatchDocument(List<Operation<T>> operations)
{
Operations = operations;
}
/// <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;
}
/// <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>
/// 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 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 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 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. 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 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 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>
/// 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 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 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 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 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 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 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 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 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 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 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 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)
{
ApplyTo(objectToApplyTo, new SimpleObjectAdapter<T>());
}
public void ApplyTo(T objectToApplyTo, IObjectAdapter<T> adapter)
{
// apply each operation in order
foreach (var op in Operations)
{
op.Apply(objectToApplyTo, adapter);
}
}
public List<Operation> GetOperations()
{
var allOps = new List<Operation>();
if (Operations != null)
{
foreach (var op in Operations)
{
var untypedOp = new Operation();
untypedOp.op = op.op;
untypedOp.value = op.value;
untypedOp.path = op.path;
untypedOp.from = op.from;
allOps.Add(untypedOp);
}
}
return allOps;
}
}
}

Просмотреть файл

@ -0,0 +1,22 @@
{
"version": "1.0.0-*",
"dependencies": {
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
"Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" },
"Newtonsoft.Json": "6.0.6"
},
"frameworks": {
"dnx451": { },
"dnxcore50": {
"dependencies": {
"Microsoft.CSharp": "4.0.0-beta-*",
"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-*"
}
}
}
}

Просмотреть файл

@ -1,6 +1,7 @@
// 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.ComponentModel.DataAnnotations;
using System.Reflection;
using Newtonsoft.Json;

Просмотреть файл

@ -0,0 +1,126 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Framework.Internal;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
namespace Microsoft.AspNet.Mvc
{
public class JsonPatchInputFormatter : InputFormatter
{
private const int DefaultMaxDepth = 32;
private JsonSerializerSettings _jsonSerializerSettings;
public JsonPatchInputFormatter()
{
SupportedEncodings.Add(Encodings.UTF8EncodingWithoutBOM);
SupportedEncodings.Add(Encodings.UTF16EncodingLittleEndian);
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;
}
}
/// <inheritdoc />
public 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))
{
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;
}
}
}
}
/// <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)
{
return new JsonTextReader(new StreamReader(readStream, effectiveEncoding));
}
/// <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);
}
}
}

Просмотреть файл

@ -22,7 +22,8 @@
"Microsoft.Framework.PropertyActivator.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.PropertyHelper.Internal": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.Runtime.Interfaces": "1.0.0-*",
"Microsoft.Framework.WebEncoders": "1.0.0-*"
"Microsoft.Framework.WebEncoders": "1.0.0-*",
"Microsoft.AspNet.JsonPatch": "1.0.0-*"
},
"frameworks": {
"dnx451": {},

Просмотреть файл

@ -60,4 +60,4 @@ namespace Microsoft.AspNet.Builder
return app.UseRouter(routes.Build());
}
}
}
}

Просмотреть файл

@ -55,6 +55,7 @@ namespace Microsoft.AspNet.Mvc
// Set up default input formatters.
options.InputFormatters.Add(new JsonInputFormatter());
options.InputFormatters.Add(new JsonPatchInputFormatter());
// Set up ValueProviders
options.ValueProviderFactories.Add(new RouteValueValueProviderFactory());

Просмотреть файл

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>81c20848-e063-4e12-ac40-0b55a532c16c</ProjectGuid>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

Просмотреть файл

@ -0,0 +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.
namespace Microsoft.AspNet.JsonPatch.Test
{
public class NestedDTO
{
public string StringProperty { get; set; }
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,20 @@
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; }
}
}

Просмотреть файл

@ -0,0 +1,21 @@
// 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.
namespace Microsoft.AspNet.JsonPatch.Test
{
public class SimpleDTOWithNestedDTO
{
public int IntegerValue { get; set; }
public NestedDTO NestedDTO { get; set; }
public SimpleDTO SimpleDTO { get; set; }
public SimpleDTOWithNestedDTO()
{
this.NestedDTO = new NestedDTO();
this.SimpleDTO = new SimpleDTO();
}
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,20 @@
{
"compilationOptions": {
"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",
"xunit.runner.aspnet": "2.0.0-aspnet-*"
},
"commands": {
"test": "xunit.runner.aspnet"
},
"frameworks": {
"dnx451": { }
}
}