使用JSON Patch将值添加到字典中

Tag*_*agc 12 c# json-patch

概观

我正在尝试使用ASP.NET Core编写Web服务,允许客户端查询和修改微控制器的状态.该微控制器包含我在我的应用中建模的许多系统 - 例如,PWM系统,执行器输入系统等.

这些系统的组件都具有可以使用JSON补丁请求查询或修改的特定属性.例如,可以使用携带的HTTP请求启用微型上的第4个PWM .为了支持这一点,我正在使用该库.{"op":"replace", "path":"/pwms/3/enabled", "value":true}AspNetCore.JsonPatch

我的问题是我正在尝试为新的"CAN数据库"系统实现JSON补丁支持,该系统在逻辑上应该将定义名称映射到特定的CAN消息定义,我不知道如何解决这个问题.

细节

下图为CAN数据库系统建模.一个CanDatabase实例理应包含形式的字典IDictionary<string, CanMessageDefinition>.

CAN数据库系统模型

为了支持创建新的消息定义,我的应用程序应该允许用户发送如下的JSON补丁请求:

{
    "op": "add",
    "path": "/candb/my_new_definition",
    "value": {
        "template": ["...", "..."],
        "repeatRate": "...",
        "...": "...",
    }
}
Run Code Online (Sandbox Code Playgroud)

这里,my_new_definition将定义定义名称,并且value应将与之关联的对象反序列化为CanMessageDefinition 对象.然后应将其存储为CanDatabase字典中的新键值对.

问题是,path应指定属性路径这对于静态类型的对象将是......嗯,静态的(一个例外是,它允许引用数组元素,例如/pwms/3如上).

我试过的

A. Leeroy Jenkins的方法

忘记我知道它不会工作的事实- 我尝试了下面的实现(尽管我需要支持动态JSON补丁路径,但它只使用静态类型)只是为了看看会发生什么.

履行

internal sealed class CanDatabaseModel : DeviceComponentModel<CanDatabaseModel>
{
    public CanDatabaseModel()
    {
        this.Definitions = new Dictionary<string, CanMessageDefinition>();
    }

    [JsonProperty(PropertyName = "candb")]
    public IDictionary<string, CanMessageDefinition> Definitions { get; }

    ...
}
Run Code Online (Sandbox Code Playgroud)

测试

{
    "op": "add",
    "path": "/candb/foo",
    "value": {
        "messageId": 171,
        "template": [17, 34],
        "repeatRate": 100,
        "canPort": 0
    }
}
Run Code Online (Sandbox Code Playgroud)

结果

InvalidCastException在我尝试将指定的更改应用于的站点上抛出一个JsonPatchDocument.

现场:

var currentModelSnapshot = this.currentModelFilter(this.currentModel.Copy());
var snapshotWithChangesApplied = currentModelSnapshot.Copy();
diffDocument.ApplyTo(snapshotWithChangesApplied);
Run Code Online (Sandbox Code Playgroud)

例外:

Unable to cast object of type 'Newtonsoft.Json.Serialization.JsonDictionaryContract' to type 'Newtonsoft.Json.Serialization.JsonObjectContract'.
Run Code Online (Sandbox Code Playgroud)

B.依靠动态JSON补丁

一个更有希望的攻击计划似乎依赖于动态JSON补丁,其中涉及对实例执行补丁操作ExpandoObject.这允许您使用JSON补丁文档来添加,删除或替换属性,因为您正在处理动态类型的对象.

履行

internal sealed class CanDatabaseModel : DeviceComponentModel<CanDatabaseModel>
{
    public CanDatabaseModel()
    {
        this.Definitions = new ExpandoObject();
    }

    [JsonProperty(PropertyName = "candb")]
    public IDictionary<string, object> Definitions { get; }

    ...
}
Run Code Online (Sandbox Code Playgroud)

测试

{
    "op": "add",
    "path": "/candb/foo",
    "value": {
        "messageId": 171,
        "template": [17, 34],
        "repeatRate": 100,
        "canPort": 0
    }
}
Run Code Online (Sandbox Code Playgroud)

