65.9K
CodeProject 正在变化。 阅读更多。
Home

在 System.Text.Json 序列化中添加 $type,就像 Newtonsoft 处理动态对象属性一样

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2020 年 11 月 6 日

CPOL

5分钟阅读

viewsIcon

36724

本文介绍了一种使用 System.Text.Json JsonSerializer 序列化包含动态类型的模型的技术,该技术不支持 $type。

引言

欢迎阅读我为 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 经验的开发者可能注意到,当您在 Postgres 数据库中存储 JSON 并将其读回时,属性的顺序将不再是之前的顺序,而是以某种方式排序——这是因为 Postgres 出于内部优化目的将 JSON 对象存储为键值对。您可以从 Postgres 读取 JSON 时获得此 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<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 类型,而是将其放入了一个包含原始 JSON 数据的 JsonElement

让我们帮助 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());
        }

如果您运行此测试,它将通过,因为现在我们从 restoredData.Model 中读取 JsonElement 并显式地对其进行了反序列化

restoredData.Model = JsonSerializer.Deserialize<MyModel>(modelJson);

因此,当知道 Model 属性对象的类型时,我们可以轻松地从原始 JSON 中恢复它。

现在有了可用的原型,我们可以在 DemoSystemTextJson 项目中将我们的实现封装到一个类中,并将 Model 类型存储在 JSON 的某个位置。

修改 Model 方法

存储 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,该属性将存储模型类型以供反序列化。

让我们创建 JsonModelConverter,它将支持 ModelFullName 属性的填充和消耗

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;
        }
    }
}

对于每个源对象类型,我们需要创建一个包装器,并将它们存储在 _modelTypes_wrapperByTypeDictionary 字典中。

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 序列化更快的方法,即包装器方法。

历史

  • 2020 年 11 月 6 日:初始版本
© . All rights reserved.