目录
介绍
用户故事5:在System.Text.Json JsonSerializer中支持动态类型
演示项目和测试
修改模型方法
包装方法
总结
Pro Coders团队最近将一个大型项目从Newtonsoft迁移到System.Text.Json序列化器,并且由于它不支持使用$type属性进行动态对象反序列化,因此我们实现了一种方法,用于对动态对象进行序列化和反序列化,并注入ModelFullName属性来通过模型类型显式反序列化。
- 从Github下载完整的解决方案
欢迎来到我为C#开发人员撰写的新文章。今天,我想考虑json序列化。最近,Microsoft将其针对WEB API的默认序列化从Newtonsoft JsonConvert更改为System.Text.Json JsonSerializer,并且开发人员意识到不再支持一项重要功能。我说的是Newtonsoft JsonConvert可以在对象序列化期间为每种复杂类型添加的“$type”属性,并使用它反序列化对象。
如果使用以下序列化设置:
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameAssemblyFormatHandling = Newtonsoft.Json.TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
};
并尝试序列化Model属性中的MyModel对象的MyState对象:
public class MyModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
public class MyState
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsReady { get; set; }
public DateTime LastUpdated { get; set; }
public object Model { get; set; }
}
Newtonsoft JsonConvert将创建一个具有上述“$type”属性的json对象:
{
"$type": "DemoSystemTextJson.Tests.MyState, DemoSystemTextJson.Tests",
"Id": 11,
"Name": "CurrentState",
"IsReady": true,
"LastUpdated": "2015-10-21T00:00:00",
"Model": {
"$type": "DemoSystemTextJson.Tests.MyModel, DemoSystemTextJson.Tests",
"FirstName": "Alex",
"LastName": "Brown",
"BirthDate": "1990-01-12T00:00:00"
}
}
如您所见,该"$type"属性已添加,用于在反序列化期间帮助识别类型。
还必须注意,该"$type"属性位于每个对象的第一项,否则,Newtonsoft JsonConvert将无法识别它。
具有PostgreSQL使用经验的开发人员可能会注意到,当您将json存储在Postgres数据库中并读回时,属性将没有以前的顺序,并且将以某种方式进行排序——这是因为Postgres将json对象存储为键值对进行内部优化。从Postgres读取此json时,可以得到它:
{
"Id": 11,
"Name": "CurrentState",
"IsReady": true,
"LastUpdated": "2015-10-21T00:00:00",
"Model": {
"FirstName": "Alex",
"LastName": "Brown",
"BirthDate": "1990-01-12T00:00:00",
"$type": "DemoSystemTextJson.Tests.MyModel, DemoSystemTextJson.Tests"
},
"$type": "DemoSystemTextJson.Tests.MyState, DemoSystemTextJson.Tests"
}
Newtonsoft JsonConvert将无法识别它。
为了处理WEB API和PostgreSQL,我们将结合使用System.Text.Json JsonSerializer和一些真正的程序员可能会添加到其代码中的魔术,让我们创建一个用户故事。
用户故事5:在System.Text.Json JsonSerializer中支持动态类型- 创建一个类,允许对包含未知类型属性的对象进行序列化和反序列化
- json属性的顺序不应影响反序列化过程
为了开始实现用户故事,我将创建一个DemoSystemTextJson类库(.NET Core),并添加xUnit测试项目(.NET Core)—— DemoSystemTextJson.Tests。
我更喜欢从编写测试开始,首先,我们需要将要序列化和反序列化的Model类,让我们将它们添加到测试项目中:
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoSystemTextJson.Tests
{
public class MyModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
public class MyState
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsReady { get; set; }
public DateTime LastUpdated { get; set; }
public object Model { get; set; }
}
}
有了这些类,我们可以创建第一个测试,该测试将检查是否可以立即反序列化MyState:
using System;
using Xunit;
using System.Text.Json;
namespace DemoSystemTextJson.Tests
{
public class JsonSerializationTests
{
public static MyState GetSampleData()
{
return new MyState
{
Id = 11,
Name = "CurrentState",
IsReady = true,
LastUpdated = new DateTime(2015, 10, 21),
Model = new MyModel { FirstName = "Alex",
LastName = "Brown", BirthDate = new DateTime(1990, 1, 12) }
};
}
[Fact]
public void CanDeserializeMyStateTest()
{
var data = GetSampleData();
Assert.Equal(typeof(MyModel), data.Model.GetType());
var json = JsonSerializer.Serialize(data);
var restoredData = JsonSerializer.Deserialize(json);
Assert.NotNull(restoredData.Model);
Assert.Equal(typeof(MyModel), restoredData.Model.GetType());
}
}
}
在测试类中,您可以看到为我们创建测试对象的static方法GetSampleData,而在CanDeserializeMyStateTest中,我们使用了该方法,并尝试将测试对象序列化为json并将其反序列化为restoredData变量。然后,我们检查restoredData.Model.GetType()是否是typeof(MyModel),但是如果您运行测试,则此Assert操作将失败。JsonSerializer无法识别Model类型,并在JsonElement其中放置原始json数据。
让我们帮助JsonSerializer并提供Model类型以在另一个测试中反序列化json原始数据:
[Fact]
public void CanDeserializeMyStateWithJsonElementTest()
{
var data = GetSampleData();
Assert.Equal(typeof(MyModel), data.Model.GetType());
var json = JsonSerializer.Serialize(data);
var restoredData = JsonSerializer.Deserialize(json);
Assert.NotNull(restoredData.Model);
Assert.Equal(typeof(JsonElement), restoredData.Model.GetType());
var modelJsonElement = (JsonElement)restoredData.Model;
var modelJson = modelJsonElement.GetRawText();
restoredData.Model = JsonSerializer.Deserialize(modelJson);
Assert.Equal(typeof(MyModel), restoredData.Model.GetType());
}
如果你运行这个测试,它会通过,因为现在我们读到JsonElement的restoredData.Model和明确的反序列化它:
restoredData.Model = JsonSerializer.Deserialize(modelJson);
因此,当我们知道Model属性对象的类型时,我们可以轻松地从原始json恢复它。
现在有了一个可以工作的原型,我们可以将实现封装在DemoSystemTextJson项目的一个类中,并将Model类型存储在json中。
修改模型方法存储Model类型的最简单直接的方法是扩展MyState类并向其添加ModelFullName属性。
让我们在DemoSystemTextJson项目中创建IJsonModelWrapper:
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoSystemTextJson
{
public interface IJsonModelWrapper
{
string ModelFullName { get; set; }
}
}
然后,将MyStateModified类添加到测试项目中以独立测试此方法:
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoSystemTextJson.Tests
{
public class MyStateModified : IJsonModelWrapper
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsReady { get; set; }
public DateTime LastUpdated { get; set; }
public object Model { get; set; }
// IJsonModelWrapper
public string ModelFullName { get; set; }
}
}
MyStateModified包含与MyState类相同的属性,并添加ModelFullName,用于存储反序列化的模型类型。
让我们创建一个将支持ModelFullName属性的填充和使用的JsonModelConverter:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
namespace DemoSystemTextJson
{
public class JsonModelConverter
{
private readonly Dictionary _modelTypes;
public JsonModelConverter()
{
_modelTypes = new Dictionary();
}
public string Serialize(IJsonModelWrapper source, Type modelType)
{
_modelTypes[modelType.FullName] = modelType;
source.ModelFullName = modelType.FullName;
var json = JsonSerializer.Serialize(source, source.GetType());
return json;
}
public T Deserialize(string json)
where T : class, IJsonModelWrapper, new()
{
var result = JsonSerializer.Deserialize(json, typeof(T)) as T;
var modelName = result.ModelFullName;
var objectProperties = typeof(T).GetProperties(BindingFlags.Public |
BindingFlags.Instance).Where(p => p.PropertyType == typeof(object));
foreach (var property in objectProperties)
{
var model = property.GetValue(result);
if (model is JsonElement)
{
var modelJsonElement = (JsonElement)model;
var modelJson = modelJsonElement.GetRawText();
var restoredModel = JsonSerializer.Deserialize
(modelJson, _modelTypes[modelName]);
property.SetValue(result, restoredModel);
}
}
return result as T;
}
}
}
您可以看到该Serialize方法使用Model类型名称填充ModelFullName属性,并且将类型保留在_modelTypes字典中以进行反序列化。
该Deserialize方法是泛型的,它期望结果对象类型作为模板参数。
它从反序列化的对象读取ModelFullName,然后找到类型为object的所有属性,然后使用在_modelTypes字典中找到的显式类型反序列化它们。
让我们用添加到测试项目中的单元测试来测试它:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Xunit;
using Xunit.Abstractions;
namespace DemoSystemTextJson.Tests
{
public class JsonModelConverterTests
{
private MyStateModified GetSampleData()
{
return new MyStateModified
{
Id = 11,
Name = "CurrentState",
IsReady = true,
LastUpdated = new DateTime(2015, 10, 21),
Model = new MyModel { FirstName = "Alex",
LastName = "Brown", BirthDate = new DateTime(1990, 1, 12) }
};
}
private readonly ITestOutputHelper _output;
public JsonModelConverterTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void JsonModelConverterSerializeTest()
{
var data = GetSampleData();
var converter = new JsonModelConverter();
var json = converter.Serialize(data, data.Model.GetType());
var restored = converter.Deserialize(json);
Assert.NotNull(restored.Model);
Assert.True(restored.Model.GetType() == typeof(MyModel));
}
[Fact]
public void JsonModelConverterPerformanceTest()
{
var sw = new Stopwatch();
sw.Start();
var converter = new JsonModelConverter();
for (int i = 0; i < 1000000; i++)
{
var data = GetSampleData();
var json = converter.Serialize(data, data.Model.GetType());
var restored = converter.Deserialize(json);
}
sw.Stop();
_output.WriteLine
($"JsonModelConverterPerformanceTest elapsed {sw.ElapsedMilliseconds} ms");
}
}
}
如果运行JsonModelConverterSerializeTest,则可以看到还原的对象具有正确的Model类型和值。
我添加了另一个测试JsonModelConverterPerformanceTest,该测试执行一百万次序列化和反序列化,并输出此操作所用的时间。
我的机器花了大约7秒钟。
它可以工作并且速度很快,但是让我们尝试另一种我们不想扩展model类的方法。
包装方法该wrapper是基于MyState的一个单独类,它具有ModelFullName特性,让我们在单元测试项目中创建它:
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoSystemTextJson.Tests
{
public class MyStateWrapper : MyState, IJsonModelWrapper
{
public string ModelFullName { get; set; }
}
}
JsonWrapperConverter 具有更复杂的实现:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
namespace DemoSystemTextJson
{
public class JsonWrapperConverter
{
private readonly Dictionary _wrapperByTypeDictionary;
private readonly Dictionary _modelTypes;
public JsonWrapperConverter()
{
_wrapperByTypeDictionary = new Dictionary();
_modelTypes = new Dictionary();
}
public void AddModel()
where M : class, new()
{
_modelTypes[typeof(M).FullName] = typeof(M);
}
public void AddWrapper()
where W : class, IJsonModelWrapper, new()
where T : class, new()
{
_wrapperByTypeDictionary[typeof(T)] = typeof(W);
}
public IJsonModelWrapper CreateInstance
(object source, Type wrapperType, Type modelType)
{
var json = JsonSerializer.Serialize(source);
var wrapper = JsonSerializer.Deserialize(json, wrapperType) as IJsonModelWrapper;
wrapper.ModelFullName = modelType.FullName;
return wrapper;
}
public string Serialize(object source, Type modelType)
{
Type wrapperType = _wrapperByTypeDictionary[source.GetType()];
var wrapper = CreateInstance(source, wrapperType, modelType);
var json = JsonSerializer.Serialize(wrapper, wrapperType);
return json;
}
public T Deserialize(string json)
where T : class, new()
{
Type wrapperType = _wrapperByTypeDictionary[typeof(T)];
var result = JsonSerializer.Deserialize(json, wrapperType) as IJsonModelWrapper;
var modelName = result.ModelFullName;
var objectProperties = typeof(T).GetProperties(BindingFlags.Public |
BindingFlags.Instance).Where(p => p.PropertyType == typeof(object));
foreach (var property in objectProperties)
{
var model = property.GetValue(result);
if (model is JsonElement)
{
var modelJsonElement = (JsonElement)model;
var modelJson = modelJsonElement.GetRawText();
var restoredModel = JsonSerializer.Deserialize
(modelJson, _modelTypes[modelName]);
property.SetValue(result, restoredModel);
}
}
return result as T;
}
}
}
对于每种源对象类型,我们将需要创建一个包装并将它们存储在_wrapperByTypeDictionary字典中和_modelTypes字典中。
AddModel和AddWrapper用于提供源和包装类型并进行存储。
CreateInstance方法被Serialize用于从源对象创建包装对象。包装对象将具有所有源属性和一个属性—— ModelFullName。
Deserialize方法还是泛型的。它通过字典中的源类型找到包装器类型,然后反序列化包装器并读取ModelFullName。然后,它使用反射来读取所有动态属性(typeof(object)),并从原始json恢复它们。
为了测试这一点,我们创建JsonWrapperConverterTests:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Xunit;
using Xunit.Abstractions;
namespace DemoSystemTextJson.Tests
{
public class JsonWrapperConverterTests
{
private MyState GetSampleData()
{
return new MyState
{
Id = 11,
Name = "CurrentState",
IsReady = true,
LastUpdated = new DateTime(2015, 10, 21),
Model = new MyModel { FirstName = "Alex",
LastName = "Brown", BirthDate = new DateTime(1990, 1, 12) }
};
}
private readonly ITestOutputHelper _output;
public JsonWrapperConverterTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void JsonWrapperConverterSerializeTest()
{
var data = GetSampleData();
var converter = new JsonWrapperConverter();
converter.AddWrapper();
converter.AddModel();
var json = converter.Serialize(data, data.Model.GetType());
var restored = converter.Deserialize(json);
Assert.NotNull(restored.Model);
Assert.True(restored.Model.GetType() == typeof(MyModel));
}
[Fact]
public void JsonWrapperConverterPerformanceTest()
{
var sw = new Stopwatch();
sw.Start();
var converter = new JsonWrapperConverter();
converter.AddWrapper();
converter.AddModel();
for (int i = 0; i < 1000000; i++)
{
var data = GetSampleData();
var json = converter.Serialize(data, data.Model.GetType());
var restored = converter.Deserialize(json);
}
sw.Stop();
_output.WriteLine($"JsonWrapperConverterPerformanceTest elapsed
{sw.ElapsedMilliseconds} ms");
}
[Fact]
public void JsonNewtonsoftPerformanceTest()
{
var sw = new Stopwatch();
sw.Start();
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameAssemblyFormatHandling =
Newtonsoft.Json.TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
};
for (int i = 0; i < 1000000; i++)
{
var data = GetSampleData();
var json = Newtonsoft.Json.JsonConvert.SerializeObject(data, settings);
var restored = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
}
sw.Stop();
_output.WriteLine($"JsonNewtonsoftPerformanceTest elapsed
{sw.ElapsedMilliseconds} ms");
}
}
}
如果运行JsonWrapperConverterSerializeTest,您将看到包装方法也有效。
我还添加JsonWrapperConverterPerformanceTest和JsonNewtonsoftPerformanceTest以检查了它们的性能。
如果运行所有性能测试,您将能够看到与我相似的结果:
JsonModelConverterPerformanceTest
5654毫秒
JsonWrapperConverterSerializeTest
9760毫秒
JsonNewtonsoftPerformanceTest
10671毫秒
总结今天,我们已经表明,如果您需要将项目从Newtonsoft迁移到System.Text.Json序列化器,则会遇到一些困难,因为System.Text.Json序列化器不支持使用该“$type”属性进行动态对象反序列化。我们实现了两种方法来对注入ModelFullName属性的动态对象进行序列化和反序列化,以按Model类型进行显式反序列化。
如果可以修改model类并添加ModelFullName属性,则可以使用最快和最简单的序列化,但是如果不能更改model类,则可以使用比Newtonsoft序列化还快的包装方法。