Skip to content

Instantly share code, notes, and snippets.

@yww325
Created December 19, 2020 04:05
Show Gist options
  • Save yww325/b71563462cb5b5f2ea29e0143634bebe to your computer and use it in GitHub Desktop.
Save yww325/b71563462cb5b5f2ea29e0143634bebe to your computer and use it in GitHub Desktop.
C# code to generate JSON Patch (rfc6902)
using System.Linq;
using Microsoft.AspNetCore.JsonPatch;
using Newtonsoft.Json.Linq;
/// <summary>
/// this a general tool to generate JsonPatchDocument by comparing two objects recursively
/// </summary>
public static class PatchHelper
{
private const char PathDelimiter = '/';
private const char Dash = '-';
public static JsonPatchDocument CompareObjects(object oldObj, object newObj, string path ="/")
{
var original = JObject.FromObject(oldObj);
var modified = JObject.FromObject(newObj);
var patch = new JsonPatchDocument();
FillPatchForObject(original, modified, patch, path);
return patch;
}
private static void FillPatchForObject(JObject orig, JObject mod, JsonPatchDocument patch, string path)
{
var origNames = orig.Properties().Select(x => x.Name).ToArray();
var modNames = mod.Properties().Select(x => x.Name).ToArray();
PatchRemovedProperty(patch, origNames, modNames, path);
PatchAddedProperty(patch, origNames, modNames, path, mod);
// Present in both
foreach (var k in origNames.Intersect(modNames))
{
var origProp = orig.Property(k);
var modProp = mod.Property(k);
if (origProp.Value.Type != modProp.Value.Type)
{
// even type is changed, like from 123 to "hello"
patch.Replace(path + modProp.Name, modProp.Value.ToObject<object>());
}
else if (!string.Equals(
origProp.Value.ToString(Newtonsoft.Json.Formatting.None),
modProp.Value.ToString(Newtonsoft.Json.Formatting.None)))
{
// type is the same, just value changed
PatchPropertyValueChange(patch, origProp, modProp, path);
}
}
}
private static void PatchPropertyValueChange(JsonPatchDocument patch, JProperty origProp, JProperty modProp, string path)
{
if (origProp.Value.Type == JTokenType.Object)
{
// Recurse into objects
FillPatchForObject(origProp.Value as JObject, modProp.Value as JObject, patch, path + modProp.Name + PathDelimiter);
}
else
{
if (origProp.Value.Type == JTokenType.Array)
{
var oldArray = origProp.Value as JArray;
var newArray = modProp.Value as JArray;
PatchArray(patch, oldArray, newArray, path + modProp.Name);
}
else
{
// Replace values directly for simple type ,like 123 to 456
patch.Replace(path + modProp.Name, modProp.Value.ToObject<object>());
}
}
}
private static void PatchArray(JsonPatchDocument patch, JArray oldArray, JArray newArray, string path)
{
for (int i = 0; i < oldArray.Count; i++)
{
if (newArray.Count - 1 < i)
{
patch.Remove(path + PathDelimiter + i);
}
else
{
if (oldArray[i] is JObject && newArray[i] is JObject)
{
// Recurse into array element
FillPatchForObject(oldArray[i] as JObject, newArray[i] as JObject, patch, path + PathDelimiter + i + PathDelimiter);
}
else
{
//direct compare
if (!JToken.DeepEquals(oldArray[i], newArray[i]))
{
patch.Replace(path + PathDelimiter + i, newArray[i]);
}
}
}
}
for (int i = oldArray.Count; i < newArray.Count; i++)
{
patch.Add(path + PathDelimiter + Dash, newArray[i]);
}
}
private static void PatchAddedProperty(JsonPatchDocument patch, string[] origNames, string[] modNames, string path, JObject mod)
{
// Names added in modified
foreach (var k in modNames.Except(origNames))
{
var prop = mod.Property(k);
patch.Add(path + prop.Name, prop.Value.ToObject<object>()); // the value will be serialized to json anyway.
}
}
private static void PatchRemovedProperty(JsonPatchDocument patch, string[] origNames, string[] modNames, string path)
{
// Names removed in modified
foreach (var k in origNames.Except(modNames))
{
patch.Remove(path + k);
}
}
}
using FluentAssertions;
using Schwab.Start.AS.UI.Api.Helpers;
using Xunit;
public class PatchHelperTests
{
[Fact]
public void ShouldReportRemoveArrayObjectElement()
{
dynamic a = new
{
array = new dynamic[]
{
new
{
P="a"
},
new
{
P="b"
},
}
};
dynamic b = new
{
array = new dynamic[]
{
new
{
P="a"
},
}
};
var patch = PatchHelper.CompareObjects((object)a, (object)b);
patch.Operations.Count.Should().Be(1);
patch.Operations.Should().Contain(o => o.op == "remove"
&& o.path == "/array/1"
);
}
[Fact]
public void ShouldReportReplaceAndRemoveArrayObjectElement()
{
dynamic a = new
{
array = new dynamic[]
{
new
{
P="a"
},
new
{
P="b"
},
}
};
dynamic b = new
{
array = new dynamic[]
{
new
{
P="b"
},
}
};
var patch = PatchHelper.CompareObjects((object)a, (object)b);
patch.Operations.Count.Should().Be(2);
patch.Operations.Should().Contain(o => o.op == "replace"
&& o.path == "/array/0/P"
&& o.value.ToString() == "b"
);
patch.Operations.Should().Contain(o => o.op == "remove"
&& o.path == "/array/1"
);
}
[Fact]
public void ShouldReportRemoveArraySimpleElement()
{
dynamic a = new
{
array = new dynamic[]
{
"a",
"b"
}
};
dynamic b = new
{
array = new dynamic[]
{
"a"
}
};
var patch = PatchHelper.CompareObjects((object)a, (object)b);
patch.Operations.Count.Should().Be(1);
patch.Operations.Should().Contain(o => o.op == "remove"
&& o.path == "/array/1"
);
}
[Fact]
public void ShouldReportReplaceAndRemoveArraySimpleElement()
{
dynamic a = new
{
array = new dynamic[]
{
"a",
"b"
}
};
dynamic b = new
{
array = new dynamic[]
{
"b"
}
};
var patch = PatchHelper.CompareObjects((object)a, (object)b);
patch.Operations.Count.Should().Be(2);
patch.Operations.Should().Contain(o => o.op == "replace"
&& o.path == "/array/0"
&& o.value.ToString() == "b"
);
patch.Operations.Should().Contain(o => o.op == "remove"
&& o.path == "/array/1"
);
}
[Fact]
public void ShouldReportAddArraySimpleElement()
{
dynamic a = new
{
array = new dynamic[]
{
"a",
"b"
}
};
dynamic b = new
{
array = new dynamic[]
{
"a",
"b",
"c"
}
};
var patch = PatchHelper.CompareObjects((object)a, (object)b);
patch.Operations.Count.Should().Be(1);
patch.Operations.Should().Contain(o => o.op == "add"
&& o.path == "/array/-"
&& o.value.ToString() == "c"
);
}
[Fact]
public void ShouldReportRemoveAndAddProperty()
{
dynamic a = new
{
pa = 1
};
dynamic b = new
{
pb = 2
};
var patch = PatchHelper.CompareObjects((object)a, (object)b);
patch.Operations.Count.Should().Be(2);
patch.Operations.Should().Contain(o => o.op == "remove"
&& o.path == "/pa"
);
patch.Operations.Should().Contain(o => o.op == "add"
&& o.path == "/pb"
&& (int)o.value == 2
);
}
[Fact]
public void ShouldReportReplaceWithChangingType()
{
dynamic a = new
{
pa = 1
};
dynamic b = new
{
pa = "hello"
};
var patch = PatchHelper.CompareObjects((object)a, (object)b);
patch.Operations.Count.Should().Be(1);
patch.Operations.Should().Contain(o => o.op == "replace"
&& o.path == "/pa"
&& (string)o.value == "hello"
);
}
[Fact]
public void ShouldReportReplaceWithDeepLayer()
{
dynamic a = new
{
pa = new
{
paa = new
{
paaa = 123,
paab = 123
}
}
};
dynamic b = new
{
pa = new
{
paa = new
{
paaa = 123,
paab = 456
}
}
};
var patch = PatchHelper.CompareObjects((object)a, (object)b);
patch.Operations.Count.Should().Be(1);
patch.Operations.Should().Contain(o => o.op == "replace"
&& o.path == "/pa/paa/paab"
&& (int)o.value == 456
);
}
}
@sankarbhamahat
Copy link

Great work and thanks for sharing. Facing a challenge while deleting an entity from a child item collection which belongs to a parent entity i.e. Order being the parent entity and OrderItem being a collection of child items. When I try to delete the 3rd item from a collection of 5, it tries to apply 'replace' operation to the properties of 3rd item with values from properties of 4th item and then finally creates the remove operation for 4th item. When it sets properties for 3rd item it also sets the PK Id property and marks it as modified. This fails as properties composing an Id cant be set to modified.

Error: System.InvalidOperationException: The property 'OrderItems.Id' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key, first delete the dependent and invoke 'SaveChanges', and then associate the dependent with the new principal.

Could you please suggest a solution or a work around? Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment