使用自定义 iDefaultContractResolver 填充和合并来自 Json 的对象集合会导致数据移动/损坏

本文关键字:集合 对象 数据 损坏 移动 Json iDefaultContractResolver 自定义 填充 合并 | 更新日期: 2023-09-27 18:30:46

这是我正在序列化/反序列化的类数据结构:

public class SettingGroup
{
    public string name { get; set; }
    public string description { get; set; }
    public bool visible { get; set; }
    public ObservableCollection<SettingGroup> groups { get; set; }
    public ObservableCollection<Setting> settings { get; set; }
    public SettingGroup()
    {
        groups = new ObservableCollection<SettingGroup>();
        settings = new ObservableCollection<Setting>();
        visible = true;
    }
}
public class Setting
{
    public string name { get; set; }
    public string description { get; set; }
    public bool visible { get; set; }
    public DescriptionVisibility descriptionVisibility { get; set; }
    public Dictionary<string, dynamic> configuration { get; set; }
    public dynamic settingValue { get; set; }
    public SettingType settingType { get; set; }
    public SettingControl settingControl { get; set; }
    public Setting()
    {
        visible = true;
        configuration = new Dictionary<string, dynamic>();
    }
}

我使用以下方法来确保只有设置名称和值存储在 JSON 中,其余的在应用程序本身内结构化,不需要通过 JSON 保存/加载;

    private static string safeFileName(string fileName)
    {
        string regexSearch = new string(Path.GetInvalidFileNameChars()) + " ";
        Regex r = new Regex(string.Format("[{0}]", Regex.Escape(regexSearch)));
        fileName = r.Replace(fileName, "");
        return fileName;
    }
    public static void saveSettings(this SettingGroup settingGroup, string fileName = "")
    {
        var jsonResolver = new IgnorableSerializerContractResolver();
        jsonResolver.Ignore(typeof(SettingGroup), "visible");
        jsonResolver.Ignore(typeof(SettingGroup), "description");
        jsonResolver.Ignore(typeof(Setting), "visible");
        jsonResolver.Ignore(typeof(Setting), "descriptionVisibility");
        jsonResolver.Ignore(typeof(Setting), "configuration");
        jsonResolver.Ignore(typeof(Setting), "settingType");
        jsonResolver.Ignore(typeof(Setting), "settingControl");
        jsonResolver.Ignore(typeof(Setting), "description");

        var jsonSettings = new JsonSerializerSettings() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, ContractResolver = jsonResolver };
        if (string.IsNullOrWhiteSpace(fileName))
            fileName = safeFileName(settingGroup.name);
        try
        {
            if (!Directory.Exists(Global.DataDirectory)) Directory.CreateDirectory(Global.DataDirectory);
            File.WriteAllText(Path.Combine(Global.DataDirectory, fileName+".json"), JsonConvert.SerializeObject(settingGroup, Newtonsoft.Json.Formatting.Indented, jsonSettings));
        }
        catch { }
    }
    public static void loadSettings(this SettingGroup settingGroup, string fileName = "")
    {
        var jsonResolver = new IgnorableSerializerContractResolver();
        jsonResolver.Ignore(typeof(SettingGroup), "visible");
        jsonResolver.Ignore(typeof(SettingGroup), "description");
        jsonResolver.Ignore(typeof(Setting), "visible");
        jsonResolver.Ignore(typeof(Setting), "descriptionVisibility");
        jsonResolver.Ignore(typeof(Setting), "configuration");
        jsonResolver.Ignore(typeof(Setting), "settingType");
        jsonResolver.Ignore(typeof(Setting), "settingControl");
        jsonResolver.Ignore(typeof(Setting), "description");
        if (string.IsNullOrWhiteSpace(fileName))
            fileName = safeFileName(settingGroup.name);
        try
        {
            if (!Directory.Exists(Global.DataDirectory)) Directory.CreateDirectory(Global.DataDirectory);
            var serializerSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Auto, ContractResolver = jsonResolver };
            JsonConvert.PopulateObject(File.ReadAllText(Path.Combine(Global.DataDirectory, fileName + ".json")), settingGroup, serializerSettings);
        }
        catch { }
    }

自定义 iDefaultContractResolver 是我在 SE 周围看到的,但为了完整性,无论如何都会包含在这里:

public class IgnorableSerializerContractResolver : DefaultContractResolver
{
    protected readonly Dictionary<Type, HashSet<string>> Ignores;
    public IgnorableSerializerContractResolver()
    {
        this.Ignores = new Dictionary<Type, HashSet<string>>();
    }
    public void Ignore(Type type, params string[] propertyName)
    {
        if (!this.Ignores.ContainsKey(type)) this.Ignores[type] = new HashSet<string>();
        foreach (var prop in propertyName)
        {
            this.Ignores[type].Add(prop);
        }
    }
    public bool IsIgnored(Type type, string propertyName)
    {
        if (!this.Ignores.ContainsKey(type)) return false;
        // if no properties provided, ignore the type entirely
        if (this.Ignores[type].Count == 0) return true;
        return this.Ignores[type].Contains(propertyName);
    }
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);
        if (this.IsIgnored(property.DeclaringType, property.PropertyName)
        // need to check basetype as well for EF -- @per comment by user576838
        || this.IsIgnored(property.DeclaringType.BaseType, property.PropertyName))
        {
            property.ShouldSerialize = instance => { return false; };
        }
        return property;
    }
}