结果

进行此更改允许我的测试的这一部分在没有引发异常的情况下运行,但JSON Patch不知道要反序列化的内容value,导致数据存储在字典中JObject而不是CanMessageDefinition:

尝试的结果B.

是否有可能"告诉"JSON Patch如何通过任何机会反序列化信息?也许就像使用JsonConverter属性一样Definitions

[JsonProperty(PropertyName = "candb")]
[JsonConverter(...)]
public IDictionary<string, object> Definitions { get; }
Run Code Online (Sandbox Code Playgroud)

摘要

  • 我需要支持将值添加到字典的JSON补丁请求
  • 我试过走下纯静态路线,但路线失败了
  • 我尝试过使用动态JSON补丁
    • 这部分工作,但我的数据存储为JObject类型而不是预期的类型
    • 是否有一个属性(或其他一些技术)我可以应用于我的属性,让它反序列化为正确的类型(不是匿名类型)?

Tag*_*agc 3

由于似乎没有任何官方方法可以做到这一点,我想出了一个临时解决方案\xe2\x84\xa2(阅读:一个工作得足够好的解决方案,所以我可能会永远保留它)。

\n\n

为了使 JSON Patch 看起来像处理类似字典的操作,我创建了一个名为 的类,DynamicDeserialisationStore它继承自DynamicObject并利用 JSON Patch 对动态对象的支持。

\n\n

更具体地说,此类重写了TrySetMemberTrySetIndexTryGetMember等方法,本质上就像字典一样,只不过它将所有这些操作委托给提供给其构造函数的回调。

\n\n

执行

\n\n

下面的代码提供了 的实现DynamicDeserialisationStore。它实现了IDictionary<string, object>(这是处理动态对象所需的 JSON Patch 签名),但我只实现了我需要的最少方法。

\n\n

JSON Patch 支持动态对象的问题在于,它将为JObject实例设置属性,即它不会像设置静态属性时那样自动执行反序列化,因为它无法推断类型。DynamicDeserialisationStore参数化为对象类型,它将尝试自动尝试反序列化这些对象JObject参数化为对象类型,当设置这些实例时,

\n\n

该类接受回调来处理基本的字典操作,而不是维护内部字典本身,因为在我的“真实”系统模型代码中,我实际上并不使用字典(出于各种原因) - 我只是让它以这种方式显示给客户端。

