使用 System.Text.Json 将 IConfiguration 序列化回 Json

Using System.Text.Json to Serialize an IConfiguration back to Json

我将一些 IConfiguration 作为 json 存储在我的 sqlserver 数据库中,这样我就可以将它们绑定到一些已经构建的 classes 以提供动态设置。

有时我可能会在运行时更改新的绑定属性,然后更新数据库。问题是,当我需要时,class 可能有更多不应绑定且不应序列化的属性。因此,我将 IConfiguration 保留为 class 的 属性。我使用这种方法的另一个原因是我需要从已加载配置的 class 实例化其他子 classes,并在我这样做时将它们保存到数据库。

问题是,当我序列化一个 IConfiguration 时,我只得到一个空的 json 字符串,如“{}”。 我想我可以利用 .AsEnumerable() 做一些恶作剧,但没有更好的方法吗?

我的示例代码看起来有点像这样

public class ConfigurableClass
{

    public int ChildrenCount { get; set; } = 1069;
    public bool IsFast { get; set; } = false;
    public bool HasChildren { get; set; } = false;

    public int Id { get; }

    public ConfigurableClass(int id) { Id = id; }
}

static void Main(string[] args)
{

    IEnumerable<string> configs = SqlConfigLoader.LoadConfig();

    foreach (var str in configs)
    {
        Console.WriteLine("Parsing new Config:");

        var builder = new ConfigurationBuilder();

        IConfiguration cfg = builder.AddJsonStream(new MemoryStream(Encoding.Default.GetBytes(str)))
                .Build();

        var stepExample = new ConfigurableClass(9);

        cfg.Bind(stepExample);

        //do work with the class that might change the value of binded properties                   

        var updatedCfg = cfg;

        Console.WriteLine(JsonSerializer.Serialize(updatedCfg));

        Console.WriteLine();
    }

    Console.ReadLine();
}

编辑

我还尝试了一种不同的方法,将 IConfiguration 转换为这样的嵌套字典

ublic static class IConfigurationExtensions
{
   public static Dictionary<string,object> ToNestedDicionary(this IConfiguration configuration)
   {
       var result = new Dictionary<string, object>();
       var children = configuration.GetChildren();
       if (children.Any()) 
           foreach (var child in children)
               result.Add(child.Key, child.ToNestedDicionary());
       else 
           if(configuration is IConfigurationSection section)
               result.Add(section.Key, section.Get(typeof(object)));

       return result;
   }        
}

但是我丢失了给定 JsonElement 背后的隐式类型:

如果我序列化生成的字典,我会得到类似“属性”的东西:“True”而不是“属性”:true

首先,原因

尝试以这种方式序列化 IConfiguration 不会按您希望的方式工作。让我们来探究一下原因。

序列化接口

部分 你没有属性的原因是因为 Serialize 的通用类型参数是 IConfiguration。换句话说,您正在调用:

JsonSerializer.Serialize<IConfiguration>(updatedCfg)

当 System.Text.Json 使用通用参数序列化时,它仅(默认情况下没有任何自定义转换器)序列化该接口的 public 属性。在这种情况下 IConfiguration 没有 public 属性(索引器除外),因此您的输出为空 json.

使用运行时类型信息

现在,一般来说 要解决这个问题,您可以使用 non-generic overload 并传递类型。例如,它看起来像:

JsonSerializer.Serialize(updatedCfg, updatedCfg.GetType());

或者使用 object 作为类型参数:

JsonSerializer.Serialize<object>(updatedCfg);

System.Text.Json 然后将使用 运行时类型 信息来确定要序列化的属性。

ConfigurationRoot

现在你的问题的第二部分是,不幸的是,由于配置系统的设计方式,这仍然无法正常工作。 ConfigurationRoot class(Build 的结果)可以聚合许多配置源。数据单独存储在每个提供商内部(甚至外部)。当您从配置中请求一个值时,它会循环遍历每个提供程序以找到匹配项。

所有这一切都表明 IConfiguration 对象的 concrete/runtime 类型仍然没有您希望序列化的 public 属性。事实上,在这种情况下传递运行时类型比模仿接口的行为更糟糕,因为它将尝试序列化唯一的 public 属性键入 (ConfigurationRoot.Providers). This will give you a list of serialized providers, each typed as IConfigurationProvider 并具有零个 public 属性。

一个潜在的解决方案

由于您尝试序列化最终绑定到对象的配置,解决方法是重新序列化 that 对象:

