首页 文章

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

提问于
浏览
10

概述

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

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

My problem 是我'm trying to implement JSON Patch support for a new 1046257 system that logically should map a definition name to a particular CAN message definition, and I'我不知道该如何解决这个问题 .

详情

下图为CAN数据库系统建模 . CanDatabase 实例应该在逻辑上包含 IDictionary<string, CanMessageDefinition> 形式的字典 .

CAN Database system model

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

{
    "op": "add",
    "path": "/candb/my_new_definition",
    "value": {
        "template": ["...", "..."],
        "repeatRate": "...",
        "...": "...",
    }
}

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

问题是 path 应指定一个 property path ,对于静态类型的对象,它将是......好的,静态的(例外情况是它允许引用数组元素,例如 /pwms/3 ,如上所述) .

我尝试过的

A. The Leeroy Jenkins approach

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

Implementation

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

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

    ...
}

Test

{
    "op": "add",
    "path": "/candb/foo",
    "value": {
        "messageId": 171,
        "template": [17, 34],
        "repeatRate": 100,
        "canPort": 0
    }
}

Outcome

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

现场:

var currentModelSnapshot = this.currentModelFilter(this.currentModel.Copy());
var snapshotWithChangesApplied = currentModelSnapshot.Copy();
diffDocument.ApplyTo(snapshotWithChangesApplied);

例外:

Unable to cast object of type 'Newtonsoft.Json.Serialization.JsonDictionaryContract' to type 'Newtonsoft.Json.Serialization.JsonObjectContract'.

B. Relying on dynamic JSON Patching

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

Implementation

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

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

    ...
}

Test

{
    "op": "add",
    "path": "/candb/foo",
    "value": {
        "messageId": 171,
        "template": [17, 34],
        "repeatRate": 100,
        "canPort": 0
    }
}

Outcome

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

Outcome of attempt B

是否有可能_1146294_ JSON补丁如何通过任何机会反序列化信息?也许是在 Definitions 上使用 JsonConverter 属性的行为?

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

摘要

  • 我需要支持向字典添加值的JSON补丁请求

  • 我试过走纯粹的静态路线,但失败了

  • 我尝试过使用动态JSON补丁

  • 这部分有效,但我的数据存储为 JObject 类型而不是预期的类型

  • 是否有一个属性(或其他技术)我可以应用于我的属性,让它反序列化为正确的类型(不是匿名类型)?

1 回答

  • 1

    由于似乎没有任何官方方法可以做到这一点,我想出了一个临时解决方案™(阅读:一个运行良好的解决方案,所以我可能永远保留它) .

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

    更具体地说,这个类重写像 TrySetMemberTrySetIndexTryGetMember 等方法基本上像字典一样,除了它将所有这些操作委托给提供给它的构造函数的回调 .

    Implementation

    下面的代码提供了 DynamicDeserialisationStore 的实现 . 它实现 IDictionary<string, object> (这是签名JSON补丁需要使用动态对象),但我只实现了我需要的最少的方法 .

    JSON Patch对动态对象的支持问题在于它将属性设置为 JObject 实例,即它赢得了't automatically perform deserialisation like it would when setting static properties, as it can' t推断类型 . DynamicDeserialisationStore 参数化对象的类型,它将尝试自动尝试将这些 JObject 实例反序列化为它们的设置 .

    该类接受回调来处理基本字典操作,而不是维护内部字典本身,因为在我的“真实”系统模型代码中,我实际上并没有使用字典(出于各种原因) - 我只是让它看起来像是通向客户端 .

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

    Tests

    下面提供了这个类的测试 . 我创建了一个模拟系统模型(见图)并在其上执行各种JSON补丁操作 .

    这是代码:

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

相关问题