\n\n
internal sealed class DynamicDeserialisationStore<T> : DynamicObject, IDictionary<string, object> where T : class\n{\n    private readonly Action<string, T> storeValue;\n    private readonly Func<string, bool> removeValue;\n    private readonly Func<string, T> retrieveValue;\n    private readonly Func<IEnumerable<string>> retrieveKeys;\n\n    public DynamicDeserialisationStore(\n        Action<string, T> storeValue,\n        Func<string, bool> removeValue,\n        Func<string, T> retrieveValue,\n        Func<IEnumerable<string>> retrieveKeys)\n    {\n        this.storeValue = storeValue;\n        this.removeValue = removeValue;\n        this.retrieveValue = retrieveValue;\n        this.retrieveKeys = retrieveKeys;\n    }\n\n    public int Count\n    {\n        get\n        {\n            return this.retrieveKeys().Count();\n        }\n    }\n\n    private IReadOnlyDictionary<string, T> AsDict\n    {\n        get\n        {\n            return (from key in this.retrieveKeys()\n                    let value = this.retrieveValue(key)\n                    select new { key, value })\n                    .ToDictionary(it => it.key, it => it.value);\n        }\n    }\n\n    public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)\n    {\n        if (indexes.Length == 1 && indexes[0] is string && value is JObject)\n        {\n            return this.TryUpdateValue(indexes[0] as string, value);\n        }\n\n        return base.TrySetIndex(binder, indexes, value);\n    }\n\n    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)\n    {\n        if (indexes.Length == 1 && indexes[0] is string)\n        {\n            try\n            {\n                result = this.retrieveValue(indexes[0] as string);\n                return true;\n            }\n            catch (KeyNotFoundException)\n            {\n                // Pass through.\n            }\n        }\n\n        return base.TryGetIndex(binder, indexes, out result);\n    }\n\n    public override bool TrySetMember(SetMemberBinder binder, object value)\n    {\n        return this.TryUpdateValue(binder.Name, value);\n    }\n\n    public override bool TryGetMember(GetMemberBinder binder, out object result)\n    {\n        try\n        {\n            result = this.retrieveValue(binder.Name);\n            return true;\n        }\n        catch (KeyNotFoundException)\n        {\n            return base.TryGetMember(binder, out result);\n        }\n    }\n\n    private bool TryUpdateValue(string name, object value)\n    {\n        JObject jObject = value as JObject;\n        T tObject = value as T;\n\n        if (jObject != null)\n        {\n            this.storeValue(name, jObject.ToObject<T>());\n            return true;\n        }\n        else if (tObject != null)\n        {\n            this.storeValue(name, tObject);\n            return true;\n        }\n\n        return false;\n    }\n\n    object IDictionary<string, object>.this[string key]\n    {\n        get\n        {\n            return this.retrieveValue(key);\n        }\n\n        set\n        {\n            this.TryUpdateValue(key, value);\n        }\n    }\n\n    public IEnumerator<KeyValuePair<string, object>> GetEnumerator()\n    {\n        return this.AsDict.ToDictionary(it => it.Key, it => it.Value as object).GetEnumerator();\n    }\n\n    public void Add(string key, object value)\n    {\n        this.TryUpdateValue(key, value);\n    }\n\n    public bool Remove(string key)\n    {\n        return this.removeValue(key);\n    }\n\n    #region Unused methods\n    bool ICollection<KeyValuePair<string, object>>.IsReadOnly\n    {\n        get\n        {\n            throw new NotImplementedException();\n        }\n    }\n\n    ICollection<string> IDictionary<string, object>.Keys\n    {\n        get\n        {\n            throw new NotImplementedException();\n        }\n    }\n\n    ICollection<object> IDictionary<string, object>.Values\n    {\n        get\n        {\n            throw new NotImplementedException();\n        }\n    }\n\n    void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)\n    {\n        throw new NotImplementedException();\n    }\n\n    void ICollection<KeyValuePair<string, object>>.Clear()\n    {\n        throw new NotImplementedException();\n    }\n\n    bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)\n    {\n        throw new NotImplementedException();\n    }\n\n    bool IDictionary<string, object>.ContainsKey(string key)\n    {\n        throw new NotImplementedException();\n    }\n\n    void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)\n    {\n        throw new NotImplementedException();\n    }\n\n    IEnumerator IEnumerable.GetEnumerator()\n    {\n        throw new NotImplementedException();\n    }\n\n    bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)\n    {\n        throw new NotImplementedException();\n    }\n\n    bool IDictionary<string, object>.TryGetValue(string key, out object value)\n    {\n        throw new NotImplementedException();\n    }\n    #endregion\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

测试

\n\n

下面提供了此类的测试。我创建了一个模拟系统模型(参见图片)并对其执行各种 JSON Patch 操作。

\n\n

\n\n

这是代码:

\n\n
public class DynamicDeserialisationStoreTests\n{\n    private readonly FooSystemModel fooSystem;\n\n    public DynamicDeserialisationStoreTests()\n    {\n        this.fooSystem = new FooSystemModel();\n    }\n\n    [Fact]\n    public void Store_Should_Handle_Adding_Keyed_Model()\n    {\n        // GIVEN the foo system currently contains no foos.\n        this.fooSystem.Foos.ShouldBeEmpty();\n\n        // GIVEN a patch document to store a foo called "test".\n        var request = "{\\"op\\":\\"add\\",\\"path\\":\\"/foos/test\\",\\"value\\":{\\"number\\":3,\\"bazzed\\":true}}";\n        var operation = JsonConvert.DeserializeObject<Operation<FooSystemModel>>(request);\n        var patchDocument = new JsonPatchDocument<FooSystemModel>(\n            new[] { operation }.ToList(),\n            new CamelCasePropertyNamesContractResolver());\n\n        // WHEN we apply this patch document to the foo system model.\n        patchDocument.ApplyTo(this.fooSystem);\n\n        // THEN the system model should now contain a new foo called "test" with the expected properties.\n        this.fooSystem.Foos.ShouldHaveSingleItem();\n        FooModel foo = this.fooSystem.Foos["test"] as FooModel;\n        foo.Number.ShouldBe(3);\n        foo.IsBazzed.ShouldBeTrue();\n    }\n\n    [Fact]\n    public void Store_Should_Handle_Removing_Keyed_Model()\n    {\n        // GIVEN the foo system currently contains a foo.\n        var testFoo = new FooModel { Number = 3, IsBazzed = true };\n        this.fooSystem.Foos["test"] = testFoo;\n\n        // GIVEN a patch document to remove a foo called "test".\n        var request = "{\\"op\\":\\"remove\\",\\"path\\":\\"/foos/test\\"}";\n        var operation = JsonConvert.DeserializeObject<Operation<FooSystemModel>>(request);\n        var patchDocument = new JsonPatchDocument<FooSystemModel>(\n            new[] { operation }.ToList(),\n            new CamelCasePropertyNamesContractResolver());\n\n        // WHEN we apply this patch document to the foo system model.\n        patchDocument.ApplyTo(this.fooSystem);\n\n        // THEN the system model should be empty.\n        this.fooSystem.Foos.ShouldBeEmpty();\n    }\n\n    [Fact]\n    public void Store_Should_Handle_Modifying_Keyed_Model()\n    {\n        // GIVEN the foo system currently contains a foo.\n        var originalFoo = new FooModel { Number = 3, IsBazzed = true };\n        this.fooSystem.Foos["test"] = originalFoo;\n\n        // GIVEN a patch document to modify a foo called "test".\n        var request = "{\\"op\\":\\"replace\\",\\"path\\":\\"/foos/test\\", \\"value\\":{\\"number\\":6,\\"bazzed\\":false}}";\n        var operation = JsonConvert.DeserializeObject<Operation<FooSystemModel>>(request);\n        var patchDocument = new JsonPatchDocument<FooSystemModel>(\n            new[] { operation }.ToList(),\n            new CamelCasePropertyNamesContractResolver());\n\n        // WHEN we apply this patch document to the foo system model.\n        patchDocument.ApplyTo(this.fooSystem);\n\n        // THEN the system model should contain a modified "test" foo.\n        this.fooSystem.Foos.ShouldHaveSingleItem();\n        FooModel foo = this.fooSystem.Foos["test"] as FooModel;\n        foo.Number.ShouldBe(6);\n        foo.IsBazzed.ShouldBeFalse();\n    }\n\n    #region Mock Models\n    private class FooModel\n    {\n        [JsonProperty(PropertyName = "number")]\n        public int Number { get; set; }\n\n        [JsonProperty(PropertyName = "bazzed")]\n        public bool IsBazzed { get; set; }\n    }\n\n    private class FooSystemModel\n    {\n        private readonly IDictionary<string, FooModel> foos;\n\n        public FooSystemModel()\n        {\n            this.foos = new Dictionary<string, FooModel>();\n            this.Foos = new DynamicDeserialisationStore<FooModel>(\n                storeValue: (name, foo) => this.foos[name] = foo,\n                removeValue: name => this.foos.Remove(name),\n                retrieveValue: name => this.foos[name],\n                retrieveKeys: () => this.foos.Keys);\n        }\n\n        [JsonProperty(PropertyName = "foos")]\n        public IDictionary<string, object> Foos { get; }\n    }\n    #endregion\n}\n
Run Code Online (Sandbox Code Playgroud)\n