Использование JSON Patch для добавления значений в словарь

Обзор

Я пытаюсь написать веб-службу с использованием ASP.NET Core, которая позволяет клиентам запрашивать и изменять состояние микроконтроллера. Этот микроконтроллер содержит ряд систем, которые я моделирую в своем приложении - например, систему ШИМ, систему ввода исполнительного механизма и т. Д.

Все компоненты этих систем имеют определенные свойства, которые можно запросить или изменить с помощью патча JSON. запрос. Например, 4-й PWM на микроконтроллере можно включить с помощью HTTP-запроса, несущего {"op":"replace", "path":"/pwms/3/enabled", "value":true}. Для поддержки этого я использую библиотеку AspNetCore.JsonPatch.

Моя проблема в том, что я пытаюсь реализовать поддержку JSON Patch для новой системы «базы данных CAN», которая логически должна сопоставлять имя определения с определением конкретного сообщения CAN, и я не уверен, как это сделать. иди об этом.

Подробности

На диаграмме ниже моделируется система базы данных CAN. Экземпляр CanDatabase должен логически содержать словарь формы IDictionary<string, CanMessageDefinition>.

Модель системы базы данных CAN

Для поддержки создания новых определений сообщений мое приложение должно позволять пользователям отправлять запросы на исправление JSON следующим образом:

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

Здесь my_new_definition будет определять определение имя, а объект, связанный с value, должен быть десериализован в CanMessageDefinition объект. Затем его следует сохранить как новую пару "ключ-значение" в словаре CanDatabase.

Проблема в том, что path должен указывать путь к свойству, который для статически типизированных объектов будет ... ну, статическим (исключением является то, что он позволяет ссылаться на элементы массива например, /pwms/3, как указано выше).

Что я пробовал

А. Подход Лироя Дженкинса

Забудьте о том, что я знаю, что это не сработает - я попробовал реализацию ниже (которая использует только статическую типизацию, несмотря на то, что мне нужно поддерживать динамические пути JSON Patch), просто чтобы посмотреть, что произойдет.

Реализация

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

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

    ...
}

Тест

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

Результат

InvalidCastException выдается на сайт, где я пытаюсь применить указанные изменения к JsonPatchDocument.

Сайт:

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'.

Б. Использование динамического исправления 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; }

    ...
}

Тест

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

Результат

Внесение этого изменения позволяет этой части моего теста работать без возникновения исключений, но JSON Patch не знает, что десериализовать value как, в результате чего данные сохраняются в словаре как JObject, а не как CanMessageDefinition:

«Результат

Можно ли «сказать» JSON Patch, как десериализовать информацию случайно? Возможно, что-то вроде использования атрибута JsonConverter на Definitions?

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

Резюме

  • Мне нужно поддерживать запросы патчей JSON, которые добавляют значения в словарь
  • Я пробовал идти по чисто статическому маршруту, но это не удалось.
  • I've tried using dynamic JSON patching
    • This partly worked, but my data was stored as a JObject type instead of the intended type
    • Есть ли атрибут (или какой-либо другой метод), который я могу применить к своему свойству, чтобы позволить ему десериализоваться до правильного типа (не анонимного типа)?

person Tagc    schedule 16.01.2017    source источник
comment
Реализация настраиваемого десериализатора JSON выглядит жизнеспособным решением. Не могли бы вы подробнее рассказать о template в value объекте? Можем ли мы переместить messageId и template в родительский объект?   -  person Ankit    schedule 16.01.2017
comment
@Ankit template представляет собой полезную нагрузку сообщения CAN (0-8 байтов), поэтому это будет массив целых чисел. messageId и template должны оставаться в прежнем виде, поскольку запросы должны соответствовать схеме JSON Patch, как описано в RFC 6902   -  person Tagc    schedule 16.01.2017
comment
Вы придумали подход? Это интересный сценарий, и я добавил его в закладки, чтобы поработать над ним, когда у меня появится свободное время.   -  person Ankit    schedule 17.01.2017
comment
@Ankit Еще нет. Я использую временный обходной путь (регистрирую обработчик событий PropertyChanged в ExpandoObject, чтобы вручную преобразовать новый JObject в CanMessageDefinition).   -  person Tagc    schedule 17.01.2017
comment
Лееееооооооой! :)   -  person ToddBFisher    schedule 13.03.2018
comment
Я не думаю, что вы можете десериализовать IDictionary<T, K>. Вы пробовали просто использовать Dictionary<T, K>? Я использую это и не имею никаких проблем.   -  person Sinaesthetic    schedule 01.07.2019


Ответы (2)


Поскольку официального способа сделать это, похоже, не существует, я придумал Temporary Solution ™ (читай: решение, которое работает достаточно хорошо, поэтому я, вероятно, сохраню его навсегда).

Чтобы казалось, что JSON Patch обрабатывает операции, подобные словарю, я создал класс с именем DynamicDeserialisationStore, который наследуется от _ 2_ и использует поддержку JSON Patch для динамических объектов.

Более конкретно, этот класс переопределяет такие методы, как TrySetMember, TrySetIndex, TryGetMember и т. Д., Чтобы по сути действовать как словарь, за исключением того, что он делегирует все эти операции обратным вызовам, предоставленным его конструктору.

Реализация

В приведенном ниже коде реализована реализация DynamicDeserialisationStore. Он реализует IDictionary<string, object> (который является сигнатурой, которая требуется JSON Patch для работы с динамическими объектами), но я реализую только минимум необходимых мне методов.

Проблема с поддержкой динамических объектов JSON Patch заключается в том, что он устанавливает свойства в JObject экземпляров, то есть он не будет автоматически выполнять десериализацию, как это было бы при установке статических свойств, поскольку он не может определить тип. 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
}

Тесты

Ниже представлены тесты для этого класса. Я создаю макет модели системы (см. Изображение) и выполняю над ней различные операции JSON Patch.

Вот код:

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
}
person Tagc    schedule 23.01.2017

Вы можете, например, десериализовать полученный Json в объект:

var dataDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);

И перебрать его, преобразовав значения KeyValuePairs, которые вы хотите исправить, в целевой тип CanMessageDefinition:

Dictionary<string, CanMessageDefinition> updateData = new Dictionary<string, CanMessageDefinition>();
foreach (var record in dataDict)
{
    CanMessageDefinition recordValue = (CanMessageDefinition)record.Value;
    if (yourExistingRecord.KeyAttributes.Keys.Contains(record.Key) && (!yourExistingRecord.KeyAttributes.Values.Equals(record.Value)))
    { 
        updateData.Add(record.Key, recordValue);
    }
    
}

И просто сохраните свой объект в своей базе данных.

Альтернативой было бы сделать это внутри JsonConverter, как вы упомянули. Ваше здоровье

person GUZZ    schedule 19.02.2021