我正在尝试使用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>.

为了支持创建新的消息定义,我的应用程序应该允许用户发送如下的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:
是否有可能"告诉"JSON Patch如何通过任何机会反序列化信息?也许就像使用JsonConverter属性一样Definitions?
[JsonProperty(PropertyName = "candb")]
[JsonConverter(...)]
public IDictionary<string, object> Definitions { get; }
Run Code Online (Sandbox Code Playgroud)
JObject类型而不是预期的类型由于似乎没有任何官方方法可以做到这一点,我想出了一个临时解决方案\xe2\x84\xa2(阅读:一个工作得足够好的解决方案,所以我可能会永远保留它)。
\n\n为了使 JSON Patch 看起来像处理类似字典的操作,我创建了一个名为 的类,DynamicDeserialisationStore它继承自DynamicObject并利用 JSON Patch 对动态对象的支持。
更具体地说,此类重写了TrySetMember、TrySetIndex、TryGetMember等方法,本质上就像字典一样,只不过它将所有这些操作委托给提供给其构造函数的回调。
执行
\n\n下面的代码提供了 的实现DynamicDeserialisationStore。它实现了IDictionary<string, object>(这是处理动态对象所需的 JSON Patch 签名),但我只实现了我需要的最少方法。
JSON Patch 支持动态对象的问题在于,它将为JObject实例设置属性,即它不会像设置静态属性时那样自动执行反序列化,因为它无法推断类型。DynamicDeserialisationStore参数化为对象类型,它将尝试自动尝试反序列化这些对象JObject参数化为对象类型,当设置这些实例时,
该类接受回调来处理基本的字典操作,而不是维护内部字典本身,因为在我的“真实”系统模型代码中,我实际上并不使用字典(出于各种原因) - 我只是让它以这种方式显示给客户端。
\n\ninternal 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}\nRun Code Online (Sandbox Code Playgroud)\n\n测试
\n\n下面提供了此类的测试。我创建了一个模拟系统模型(参见图片)并对其执行各种 JSON Patch 操作。
\n\n
这是代码:
\n\npublic 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}\nRun Code Online (Sandbox Code Playgroud)\n
| 归档时间: |
|
| 查看次数: |
3539 次 |
| 最近记录: |