阅读目录
4.2、使用System.Text.Json序列化&反序列化
4.4、System.Text.Json和Newtonsoft.Json的一些差异
序列化:将对象的状态信息及类型信息,转换为一种易于传输或存储形式(流,即字节序列)的过程。
反序列化:与序列化相反,将流转换为对象的过程。
.NET有以下序列化技术:
二进制序列化:保持类型保真,这对于多次调用应用程序时保持对象状态非常有用。 例如,通过将对象序列化到剪贴板,可在不同的应用程序之间共享对象。 您可以将对象序列化到流、磁盘、内存和网络等。 远程处理使用序列化,“按值”在计算机或应用程序域之间传递对象。
XML和SOAP序列化:只序列化公共属性和字段,并且不保持类型保真。 当您希望提供或使用数据而不限制使用该数据的应用程序时,这一点非常有用。 由于 XML 是开放式的标准,因此它对于通过 Web 共享数据来说是一个理想选择。 SOAP 同样是开放式的标准,这使它也成为一个理想选择。
JSON序列化:只序列化公共属性,并且不保持类型保真。 JSON 是开放式的标准,对于通过 Web 共享数据来说是一个理想选择。
二进制序列化是将.NET对象转换为数据流,存放到内存中。需要注意两点:①二进制序列化的时候,可以将对象的所有属性进行序列化,包括私有属性。②在反序列化的时候,不会调用无参的构造函数,可以借用这点来进行对象的深拷贝。序列化和反序列化可以通过BinaryFormatter类来实现的,这个类位于System.Runtime.Serialization.Formatters.Binary命名空间下。要使一个类能够序列化,需要使用Serializable属性标记类,如果不想被二进制序列化,在属性字段上使用NonSerialized标记。
1 [Serializable] 2 public class SerializeObject 3 { 4 public string Name { get; set; } 5 //二进制序列化会把所有的属性字段都序列化,包括私有private, 6 //如果不想序列化,使用特性[NonSerialized] 7 private string _desc { get; set; } 8 public int _Age; 9 [NonSerialized] 10 private bool _sex; 11 public SerializeObject(string name,string desc,int age,bool sex) 12 { 13 this.Name = name; 14 this._desc = desc; 15 this._Age = age; 16 this._sex = sex; 17 } 18 }
1 static void Main(string[] args) 2 { 3 SerializeObject serializeObject = new SerializeObject("BigBox777","永远18岁",18,true); 4 BinaryFormatter formatter = new BinaryFormatter(); 5 using (Stream stream=new FileStream("MyFile.bin",FileMode.OpenOrCreate,FileAccess.Write,FileShare.None)) 6 { 7 formatter.Serialize(stream, serializeObject); 8 9 Console.WriteLine(stream.Length); 10 } 11 12 Console.ReadLine(); 13 }
1 //反序列化代码 2 BinaryFormatter formatter = new BinaryFormatter(); 3 using (Stream stream = new FileStream("MyFile.bin", FileMode.Open, FileAccess.Read, FileShare.None)) 4 { 5 var obj = formatter.Deserialize(stream); 6 if(obj is SerializeObject) 7 { 8 var serializeObject = (SerializeObject)obj; 9 } 10 } 11 Console.ReadLine();
在序列化期间或者序列化之后可以运行自定义方法。最佳做法是,在需要执行的方法使用特定的属性进行标记。
- OnDeserializedAttribute:反序列化之后执行
- OnDeserializingAttribute:反序列化的时候执行
- OnSerializedAttribute:序列化完成后执行
- OnSerializingAttribute:执行序列化的时候执行
1 [Serializable] 2 public class CustomerSerialize 3 { 4 public string Name { get; set; } 5 //二进制序列化会把所有的属性字段都序列化,包括私有private, 6 //如果不想序列化,使用特性[NonSerialized] 7 private string _desc { get; set; } 8 public int _Age; 9 [NonSerialized] 10 private bool _sex; 11 public CustomerSerialize(string name, string desc, int age, bool sex) 12 { 13 this.Name = name; 14 this._desc = desc; 15 this._Age = age; 16 this._sex = sex; 17 } 18 [OnSerializing] //序列化的时候执行 19 public void OnSerializingMothed(StreamingContext context) 20 { 21 Console.WriteLine("序列化的时候执行"); 22 } 23 [OnSerialized] //序列化完成后执行 24 public void OnSerializedMothed(StreamingContext context) 25 { 26 Console.WriteLine("序列化完成后执行"); 27 } 28 [OnDeserializing] //反序列化的时候执行 29 public void OnDeserializingMothed(StreamingContext context) 30 { 31 Console.WriteLine("反序列化的时候执行"); 32 } 33 [OnDeserialized] //反序列化完成执行 34 public void OnDeserializedMothed(StreamingContext context) 35 { 36 Console.WriteLine("反序列化完成执行"); 37 } 38 }
1 //自定义序列化 2 CustomerSerialize customerSerialize = new CustomerSerialize("BigBox777", "永远18岁", 18, true); 3 BinaryFormatter formatter = new BinaryFormatter(); 4 using (Stream stream = new FileStream("MyFile.bin", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)) 5 { 6 formatter.Serialize(stream, customerSerialize); 7 Console.WriteLine(stream.Length); 8 } 9 Console.ReadLine();
1 //自定义反序列化 2 BinaryFormatter formatter = new BinaryFormatter(); 3 using (Stream stream = new FileStream("MyFile.bin", FileMode.Open, FileAccess.Read, FileShare.None)) 4 { 5 var obj = formatter.Deserialize(stream); 6 if (obj is CustomerSerialize) 7 { 8 var customerSerialize = (CustomerSerialize)obj; 9 } 10 } 11 Console.ReadLine();
XML 序列化是将对象的公共属性 (Property) 和字段转换为序列格式(这里是指 XML)以便存储或传输的过程。反序列化则是从 XML 输出中重新创建原始状态的对象。因此,可以将序列化视为将对象的状态保存到流或缓冲区的方法。序列化和反序列化通过XmlSerializer 类来实现的,这个类位于System.Xml.Serialization命名空间下。注意:①XML序列化不转换方法、索引器、私有字段或只读属性(只读集合除外)。要序列化对象的所有字段和属性(公共的和私有的),请使用 BinaryFormatter。②被序列化对象一定要有无参构造函数。
1 using System; 2 using System.Xml.Serialization; 3 4 namespace Demo06_XmlSerializer 5 { 6 [Serializable] 7 public class SerializeObject 8 { 9 public string Name { get; set; } 10 public int _Age; 11 public MyObject MyObjectDate { get; set; } 12 [XmlIgnore] //加特性后不进行序列化 13 public int Count { get; set; } 14 private bool _sex; //私有属性字段不序列化 15 public SerializeObject(string name, int age, bool sex) 16 { 17 this.Name = name; 18 this._Age = age; 19 this._sex = sex; 20 this.MyObjectDate=new MyObject() { CreateTime = DateTime.Now }; 21 } 22 public SerializeObject() //序列化的时候必须要无参的构造函数 23 { 24 25 } 26 } 27 public class MyObject 28 { 29 public DateTime CreateTime { get; set; } 30 } 31 }
1 using System; 2 using System.IO; 3 using System.Xml.Serialization; 4 5 namespace Demo06_XmlSerializer 6 { 7 internal class Program 8 { 9 static void Main(string[] args) 10 { 11 //XML序列化 12 SerializeObject serializeObject = new SerializeObject("BigBox777", 18, true); 13 using (StreamWriter myWriter = new StreamWriter("myFileName.xml")) 14 { 15 XmlSerializer xmlSerializer = new XmlSerializer(typeof(SerializeObject)); 16 xmlSerializer.Serialize(myWriter, serializeObject); 17 } 18 19 ////XML反序列化 20 //var mySerializer = new XmlSerializer(typeof(SerializeObject)); 21 //using (var myFileStream = new FileStream("myFileName.xml", FileMode.Open)) 22 //{ 23 // var myObject = (SerializeObject)mySerializer.Deserialize(myFileStream); 24 //} 25 26 Console.ReadLine(); 27 } 28 } 29 }
1 <?xml version="1.0" encoding="utf-8"?> 2 <SerializeObject xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 3 <_Age>18</_Age> 4 <Name>BigBox777</Name> 5 <MyObjectDate> 6 <CreateTime>2021-12-11T16:56:06.9724693+08:00</CreateTime> 7 </MyObjectDate> 8 </SerializeObject>
通过将下表中的特性应用于类和类成员,可以控制 XmlSerializer 序列化或反序列化该类的实例的方式。 这些属性还可用于控制 XML Web services 生成的文本样式的 SOAP 消息。除了这些特性(全部位于 System.Xml.Serialization 命名空间中)之外,还可以将 DefaultValueAttribute 特性应用于字段。 如果没有指定值,使用 DefaultValueAttribute 可设置将自动分配给成员的值。
特性 | 适用对象 | 指定 |
---|---|---|
XmlAnyAttributeAttribute | 公共字段、属性、参数或返回 XmlAttribute 对象数组的返回值。 | 反序列化时,将会使用 XmlAttribute 对象填充数组,而这些对象代表对于架构未知的所有 XML 特性。 |
XmlAnyElementAttribute | 公共字段、属性、参数或返回 XmlElement 对象数组的返回值。 | 反序列化时,将会使用 XmlElement 对象填充数组,而这些对象代表对于架构未知的所有 XML 元素。 |
XmlArrayAttribute | 公共字段、属性、参数或返回复杂对象的数组的返回值。 | 数组成员将作为 XML 数组的成员生成。 |
XmlArrayItemAttribute | 公共字段、属性、参数或返回复杂对象的数组的返回值。 | 可以插入数组的派生类型。 通常与 XmlArrayAttribute 一起应用。 |
XmlAttributeAttribute | 公共字段、属性、参数或返回值。 | 成员将作为 XML 属性进行序列化。 |
XmlChoiceIdentifierAttribute | 公共字段、属性、参数或返回值。 | 可以使用枚举进一步消除成员的歧义。 |
XmlElementAttribute | 公共字段、属性、参数或返回值。 | 字段或属性将作为 XML 元素进行序列化。 |
XmlEnumAttribute | 作为枚举标识符的公共字段。 | 枚举成员的元素名称。 |
XmlIgnoreAttribute | 公共属性和公共字段。 | 序列化包含类时,应该忽略属性或字段。 |
XmlIncludeAttribute | 公共派生类声明,以及 Web 服务描述语言 (WSDL) 文档的公共方法的返回值。 | 生成要在序列化时识别的架构时,应该将该类包括在内。 |
XmlRootAttribute | 公共类声明。 | 控制视为 XML 根元素的属性目标的 XML 序列化。 使用该属性可进一步指定命名空间和元素名称。 |
XmlTextAttribute | 公共属性和公共字段。 | 属性或字段应该作为 XML 文本进行序列化。 |
XmlTypeAttribute | 公共类声明。 | XML 类型的名称和命名空间。 |
3.4、使用 XmlAttributeOverrides 类重写序列化逻辑
案例是序列化一个名为 Orchestra(管弦乐队) 的类,该类包含一个名为 Instruments(乐器) 的字段,该字段 Instruments 返回对象的数组 Instrument(乐器) 类 。 名为 Brass(铜管乐器) 的第二个类 从 Instrument 类继承。 该示例使用类的实例 XmlAttributeOverrides 来重写 Instrument 字段,允许字段接受 Brass 对象。
1 namespace Demo06_OverwriteXmlSerializer 2 { 3 /// <summary> 4 /// 管弦乐队 5 /// </summary> 6 public class Orchestra 7 { 8 public Instrument[] Instruments; 9 } 10 /// <summary> 11 /// 乐器 12 /// </summary> 13 public class Instrument 14 { 15 public string Name; 16 } 17 /// <summary> 18 /// 铜管乐器 19 /// </summary> 20 public class Brass: Instrument 21 { 22 public bool IsValved; 23 } 24 }
1 using System; 2 using System.IO; 3 using System.Xml.Serialization; 4 5 namespace Demo06_OverwriteXmlSerializer 6 { 7 internal class Program 8 { 9 static void Main(string[] args) 10 { 11 Program test = new Program(); 12 test.SerializeObject("Override.xml"); 13 test.DeserializeObject("Override.xml"); 14 } 15 public void SerializeObject(string filename) 16 { 17 18 XmlAttributes attrs = new XmlAttributes(); 19 20 /* Create an XmlElementAttribute to override the 21 field that returns Instrument objects. The overridden field 22 returns Brass objects instead. */ 23 //创建XmlElementAttribute以覆盖返回仪器对象的字段。替代的字段返回黄铜对象。 24 XmlElementAttribute attr = new XmlElementAttribute(); 25 attr.ElementName = "Brass"; 26 attr.Type = typeof(Brass); 27 28 // 添加元素到集合 29 attrs.XmlElements.Add(attr); 30 31 // 创建XmlAttributeOverrides 32 XmlAttributeOverrides attrOverrides = new XmlAttributeOverrides(); 33 34 /* Add the type of the class that contains the overridden 35 member and the XmlAttributes to override it with to the 36 XmlAttributeOverrides object. */ 37 //将包含重写成员的类的类型以及要用其重写的XmlAttributeOverrides对象的XmlAttributes添加到XmlAttributeOverrides对象。 38 attrOverrides.Add(typeof(Orchestra), "Instruments", attrs); 39 40 // Create the XmlSerializer using the XmlAttributeOverrides. 41 XmlSerializer s = new XmlSerializer(typeof(Orchestra), attrOverrides); 42 43 using (TextWriter writer = new StreamWriter(filename)) 44 { 45 // Create the object that will be serialized. 46 Orchestra band = new Orchestra(); 47 48 // Create an object of the derived type. 49 Brass i = new Brass(); 50 i.Name = "Trumpet"; 51 i.IsValved = true; 52 Instrument[] myInstruments = { i }; 53 band.Instruments = myInstruments; 54 55 // Serialize the object. 56 s.Serialize(writer, band); 57 } 58 59 60 } 61 62 public void DeserializeObject(string filename) 63 { 64 XmlAttributeOverrides attrOverrides = 65 new XmlAttributeOverrides(); 66 XmlAttributes attrs = new XmlAttributes(); 67 68 // Create an XmlElementAttribute to override the Instrument. 69 XmlElementAttribute attr = new XmlElementAttribute(); 70 attr.ElementName = "Brass"; 71 attr.Type = typeof(Brass); 72 73 // Add the XmlElementAttribute to the collection of objects. 74 attrs.XmlElements.Add(attr); 75 76 attrOverrides.Add(typeof(Orchestra), "Instruments", attrs); 77 78 // Create the XmlSerializer using the XmlAttributeOverrides. 79 XmlSerializer s = 80 new XmlSerializer(typeof(Orchestra), attrOverrides); 81 82 FileStream fs = new FileStream(filename, FileMode.Open); 83 Orchestra band = (Orchestra)s.Deserialize(fs); 84 Console.WriteLine("Brass:"); 85 86 /* The difference between deserializing the overridden 87 XML document and serializing it is this: To read the derived 88 object values, you must declare an object of the derived type 89 (Brass), and cast the Instrument instance to it. */ 90 Brass b; 91 foreach (Instrument i in band.Instruments) 92 { 93 b = (Brass)i; 94 Console.WriteLine( 95 b.Name + "\n" + 96 b.IsValved); 97 } 98 } 99 } 100 }
1 <?xml version="1.0" encoding="utf-8"?> 2 <Orchestra xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 3 <Brass> 4 <Name>Trumpet</Name> 5 <IsValved>true</IsValved> 6 </Brass> 7 </Orchestra>
XmlAttributeOverrides类重写默认序列化逻辑具体可见:https://docs.microsoft.com/zh-cn/dotnet/api/system.xml.serialization.xmlattributeoverrides?redirectedfrom=MSDN&view=net-6.0
将对象序列化为 SOAP 编码的 XML 流具体可见:https://docs.microsoft.com/zh-cn/dotnet/standard/serialization/how-to-serialize-an-object-as-a-soap-encoded-xml-stream
JSON序列化目前有两种方式:①使用JsonSerializer类,System.Text.Json 命名空间包含所有入口点和主要类型,System.Text.Json.Serialization 命名空间包含用于高级方案的特性和 API,以及特定于序列化和反序列化的自定义。②使用开源库Newtonsoft.Json。
4.2、使用System.Text.Json序列化&反序列化
本节演示如何使用 System.Text.Json 命名空间向/从 JavaScript 对象表示法 (JSON) 进行序列化和反序列化。 注意:大多数序列化示例代码将 JsonSerializerOptions.WriteIndented 设置为 true,以 JSON 进行优质打印(包含缩进和空格,以提高可读性)。 对于生产用途,通常对于此设置会接受默认值 false。
1 using System; 2 3 namespace Demo06_JsonSerializer 4 { 5 public class SerializeObject 6 { 7 public string Name { get; set; } 8 public int _Age; 9 public MyObject MyObjectDate { get; set; } 10 public int Count { get; set; } 11 private bool _sex; 12 public SerializeObject(string name, int age, bool sex) 13 { 14 this.Name = name; 15 this._Age = age; 16 this._sex = sex; 17 this.MyObjectDate = new MyObject() { CreateTime = DateTime.Now }; 18 } 19 public SerializeObject() //序列化的时候必须要无参的构造函数 20 { 21 Console.WriteLine("SerializeObject对象构造函数。"); 22 } 23 } 24 public class MyObject 25 { 26 public DateTime CreateTime { get; set; } 27 } 28 }
1 using System; 2 using System.IO; 3 using System.Text.Json; 4 5 namespace Demo06_JsonSerializer 6 { 7 internal class Program 8 { 9 static void Main(string[] args) 10 { 11 //JSON序列化 12 SerializeObject serializeObject = new SerializeObject("BigBox777", 18, true); 13 using (TextWriter tw = new StreamWriter("JsonSerializer.json")) 14 { 15 //将 JsonSerializerOptions.WriteIndented 设置为 true,以 JSON 进行优质打印 16 //(包含缩进和空格,以提高可读性)。 对于生产用途,通常对于此设置会接受 17 //默认值 false。 18 JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(); 19 jsonSerializerOptions.WriteIndented = true; 20 string jsonStr = JsonSerializer.Serialize<SerializeObject>(serializeObject, jsonSerializerOptions); 21 tw.Write(jsonStr); 22 Console.WriteLine(jsonStr); 23 } 24 25 ////JSON反序列化 26 //using (TextReader tr=new StreamReader("JsonSerializer.json")) 27 //{ 28 // SerializeObject serializeObject1 = JsonSerializer.Deserialize<SerializeObject>(tr.ReadToEnd()); 29 //} 30 31 Console.ReadLine(); 32 } 33 } 34 }
1 { 2 "Name": "BigBox777", 3 "MyObjectDate": { 4 "CreateTime": "2021-12-16T09:07:30.2494361+08:00" 5 }, 6 "Count": 0 7 }
使用Newtonsoft.Json进行序列化&反序列化之前需要安装Newtonsoft.Json开源库,在项目上使用nuget包管理器,搜索 Newtonsoft.Json 进行安装,我这里安装稳定版13.0.1。
1 using System; 2 using System.Collections.Generic; 3 4 namespace Demo06_NewtonsoftJson 5 { 6 public class WeatherForecastWithPOCOs 7 { 8 public DateTimeOffset Date { get; set; } 9 public int TemperatureCelsius { get; set; } 10 public string Summary { get; set; } 11 public string SummaryField; 12 public IList<DateTimeOffset> DatesAvailable { get; set; } 13 public Dictionary<string, HighLowTemps> TemperatureRanges { get; set; } 14 public string[] SummaryWords { get; set; } 15 } 16 17 public class HighLowTemps 18 { 19 public int High { get; set; } 20 public int Low { get; set; } 21 } 22 }
1 using Newtonsoft.Json; 2 using System; 3 using System.Collections.Generic; 4 using System.IO; 5 6 namespace Demo06_NewtonsoftJson 7 { 8 internal class Program 9 { 10 static void Main(string[] args) 11 { 12 ////序列化 13 //WeatherForecastWithPOCOs weatherForecastWithPOCOs = new WeatherForecastWithPOCOs() 14 //{ 15 // Date = DateTime.Now, 16 // TemperatureCelsius = 25, 17 // Summary = "Hot", 18 // DatesAvailable = new List<DateTimeOffset>() { DateTimeOffset.Now, DateTimeOffset.Now }, 19 // SummaryWords = new string[] { "Cool", "Windy", "mid" } 20 //}; 21 //weatherForecastWithPOCOs.TemperatureRanges = new Dictionary<string, HighLowTemps>(); 22 //weatherForecastWithPOCOs.TemperatureRanges.Add("Cold",new HighLowTemps() { High=20, Low=-10}); 23 //weatherForecastWithPOCOs.TemperatureRanges.Add("Hot", new HighLowTemps() { High = 60, Low = 20 }); 24 //using (TextWriter tw=new StreamWriter("newtonsoftJson.json")) 25 //{ 26 // string jsonStr = JsonConvert.SerializeObject(weatherForecastWithPOCOs); 27 // tw.WriteLine(jsonStr); 28 // Console.WriteLine(jsonStr); 29 //} 30 31 //反序列化 32 using (TextReader tr = new StreamReader("newtonsoftJson.json")) 33 { 34 WeatherForecastWithPOCOs weatherForecastWithPOCOs = JsonConvert.DeserializeObject<WeatherForecastWithPOCOs>(tr.ReadToEnd()); 35 } 36 37 Console.ReadLine(); 38 } 39 } 40 }
1 { 2 "SummaryField": null, 3 "Date": "2021-12-16T09:52:05.1358337+08:00", 4 "TemperatureCelsius": 25, 5 "Summary": "Hot", 6 "DatesAvailable": [ "2021-12-16T09:52:05.1397264+08:00", "2021-12-16T09:52:05.1400572+08:00" ], 7 "TemperatureRanges": { 8 "Cold": { 9 "High": 20, 10 "Low": -10 11 }, 12 "Hot": { 13 "High": 60, 14 "Low": 20 15 } 16 }, 17 "SummaryWords": [ "Cool", "Windy", "mid" ] 18 }
4.4、System.Text.Json和Newtonsoft.Json的一些差异
①在反序列化过程中,默认情况下 Newtonsoft.Json 进行不区分大小写的属性名称匹配。 System.Text.Json 默认值区分大小写,这可提供更好的性能,因为它执行精确匹配。若要更改该行为,请将 JsonSerializerOptions.PropertyNameCaseInsensitive 设置为 true。
②在序列化过程中,Newtonsoft.Json 对于让字符通过而不进行转义相对宽松。 也就是说,它不会将它们替换为 \uxxxx(其中 xxxx 是字符的码位)。 对字符进行转义时,它会通过在字符前发出 \ 来实现此目的(例如," 会变为 \")。 System.Text.Json 会在默认情况下转义较多字符,以对跨站点脚本 (XSS) 或信息泄露攻击提供深度防御保护,并使用六字符序列执行此操作。 System.Text.Json 会在默认情况下转义所有非 ASCII 字符,因此如果在 Newtonsoft.Json 中使用 StringEscapeHandling.EscapeNonAscii,则无需执行任何操作。 System.Text.Json 在默认情况下还会转义 HTML 敏感字符。
③在反序列化过程中,Newtonsoft.Json 在默认情况下会忽略 JSON 中的注释。 System.Text.Json 默认值是对注释引发异常,因为 RFC 8259 规范不包含它们。
④在反序列化过程中,默认情况下 Newtonsoft.Json 会忽略尾随逗号。 它还会忽略多个尾随逗号(例如 [{"Color":"Red"},{"Color":"Green"},,])。 System.Text.Json 默认值是对尾随逗号引发异常,因为 RFC 8259 规范不允许使用它们。默认情况下,JSON 中不允许使用注释和尾随逗号。 若要在 JSON 中允许注释,请将 JsonSerializerOptions.ReadCommentHandling 属性设置为 JsonCommentHandling.Skip。 若要允许尾随逗号,请将 JsonSerializerOptions.AllowTrailingCommas 属性设置为 true。
.NET 中的序列化 & 反序列化(雪飞鸿博客) https://www.cnblogs.com/Cwj-XFH/p/10330671.html
Json的序列化与反序列化(CesarLai博客) https://www.cnblogs.com/cesarlai/p/7538437.html
C#对象序列化与反序列化(KyrieYang博客) https://www.cnblogs.com/yzenet/p/3622833.html
C#编程总结(一)序列化(停留的风博客) https://www.cnblogs.com/yank/p/3198082.html
.NET 中的序列化(微软docs) https://docs.microsoft.com/zh-cn/dotnet/standard/serialization/