Ссылка на объект в JSON

103
26 февраля 2022, 07:10

Есть некий проект, в котором пользователь выбирает модификации, по началу вроде все просто было, а именно создал класс, который имел Id, Name и другую нужную информацию.

public class Modification
{
    public Modification(string name)
    {
        Id = Guid.NewGuid();
        Name = name;
    }
    public Guid Id { get; set; }
    public string Name { get; set; }
}

Проект развивался и стало необходимо следить, какая модификация необходима для другой (зависимости) и какая модификация конфликтует с другой (конфликты). Переделал я класс, добавил в него свойство Dependency с типом List<Modification>, в котором хотел хранить ссылки на все зависимости. Сериализую в JSON и получаю следующее:

[
  {
    "Id": "b8892913-a3be-4ec5-ac09-ee24fd6307b2",
    "Name": "Mod1",
    "Dependency": []
  },
  {
    "Id": "05be1efd-1f66-49fa-8b7c-82704121ce42",
    "Name": "Mod2",
    "Dependency": [
      {
        "Id": "b8892913-a3be-4ec5-ac09-ee24fd6307b2",
        "Name": "Mod1",
        "Dependency": []
      }
    ]
  }
]

Вроде 2 объекта, у второго есть зависимость от первого, все бы хорошо. Десериализую обратно, меняю имя у первой модификации, но оно не меняется у той модификации, что указана в зависимостях. Тут я понимаю, что это совершенно два разных объекта, да и вид зависимостей мне явно не нравиться.

Сокращаю сам JSON, оставляю в зависимостях только ID:

[
  {
    "Id": "b8892913-a3be-4ec5-ac09-ee24fd6307b2",
    "Name": "Mod1",
    "Dependency": []
  },
  {
    "Id": "05be1efd-1f66-49fa-8b7c-82704121ce42",
    "Name": "Mod2",
    "Dependency": [ "b8892913-a3be-4ec5-ac09-ee24fd6307b2" ]
  }
]

В коде я пытаюсь сделать конвертер, но застреваю в самом начале, ибо я без понятия как мне найти через конвертер нужный объект, ибо на сколько мне известно, там все грузится поочередно и нужный объект попросту может быть еще не загружен, а значит мне его не от куда взять.

Единственный вариант, который я здесь вижу, это сделать public List<object> Dependency { get; set; }, затем загрузить данные, где в JSON будут только строковые id у зависимостей, а потом циклом искать нужные модификации, что то вроде этого:

var jsonData = JsonConvert.DeserializeObject<List<Modification>>(File.ReadAllText("Data.json"));
foreach (var item in jsonData)
{
    for (int i = 0; i < item.Dependency.Count; i++)
    {
        var id = Guid.Parse($"{item.Dependency[i]}");
        item.Dependency[i] = jsonData.FirstOrDefault(x => x.Id == id);
    }
}
var first = jsonData.FirstOrDefault();
var last = jsonData.LastOrDefault();
Console.WriteLine(first.Name); //Mod1
Console.WriteLine(((Modification)last.Dependency[0]).Name); //Mod1
first.Name = "someName";
Console.WriteLine(first.Name); //someName
Console.WriteLine(((Modification)last.Dependency[0]).Name); //someName

Тогда да, получаем два объекта и у нужных ссылки на зависимости, но как по мне это костыли, да и все время тип гонять, ну такое....

В общем, прошу вашей помощи, как сделать задуманное, или я вовсе копаю не в ту сторону и такое делается по другому?

Answer 1

Перечитал ваш вопрос. Не очень мне ясна ваша проблема.

Допустим, у нас есть json

string json = @"[
  {
    ""Id"": ""b8892913-a3be-4ec5-ac09-ee24fd6307b2"",
    ""Name"": ""Mod1"",
    ""Dependency"": []
  },
  {
    ""Id"": ""05be1efd-1f66-49fa-8b7c-82704121ce42"",
    ""Name"": ""Mod2"",
    ""Dependency"": [ ""b8892913-a3be-4ec5-ac09-ee24fd6307b2"" ]
  }
]";

И есть класс

public class Modification
{
    public string Id {get;set;}
    public string Name{get;set;}
    public HashSet<string> Dependency{get;set;} = new HashSet<string>();    
}

Мы можем это прочитать и сборать из этого, например, словарь

var data = JsonConvert.DeserializeObject<Modification[]>(json); 
var registry = data.ToDictionary(x=>x.Id);

Имея словарь, можно сделать многое. Например, проверить его на циклические зависимости

bool HasCycleDependency(Dictionary<string, Modification> registry){     
    foreach(var id in registry.Keys)        
        if (HasCycleDependency(registry, id, new HashSet<string>())) return true;       
    return false;   
}
bool HasCycleDependency(Dictionary<string, Modification> registry, string current, HashSet<string> onTrack)
{
    if (onTrack.Contains(current)) return true;
    onTrack.Add(current);
    foreach(var dep in registry[current].Dependency)
    if (HasCycleDependency(registry, dep, onTrack)) return true;
    onTrack.Remove(current);
    return false;
}

Проверка

Console.WriteLine($"Has cycles: {HasCycleDependency(registry)}");

Вывод

Has cycles: False

Ну, или напечатать полные зависимости наших модификаций

void PrintInfo(Dictionary<string, Modification> registry){
    foreach(var mod in registry.Values.OrderBy(m=>m.Name))
    {
        Console.WriteLine($"--------------------");
        PrintInfo(registry, mod);
    }
}
void PrintInfo(Dictionary<string, Modification> registry, Modification mod, string shift = "")
{   
    Console.WriteLine($"{shift}Mod id: {mod.Id}");
    Console.WriteLine($"{shift}Mod name: {mod.Name}");
    if (mod.Dependency.Count > 0) Console.WriteLine($"{shift}Dependencies:");
    foreach(var d in mod.Dependency){
        PrintInfo(registry, registry[d], shift + "\t");
    }
}

Проверка

PrintInfo(registry);

Вывод

--------------------
Mod id: b8892913-a3be-4ec5-ac09-ee24fd6307b2
Mod name: Mod1
--------------------
Mod id: 05be1efd-1f66-49fa-8b7c-82704121ce42
Mod name: Mod2
Dependencies:
  Mod id: b8892913-a3be-4ec5-ac09-ee24fd6307b2
  Mod name: Mod1
Answer 2

Для соединения "ссылками" данные в JSON нам нужно будет сделать следующее:

  1. Сделать интерфейс, по которому мы будем сверять Id объекта и находить необходимые объекты:

    interface IJsonLinked
    {
        string Id { get; }
    }
    
  2. Некий класс, который мы зададим как StreamingContext сериализатору/десериализатору.
    В данном классе определим метод, который вернет объект по его Id, а также данный класс будет содержать в себе словарь всех ссылок:

    class JsonLinkedContext
    {
        private readonly IDictionary<Type, IDictionary<string, object>> links = new Dictionary<Type, IDictionary<string, object>>();
        public static object GetLinkedValue(JsonSerializer serializer, Type type, string reference)
        {
            var context = (JsonLinkedContext)serializer.Context.Context;
            if (!context.links.TryGetValue(type, out IDictionary<string, object> links))
                context.links[type] = links = new Dictionary<string, object>();
            if (!links.TryGetValue(reference, out object value))
                links[reference] = value = FormatterServices.GetUninitializedObject(type);
            return value;
        }
    }
    
  3. Теперь нам нужен конвертер для свойств, которые нужно "сопоставить" с их ссылками по Id:
    Тут все довольно просто, если объект реализует IJsonLinked, то при сериализации мы берем его Id, а при десериализации (при помощи ранее написанного метода) получаем основную ссылку на объект.

    class JsonRefConverter : JsonConverter
    {
        public override bool CanConvert(Type type) => type.IsAssignableFrom(typeof(IJsonLinked));
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue(((IJsonLinked)value).Id);
        public override object ReadJson(JsonReader reader, Type type, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType != JsonToken.String) throw new Exception("Ref value must be a string.");
            return JsonLinkedContext.GetLinkedValue(serializer, type, reader.Value.ToString());
        }
    }
    
  4. Ну и последнее, нам нужен конвертер, который сопоставит ссылку с нужным объектом:

    class JsonLinkConverter : JsonConverter
    {
        public override bool CanConvert(Type type) => type.IsAssignableFrom(typeof(IJsonLinked));
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => serializer.Serialize(writer, value);
        public override object ReadJson(JsonReader reader, Type type, object existingValue, JsonSerializer serializer)
        {
            var jo = JObject.Load(reader);
            var value = JsonLinkedContext.GetLinkedValue(serializer, type, (string)jo.PropertyValues().First());
            serializer.Populate(jo.CreateReader(), value);
            return value;
        }
    }
    

