如何从比较两个C#对象创建一个JsonPatchDocument?

29

假设我有两个相同类型的c#对象,我想比较它们以创建一个JsonPatchDocument。

我有一个StyleDetail类定义如下:

public class StyleDetail
    {
        public string Id { get; set; }
        public string Code { get; set; }
        public string Name { get; set; }
        public decimal OriginalPrice { get; set; }
        public decimal Price { get; set; }
        public string Notes { get; set; }
        public string ImageUrl { get; set; }
        public bool Wishlist { get; set; }
        public List<string> Attributes { get; set; }
        public ColourList Colours { get; set; }
        public SizeList Sizes { get; set; }
        public ResultPage<Style> Related { get; set; }
        public ResultPage<Style> Similar { get; set; }
        public List<Promotion> Promotions { get; set; }
        public int StoreStock { get; set; }
        public StyleDetail()
        {
            Attributes = new List<string>();
            Colours = new ColourList();
            Sizes = new SizeList();
            Promotions = new List<Promotion>();
        }
    }

如果我有两个StyleDetail对象

StyleDetail styleNew = db.GetStyle(123);
StyleDetail styleOld = db.GetStyle(456);

我现在想创建一个JsonPatchDocument,以便我可以将差异发送到我的REST API... 怎么做?

JsonPatchDocument patch = new JsonPatchDocument();
// Now I want to populate patch with the differences between styleNew and styleOld - how?

在JavaScript中,有一个库可以实现此功能https://www.npmjs.com/package/rfc6902

计算两个对象之间的差异:

rfc6902.createPatch({first: 'Chris'}, {first: 'Chris', last: 'Brown'});

[ { op: 'add', path: '/last', value: 'Brown' } ]
但我正在寻找一个 C# 实现。

1
我知道这有点过时了...但是你最终解决了这个问题吗?我也在寻找完全相同的东西! - brazilianldsjaguar
你可以使用反射来迭代属性并进行比较。参见此问题以获取迭代属性的示例:https://dev59.com/3HM_5IYBdhLWcg3w2XHw - Jack A.
3个回答

36

让我们利用你的类可序列化为JSON的事实来进行滥用!这是第一次尝试创建补丁,它不关心你实际对象,只关心该对象的JSON表示。

public static JsonPatchDocument CreatePatch(object originalObject, object modifiedObject)
{
    var original = JObject.FromObject(originalObject);
    var modified = JObject.FromObject(modifiedObject);

    var patch = new JsonPatchDocument();
    FillPatchForObject(original, modified, patch, "/");

    return patch;
}

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();

    // Names removed in modified
    foreach (var k in origNames.Except(modNames))
    {
        var prop = orig.Property(k);
        patch.Remove(path + prop.Name);
    }

    // Names added in modified
    foreach (var k in modNames.Except(origNames))
    {
        var prop = mod.Property(k);
        patch.Add(path + prop.Name, prop.Value);
    }

    // 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)
        {
            patch.Replace(path + modProp.Name, modProp.Value);
        }
        else if (!string.Equals(
                        origProp.Value.ToString(Newtonsoft.Json.Formatting.None),
                        modProp.Value.ToString(Newtonsoft.Json.Formatting.None)))
        {
            if (origProp.Value.Type == JTokenType.Object)
            {
                // Recurse into objects
                FillPatchForObject(origProp.Value as JObject, modProp.Value as JObject, patch, path + modProp.Name +"/");
            }
            else
            {
                // Replace values directly
                patch.Replace(path + modProp.Name, modProp.Value);
            }
        }       
    }
}

使用方法:

var patch = CreatePatch(
    new { Unchanged = new[] { 1, 2, 3, 4, 5 }, Changed = "1", Removed = "1" },
    new { Unchanged = new[] { 1, 2, 3, 4, 5 }, Changed = "2", Added = new { x = "1" } });

// Result of JsonConvert.SerializeObject(patch)
[
  {
    "path": "/Removed",
    "op": "remove"
  },
  {
    "value": {
      "x": "1"
    },
    "path": "/Added",
    "op": "add"
  },
  {
    "value": "2",
    "path": "/Changed",
    "op": "replace"
  }
]

顺便提一下 - 您可以使用完全相同的代码来比较两个JSON字符串,只需从字符串创建JObjects,然后调用FillPatchForObject - gnud
谢谢!非常有帮助。但是,如果我们在“StyleDetail”类中更改“颜色”,它就无法正常工作。 - Manuel Quelhas
如果您直接将“StyleDetail”序列化为JSON,它是否被正确序列化?否则,我的方法将无法正常工作。您能给出“StyleDetail”的定义以及两个导致错误的示例对象吗? - gnud
这里的区别在于数组。我并不试图以一种好的方式处理数组,如果有差异,我只是替换整个数组。逐个元素进行比较并不难,但是想象一下,如果你有一个包含700个元素的数组,并且你删除了第0个元素。检测这些变化是很复杂的。朴素比较数组的方法会“替换”元素0-698,并删除元素699。 - gnud
当所有属性都是对象时,它可以完美地工作。但如果一个属性是数组,补丁将替换整个数组,这是多余的,并且在某些情况下会影响性能。 - Wei J. Zheng
2
这是一个更新版本,支持替换数组元素而不是整个数组。https://gist.github.com/yww325/b71563462cb5b5f2ea29e0143634bebe - yww325

0
你可以使用我的DiffAnalyzer。它基于反射,而且你可以配置你想要分析的深度。

https://github.com/rcarubbi/Carubbi.DiffAnalyzer

var before = new User { Id = 1, Name="foo"};
var after= new User  { Id = 2, Name="bar"};
var analyzer = new DiffAnalyzer();
var results = analyzer.Compare(before, after);

这会返回一个 JSON 补丁吗? - Oylex
不,你仍然需要将结果转换为JsonPatch。但是你已经拥有了所有的差异。 - rcarubbi

-3

你可以使用this

你可以通过NuGet安装,前往NuGet.org查看SimpleHelpers.ObjectDiffPatch

PM> Install-Package SimpleHelpers.ObjectDiffPatch

使用:

StyleDetail styleNew = new StyleDetail() { Id = "12", Code = "first" };
StyleDetail styleOld = new StyleDetail() { Id = "23", Code = "second" };
var diff = ObjectDiffPatch.GenerateDiff (styleOld , styleNew );

// original properties values
Console.WriteLine (diff.OldValues.ToString());

// updated properties values
Console.WriteLine (diff.NewValues.ToString());

这并没有回答问题。你是手动添加要在代码中替换的项,而不是通过比较两个对象生成补丁文档。 - jmc
抱歉,我已经修好了。 - Serg csharp .Net Developer
1
这个项目执行了一个差异比较,但它没有生成JsonPatchDocument。 - jmc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接