var stepExample = new ConfigurableClass(9);
cfg.Bind(stepExample);
var json1 = JsonSerializer.Serialize(stepExample, stepExample.GetType());
// or with the generic version which will work here
var json2 = JsonSerializer.Serialize(stepExample);

另一种解决方案 - AsEnumerable

IConfiguration归根结底是键值对的集合。我们可以使用 AsEnumerable 扩展方法从整个配置中创建一个 List<KeyValuePair<string, string>>。稍后可以将其反序列化并传递给 AddInMemoryCollection

您将需要 Microsoft.Extensions.Configuration.Abstractions package(可能已经被传递引用)和以下 using 指令:

using Microsoft.Extensions.Configuration;

然后您可以创建所有值的列表(使用 Section:Key 格式的键)

var configAsList = cfg.AsEnumerable().ToList();
var json = JsonSerializer.Serialize(configAsList);

或者您可以使用 ToDictionary 并将其序列化。

var configAsDict = cfg.AsEnumerable().ToDictionary(c => c.Key, c => c.Value);
var json = JsonSerializer.Serialize(configAsDict);

两种格式都适用于 AddInMemoryCollection,因为它只需要一个 IEnumerable<KeyValuePair<string, string>>(这两种类型都是)。但是,如果您希望使用 AddJsonFile/Stream,您可能需要字典格式,因为我 认为 不支持 key/value 对数组。

字符串,字符串,只有字符串

您的印象似乎是 IConfiguration 对象正在存储 ints、bools 等(例如)对应于 JSON 元素类型。这是不正确的。 IConfiguration 中的所有数据都以 字符串化形式 存储。基本配置提供程序 class 都需要 IDictionary<string, string> 填充数据。甚至 JSON 配置提供程序 perform an explicit ToString 上的值。

stringy-typed 值在调用 Bind, Get<>GetValue<>。这些使用配置 binder,后者又使用注册的 TypeConverters 和众所周知的字符串解析方法。但在幕后,一切仍然是一个字符串。这意味着您的 json 文件是否包含值为 "True" 的字符串 属性 或值为 true 的布尔值 属性 并不重要。当映射到 boolean 属性.

时,活页夹将适当地转换值

使用上述字典序列化方法将按预期工作。

只是 运行 进入类似的需求。序列化 IConfiguration 以通过总线发送。这是我想出的

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;

namespace A6k
{
    public class ConfigurationConverter : JsonConverter<IConfiguration>
    {
        public override IConfiguration Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var root = new ConfigurationRoot(new List<IConfigurationProvider>(new[] { new MemoryConfigurationProvider(new MemoryConfigurationSource()) }));

            var pathParts = new Stack<string>();
            string currentProperty = null;
            string currentPath = null;
            while (reader.Read() && (reader.TokenType != JsonTokenType.EndObject || pathParts.Count > 0))
            {
                switch (reader.TokenType)
                {
                    case JsonTokenType.PropertyName:
                        currentProperty = reader.GetString();
                        break;
                    case JsonTokenType.String:
                        if (pathParts.Count == 0)
                            root[currentProperty] = reader.GetString();
                        else
                            root[ConfigurationPath.Combine(currentPath, currentProperty)] = reader.GetString();
                        break;
                    case JsonTokenType.StartObject:
                        pathParts.Push(currentProperty);
                        currentPath = ConfigurationPath.Combine(pathParts);
                        break;
                    case JsonTokenType.EndObject:
                        pathParts.Pop();
                        currentPath = ConfigurationPath.Combine(pathParts);
                        break;
                }
            }
            return root;
        }

        public override void Write(Utf8JsonWriter writer, IConfiguration value, JsonSerializerOptions options)
        {
            if (value is IConfigurationSection section)
            {
                if (section.Value is null)
                    writer.WriteStartObject(section.Key);
                else
                {
                    writer.WriteString(section.Key, section.Value);
                    return;
                }
            }
            else
                writer.WriteStartObject();

            foreach (var child in value.GetChildren())
                Write(writer, child, options);

            writer.WriteEndObject();
        }
    }
}

它在反序列化时只使用一个内存配置提供程序。但结果 IConfiguration 的行为方式与发送的实例相同。

只需将其作为转换器添加到您的序列化中即可:

var json = JsonSerializer.Serialize(configuration, new JsonSerializerOptions
{
  Converters = { new ConfigurationConverter() },
  WriteIndented = true
});

或作为属性添加到 属性

public class MyThing
{
    [JsonConverter(typeof(ConfigurationConverter))]
    public IConfiguration Config { get; set; }
}

祝你好运!