c# - .Net Core 3.0 JsonSerializer 填充现有对象

标签 c# asp.net-core razor-pages asp.net-core-3.0 system.text.json

我正在准备从 ASP.NET Core 2.2 迁移到 3.0。

因为我没有使用更高级的 JSON 特性(但可能如下所述),并且 3.0 现在带有一个内置的 JSON 命名空间/类,System.Text.Json ,我决定看看是否可以删除以前的默认值 Newtonsoft.Json .
请注意,我知道 System.Text.Json不会完全取代 Newtonsoft.Json .

我设法在任何地方都做到了,例如

var obj = JsonSerializer.Parse<T>(jsonstring);

var jsonstring = JsonSerializer.ToString(obj);

但在一个地方,我填充了一个现有对象。

Newtonsoft.Json一个可以做到

JsonConvert.PopulateObject(jsonstring, obj);

内置 System.Text.Json namespace 有一些额外的类,比如 JsonDocumnet , JsonElementUtf8JsonReader ,尽管我找不到任何将现有对象作为参数的对象。

我也没有足够的经验来了解如何利用现有的。

可能有 a possible upcoming feature in .Net Core (感谢 Mustafa Gursel 的链接),但与此同时(如果没有怎么办)...

...我现在想知道,是否有可能实现与 PopulateObject 类似的东西? ?

我的意思是,是否可以使用其他任何一个 System.Text.Json类来完成相同的任务,并仅更新/替换属性集?,...或其他一些巧妙的解决方法?


这是我正在寻找的示例输入/输出,它需要是通用的,因为传递给反序列化方法的对象是 <T> 类型的).我有 2 个 Json 字符串要解析成一个对象,其中第一个设置了一些默认属性,第二个设置了一些默认属性,例如

请注意,属性值可以是除 string 之外的任何其他类型.

Json 字符串 1:

{
  "Title": "Startpage",
  "Link": "/index",
}

Json 字符串 2:

{
  "Head": "Latest news"
  "Link": "/news"
}

使用上面的 2 个 Json 字符串,我想要一个对象导致:

{
  "Title": "Startpage",
  "Head": "Latest news",
  "Link": "/news"
}

如上例所示,如果第二个属性有值/已设置,它将替换第一个中的值(如“Head”和“Link”),如果没有,则现有值保持不变(如“Title” )

最佳答案

因此,假设 Core 3 不支持开箱即用的功能,让我们尝试解决这个问题。那么,我们的问题是什么?

我们想要一种方法,用 json 字符串中的属性覆盖现有对象的某些属性。所以我们的方法将有一个签名:

void PopulateObject<T>(T target, string jsonSource) where T : class

我们真的不想要任何自定义解析,因为它很麻烦,所以我们将尝试明显的方法 - 反序列化 jsonSource 并将结果属性复制到我们的对象中。然而,我们不能就这么走了

T updateObject = JsonSerializer.Parse<T>(jsonSource);
CopyUpdatedProperties(target, updateObject);

那是因为对于一个类型

class Example
{
    int Id { get; set; }
    int Value { get; set; }
}

和一个 JSON

{
    "Id": 42
}

我们将得到 updateObject.Value == 0。现在我们不知道 0 是新的更新值还是它只是没有更新,所以我们需要确切地知道 jsonSource 包含哪些属性。

幸运的是,System.Text.Json API 允许我们检查已解析的 JSON 的结构。

using var json = JsonDocument.Parse(jsonSource).RootElement;

我们现在可以枚举所有属性并复制它们。

foreach (var property in json.EnumerateObject())
{
    OverwriteProperty(target, property);
}

我们将使用反射复制值:

void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
    var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);

    if (propertyInfo == null)
    {
        return;
    }

    var propertyType = propertyInfo.PropertyType;
    v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
    var parsedValue = JsonSerializer.Deserialize(
        updatedProperty.Value.GetRawText(), 
        propertyType);

    propertyInfo.SetValue(target, parsedValue);
} 

我们可以在这里看到我们正在做的是更新。如果该对象包含另一个复杂对象作为其属性,则该对象将作为一个整体被复制和覆盖,而不是更新。如果您需要深度更新,则需要更改此方法以提取属性的当前值,然后如果属性的类型是引用类型(即还需要接受 Type 作为 PopulateObject 中的参数。

将它们结合在一起我们得到:

void PopulateObject<T>(T target, string jsonSource) where T : class
{
    using var json = JsonDocument.Parse(jsonSource).RootElement;

    foreach (var property in json.EnumerateObject())
    {
        OverwriteProperty(target, property);
    }
}

void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
    var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);

    if (propertyInfo == null)
    {
        return;
    }

    var propertyType = propertyInfo.PropertyType;
    v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
    var parsedValue = JsonSerializer.Deserialize(
        updatedProperty.Value.GetRawText(), 
        propertyType);

    propertyInfo.SetValue(target, parsedValue);
} 

这有多稳健?好吧,它肯定不会对 JSON 数组做任何有意义的事情,但我不确定您如何期望 PopulateObject 方法开始对数组起作用。我不知道它与 Json.Net 版本的性能相比如何,您必须自己测试。根据设计,它还会默默地忽略不在目标类型中的属性。我认为这是最明智的方法,但您可能不这么认为,在那种情况下,属性 null-check 必须替换为异常抛出。

编辑:

我继续实现深拷贝:

void PopulateObject<T>(T target, string jsonSource) where T : class => 
    PopulateObject(target, jsonSource, typeof(T));

void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>
    OverwriteProperty(target, updatedProperty, typeof(T));

void PopulateObject(object target, string jsonSource, Type type)
{
    using var json = JsonDocument.Parse(jsonSource).RootElement;

    foreach (var property in json.EnumerateObject())
    {
        OverwriteProperty(target, property, type);
    }
}

void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
    var propertyInfo = type.GetProperty(updatedProperty.Name);

    if (propertyInfo == null)
    {
        return;
    }

    var propertyType = propertyInfo.PropertyType;
    object parsedValue;

    if (propertyType.IsValueType || propertyType == typeof(string))
    {
        ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
        parsedValue = JsonSerializer.Deserialize(
            updatedProperty.Value.GetRawText(),
            propertyType);
    }
    else
    {
        parsedValue = propertyInfo.GetValue(target);
        P̶o̶p̶u̶l̶a̶t̶e̶O̶b̶j̶e̶c̶t̶(̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶,̶ ̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
        PopulateObject(
            parsedValue, 
            updatedProperty.Value.GetRawText(), 
            propertyType);
    }

    propertyInfo.SetValue(target, parsedValue);
}

为了使它更健壮,你要么必须有一个单独的 PopulateObjectDeep 方法,要么传递 PopulateObjectOptions 或类似的带有深/浅标志的东西。

编辑 2:

深度复制的重点是如果我们有一个对象

{
    "Id": 42,
    "Child":
    {
        "Id": 43,
        "Value": 32
    },
    "Value": 128
}

并填充它

{
    "Child":
    {
        "Value": 64
    }
}

我们会得到

{
    "Id": 42,
    "Child":
    {
        "Id": 43,
        "Value": 64
    },
    "Value": 128
}

在浅拷贝的情况下,我们会在复制的 child 中得到 Id = 0

编辑 3:

正如@ldam 指出的那样,这在稳定的 .NET Core 3.0 中不再有效,因为 API 已更改。 Parse 方法现在是 Deserialize,您必须深入挖掘才能获得 JsonElement 的值。有 an active issue in the corefx repo允许直接反序列化 JsonElement。现在最接近的解决方案是使用 GetRawText()。我继续编辑上面的代码以使其工作,留下旧版本删除线。

关于c# - .Net Core 3.0 JsonSerializer 填充现有对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56835040/

相关文章:

.net - Blazor 应用程序如何能够在不调用 Startup.cs 中的 MapRazorPages() 的情况下公开 Razor Pages 端点?

c# - 从字符串中提取一些参数

c# - 如何在场景的 C# 代码中访问我在 AutoLoad 脚本中创建的单例? (戈多 3)

c# - 对排序列表进行分组而不影响 linq 中的排序

asp.net-core - 用于 DataAnnotations 本地化的单个资源文件

asp.net-core - 无法刷新 Hangfire 仪表板中的统计信息

c# - 保留一个 Dictionary<Type, MyClass<T>> ,其中元素可以按类型引用

asp.net-core - 不能将 'Microsoft.AspNet.OData.Routing.ODataRoute' 与端点路由一起使用。 ASP Net Core 2.2 的异常

ASP.Net Core razor 页面处理程序成为一条包罗万象的路线

razor-pages - ASP.NET Core 3.1 Razor 页面 : How to automatically redirect to Login page from Index page?