【问题标题】:Using System.Text.Json to Serialize an IConfiguration back to Json使用 System.Text.Json 将 IConfiguration 序列化回 Json
【发布时间】:2020-10-24 01:36:15
【问题描述】:

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

在某些时候,我可能会在运行时更改新的绑定属性,然后更新数据库。问题是,当我需要时,该类可能具有更多不应绑定且不应序列化的属性。因此,我将 IConfiguration 作为我班级的一个属性。我使用这种方法的另一个原因是我需要从已加载配置的类中提取其他子类,并在我这样做时将它们保存到 db。

问题是,当我序列化 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 背后的隐式类型:

如果我对结果字典进行序列化,我会得到类似 "Property": "True" 而不是 "Property": true

【问题讨论】:

    标签: c# .net .net-core configuration system.text.json


    【解决方案1】:

    刚刚遇到了类似的要求。序列化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; }
    }
    

    祝你好运!

    【讨论】:

      【解决方案2】:

      首先,为什么

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

      序列化接口

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

      JsonSerializer.Serialize<IConfiguration>(updatedCfg)
      

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

      使用运行时类型信息

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

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

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

      JsonSerializer.Serialize<object>(updatedCfg);
      

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

      ConfigurationRoot

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

      所有这一切都说明您的 IConfiguration 对象的具体/运行时类型仍然没有您希望序列化的公共属性。事实上,在这种情况下传递运行时类型会比模仿接口的行为更糟糕,因为它将尝试序列化该类型的唯一公共属性 (ConfigurationRoot.Providers)。这将为您提供一个序列化提供程序的列表,每个提供程序类型为IConfigurationProvider,并且公共属性为零。

      一个潜在的解决方案

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

      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&lt;KeyValuePair&lt;string, string&gt;&gt;。稍后可以将其反序列化并传递给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&lt;KeyValuePair&lt;string, string&gt;&gt;(两种类型都是)。但是,如果您希望使用 AddJsonFile/Stream,您可能需要 Dictionary 格式,因为我不认为那些支持键/值对数组。

      字符串,字符串,除了字符串什么都没有

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

      当您调用 Bind、Get&lt;&gt; 或 GetValue&lt;&gt; 时,stringyly-typed 值会转换为 strong>strong>strong-typed 值。这些使用配置 binder ,而该配置又使用注册的TypeConverters 和众所周知的字符串解析方法。但在幕后一切仍然是一个字符串。这意味着您的 json 文件是否具有值为"True" 的字符串属性或值为true 的布尔属性并不重要。当映射到boolean 属性时,活页夹会适当地转换值。

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

      【讨论】:

      • 问题是我想利用 IConfig 动态性,因为该对象实际上是一个派生类,可能具有或不具有某些属性,我在工厂类中使用 IConfiguration 来确定正确的派生类,并且,调用正确的构造函数后,继续绑定自定义属性
      • IConfiguration 似乎是一个很好的工具,这样自定义派生类可以同时具有其解析设置和原始 IConfiguration。目标是正确加载每个实例,使其变异并保存回数据库
      • 我猜我们的解决方案有这个共同的缺陷(我更新了我的帖子),也许我应该尝试一种不同的方法,利用动态而不是 IConfiguration...
      • 响应您对编辑的评论,IConfiguration 没有整数/布尔值的概念。一切都是字符串直到,它绑定到适当类型的容器。没有理由序列化到字典然后再返回不起作用,因为当它反弹到您的对象时,正确的类型转换器将启动。无论 json 元素的类型是什么都无关紧要。它总是是底层配置对象中的一个字符串。它正在调用Bind、Get&lt;&gt;、GetValue&lt;&gt;,其中包含执行转换的逻辑。 true.ToString 为“True”,它将正确往返
      • 为了证明这一点,请查看 JSON 文件提供程序中的这一行。这是一个例程的一部分,它根据配置基础结构github.com/dotnet/runtime/blob/… 的要求返回一个 Dictionary&lt;string, string&gt;,您会看到它对您相信的所有值强制调用 ToString > 它保留。它没有!您所指的“动态”是用于翻译配置的活页夹的一部分,而不是配置本身。我的回答有效:)
      猜你喜欢
      • 2021-03-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-03-24
      • 2021-12-23
      • 2021-02-23
      • 1970-01-01
      • 2021-12-08
      相关资源
      最近更新 更多