// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Linq; using System.Collections.Generic; using System.Globalization; using System.Linq.Expressions; using Microsoft.AspNetCore.JsonPatch.Adapters; using Microsoft.AspNetCore.JsonPatch.Converters; using Microsoft.AspNetCore.JsonPatch.Exceptions; using Microsoft.AspNetCore.JsonPatch.Internal; using Microsoft.AspNetCore.JsonPatch.Operations; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.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 : IJsonPatchDocument where TModel : class { public List> Operations { get; private set; } [JsonIgnore] public IContractResolver ContractResolver { get; set; } public JsonPatchDocument() { Operations = new List>(); ContractResolver = new DefaultContractResolver(); } // Create from list of operations public JsonPatchDocument(List> operations, IContractResolver contractResolver) { Operations = operations ?? throw new ArgumentNullException(nameof(operations)); ContractResolver = contractResolver ?? throw new ArgumentNullException(nameof(contractResolver)); } /// /// Add operation. Will result in, for example, /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } /// /// value type /// target location /// value /// public JsonPatchDocument Add(Expression> path, TProp value) { if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "add", GetPath(path, null), from: null, value: value)); return this; } /// /// Add value to list at given position /// /// value type /// target location /// value /// position /// public JsonPatchDocument Add( Expression>> path, TProp value, int position) { if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "add", GetPath(path, position.ToString()), from: null, value: value)); return this; } /// /// Add value to the end of the list /// /// value type /// target location /// value /// public JsonPatchDocument Add(Expression>> path, TProp value) { if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "add", GetPath(path, "-"), from: null, value: value)); return this; } /// /// Remove value at target location. Will result in, for example, /// { "op": "remove", "path": "/a/b/c" } /// /// target location /// public JsonPatchDocument Remove(Expression> path) { if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation("remove", GetPath(path, null), from: null)); return this; } /// /// Remove value from list at given position /// /// value type /// target location /// position /// public JsonPatchDocument Remove(Expression>> path, int position) { if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "remove", GetPath(path, position.ToString()), from: null)); return this; } /// /// Remove value from end of list /// /// value type /// target location /// public JsonPatchDocument Remove(Expression>> path) { if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "remove", GetPath(path, "-"), from: null)); return this; } /// /// Replace value. Will result in, for example, /// { "op": "replace", "path": "/a/b/c", "value": 42 } /// /// target location /// value /// public JsonPatchDocument Replace(Expression> path, TProp value) { if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "replace", GetPath(path, null), from: null, value: value)); return this; } /// /// Replace value in a list at given position /// /// value type /// target location /// value /// position /// public JsonPatchDocument Replace(Expression>> path, TProp value, int position) { if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "replace", GetPath(path, position.ToString()), from: null, value: value)); return this; } /// /// Replace value at end of a list /// /// value type /// target location /// value /// public JsonPatchDocument Replace(Expression>> path, TProp value) { if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "replace", GetPath(path, "-"), from: null, value: value)); return this; } /// /// 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" } /// /// source location /// target location /// public JsonPatchDocument Move( Expression> from, Expression> path) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "move", GetPath(path, null), GetPath(from, null))); return this; } /// /// Move from a position in a list to a new location /// /// /// source location /// position /// target location /// public JsonPatchDocument Move( Expression>> from, int positionFrom, Expression> path) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "move", GetPath(path, null), GetPath(from, positionFrom.ToString()))); return this; } /// /// Move from a property to a location in a list /// /// /// source location /// target location /// position /// public JsonPatchDocument Move( Expression> from, Expression>> path, int positionTo) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "move", GetPath(path, positionTo.ToString()), GetPath(from, null))); return this; } /// /// Move from a position in a list to another location in a list /// /// /// source location /// position (source) /// target location /// position (target) /// public JsonPatchDocument Move( Expression>> from, int positionFrom, Expression>> path, int positionTo) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "move", GetPath(path, positionTo.ToString()), GetPath(from, positionFrom.ToString()))); return this; } /// /// Move from a position in a list to the end of another list /// /// /// source location /// position /// target location /// public JsonPatchDocument Move( Expression>> from, int positionFrom, Expression>> path) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "move", GetPath(path, "-"), GetPath(from, positionFrom.ToString()))); return this; } /// /// Move to the end of a list /// /// /// source location /// target location /// public JsonPatchDocument Move( Expression> from, Expression>> path) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "move", GetPath(path, "-"), GetPath(from, null))); return this; } /// /// 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" } /// /// source location /// target location /// public JsonPatchDocument Copy( Expression> from, Expression> path) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "copy", GetPath(path, null), GetPath(from, null))); return this; } /// /// Copy from a position in a list to a new location /// /// /// source location /// position /// target location /// public JsonPatchDocument Copy( Expression>> from, int positionFrom, Expression> path) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "copy", GetPath(path, null), GetPath(from, positionFrom.ToString()))); return this; } /// /// Copy from a property to a location in a list /// /// /// source location /// target location /// position /// public JsonPatchDocument Copy( Expression> from, Expression>> path, int positionTo) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "copy", GetPath(path, positionTo.ToString()), GetPath(from, null))); return this; } /// /// Copy from a position in a list to a new location in a list /// /// /// source location /// position (source) /// target location /// position (target) /// public JsonPatchDocument Copy( Expression>> from, int positionFrom, Expression>> path, int positionTo) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "copy", GetPath(path, positionTo.ToString()), GetPath(from, positionFrom.ToString()))); return this; } /// /// Copy from a position in a list to the end of another list /// /// /// source location /// position /// target location /// public JsonPatchDocument Copy( Expression>> from, int positionFrom, Expression>> path) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "copy", GetPath(path, "-"), GetPath(from, positionFrom.ToString()))); return this; } /// /// Copy to the end of a list /// /// /// source location /// target location /// public JsonPatchDocument Copy( Expression> from, Expression>> path) { if (from == null) { throw new ArgumentNullException(nameof(from)); } if (path == null) { throw new ArgumentNullException(nameof(path)); } Operations.Add(new Operation( "copy", GetPath(path, "-"), GetPath(from, null))); return this; } /// /// Apply this JsonPatchDocument /// /// Object to apply the JsonPatchDocument to public void ApplyTo(TModel objectToApplyTo) { if (objectToApplyTo == null) { throw new ArgumentNullException(nameof(objectToApplyTo)); } ApplyTo(objectToApplyTo, new ObjectAdapter(ContractResolver, logErrorAction: null)); } /// /// Apply this JsonPatchDocument /// /// Object to apply the JsonPatchDocument to /// Action to log errors public void ApplyTo(TModel objectToApplyTo, Action logErrorAction) { if (objectToApplyTo == null) { throw new ArgumentNullException(nameof(objectToApplyTo)); } var adapter = new ObjectAdapter(ContractResolver, logErrorAction); foreach (var op in Operations) { try { op.Apply(objectToApplyTo, adapter); } catch (JsonPatchException jsonPatchException) { var errorReporter = logErrorAction ?? ErrorReporter.Default; errorReporter(new JsonPatchError(objectToApplyTo, op, jsonPatchException.Message)); // As per JSON Patch spec if an operation results in error, further operations should not be executed. break; } } } /// /// Apply this JsonPatchDocument /// /// Object to apply the JsonPatchDocument to /// IObjectAdapter instance to use when applying public void ApplyTo(TModel objectToApplyTo, IObjectAdapter adapter) { if (objectToApplyTo == null) { throw new ArgumentNullException(nameof(objectToApplyTo)); } if (adapter == null) { throw new ArgumentNullException(nameof(adapter)); } // apply each operation in order foreach (var op in Operations) { op.Apply(objectToApplyTo, adapter); } } IList IJsonPatchDocument.GetOperations() { var allOps = new List(); if (Operations != null) { foreach (var op in Operations) { var untypedOp = new Operation { op = op.op, value = op.value, path = op.path, from = op.from }; allOps.Add(untypedOp); } } return allOps; } // Internal for testing internal string GetPath(Expression> expr, string position) { var segments = GetPathSegments(expr.Body); var path = String.Join("/", segments); if (position != null) { path += "/" + position; if (segments.Count == 0) { return path; } } return "/" + path; } private List GetPathSegments(Expression expr) { var listOfSegments = new List(); switch (expr.NodeType) { case ExpressionType.ArrayIndex: var binaryExpression = (BinaryExpression)expr; listOfSegments.AddRange(GetPathSegments(binaryExpression.Left)); listOfSegments.Add(binaryExpression.Right.ToString()); return listOfSegments; case ExpressionType.Call: var methodCallExpression = (MethodCallExpression)expr; listOfSegments.AddRange(GetPathSegments(methodCallExpression.Object)); listOfSegments.Add(EvaluateExpression(methodCallExpression.Arguments[0])); return listOfSegments; case ExpressionType.Convert: listOfSegments.AddRange(GetPathSegments(((UnaryExpression)expr).Operand)); return listOfSegments; case ExpressionType.MemberAccess: var memberExpression = expr as MemberExpression; listOfSegments.AddRange(GetPathSegments(memberExpression.Expression)); // Get property name, respecting JsonProperty attribute listOfSegments.Add(GetPropertyNameFromMemberExpression(memberExpression)); return listOfSegments; case ExpressionType.Parameter: // Fits "x => x" (the whole document which is "" as JSON pointer) return listOfSegments; default: throw new InvalidOperationException(Resources.FormatExpressionTypeNotSupported(expr)); } } private string GetPropertyNameFromMemberExpression(MemberExpression memberExpression) { var jsonObjectContract = ContractResolver.ResolveContract(memberExpression.Expression.Type) as JsonObjectContract; if (jsonObjectContract != null) { return jsonObjectContract.Properties .First(jsonProperty => jsonProperty.UnderlyingName == memberExpression.Member.Name) .PropertyName; } return null; } private static bool ContinueWithSubPath(ExpressionType expressionType) { return (expressionType == ExpressionType.ArrayIndex || expressionType == ExpressionType.Call || expressionType == ExpressionType.Convert || expressionType == ExpressionType.MemberAccess); } // Evaluates the value of the key or index which may be an int or a string, // or some other expression type. // The expression is converted to a delegate and the result of executing the delegate is returned as a string. private static string EvaluateExpression(Expression expression) { var converted = Expression.Convert(expression, typeof(object)); var fakeParameter = Expression.Parameter(typeof(object), null); var lambda = Expression.Lambda>(converted, fakeParameter); var func = lambda.Compile(); return Convert.ToString(func(null), CultureInfo.InvariantCulture); } } }