将$type添加到System.Text.Json序列化中,就像Newtonsoft那样用于动态对象属性

目录

介绍

用户故事5:在System.Text.Json JsonSerializer中支持动态类型

演示项目和测试

修改模型方法

包装方法

总结


Pro Coders团队最近将一个大型项目从Newtonsoft迁移到System.Text.Json序列化器,并且由于它不支持使用$type属性进行动态对象反序列化,因此我们实现了一种方法,用于对动态对象进行序列化和反序列化,并注入ModelFullName属性来通过模型类型显式反序列化。

介绍

欢迎来到我为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数据库中并读回时,属性将没有以前的顺序,并且将以某种方式进行排序——这是因为Postgresjson对象存储为键值对进行内部优化。从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 APIPostgreSQL,我们将结合使用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<MyState>(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<MyState>(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<MyModel>(modelJson);
    Assert.Equal(typeof(MyModel), restoredData.Model.GetType());
}

如果你运行这个测试,它会通过,因为现在我们读到JsonElementrestoredData.Model和明确的反序列化它:

restoredData.Model = JsonSerializer.Deserialize<MyModel>(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<string, Type> _modelTypes;

        public JsonModelConverter()
        {
            _modelTypes = new Dictionary<string, Type>();
        }

        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<T>(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<MyStateModified>(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<MyStateModified>(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<Type, Type> _wrapperByTypeDictionary;
        private readonly Dictionary<string, Type> _modelTypes;

        public JsonWrapperConverter()
        {
            _wrapperByTypeDictionary = new Dictionary<Type, Type>();
            _modelTypes = new Dictionary<string, Type>();
        }

        public void AddModel<M>()
            where M : class, new()
        {
            _modelTypes[typeof(M).FullName] = typeof(M);
        }

        public void AddWrapper<W, T>()
            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<T>(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字典中。

AddModelAddWrapper用于提供源和包装类型并进行存储。

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<MyStateWrapper, MyState>();
            converter.AddModel<MyModel>();

            var json = converter.Serialize(data, data.Model.GetType());
            var restored = converter.Deserialize<MyState>(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<MyStateWrapper, MyState>();
            converter.AddModel<MyModel>();

            for (int i = 0; i < 1000000; i++)
            {
                var data = GetSampleData();
                var json = converter.Serialize(data, data.Model.GetType());
                var restored = converter.Deserialize<MyState>(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<MyState>(json);
            }

            sw.Stop();
            _output.WriteLine($"JsonNewtonsoftPerformanceTest elapsed 
                             {sw.ElapsedMilliseconds} ms");
        }
    }
}

如果运行JsonWrapperConverterSerializeTest,您将看到包装方法也有效。

我还添加JsonWrapperConverterPerformanceTestJsonNewtonsoftPerformanceTest以检查了它们的性能。

如果运行所有性能测试,您将能够看到与我相似的结果:

JsonModelConverterPerformanceTest

5654毫秒

JsonWrapperConverterSerializeTest

9760毫秒

JsonNewtonsoftPerformanceTest

10671毫秒

总结

今天,我们已经表明,如果您需要将项目从Newtonsoft迁移到System.Text.Json序列化器,则会遇到一些困难,因为System.Text.Json序列化器不支持使用该“$type”属性进行动态对象反序列化。我们实现了两种方法来对注入ModelFullName属性的动态对象进行序列化和反序列化,以按Model类型进行显式反序列化。

如果可以修改model类并添加ModelFullName属性,则可以使用最快和最简单的序列化,但是如果不能更改model类,则可以使用比Newtonsoft序列化还快的包装方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值