正如我所期望的那样,它保存得很好,只有我想要保存的属性,但是加载会导致数据"移动",所有设置值都将加载到错误的设置中,等等,并在尝试访问它们时导致类型不匹配。我已经尝试在加载部分使用和不使用自定义 Json 设置,行为没有差异。另外值得注意的是,如果没有 iDefaultContractResolver,整个结构可以很好地保存/加载,但 id 不会用不需要的数据弄乱我的 JSON 文件。

下面是正在构造和填充示例数据的设置类:

        SettingGroup Settings = new SettingGroup();
        Settings.name = "Application Settings";
        Settings.description = "Common application settings.";
        SettingGroup generalSettings = new SettingGroup();
        generalSettings.name = "General settings";
        Settings.groups.Add(generalSettings);
        SettingGroup themeSettings = new SettingGroup();
        themeSettings.name = "Theme settings";
        Settings.groups.Add(themeSettings);
        SettingGroup updateSettings = new SettingGroup();
        updateSettings.name = "Update settings";
        Settings.groups.Add(updateSettings);
        SettingGroup startupSettings = new SettingGroup();
        startupSettings.name = "Startup settings";
        generalSettings.groups.Add(startupSettings);
        Setting startWithWindows = new Setting();
        startWithWindows.name = "Start IM with Windows";
        startWithWindows.settingValue = true;
        startWithWindows.settingControl = SettingControl.Checkbox;
        startupSettings.settings.Add(startWithWindows);
        Setting startMinimized = new Setting();
        startMinimized.name = "Start IM minimized";
        startMinimized.settingValue = true;
        startMinimized.settingControl = SettingControl.Checkbox;
        startupSettings.settings.Add(startMinimized);
        SettingGroup performanceSettings = new SettingGroup();
        performanceSettings.name = "Performance settings";
        generalSettings.groups.Add(performanceSettings);
        Setting threadPriority = new Setting();
        threadPriority.name = "Thread priority";
        threadPriority.description = "This setting may not have a noticeible impact on all platforms, especially higer end ones.";
        threadPriority.settingValue = 3;
        threadPriority.settingControl = SettingControl.Slider;
        threadPriority.configuration.Add("lowVal",0);
        threadPriority.configuration.Add("highVal", 7);
        threadPriority.configuration.Add("interval", 1);
        performanceSettings.settings.Add(threadPriority);

经过编辑以包含 Json 源和结果:

具有设置信息和默认值的完整 JSON 结构:

{
  "name": "Application Settings",
  "description": "Common application settings.",
  "visible": true,
  "groups": [
    {
      "name": "General settings",
      "description": null,
      "visible": true,
      "groups": [
        {
          "name": "Startup settings",
          "description": null,
          "visible": true,
          "groups": [],
          "settings": [
            {
              "name": "Start IM with Windows",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": true,
              "settingType": 0,
              "settingControl": 0
            },
            {
              "name": "Start IM minimized",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": true,
              "settingType": 0,
              "settingControl": 0
            }
          ]
        },
        {
          "name": "Performance settings",
          "description": null,
          "visible": true,
          "groups": [],
          "settings": [
            {
              "name": "Thread priority",
              "description": "This setting may not have a noticeible impact on all platforms, especially higer end ones.",
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {
                "lowVal": 0,
                "highVal": 7,
                "interval": 1
              },
              "settingValue": 3,
              "settingType": 0,
              "settingControl": 2
            }
          ]
        }
      ],
      "settings": []
    },
    {
      "name": "Theme settings",
      "description": null,
      "visible": true,
      "groups": [],
      "settings": []
    },
    {
      "name": "Update settings",
      "description": null,
      "visible": true,
      "groups": [],
      "settings": []
    },
    {
      "name": "General settings",
      "description": null,
      "visible": true,
      "groups": [
        {
          "name": "Startup settings",
          "description": null,
          "visible": true,
          "groups": [],
          "settings": [
            {
              "name": "Start IM with Windows",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": true,
              "settingType": 0,
              "settingControl": 0
            },
            {
              "name": "Start IM minimized",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": true,
              "settingType": 0,
              "settingControl": 0
            }
          ]
        },
        {
          "name": "Performance settings",
          "description": null,
          "visible": true,
          "groups": [],
          "settings": [
            {
              "name": "Thread priority",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": 3,
              "settingType": 0,
              "settingControl": 0
            }
          ]
        }
      ],
      "settings": []
    },
    {
      "name": "Theme settings",
      "description": null,
      "visible": true,
      "groups": [],
      "settings": []
    },
    {
      "name": "Update settings",
      "description": null,
      "visible": true,
      "groups": [],
      "settings": []
    }
  ],
  "settings": []
}

