.NET 中的序列化和反序列化在 ASP.NET Core 应用中框架会屏蔽了很多实现序列化和反序列化的细节我们只需要定义参数模型ASP.NET Core 会自动将 http 请求的 Body 反序列化为模型对象。但是日常开发中我们会对序列化和反序列化做许多定制配置比如忽略值为 null 的字段、时间格式处理、忽略大小写、字段类型转换等各种情况。因此笔者单独使用一章讲解序列化框架的使用以及如何进行定制深入了解 .NET 中序列化和反序列化机制。System.Text.Json 是 .NET 框架自带的序列化框架简单易用并且性能也很出色使用 System.Text.Json 反序列化字符串为对象是很简单的示例如下// 自定义序列化配置 static JsonSerializerOptions jsonSerializerOptions new JsonSerializerOptions() { PropertyNameCaseInsensitive true, WriteIndented true }; public static void Main() { const string json { Name: 工良 } ; var obj JsonSerializer.DeserializeModel(json, jsonSerializerOptions); } public class Model { public string Name { get; set; } }JsonSerializerOptions 的属性定义了如何序列化和反序列化其常用属性如下属性类型说明AllowTrailingCommasbool忽略 JSON 中多余的逗号ConvertersIListJsonConverter转换器列表DefaultBufferSizeint默认缓冲区大小DefaultIgnoreConditionJsonIgnoreCondition当字段/属性的值为默认值时是否忽略DictionaryKeyPolicyJsonNamingPolicy字典 Key 重命名规则如首字母生成小写IgnoreNullValuesbool忽略 JSON 中值为 null 的字段/属性IgnoreReadOnlyFieldsbool忽略只读字段IgnoreReadOnlyPropertiesbool忽略只读属性IncludeFieldsbool是否处理字段默认只处理属性MaxDepthint最大嵌套深度默认最大深度为 64NumberHandlingJsonNumberHandling如何处理数字类型PropertyNameCaseInsensitivebool忽略大小写PropertyNamingPolicyJsonNamingPolicy重命名规则如首字母生成小写ReadCommentHandlingJsonCommentHandling处理注释WriteIndentedbool序列化时格式化 JSON如换行、空格、缩进接下来笔者将会列举一些常用的定制场景和编码方法为了避免混肴在本章中所指的 “字段” 或 “属性”等同于类型的“字段和属性”。编写类型转换器类型转换器的作用是当 json 对象字段和模型类字段类型不一致时可以自动转换对应的类型下面笔者介绍常用的几种类型转换器。枚举转换器.NET 是如何序列化枚举编写 WebAPI 的模型类时常常会用到枚举枚举类型默认会以数值的形式输出到 json 中。C# 代码示例如下// 枚举 public enum NetworkType { Unknown 0, IPV4 1, IPV6 2 } // 类型 public class Model { public string Name { get; set; } public NetworkType Netwotk1 { get; set; } public NetworkType? Netwotk2 { get; set; } } var model new Model { Name 工良, Netwotk1 NetworkType.IPV4, Netwotk2 NetworkType.IPV6 }当我们序列化对象时会得到这样的结果{ Name: 工良, Netwotk1: 1, Netwotk2: 2 }但是这样会在阅读上带来难题数字记忆比较困难并且后期需要扩展枚举字段时可能会导致对应数值的变化那么已经对接的代码都需要修改如果枚举涉及的范围比较广那么要做出修改就会变得十分困难。比如说突然出现了一个 IPV5那么我们除了改代码可能还要修改以及对接的其它应用。public enum NetworkType { Unknown 0, IPV4 1, IPV5 2, IPV6 3 }因此我们需要一种方法能够让枚举序列化后使用对应的名称表示以及能够使用这个字符串转化为对应的枚举类型后期需要扩展或中间插入时对以前的代码和数据库完全没有影响。比如反序列化时得到的是这样的 jsonNetwotk1: IPV4 Netwotk2: IPV6即使后来中间插入一个 IPV5生成新的字符串即可完全不需要重新排序枚举值。Netwotk1: IPV4 Netwotk2: IPV6 Netwotk3: IPV5在 C# 模型类中使用枚举而 json 中使用字符串要实现这种形式的枚举转换有两种方法。在模型类的枚举字段或属性上放置一个特性注解序列化反序列化时从这个特性注解中获取转换器。使用 JsonSerializerOptions 添加转换器在反序列化或序列化时传递自定义配置。无论哪种方法我们都需要实现一个转换器能够将模型类中的枚举使用对应的名称序列化到 json 中。在实现自定义转换器示例之前我们来了解相关的一些知识。自定义转换器需要继承JsonConverter或JsonConverterT当反序列化 json 的字段或序列化对象的字段属性时框架会自动调用转换器。以JsonConverterT为例里面有好几个抽象接口我们一般只需要实现转换器的两个抽象接口即可// json 值 对象字段 public abstract T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options); // 对象字段 json 值 public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);不过我们一定要注意 C# 中的可空类型比如NetworkType和NetworkType?实际上是两种类型可空类型本质是使用NullableT包装的类型。NullableT的定义如下public struct NullableT where T : struct另外NullableT实现了和 T 类型的隐式和显式转换重载所以我们在使用可空类型时可能不太容易感受出NullableT和 T 区别比如可以在使用可空类型T?时直接将NullableT与T类型隐式和显式转换如Nullableint value 100但是在使用反射时由于T和T?是两种不同的类型因此我们编写转换器时必须留意到这种区别否则会出现错误。实现枚举转换器本节示例代码在 Demo4.Console 中。编写一个枚举字符串转换器代码示例如下public class EnumStringConverterTEnum : JsonConverterTEnum { private readonly bool _isNullable; public EnumStringConverter(bool isNullType) { _isNullable isNullType; } // 判断当前类型是否可以使用该转换器转换 public override bool CanConvert(Type objectType) EnumStringConverterFactory.IsEnum(objectType); // 从 json 中读取数据 // JSON 值 // typeToConvert: 模型类属性/字段的类型 public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 读取 json var value reader.GetString(); if (value null) { if (_isNullable) return default; throw new ArgumentNullException(nameof(value)); } // 是否为可空类型 var sourceType EnumStringConverterFactory.GetSourceType(typeof(TEnum)); if (Enum.TryParse(sourceType, value.ToString(), out var result)) { return (TEnum)result!; } throw new InvalidOperationException(${value} 值不在枚举 {typeof(TEnum).Name} 范围中); } // 值 JSON public override void Write(Utf8JsonWriter writer, TEnum? value, JsonSerializerOptions options) { if (value null) writer.WriteNullValue(); else writer.WriteStringValue(Enum.GetName(value.GetType(), value)); } }由于 Utf8JsonReader 日常出行的机会不多因此读者可能不太了解在本章的末尾笔者会简单介绍。一般情况下我们不会直接使用 EnumStringConverter 为了能够适应所有枚举类型还需要编写一个枚举转换工厂通过工厂模式判断输入类型之后再创建对应的转换器。public class EnumStringConverterFactory : JsonConverterFactory { // 获取需要转换的类型 public static bool IsEnum(Type objectType) { if (objectType.IsEnum) return true; var sourceType Nullable.GetUnderlyingType(objectType); return sourceType is not null sourceType.IsEnum; } // 如果类型是可空类型则获取原类型 public static Type GetSourceType(Type typeToConvert) { if (typeToConvert.IsEnum) return typeToConvert; return Nullable.GetUnderlyingType(typeToConvert); } // 判断该类型是否属于枚举 public override bool CanConvert(Type typeToConvert) IsEnum(typeToConvert); // 为该字段创建一个对应的类型转换器 public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var sourceType GetSourceType(typeToConvert); var converter typeof(EnumStringConverter).MakeGenericType(typeToConvert); return (JsonConverter)Activator.CreateInstance(converter, new object[] { sourceType ! typeToConvert }); } }当 System.Text.Json 处理一个字段时会调用 EnumStringConverterFactory 的CanConvert方法如果返回 true则会调用 EnumStringConverterFactory 的CreateConverter方法创转换器最后调用转换器处理字段这样一来我们可以通过泛型类EnumStringConverterTEnum处理各种枚举。然后定义特性注解能够将模型类的属性字段绑定到一个转换器上。[AttributeUsage(AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple false)] public class EnumConverterAttribute : JsonConverterAttribute { public override JsonConverter CreateConverter(Type typeToConvert) { return new EnumStringConverterFactory(); } }如何使用类型转换器使用自定义类型转换器有三种方法。方法一在枚举字段中使用自定义特性public class Model { public string Name { get; set; } [EnumConverter] public NetworkType Netwotk1 { get; set; } [EnumConverter] public NetworkType? Netwotk2 { get; set; } }方法二使用JsonConverter特性。public class Model { public string Name { get; set; } [JsonConverter(typeof(EnumConverter))] public NetworkType Netwotk1 { get; set; } [JsonConverter(typeof(EnumConverter))] public NetworkType? Netwotk2 { get; set; } }方法三在配置中添加转换器。jsonSerializerOptions.Converters.Add(new EnumStringConverterFactory()); var obj JsonSerializer.DeserializeModel(json, jsonSerializerOptions);在模型类中使用转换器特性之后我们可以通过字符串反序列化为枚举类型const string json { Name: 工良, Netwotk1: IPV4, Netwotk2: IPV6 } ; var obj JsonSerializer.DeserializeModel(json, jsonSerializerOptions);使用官方的转换器System.Text.Json 中已经实现了很多转换器可以在官方源码的System/Text/Json/Serialization/Converters/Value下找到所有自带的转换器其中官方实现的枚举字符串转换器叫 JsonStringEnumConverter 使用方法跟我们的自定义转换器一致。这里我们可以使用官方的 JsonStringEnumConverter 转换器替代EnumStringConverterTEnumpublic class Model { public string Name { get; set; } public NetworkType Netwotk1 { get; set; } public NetworkType? Netwotk2 { get; set; } }JsonSerializerOptions jsonSerializerOptions new(); jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); const string json { Name: 工良, Netwotk1: IPV4, Netwotk2: IPV6 } ; var obj JsonSerializer.DeserializeModel(json, jsonSerializerOptions);字符串和值类型转换很多情况下会在模型类下使用数值类型序列化到 json 时使用字符串。比如对应浮点型的数值为了保证其准确性我们会使用字符串形式保存到 json 中这样可以避免传输时对浮点型处理而丢失其准确性。又比如前端处理超过 16 位数值时数字会丢失精确度16位数字存储毫秒格式的时间戳足够了很多时候我们会使用分布式 id雪花算法有很多种其生成的 id 往往会超过 16 位。JS 中处理超过 16 位数字时会出现很精确度丢失的问题console.log(11111111111111111); 输出: 11111111111111112 console.log(111111111111111111); 输出: 111111111111111100有个最简单的方法是在 JsonSerializerOptions 中将所有数值字段转换为字符串new JsonSerializerOptions { NumberHandling JsonNumberHandling.AllowReadingFromString };但是这样会导致所有值类型字段序列化为 json 时变成字符串如果只需要处理几个字段而不是处理所有字段那就需要我们自己编写类型转换器了。要实现字符串转数值需要考虑很多种数值类型如 byte、int、double、long 等从值类型转换为字符串是很简单的但是要实现一个字符串转任意类型值类型那就很麻烦这也是我们编写转换器的重点。编写 json 字符串和模型类值类型转换器的代码示例如下public class StringNumberConverterT : JsonConverterT { private static readonly TypeCode typeCode Type.GetTypeCode(typeof(T)); // 从 json 中读取字符串转换为对应的值类型 public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { switch (reader.TokenType) { case JsonTokenType.Number: if (typeCode TypeCode.Int32) { if (reader.TryGetInt32(out var value)) { return Unsafe.Asint, T(ref value); } } if (typeCode TypeCode.Int64) { if (reader.TryGetInt64(out var value)) { return Unsafe.Aslong, T(ref value); } } if (typeCode TypeCode.Decimal) { if (reader.TryGetDecimal(out var value)) { return Unsafe.Asdecimal, T(ref value); } } if (typeCode TypeCode.Double) { if (reader.TryGetDouble(out var value)) { return Unsafe.Asdouble, T(ref value); } } if (typeCode TypeCode.Single) { if (reader.TryGetSingle(out var value)) { return Unsafe.Asfloat, T(ref value); } } if (typeCode TypeCode.Byte) { if (reader.TryGetByte(out var value)) { return Unsafe.Asbyte, T(ref value); } } if (typeCode TypeCode.SByte) { if (reader.TryGetSByte(out var value)) { return Unsafe.Assbyte, T(ref value); } } if (typeCode TypeCode.Int16) { if (reader.TryGetInt16(out var value)) { return Unsafe.Asshort, T(ref value); } } if (typeCode TypeCode.UInt16) { if (reader.TryGetUInt16(out var value)) { return Unsafe.Asushort, T(ref value); } } if (typeCode TypeCode.UInt32) { if (reader.TryGetUInt32(out var value)) { return Unsafe.Asuint, T(ref value); } } if (typeCode TypeCode.UInt64) { if (reader.TryGetUInt64(out var value)) { return Unsafe.Asulong, T(ref value); } } break; case JsonTokenType.String: IConvertible str reader.GetString() ?? ; return (T)str.ToType(typeof(T), null); } throw new NotSupportedException($无法将{reader.TokenType}转换为{typeToConvert}); } // 将值类型转换为 json 字符串 public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { switch (typeCode) { case TypeCode.Int32: writer.WriteNumberValue(Unsafe.AsT, int(ref value)); break; case TypeCode.UInt32: writer.WriteNumberValue(Unsafe.AsT, uint(ref value)); break; case TypeCode.Decimal: writer.WriteNumberValue(Unsafe.AsT, decimal(ref value)); break; case TypeCode.Double: writer.WriteNumberValue(Unsafe.AsT, double(ref value)); break; case TypeCode.Single: writer.WriteNumberValue(Unsafe.AsT, uint(ref value)); break; case TypeCode.UInt64: writer.WriteNumberValue(Unsafe.AsT, ulong(ref value)); break; case TypeCode.Int64: writer.WriteNumberValue(Unsafe.AsT, long(ref value)); break; case TypeCode.Int16: writer.WriteNumberValue(Unsafe.AsT, short(ref value)); break; case TypeCode.UInt16: writer.WriteNumberValue(Unsafe.AsT, ushort(ref value)); break; case TypeCode.Byte: writer.WriteNumberValue(Unsafe.AsT, byte(ref value)); break; case TypeCode.SByte: writer.WriteNumberValue(Unsafe.AsT, sbyte(ref value)); break; default: throw new NotSupportedException($不支持非数字类型{typeof(T)}); } } }编写字符串转换为各种类型的值类型主要有一个难点泛型转换我们使用reader.TryGetInt32()读取 int 值之后明明知道泛型 T 是 int但是我们却不能直接返回 int 我们必须要有一个手段可以将值转换为泛型 T。如果使用反射会带来很大的性能消耗还可能伴随着装箱拆箱所以这里使用了Unsafe.As其作用是将转换类型的指针使得相关的值类型可以转换为泛型 T。实现字符串和值类型转换器之后接着实现转换工厂public class JsonStringToNumberConverter : JsonConverterFactory { public static JsonStringToNumberConverter Default { get; } new JsonStringToNumberConverter(); public override bool CanConvert(Type typeToConvert) { var typeCode Type.GetTypeCode(typeToConvert); return typeCode TypeCode.Int32 || typeCode TypeCode.Decimal || typeCode TypeCode.Double || typeCode TypeCode.Single || typeCode TypeCode.Int64 || typeCode TypeCode.Int16 || typeCode TypeCode.Byte || typeCode TypeCode.UInt32 || typeCode TypeCode.UInt64 || typeCode TypeCode.UInt16 || typeCode TypeCode.SByte; } public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var type typeof(StringNumberConverter).MakeGenericType(typeToConvert); var converter Activator.CreateInstance(type); if (converter null) { throw new InvalidOperationException($无法创建 {type.Name} 类型的转换器); } return (JsonConverter)converter; } }时间类型转换器json 中规定了标准的时间格式部分常用时间格式如下YYYY-MM-DDTHH:mm:ss.sssZ YYYY-MM-DDTHH:mm:ss.sssHH:mm YYYY-MM-DDTHH:mm:ss.sss-HH:mm示例2023-08-15T20:20:0008:00但是在项目开发中我们很多使用需要使用定制的格式如2023-02-15 20:20:20那么就需要自行编写转换器以便能够正确序列化或反序列化时间字段。在 C# 中有一个指定 DateTtime 如何解析字符串时间的接口即DateTime.ParseExact(String, String, IFormatProvider)为了能够适应各种字符串时间格式我们可以利用该接口将字符串转换为时间。编写 json 字符串时间与 DateTime 互转的代码示例如下public class CustomDateTimeConverter : JsonConverterDateTime { private readonly string _format; // format 参数是时间的字符串格式 public CustomDateTimeConverter(string format) { _format format; } public override void Write(Utf8JsonWriter writer, DateTime date, JsonSerializerOptions options) { writer.WriteStringValue(date.ToString(_format)); } public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value reader.GetString() ?? throw new FormatException(当前字段格式错误); return DateTime.ParseExact(value, _format, null); } }转换器中不需要判断 json 字符串时间的各种而是在使用时指定格式在构造函数中注入。使用示例jsonSerializerOptions.Converters.Add(new CustomDateTimeConverter(yyyy/MM/dd HH:mm:ss));其实使用默认的 json 时间格式是一个很好的习惯。据笔者经验在项目中修改默认的 json 时间格式在后期项目开发和对接中很有可能出现序列化问题。如果某些地方需要更高精细度如需要毫秒、使用转换为时间戳、第三方系统对接需要特殊格式等可以在需要的模型类上使用特性标记对应的时间转换器格式最好不要全局修改 json 时间格式。从底层处理 JSON在本节中笔者将会介绍如何使用 Utf8JsonReader 高性能地解析 json 文件然后编写对 Utf8JsonReader 的性能测试通过相关的示例让读者掌握 Utf8JsonReader 的使用以及如何对代码进行性能测试。Utf8JsonReaderUtf8JsonReader 和 Utf8JsonWriter 是 C# 中读取写入 json 的高性能 API通过 Utf8JsonReader 和 Utf8JsonWriter 我们可以逐步读取 json 或写入 json。Utf8JsonReader 使用比较广泛例如官方的 JsonConfigurationProvider 便是使用 Utf8JsonReader 逐步读取 json 文件生成 key/value 结构而在后面的章节中笔者也会介绍如何利用 Utf8JsonReader 实现 i18n 多语言的配置。由于 Utf8JsonReader 的使用最广泛而 Utf8JsonWriter 并不常见所以笔者只介绍 Utf8JsonReader 的使用方法。Utf8JsonReader 和 Utf8JsonWriter 都是结构体其定义如下public ref struct Utf8JsonReader public ref struct Utf8JsonWriter由于其是 ref 结构体因此使用上有较多限制例如不能在异步中使用不能作为类型参数在数组、List、字典等中使用只能被放到ref struct类型中当作字段或属性或在函数参数中使用。使用 Utf8JsonReader 读取 json 时开发者需要自行处理闭合括号{}、[]等也需要自行判断处理 json 类型因此读取过程也稍为复杂 。下面笔者来设定一个场景就是使用 Utf8JsonReader 来实现读取 json 文件将读取到的字段全都存到字典中如果有多层结构则使用:拼接层级生成 IConfiguration 中的能够直接读取的 key/value 格式。比如// json { A: { B: test } } // C# new Dictionarystring, string() { {A:B,test } };新建一个静态类 ReadJsonHelper在这个类型中编写解析 json 的代码。public static class ReadJsonHelper { }首先是读取字段值的代码当从 json 读取字段时如果字段不是对象或数组类型则直接读取其值即可。// 读取字段值 private static object? ReadObject(ref Utf8JsonReader reader) { switch (reader.TokenType) { case JsonTokenType.Null or JsonTokenType.None: return null; case JsonTokenType.False: return reader.GetBoolean(); case JsonTokenType.True: return reader.GetBoolean(); case JsonTokenType.Number: return reader.GetDouble(); case JsonTokenType.String: return reader.GetString() ?? ; default: return null; } }读取 json 字段时我们会碰到复杂的嵌套结构因此需要判断当前读取的是对象还是数组而且两者可以相互嵌套这就增加了我们的解析难度。比如{ ... ... } [... ...] [{...}, {...} ...]第一步是判断一个 json 的根结构是{}还是[]然后逐步解析。// 解析 json 对象 private static void BuildJsonField(ref Utf8JsonReader reader, Dictionarystring, object map, string? baseKey) { while (reader.Read()) { // 顶级数组 [123,123] if (reader.TokenType is JsonTokenType.StartArray) { ParseArray(ref reader, map, baseKey); } // 碰到 } 符号 else if (reader.TokenType is JsonTokenType.EndObject) break; // 碰到字段 else if (reader.TokenType is JsonTokenType.PropertyName) { var key reader.GetString()!; var newkey baseKey is null ? key : ${baseKey}:{key}; // 判断字段是否为对象 reader.Read(); if (reader.TokenType is JsonTokenType.StartArray) { ParseArray(ref reader, map, newkey); } else if (reader.TokenType is JsonTokenType.StartObject) { BuildJsonField(ref reader, map, newkey); } else { map[newkey] ReadObject(ref reader); } } } }json 数组有很多种情况json 数组的元素可以是任意类型因此处理起来稍微麻烦所以针对数组类型我们还应该支持解析元素使用序号来访问对应位置的元素。解析数组// 解析数组 private static void ParseArray(ref Utf8JsonReader reader, Dictionarystring, object map, string? baseKey) { int i 0; while (reader.Read()) { if (reader.TokenType is JsonTokenType.EndArray) break; var newkey baseKey is null ? $[{i}] : ${baseKey}[{i}]; i; switch (reader.TokenType) { // [...,null,...] case JsonTokenType.Null: map[newkey] null; break; // [...,123.666,...] case JsonTokenType.Number: map[newkey] reader.GetDouble(); break; // [...,123,...] case JsonTokenType.String: map[newkey] reader.GetString(); break; // [...,true,...] case JsonTokenType.True: map[newkey] reader.GetBoolean(); break; case JsonTokenType.False: map[newkey] reader.GetBoolean(); break; // [...,{...},...] case JsonTokenType.StartObject: BuildJsonField(ref reader, map, newkey); break; // [...,[],...] case JsonTokenType.StartArray: ParseArray(ref reader, map, newkey); break; default: map[newkey] JsonValueKind.Null; break; } } }最后我们编写一个解析 json 的入口通过用户传递的 json 文件解析出字典。public static Dictionarystring, object Read(ReadOnlySequencebyte sequence, JsonReaderOptions jsonReaderOptions) { var reader new Utf8JsonReader(sequence, jsonReaderOptions); var map new Dictionarystring, object(); BuildJsonField(ref reader, map, null); return map; }JsonReaderOptions 用于配置 Utf8JsonReader 读取策略其主要属性如下属性说明AllowTrailingCommasbool是否允许和忽略对象或数组成员末尾多余的逗号CommentHandlingJsonCommentHandling如何处理 JSON 注释MaxDepthint最大嵌套深度默认最大 64 层读取文件生成字典示例// 注意不能直接 File.ReadAllBytes() 读取文件因为文件有 bom 头 var text Encoding.UTF8.GetBytes(File.ReadAllText(read.json)); var dic ReadJsonHelper.Read(new ReadOnlySequencebyte(text), new JsonReaderOptions { AllowTrailingCommas true });在 Demo4.Console 示例项目中有一个 read.json 文件其内容较为复杂可以使用这个 json 验证代码。另外我们可以利用 Utf8JsonReader 结合第三章中的自定义配置教程将 json 文件解析到 IConfiguration 中。var config new ConfigurationBuilder() .AddInMemoryCollection(dic.ToDictionary(x x.Key, x x.Value.ToString())) .Build();Utf8JsonReader 和 JsonNode 解析 JSON 性能测试JsonNode 也是我们读取 json 常用的方法之一在本节中笔者会介绍如何使用 BenchmarkDotNet 编写性能测试对比 Utf8JsonReader 和 JsonNode 读取 json 的性能。在 Demo4.Benchmark 示例项目中有三个存储了大量对象数组的 json 文件这些文件使用工具批量生成我们将会使用这三个 json 进行性能测试。对象格式{ a_tttttttttttt: 1001, b_tttttttttttt: 邱平, c_tttttttttttt: Nancy Lee, d_tttttttttttt: buqdu, e_tttttttttttt: 81.26, f_tttttttttttt: 60, g_tttttttttttt: 1990-04-18 10:52:59, h_tttttttttttt: 35812178, i_tttttttttttt: 18935330000, j_tttttttttttt: w.nsliozyembwrxiyf.ug, k_tttttttttttt: 浙江省 金华市 兰溪市 }首先安装 BenchmarkDotNet 框架然后创建一个性能测试入口加载 json 文件。[SimpleJob(RuntimeMoniker.Net80)] [SimpleJob(RuntimeMoniker.NativeAot80)] [MemoryDiagnoser] [ThreadingDiagnoser] [MarkdownExporter, AsciiDocExporter, HtmlExporter, CsvExporter, RPlotExporter] public class ParseJson { private ReadOnlySequencebyte sequence; [Params(100.json, 1000.json, 10000.json)] public string FileName; [GlobalSetup] public async Task Setup() { var text File.ReadAllText(Path.Combine(Environment.CurrentDirectory, $json/{FileName})); var bytes Encoding.UTF8.GetBytes(text); sequence new ReadOnlySequencebyte(bytes); } }在 ParseJson 中添加相关的方法使用 Utf8JsonReader 解析 json :[Benchmark] public void Utf8JsonReader() { var reader new Utf8JsonReader(sequence, new JsonReaderOptions()); U8Read(ref reader); } private static void U8Read(ref Utf8JsonReader reader) { while (reader.Read()) { if (reader.TokenType is JsonTokenType.StartArray) { U8ReadArray(ref reader); } else if (reader.TokenType is JsonTokenType.EndObject) break; else if (reader.TokenType is JsonTokenType.PropertyName) { reader.Read(); if (reader.TokenType is JsonTokenType.StartArray) { // 进入数组处理 U8ReadArray(ref reader); } else if (reader.TokenType is JsonTokenType.StartObject) { U8Read(ref reader); } else { } } } } private static void U8ReadArray(ref Utf8JsonReader reader) { while (reader.Read()) { if (reader.TokenType is JsonTokenType.EndArray) break; switch (reader.TokenType) { case JsonTokenType.StartObject: U8Read(ref reader); break; // [...,[],...] case JsonTokenType.StartArray: U8ReadArray(ref reader); break; } } }在 ParseJson 中增加 JsonNode 解析 json 的代码[Benchmark] public void JsonNode() { var reader new Utf8JsonReader(sequence, new JsonReaderOptions()); var nodes System.Text.Json.Nodes.JsonNode.Parse(ref reader, null); if (nodes is JsonObject o) { JNRead(o); } else if (nodes is JsonArray a) { JNArray(a); } } private static void JNRead(JsonObject obj) { foreach (var item in obj) { var v item.Value; if (v is JsonObject o) { JNRead(o); } else if (v is JsonArray a) { JNArray(a); } else if (v is JsonValue value) { var el value.GetValueJsonElement(); JNValue(el); } } } private static void JNArray(JsonArray obj) { foreach (var v in obj) { if (v is JsonObject o) { JNRead(o); } else if (v is JsonArray a) { JNArray(a); } else if (v is JsonValue value) { var el value.GetValueJsonElement(); JNValue(el); } } } private static void JNValue(JsonElement obj){}然后在 Main 方法中启动性能 Benchmark 框架进行测试。static void Main() { var summary BenchmarkRunner.Run(typeof(Program).Assembly); Console.Read(); }以 Release 模式编译项目后启动程序进行性能测试。笔者所用机器配置AMD Ryzen 5 5600G with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores可以看到两者的性能差异比较大所以在需要高性能的场景下我们使用 Utf8JsonReader 的性能会高一点还可以降低内存的使用量。MethodJobFileNameMeanGen0Gen1Gen2AllocatedUtf8JsonReader.NET 8.0100.json42.87 us----JsonNode.NET 8.0100.json237.57 us37.109424.4141-312624 BUtf8JsonReaderNativeAOT 8.0100.json49.81 us----JsonNodeNativeAOT 8.0100.json301.11 us37.109424.4141-312624 BUtf8JsonReader.NET 8.01000.json427.07 us----JsonNode.NET 8.01000.json2,699.76 us484.3750460.9375199.21883120511 BUtf8JsonReaderNativeAOT 8.01000.json494.87 us----JsonNodeNativeAOT 8.01000.json3,652.08 us484.3750464.8438199.21883120513 BUtf8JsonReader.NET 8.010000.json4,306.30 us---3 BJsonNode.NET 8.010000.json60,883.56 us4000.00003888.88891222.222231215842 BUtf8JsonReaderNativeAOT 8.010000.json4,946.71 us---3 BJsonNodeNativeAOT 8.010000.json62,864.68 us4125.00004000.00001250.000031216863 B