diff --git a/Mvc.sln b/Mvc.sln index d0bc676261..487d3c64e4 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22726.0 +VisualStudioVersion = 14.0.22806.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -160,6 +160,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch. EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch", "src\Microsoft.AspNet.JsonPatch\Microsoft.AspNet.JsonPatch.xproj", "{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "JsonPatchWebSite", "test\WebSites\JsonPatchWebSite\JsonPatchWebSite.xproj", "{DAB1252D-577C-4912-98BE-1A812BF83F86}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -950,6 +952,18 @@ Global {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 + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Debug|x86.ActiveCfg = Debug|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Debug|x86.Build.0 = Debug|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|Any CPU.Build.0 = Release|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|x86.ActiveCfg = Release|Any CPU + {DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1028,5 +1042,6 @@ Global {B42D4844-FFF8-4EC2-88D1-3AE95234D9EB} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {81C20848-E063-4E12-AC40-0B55A532C16C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {DAB1252D-577C-4912-98BE-1A812BF83F86} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/samples/MvcSample.Web/Controllers/JsonPatchController.cs b/samples/MvcSample.Web/Controllers/JsonPatchController.cs index 2e6c964c19..747be302f3 100644 --- a/samples/MvcSample.Web/Controllers/JsonPatchController.cs +++ b/samples/MvcSample.Web/Controllers/JsonPatchController.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Linq; using Microsoft.AspNet.JsonPatch; using Microsoft.AspNet.Mvc; @@ -34,7 +35,12 @@ namespace MvcSample.Web.Controllers } }; - patchDoc.ApplyTo(customer); + patchDoc.ApplyTo(customer, ModelState); + + if (!ModelState.IsValid) + { + return HttpBadRequest(ModelState); + } return new ObjectResult(customer); } diff --git a/samples/MvcSample.Web/Views/JsonPatch/Index.cshtml b/samples/MvcSample.Web/Views/JsonPatch/Index.cshtml index 4e86235cf9..660fb9c426 100644 --- a/samples/MvcSample.Web/Views/JsonPatch/Index.cshtml +++ b/samples/MvcSample.Web/Views/JsonPatch/Index.cshtml @@ -7,9 +7,9 @@ "Orders": [{"OrderName": "Order1"}, {"OrderName": "Order2"}] }; - $('#currentlabel').text(JSON.stringify(currentObj)) + $('#currentLabel').text(JSON.stringify(currentObj)) - $('#addorder').on("click", function () { + $('#addOrder').on("click", function () { var obj = [{ "op": "Add", "path": "Customer/Orders/2", @@ -22,12 +22,34 @@ dataType: 'json', contentType: 'application/json-patch+json', success: function (data) { - $('#addlabel').text(JSON.stringify(data)) + $('#addLabel').text(JSON.stringify(data)) } }); }); - $('#removeorder').on("click", function () { + $('#invalidAddOrder').on("click", function () { + var obj = [{ + "op": "Add", + "path": "Customer/Orders/5", + "value": { "OrderName": "Name5" } + }] + $.ajax({ + url: 'jsonpatch', + type: 'PATCH', + data: JSON.stringify(obj), + dataType: 'json', + contentType: 'application/json-patch+json', + success: function (data) { + $('#invalidAddLabel').text(data) + }, + error: function (request, status, error) { + $('#invalidAddLabel').text(error) + } + }); + }); + + + $('#removeOrder').on("click", function () { var obj = [{ "op": "Remove", "path": "Customer/Name" @@ -39,12 +61,12 @@ dataType: 'json', contentType: 'application/json-patch+json', success: function (data) { - $('#removelabel').text(JSON.stringify(data)) + $('#removeLabel').text(JSON.stringify(data)) } }); }); - $('#moveorder').on("click", function () { + $('#moveOrder').on("click", function () { var obj = [{ "op": "Move", "from": "Customer/Orders/0", @@ -57,12 +79,12 @@ dataType: 'json', contentType: 'application/json-patch+json', success: function (data) { - $('#movelabel').text(JSON.stringify(data)) + $('#moveLabel').text(JSON.stringify(data)) } }); }); - $('#replaceorder').on("click", function () { + $('#replaceOrder').on("click", function () { var obj = [{ "op": "Replace", "path": "Customer/Name", @@ -75,7 +97,7 @@ dataType: 'json', contentType: 'application/json-patch+json', success: function (data) { - $('#replacelabel').text(JSON.stringify(data)) + $('#replaceLabel').text(JSON.stringify(data)) } }); }); @@ -86,29 +108,35 @@

- +
- +
- +
- +
- -
-
- -
- +
- +
- + +
+
+ +
+ +
+
+
+ +
+
diff --git a/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs b/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs index a4f8f10b87..76e11d7cb9 100644 --- a/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs +++ b/src/Microsoft.AspNet.JsonPatch/Adapters/IObjectAdapter.cs @@ -1,16 +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. +using Microsoft.AspNet.JsonPatch.Operations; + namespace Microsoft.AspNet.JsonPatch.Adapters { - public interface IObjectAdapter - where T : class + /// + /// Defines the operations that can be performed on a JSON patch document. + /// + /// + public interface IObjectAdapter where T : class { - void Add(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); - void Copy(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); - void Move(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); - void Remove(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); - void Replace(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); - void Test(Microsoft.AspNet.JsonPatch.Operations.Operation operation, T objectToApplyTo); + void Add(Operation operation, T objectToApplyTo); + void Copy(Operation operation, T objectToApplyTo); + void Move(Operation operation, T objectToApplyTo); + void Remove(Operation operation, T objectToApplyTo); + void Replace(Operation operation, T objectToApplyTo); + void Test(Operation operation, T objectToApplyTo); } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs b/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs index 0a30834592..94da0289b5 100644 --- a/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs +++ b/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs @@ -7,20 +7,34 @@ using System.Reflection; using Microsoft.AspNet.JsonPatch.Exceptions; using Microsoft.AspNet.JsonPatch.Helpers; using Microsoft.AspNet.JsonPatch.Operations; -using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNet.JsonPatch.Adapters { + /// public class ObjectAdapter : IObjectAdapter where T : class { - public IContractResolver ContractResolver { get; set; } - - public ObjectAdapter(IContractResolver contractResolver) + /// + /// Initializes a new instance of . + /// + /// The . + /// The for logging . + public ObjectAdapter(IContractResolver contractResolver, Action> logErrorAction) { ContractResolver = contractResolver; + LogErrorAction = logErrorAction; } + /// + /// Gets or sets the . + /// + public IContractResolver ContractResolver { get; } + + /// + /// Action for logging . + /// + public Action> LogErrorAction { get; } + /// /// The "add" operation performs one of the following functions, /// depending upon what the target location references: @@ -123,7 +137,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters .FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver); // does property at path exist? - CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path); + if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path)) + { + return; + } // 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, @@ -139,7 +156,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters var conversionResult = PropertyHelpers.ConvertToActualType(genericTypeOfArray, value); - CheckIfPropertyCanBeSet(conversionResult, objectToApplyTo, operationToReport, path); + if (!CheckIfPropertyCanBeSet(conversionResult, objectToApplyTo, operationToReport, path)) + { + return; + } // get value (it can be cast, we just checked that) var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); @@ -157,19 +177,23 @@ namespace Microsoft.AspNet.JsonPatch.Adapters } else { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operationToReport, - Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path), - objectToApplyTo); + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + + return; } } } else { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operationToReport, - Resources.FormatInvalidPathForArrayProperty(operationToReport.op, path), - objectToApplyTo); + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + + return; } } else @@ -179,7 +203,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters value); // Is conversion successful - CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operationToReport, path); + if (!CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operationToReport, path)) + { + return; + } patchProperty.Property.ValueProvider.SetValue( patchProperty.Parent, @@ -229,7 +256,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters .FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver); // does property at from exist? - CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from); + if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from)) + { + return; + } // 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. @@ -245,20 +275,24 @@ namespace Microsoft.AspNet.JsonPatch.Adapters if (array.Count <= positionAsInteger) { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operation, - Resources.FormatInvalidIndexForArrayProperty(operation.op, operation.from), - objectToApplyTo); + Resources.FormatInvalidIndexForArrayProperty(operation.op, operation.from))); + + return; } valueAtFromLocation = array[positionAsInteger]; } else { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operation, - Resources.FormatInvalidPathForArrayProperty(operation.op, operation.from), - objectToApplyTo); + Resources.FormatInvalidPathForArrayProperty(operation.op, operation.from))); + + return; } } else @@ -324,7 +358,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters .FindPropertyAndParent(objectToApplyTo, actualPathToProperty, ContractResolver); // does the target location exist? - CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path); + if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path)) + { + return; + } // 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 @@ -352,19 +389,23 @@ namespace Microsoft.AspNet.JsonPatch.Adapters } else { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operationToReport, - Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path), - objectToApplyTo); + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + + return; } } } else { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operationToReport, - Resources.FormatInvalidPathForArrayProperty(operationToReport.op, path), - objectToApplyTo); + Resources.FormatInvalidPathForArrayProperty(operationToReport.op, path))); + + return; } } else @@ -451,10 +492,13 @@ namespace Microsoft.AspNet.JsonPatch.Adapters .FindPropertyAndParent(objectToApplyTo, actualPathProperty, ContractResolver); // does property at path exist? - CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.path); + if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.path)) + { + return; + } // get the property path - Type typeOfFinalPropertyAtPathLocation; + Type typeOfFinalPropertyAtPathLocation = null; // 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. @@ -471,20 +515,24 @@ namespace Microsoft.AspNet.JsonPatch.Adapters if (array.Count <= positionInPathAsInteger) { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operation, - Resources.FormatInvalidIndexForArrayProperty(operation.op, operation.path), - objectToApplyTo); + Resources.FormatInvalidIndexForArrayProperty(operation.op, operation.path))); + + return; } valueAtPathLocation = array[positionInPathAsInteger]; } else { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operation, - Resources.FormatInvalidPathForArrayProperty(operation.op, operation.path), - objectToApplyTo); + Resources.FormatInvalidPathForArrayProperty(operation.op, operation.path))); + + return; } } else @@ -499,7 +547,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters operation.value); // Is conversion successful - CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operation, operation.path); + if (!CheckIfPropertyCanBeSet(conversionResultTuple, objectToApplyTo, operation, operation.path)) + { + return; + } //Compare } @@ -571,7 +622,10 @@ namespace Microsoft.AspNet.JsonPatch.Adapters .FindPropertyAndParent(objectToApplyTo, actualFromProperty, ContractResolver); // does property at from exist? - CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from); + if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operation, operation.from)) + { + return; + } // get the property path // is the path an array (but not a string (= char[]))? In this case, @@ -588,19 +642,24 @@ namespace Microsoft.AspNet.JsonPatch.Adapters if (array.Count <= positionAsInteger) { - throw new JsonPatchException(operation, - Resources.FormatInvalidIndexForArrayProperty(operation.op, operation.from), - objectToApplyTo); + LogError(new JsonPatchError( + objectToApplyTo, + operation, + Resources.FormatInvalidIndexForArrayProperty(operation.op, operation.from))); + + return; } valueAtFromLocation = array[positionAsInteger]; } else { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operation, - Resources.FormatInvalidPathForArrayProperty(operation.op, operation.from), - objectToApplyTo); + Resources.FormatInvalidPathForArrayProperty(operation.op, operation.from))); + + return; } } else @@ -614,7 +673,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters Add(operation.path, valueAtFromLocation, objectToApplyTo, operation); } - private void CheckIfPropertyExists( + private bool CheckIfPropertyExists( JsonPatchProperty patchProperty, T objectToApplyTo, Operation operation, @@ -622,18 +681,25 @@ namespace Microsoft.AspNet.JsonPatch.Adapters { if (patchProperty == null) { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operation, - Resources.FormatPropertyDoesNotExist(propertyPath), - objectToApplyTo); + Resources.FormatPropertyDoesNotExist(propertyPath))); + + return false; } + if (patchProperty.Property.Ignored) { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operation, - Resources.FormatCannotUpdateProperty(propertyPath), - objectToApplyTo); + Resources.FormatCannotUpdateProperty(propertyPath))); + + return false; } + + return true; } private bool IsNonStringArray(JsonPatchProperty patchProperty) @@ -643,7 +709,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters patchProperty.Property.PropertyType.GetTypeInfo()); } - private void CheckIfPropertyCanBeSet( + private bool CheckIfPropertyCanBeSet( ConversionResult result, T objectToApplyTo, Operation operation, @@ -651,10 +717,26 @@ namespace Microsoft.AspNet.JsonPatch.Adapters { if (!result.CanBeConverted) { - throw new JsonPatchException( + LogError(new JsonPatchError( + objectToApplyTo, operation, - Resources.FormatInvalidValueForProperty(result.ConvertedInstance, path), - objectToApplyTo); + Resources.FormatInvalidValueForProperty(result.ConvertedInstance, path))); + + return false; + } + + return true; + } + + private void LogError(JsonPatchError jsonPatchError) + { + if (LogErrorAction != null) + { + LogErrorAction(jsonPatchError); + } + else + { + throw new JsonPatchException(jsonPatchError); } } } diff --git a/src/Microsoft.AspNet.JsonPatch/Exceptions/JsonPatchException.cs b/src/Microsoft.AspNet.JsonPatch/Exceptions/JsonPatchException.cs index ae9114d4d9..bca6d2b5a9 100644 --- a/src/Microsoft.AspNet.JsonPatch/Exceptions/JsonPatchException.cs +++ b/src/Microsoft.AspNet.JsonPatch/Exceptions/JsonPatchException.cs @@ -26,15 +26,15 @@ namespace Microsoft.AspNet.JsonPatch.Exceptions } - public JsonPatchException(Operation operation, string message, T affectedObject) + public JsonPatchException(JsonPatchError jsonPatchError) { - FailedOperation = operation; - _message = message; - AffectedObject = affectedObject; + FailedOperation = jsonPatchError.Operation; + _message = jsonPatchError.ErrorMessage; + AffectedObject = jsonPatchError.AffectedObject; } - public JsonPatchException(Operation operation, string message, T affectedObject, Exception innerException) - : this(operation, message, affectedObject) + public JsonPatchException(JsonPatchError jsonPatchError, Exception innerException) + : this(jsonPatchError) { InnerException = innerException; } diff --git a/src/Microsoft.AspNet.JsonPatch/JsonPatchError.cs b/src/Microsoft.AspNet.JsonPatch/JsonPatchError.cs new file mode 100644 index 0000000000..a441abda55 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/JsonPatchError.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.JsonPatch +{ + /// + /// Captures error message and the related entity and the operation that caused it. + /// + public class JsonPatchError where T : class + { + /// + /// Initializes a new instance of . + /// + /// The object that is affected by the error. + /// The that caused the error. + /// The error message. + public JsonPatchError( + [NotNull] T affectedObject, + [NotNull] Operation operation, + [NotNull] string errorMessage) + { + AffectedObject = affectedObject; + Operation = operation; + ErrorMessage = errorMessage; + } + + /// + /// Gets the object that is affected by the error. + /// + public T AffectedObject { get; } + + /// + /// Gets the that caused the error. + /// + public Operation Operation { get; } + + /// + /// Gets the error message. + /// + public string ErrorMessage { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs b/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs index 3417960508..4bc1721862 100644 --- a/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs +++ b/src/Microsoft.AspNet.JsonPatch/TypedJsonPatchDocument.cs @@ -354,12 +354,16 @@ namespace Microsoft.AspNet.JsonPatch public void ApplyTo(T objectToApplyTo) { - ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver)); + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null)); + } + + public void ApplyTo(T objectToApplyTo, Action> logErrorAction) + { + ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction)); } public void ApplyTo(T objectToApplyTo, IObjectAdapter adapter) { - // apply each operation in order foreach (var op in Operations) { diff --git a/src/Microsoft.AspNet.Mvc/JsonPatchExtensions.cs b/src/Microsoft.AspNet.Mvc/JsonPatchExtensions.cs new file mode 100644 index 0000000000..1a9140a6f8 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc/JsonPatchExtensions.cs @@ -0,0 +1,51 @@ +// 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; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// Extensions for + /// + public static class JsonPatchExtensions + { + /// + /// Applies JSON patch operations on object and logs errors in . + /// + /// The . + /// The entity on which is applied. + /// The to add errors. + public static void ApplyTo( + [NotNull] this JsonPatchDocument patchDoc, + [NotNull] T objectToApplyTo, + [NotNull] ModelStateDictionary modelState) where T : class + { + patchDoc.ApplyTo(objectToApplyTo, modelState, prefix: string.Empty); + } + + /// + /// Applies JSON patch operations on object and logs errors in . + /// + /// The . + /// The entity on which is applied. + /// The to add errors. + /// The prefix to use when looking up values in . + public static void ApplyTo( + [NotNull] this JsonPatchDocument patchDoc, + [NotNull] T objectToApplyTo, + [NotNull] ModelStateDictionary modelState, + string prefix) where T : class + { + patchDoc.ApplyTo(objectToApplyTo, jsonPatchError => + { + var affectedObjectName = jsonPatchError.AffectedObject.GetType().Name; + var key = string.IsNullOrEmpty(prefix) ? affectedObjectName : prefix + "." + affectedObjectName; + + modelState.TryAddModelError(key, jsonPatchError.ErrorMessage); + }); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs index 65e75d7d03..eb398df775 100644 --- a/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs +++ b/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs @@ -282,8 +282,10 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); - Assert.Equal("For operation 'add' on array property at path '/simpledto/integerlist/4', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'add' on array property at path '/simpledto/integerlist/4', the index is " + + "larger than the array size.", + exception.Message); } @@ -311,8 +313,39 @@ namespace Microsoft.AspNet.JsonPatch.Test { deserialized.ApplyTo(doc); }); - Assert.Equal("For operation 'add' on array property at path '/simpledto/integerlist/4', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'add' on array property at path '/simpledto/integerlist/4', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void AddToListInvalidPositionTooLarge_LogsError() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4, 4); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + //Assert + Assert.Equal( + "For operation 'add' on array property at path '/simpledto/integerlist/4', the index is larger than " + + "the array size.", + logger.ErrorMessage); + } [Fact] @@ -363,6 +396,31 @@ namespace Microsoft.AspNet.JsonPatch.Test Assert.Equal("Property does not exist at path '/simpledto/integerlist/-1'.", exception.Message); } + [Fact] + public void AddToListInvalidPositionTooSmall_LogsError() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.SimpleDTO.IntegerList, 4, -1); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + //Assert + Assert.Equal("Property does not exist at path '/simpledto/integerlist/-1'.", logger.ErrorMessage); + } + [Fact] public void AddToListAppend() { @@ -528,8 +586,10 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); - Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); } [Fact] @@ -556,8 +616,38 @@ namespace Microsoft.AspNet.JsonPatch.Test { deserialized.ApplyTo(doc); }); - Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLarge_LogsError() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList, 3); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal( + "For operation 'remove' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + logger.ErrorMessage); } [Fact] @@ -608,6 +698,31 @@ namespace Microsoft.AspNet.JsonPatch.Test Assert.Equal("Property does not exist at path '/simpledto/integerlist/-1'.", exception.Message); } + [Fact] + public void RemoveFromListInvalidPositionTooSmall_LogsError() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.SimpleDTO.IntegerList, -1); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal("Property does not exist at path '/simpledto/integerlist/-1'.", logger.ErrorMessage); + } + [Fact] public void RemoveFromEndOfList() { @@ -1014,8 +1129,10 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); - Assert.Equal("For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); } [Fact] @@ -1042,8 +1159,38 @@ namespace Microsoft.AspNet.JsonPatch.Test { deserialized.ApplyTo(doc); }); - Assert.Equal("For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void ReplaceInListInvalid_PositionTooLarge_LogsError() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5, 3); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal( + "For operation 'replace' on array property at path '/simpledto/integerlist/3', the index is " + + "larger than the array size.", + logger.ErrorMessage); } [Fact] @@ -1091,6 +1238,31 @@ namespace Microsoft.AspNet.JsonPatch.Test Assert.Equal("Property does not exist at path '/simpledto/integerlist/-1'.", exception.Message); } + [Fact] + public void ReplaceInListInvalidPositionTooSmall_LogsError() + { + // Arrange + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5, -1); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal("Property does not exist at path '/simpledto/integerlist/-1'.", logger.ErrorMessage); + } + [Fact] public void Copy() { diff --git a/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs index b2a21f27a6..d7a16b0cff 100644 --- a/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs +++ b/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs @@ -113,8 +113,10 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); - Assert.Equal("For operation 'add' on array property at path '/integerlist/4', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'add' on array property at path '/integerlist/4', the index is " + + "larger than the array size.", + exception.Message); } [Fact] @@ -135,8 +137,35 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws>(() => { deserialized.ApplyTo(doc); }); - Assert.Equal("For operation 'add' on array property at path '/integerlist/4', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'add' on array property at path '/integerlist/4', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void AddToListInvalidPositionTooLarge_LogsError() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, 4); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal( + "For operation 'add' on array property at path '/integerlist/4', the index is " + + "larger than the array size.", + logger.ErrorMessage); } [Fact] @@ -264,6 +293,28 @@ namespace Microsoft.AspNet.JsonPatch.Test Assert.Equal("Property does not exist at path '/integerlist/-1'.", exception.Message); } + [Fact] + public void AddToListInvalidPositionTooSmall_LogsError() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Add(o => o.IntegerList, 4, -1); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal("Property does not exist at path '/integerlist/-1'.", logger.ErrorMessage); + } + [Fact] public void AddToListAppend() { @@ -408,8 +459,10 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws>(() => { patchDoc.ApplyTo(doc); }); - Assert.Equal("For operation 'remove' on array property at path '/integerlist/3', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'remove' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); } [Fact] @@ -430,8 +483,35 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws>(() => { deserialized.ApplyTo(doc); }); - Assert.Equal("For operation 'remove' on array property at path '/integerlist/3', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'remove' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLarge_LogsError() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, 3); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal( + "For operation 'remove' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + logger.ErrorMessage); } [Fact] @@ -473,6 +553,28 @@ namespace Microsoft.AspNet.JsonPatch.Test Assert.Equal("Property does not exist at path '/integerlist/-1'.", exception.Message); } + [Fact] + public void RemoveFromListInvalidPositionTooSmall_LogsError() + { + // Arrange + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + var patchDoc = new JsonPatchDocument(); + patchDoc.Remove(o => o.IntegerList, -1); + + var logger = new TestErrorLogger(); + + // Act + patchDoc.ApplyTo(doc, logger.LogErrorMessage); + + // Assert + Assert.Equal("Property does not exist at path '/integerlist/-1'.", logger.ErrorMessage); + } + [Fact] public void RemoveFromEndOfList() { @@ -971,8 +1073,10 @@ namespace Microsoft.AspNet.JsonPatch.Test { patchDoc.ApplyTo(doc); }); - Assert.Equal("For operation 'replace' on array property at path '/integerlist/3', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'replace' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); } [Fact] @@ -995,8 +1099,10 @@ namespace Microsoft.AspNet.JsonPatch.Test { deserialized.ApplyTo(doc); }); - Assert.Equal("For operation 'replace' on array property at path '/integerlist/3', the index is " + - "larger than the array size.", exception.Message); + Assert.Equal( + "For operation 'replace' on array property at path '/integerlist/3', the index is " + + "larger than the array size.", + exception.Message); } [Fact] diff --git a/test/Microsoft.AspNet.JsonPatch.Test/TestErrorLogger.cs b/test/Microsoft.AspNet.JsonPatch.Test/TestErrorLogger.cs new file mode 100644 index 0000000000..826aab3a69 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/TestErrorLogger.cs @@ -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.Test +{ + public class TestErrorLogger where T: class + { + public string ErrorMessage { get; set; } + + public void LogErrorMessage(JsonPatchError patchError) + { + ErrorMessage = patchError.ErrorMessage; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/JsonPatchTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/JsonPatchTest.cs new file mode 100644 index 0000000000..ac442f15ff --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/JsonPatchTest.cs @@ -0,0 +1,145 @@ +// 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.Net.Http; +using System.Text; +using System.Threading.Tasks; +using JsonPatchWebSite; +using JsonPatchWebSite.Models; +using Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class JsonPatchTest + { + private const string SiteName = nameof(JsonPatchWebSite); + private readonly Action _app = new Startup().Configure; + private readonly Action _configureServices = new Startup().ConfigureServices; + + [Theory] + [InlineData("http://localhost/jsonpatch/JsonPatchWithModelState")] + [InlineData("http://localhost/jsonpatch/JsonPatchWithModelStateAndPrefix?prefix=Patch")] + public async Task JsonPatch_ValidAddOperation_List(string url) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + + var input = "[{ \"op\": \"add\", \"path\": \"Customer/Orders/2\", " + + "\"value\": { \"OrderName\": \"Name2\" }}]"; + var request = new HttpRequestMessage + { + Content = new StringContent(input, Encoding.UTF8, "application/json-patch+json"), + Method = new HttpMethod("PATCH"), + RequestUri = new Uri(url) + }; + + // Act + var response = await client.SendAsync(request); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + var customer = JsonConvert.DeserializeObject(body); + Assert.Equal("Name2", customer.Orders[2].OrderName); + } + + [Theory] + [InlineData("http://localhost/jsonpatch/JsonPatchWithModelState")] + [InlineData("http://localhost/jsonpatch/JsonPatchWithModelStateAndPrefix?prefix=Patch")] + public async Task JsonPatch_MultipleValidOperations_Success(string url) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + + var input = "[{ \"op\": \"add\", \"path\": \"Customer/Orders/2\", " + + "\"value\": { \"OrderName\": \"Name2\" }}, {\"op\": \"copy\", \"from\": \"Customer/Orders/2\", " + + "\"path\": \"Customer/Orders/3\" }, {\"op\": \"replace\", \"path\": \"Customer/Orders/2/OrderName\", " + + "\"value\": \"ReplacedName\" }]"; + var request = new HttpRequestMessage + { + Content = new StringContent(input, Encoding.UTF8, "application/json-patch+json"), + Method = new HttpMethod("PATCH"), + RequestUri = new Uri(url) + }; + + // Act + var response = await client.SendAsync(request); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + var customer = JsonConvert.DeserializeObject(body); + Assert.Equal("ReplacedName", customer.Orders[2].OrderName); + Assert.Equal("Name2", customer.Orders[3].OrderName); + } + + public static IEnumerable InvalidJsonPatchData + { + get + { + return new[] + { + new object[] { + "http://localhost/jsonpatch/JsonPatchWithModelStateAndPrefix?prefix=Patch", + "[{ \"op\": \"add\", \"path\": \"Customer/Orders/5\", " + + "\"value\": { \"OrderName\": \"Name5\" }}]", + "{\"Patch.Customer\":[\"For operation 'add' on array property at path " + + "'Customer/Orders/5', the index is larger than the array size.\"]}" + }, + new object[] { + "http://localhost/jsonpatch/JsonPatchWithModelState", + "[{ \"op\": \"add\", \"path\": \"Customer/Orders/5\", " + + "\"value\": { \"OrderName\": \"Name5\" }}]", + "{\"Customer\":[\"For operation 'add' on array property at path " + + "'Customer/Orders/5', the index is larger than the array size.\"]}" + }, + new object[] { + "http://localhost/jsonpatch/JsonPatchWithModelStateAndPrefix?prefix=Patch", + "[{ \"op\": \"add\", \"path\": \"Customer/Orders/2\", \"value\": " + + "{ \"OrderName\": \"Name2\" }}, {\"op\": \"copy\", \"from\": \"Customer/Orders/4\", " + + "\"path\": \"Customer/Orders/3\" }, {\"op\": \"replace\", \"path\": " + + "\"Customer/Orders/2/OrderName\", \"value\": \"ReplacedName\" }]", + "{\"Patch.Customer\":[\"For operation 'copy' on array property at path " + + "'Customer/Orders/4', the index is larger than the array size.\"]}" + }, + new object[] { + "http://localhost/jsonpatch/JsonPatchWithModelState", + "[{ \"op\": \"add\", \"path\": \"Customer/Orders/2\", \"value\": " + + "{ \"OrderName\": \"Name2\" }}, {\"op\": \"copy\", \"from\": \"Customer/Orders/4\", " + + "\"path\": \"Customer/Orders/3\" }, {\"op\": \"replace\", \"path\": " + + "\"Customer/Orders/2/OrderName\", \"value\": \"ReplacedName\" }]", + "{\"Customer\":[\"For operation 'copy' on array property at path " + + "'Customer/Orders/4', the index is larger than the array size.\"]}" + } + }; + } + } + + [Theory, MemberData("InvalidJsonPatchData")] + public async Task JsonPatch_InvalidOperations_failure(string url, string input, string errorMessage) + { + // Arrange + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + + var request = new HttpRequestMessage + { + Content = new StringContent(input, Encoding.UTF8, "application/json-patch+json"), + Method = new HttpMethod("PATCH"), + RequestUri = new Uri(url) + }; + + // Act + var response = await client.SendAsync(request); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(errorMessage, body); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index d3857e6663..597944f135 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -28,6 +28,7 @@ "FormatFilterWebSite": "1.0.0-*", "FormatterWebSite": "1.0.0", "InlineConstraintsWebSite": "1.0.0", + "JsonPatchWebSite": "1.0.0", "LoggingWebSite": "1.0.0", "LowercaseUrlsWebSite": "1.0.0-*", "Microsoft.AspNet.Mvc": "6.0.0-*", diff --git a/test/Microsoft.AspNet.Mvc.Test/JsonPatchExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.Test/JsonPatchExtensionsTest.cs new file mode 100644 index 0000000000..53e4fdeae0 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Test/JsonPatchExtensionsTest.cs @@ -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 Microsoft.AspNet.JsonPatch; +using Microsoft.AspNet.JsonPatch.Operations; +using Microsoft.AspNet.Mvc.ModelBinding; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class JsonPatchExtensionsTest + { + [Fact] + public void ApplyTo_JsonPatchDocument_ModelState() + { + // Arrange + var operation = new Operation("add", "Customer/CustomerId", from: null, value: "TestName"); + var patchDoc = new JsonPatchDocument(); + patchDoc.Operations.Add(operation); + + var modelState = new ModelStateDictionary(); + + // Act + patchDoc.ApplyTo(new Customer(), modelState); + + // Assert + var error = Assert.Single(modelState["Customer"].Errors); + Assert.Equal("Property does not exist at path 'Customer/CustomerId'.", error.ErrorMessage); + } + + [Fact] + public void ApplyTo_JsonPatchDocument_PrefixModelState() + { + // Arrange + var operation = new Operation("add", "Customer/CustomerId", from: null, value: "TestName"); + var patchDoc = new JsonPatchDocument(); + patchDoc.Operations.Add(operation); + + var modelState = new ModelStateDictionary(); + + // Act + patchDoc.ApplyTo(new Customer(), modelState, "jsonpatch"); + + // Assert + var error = Assert.Single(modelState["jsonpatch.Customer"].Errors); + Assert.Equal("Property does not exist at path 'Customer/CustomerId'.", error.ErrorMessage); + } + + public class Customer + { + public string CustomerName { get; set; } + } + } +} \ No newline at end of file diff --git a/test/WebSites/JsonPatchWebSite/Controllers/JsonPatchController.cs b/test/WebSites/JsonPatchWebSite/Controllers/JsonPatchController.cs new file mode 100644 index 0000000000..d9f240a8d0 --- /dev/null +++ b/test/WebSites/JsonPatchWebSite/Controllers/JsonPatchController.cs @@ -0,0 +1,65 @@ +// 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 JsonPatchWebSite.Models; +using Microsoft.AspNet.JsonPatch; +using Microsoft.AspNet.Mvc; + +namespace JsonPatchWebSite.Controllers +{ + [Route("jsonpatch/[action]")] + public class JsonPatchController : Controller + { + [HttpPatch] + public IActionResult JsonPatchWithModelState([FromBody] JsonPatchDocument patchDoc) + { + var customer = CreateCustomer(); + + patchDoc.ApplyTo(customer, ModelState); + + if (!ModelState.IsValid) + { + return HttpBadRequest(ModelState); + } + + return new ObjectResult(customer); + } + + [HttpPatch] + public IActionResult JsonPatchWithModelStateAndPrefix( + [FromBody] JsonPatchDocument patchDoc, + string prefix) + { + var customer = CreateCustomer(); + + patchDoc.ApplyTo(customer, ModelState, prefix); + + if (!ModelState.IsValid) + { + return HttpBadRequest(ModelState); + } + + return new ObjectResult(customer); + } + + private Customer CreateCustomer() + { + return new Customer + { + CustomerName = "John", + Orders = new List() + { + new Order + { + OrderName = "Order1" + }, + new Order + { + OrderName = "Order2" + } + } + }; + } + } +} diff --git a/test/WebSites/JsonPatchWebSite/JsonPatchWebSite.xproj b/test/WebSites/JsonPatchWebSite/JsonPatchWebSite.xproj new file mode 100644 index 0000000000..d83238b02c --- /dev/null +++ b/test/WebSites/JsonPatchWebSite/JsonPatchWebSite.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + dab1252d-577c-4912-98be-1a812bf83f86 + JsonPatchWebSite + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 29149 + + + \ No newline at end of file diff --git a/test/WebSites/JsonPatchWebSite/Models/Customer.cs b/test/WebSites/JsonPatchWebSite/Models/Customer.cs new file mode 100644 index 0000000000..a54b4bd298 --- /dev/null +++ b/test/WebSites/JsonPatchWebSite/Models/Customer.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace JsonPatchWebSite.Models +{ + public class Customer + { + public string CustomerName { get; set; } + + public List Orders { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/JsonPatchWebSite/Models/Order.cs b/test/WebSites/JsonPatchWebSite/Models/Order.cs new file mode 100644 index 0000000000..7a7acbccb4 --- /dev/null +++ b/test/WebSites/JsonPatchWebSite/Models/Order.cs @@ -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 JsonPatchWebSite.Models +{ + public class Order + { + public string OrderName { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/JsonPatchWebSite/Startup.cs b/test/WebSites/JsonPatchWebSite/Startup.cs new file mode 100644 index 0000000000..5fed6ca599 --- /dev/null +++ b/test/WebSites/JsonPatchWebSite/Startup.cs @@ -0,0 +1,26 @@ +// 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.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace JsonPatchWebSite +{ + public class Startup + { + // Set up application services + public void ConfigureServices(IServiceCollection services) + { + // Add MVC services to the services container + services.AddMvc(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseCultureReplacer(); + + // Add MVC to the request pipeline + app.UseMvc(); + } + } +} diff --git a/test/WebSites/JsonPatchWebSite/project.json b/test/WebSites/JsonPatchWebSite/project.json new file mode 100644 index 0000000000..a08db9734c --- /dev/null +++ b/test/WebSites/JsonPatchWebSite/project.json @@ -0,0 +1,19 @@ +{ + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000" + }, + "dependencies": { + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + }, + "webroot": "wwwroot" +} diff --git a/test/WebSites/JsonPatchWebSite/readme.md b/test/WebSites/JsonPatchWebSite/readme.md new file mode 100644 index 0000000000..970d684af2 --- /dev/null +++ b/test/WebSites/JsonPatchWebSite/readme.md @@ -0,0 +1,4 @@ +JsonPatchWebSite +=== + +This web site illustrates how to use JSON Patch operation on an object. diff --git a/test/WebSites/JsonPatchWebSite/wwwroot/HelloWorld.htm b/test/WebSites/JsonPatchWebSite/wwwroot/HelloWorld.htm new file mode 100644 index 0000000000..3da1ec26e9 --- /dev/null +++ b/test/WebSites/JsonPatchWebSite/wwwroot/HelloWorld.htm @@ -0,0 +1 @@ +HelloWorld