Осталось теперь переделать немного класс, который будет работать с JSON, задать некие данные для теста и сериализировать/десериализировать данные:

  • Классы для сериализации/десериализации:

    class Root
    {
        [JsonProperty(ItemConverterType = typeof(JsonLinkConverter))]
        public List<Modification> Modifications { get; set; }
    }
    public class Modification : IJsonLinked
    {
        public Modification(string name)
        {
            Id = Guid.NewGuid().ToString("N");
            Name = name;
        }
        public string Id { get; set; }
        public string Name { get; set; }
        [JsonProperty(ItemConverterType = typeof(JsonRefConverter))]
        public List<Modification> Dependences { get; set; }
        string IJsonLinked.Id => Id;
    }
    
  • Тестовые данные:

    var mod1 = new Modification("mod1");
    var mod2 = new Modification("mod2")
    {
        Dependences = new List<Modification> { mod1 }
    };
    var mod3 = new Modification("mod3")
    {
        Dependences = new List<Modification> { mod1, mod2 }
    };
    var root = new Root() { Modifications = new List<Modification> { mod1, mod2, mod3 } };
    
  • Сериализация:
    Тут ставим игнорирование NULL значений.

    var json = JsonConvert.SerializeObject(root, Formatting.Indented, new JsonSerializerSettings
    {
        NullValueHandling = NullValueHandling.Ignore
    });
    
  • Десериализация: Задаем StreamingContext.

    var rootModifications = JsonConvert.DeserializeObject<Root>(json, new JsonSerializerSettings
    {
        Context = new StreamingContext(StreamingContextStates.All, new JsonLinkedContext()),
    });
    

Результат:

{
  "Modifications": [
    {
      "Id": "66fe811290df4105839a850153116250",
      "Name": "mod1"
    },
    {
      "Id": "1ba03f9480c94a86ba8f2d40e66e95cb",
      "Name": "mod2",
      "Dependences": [
        "66fe811290df4105839a850153116250"
      ]
    },
    {
      "Id": "b936eec3eadb4e38b7f19bd200c0e673",
      "Name": "mod3",
      "Dependences": [
        "66fe811290df4105839a850153116250",
        "1ba03f9480c94a86ba8f2d40e66e95cb"
      ]
    }
  ]
}

Для того, чтобы убедиться, что это действительно ссылки сделал простенький вывод имени мода из зависимостей и оригинала, после изменил имя и опять вывел имена:

Console.WriteLine(rootModifications.Modifications[0].Name);
Console.WriteLine(rootModifications.Modifications[1].Dependences[0].Name);
rootModifications.Modifications[0].Name = "test";
Console.WriteLine(rootModifications.Modifications[0].Name);
Console.WriteLine(rootModifications.Modifications[1].Dependences[0].Name);

Вывод:

mod1
mod1
test
test

За основу взял ответ @Athari.

READ ALSO
Зачем придумали интерфейсы?

Зачем придумали интерфейсы?

В интернете много статей на тему интерфейсов, что это такое и как их реализовыватьНо я не нашел внятного ответа кто и зачем их придумал? Я только...

91
Сравнение 2-х текстовых файлов

Сравнение 2-х текстовых файлов

Сравниваю 1 и 2 текстовый файл, если в 1 нету тех строк, которые во 2 текстовом файле, то он создает третий текстовый файл и записывает в негоКак...

134
Размер картинок для смарфонов

Размер картинок для смарфонов

У меня на сайте есть галерея картинокКартинки в довольно хорошем качестве, где-то 2000х1500

173