由我的 saveSettings 扩展保存的数据,该扩展旨在仅保存所需的信息,例如组名子项以及设置名称和值(这看起来正确)

{
  "name": "Application Settings",
  "groups": [
    {
      "name": "General settings",
      "groups": [
        {
          "name": "Startup settings",
          "groups": [],
          "settings": [
            {
              "name": "Start IM with Windows",
              "settingValue": true
            },
            {
              "name": "Start IM minimized",
              "settingValue": true
            }
          ]
        },
        {
          "name": "Performance settings",
          "groups": [],
          "settings": [
            {
              "name": "Thread priority",
              "settingValue": 3
            }
          ]
        }
      ],
      "settings": []
    },
    {
      "name": "Theme settings",
      "groups": [],
      "settings": []
    },
    {
      "name": "Update settings",
      "groups": [],
      "settings": []
    }
  ],
  "settings": []
}

但是,当加载并与现有类对象合并时,这就是结果数据的样子,请注意控件类型中的重复和无效值,这会导致类型不匹配 IE 无法将long值加载到复选框使用的bool中。

{
  "name": "Application Settings",
  "description": "Common application settings.",
  "visible": true,
  "groups": [
    {
      "name": "General settings",
      "description": null,
      "visible": true,
      "groups": [
        {
          "name": "Startup settings",
          "description": null,
          "visible": true,
          "groups": [],
          "settings": [
            {
              "name": "Start IM with Windows",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": true,
              "settingType": 0,
              "settingControl": 0
            },
            {
              "name": "Start IM minimized",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": true,
              "settingType": 0,
              "settingControl": 0
            }
          ]
        },
        {
          "name": "Performance settings",
          "description": null,
          "visible": true,
          "groups": [],
          "settings": [
            {
              "name": "Thread priority",
              "description": "This setting may not have a noticeible impact on all platforms, especially higer end ones.",
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {
                "lowVal": 0,
                "highVal": 7,
                "interval": 1
              },
              "settingValue": 3,
              "settingType": 0,
              "settingControl": 2
            }
          ]
        }
      ],
      "settings": []
    },
    {
      "name": "Theme settings",
      "description": null,
      "visible": true,
      "groups": [],
      "settings": []
    },
    {
      "name": "Update settings",
      "description": null,
      "visible": true,
      "groups": [],
      "settings": []
    },
    {
      "name": "General settings",
      "description": null,
      "visible": true,
      "groups": [
        {
          "name": "Startup settings",
          "description": null,
          "visible": true,
          "groups": [],
          "settings": [
            {
              "name": "Start IM with Windows",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": true,
              "settingType": 0,
              "settingControl": 0
            },
            {
              "name": "Start IM minimized",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": true,
              "settingType": 0,
              "settingControl": 0
            }
          ]
        },
        {
          "name": "Performance settings",
          "description": null,
          "visible": true,
          "groups": [],
          "settings": [
            {
              "name": "Thread priority",
              "description": null,
              "visible": true,
              "descriptionVisibility": 0,
              "configuration": {},
              "settingValue": 3,
              "settingType": 0,
              "settingControl": 0
            }
          ]
        }
      ],
      "settings": []
    },
    {
      "name": "Theme settings",
      "description": null,
      "visible": true,
      "groups": [],
      "settings": []
    },
    {
      "name": "Update settings",
      "description": null,
      "visible": true,
      "groups": [],
      "settings": []
    }
  ],
  "settings": []
}

使用自定义 iDefaultContractResolver 填充和合并来自 Json 的对象集合会导致数据移动/损坏

您应该能够使用KeyedIListMergeConverter(不是专门用于合并List<T>集合的原始KeyedListMergeConverter)来自 Json.Net 填充对象 - 基于 ID 更新列表元素,并将[JsonMergeKey]附加到名称:

public class SettingGroup
{
    [JsonMergeKey]
    public string name { get; set; }
    // Remainder unchanged
}
public class Setting
{
    [JsonMergeKey]
    public string name { get; set; }
    // Remainder unchanged
}

然后像这样使用它:

        if (!Directory.Exists(Global.DataDirectory))
            Directory.CreateDirectory(Global.DataDirectory);
        var serializerSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Auto, ContractResolver = jsonResolver };
        serializerSettings.Converters.Add(new KeyedIListMergeConverter(settings.ContractResolver));
        JsonConvert.PopulateObject(File.ReadAllText(Path.Combine(Global.DataDirectory, fileName + ".json")), settingGroup, serializerSettings);