【问题标题】:How to deal with nullable reference types with System.Text.Json?如何使用 System.Text.Json 处理可为空的引用类型?
【发布时间】:2020-02-02 14:07:58
【问题描述】:

我已将我的项目升级到 netcore 3.0,我正在重构一个项目以使用新的可为空引用类型功能,但由于以下问题很快就卡住了。

假设我使用了一个返回以下 JSON 的 REST api:

{
  "Name": "Volvo 240",
  "Year": 1989
}

此 api 始终返回名称/年份,因此它们不可为空。

我会使用这个简单的类进行反序列化:

public class Car
{
    public string Name {get; set;}
    public int Year {get; set;}
}

我会使用新的 System.Text.Json 将其反序列化为 Car 实例

var car = JsonSerializer.Deserialize<Car>(json);

这一切都有效,但是当启用可空引用类型时,我在Car 类中收到警告,Name 被声明为不可空但可以为空。我明白为什么我会得到这个,因为可以在不初始化 Name 属性的情况下实例化这个对象。

所以理想情况下Car 应该是这样的:

public class Car
{
    public string Name { get; }
    public int Year { get; }

    public Car(string name, int year)
    {
        Name = name;
        Year = year;
    }
}

但这不起作用,因为System.Text.Json 序列化程序不支持带参数的构造函数。

所以我的问题是:我将如何声明 Car 以使 Name 不可为空并使其与 System.Text.Json 一起使用而不会收到“不可为空”警告?`

我不想让它可以为空,因为在启用可以为空的引用类型时,我基本上必须对所有内容进行空检查,并且由于我的示例中的 REST API 说它们总是被提供,它们不应该可以为空.

【问题讨论】:

    标签: json .net-core-3.0 nullable-reference-types system.text.json


    【解决方案1】:

    更新

    .NET 5 的System.Text.Json 现在支持参数化构造函数,所以这应该不再是问题了。

    https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-immutability?pivots=dotnet-5-0

    下面的旧答案

    在阅读了msdocs 之后,我发现了如何解决这个问题。

    所以在System.Text.Json 无法在其构造函数中实例化带有参数的类之前,Car 类必须如下所示:

    public class Car
    {
        public string Name { get; set; } = default!;
        public int Year { get; set; }
    }
    

    【讨论】:

    • 就我个人而言,我宁愿使用= "";,否则你基本上是在说(在我看来)“这个属性永远不会为空(开玩笑)” .
    • 或者使用 JSON.NET。 System.Text.Json还没有完全替代。 .至于= default……还没有。当我在 2019 年 10 月读到这篇文章时,我想You are setting this to null!。也许明年。
    • 这类似于微软推荐的实体对象。 EF 团队实际上建议使用null! 作为一种说法,“我知道这绝不是null。”尽管我喜欢这种可以为空的东西的潜力,但这确实感觉有点不对劲......
    • @MattJacobi System.Text.Json 很乐意将null 反序列化为不可为空的字符串属性;至少在默认情况下,不管反序列化类的 #nullable 状态如何(我刚刚尝试过)——是否有一些选项可以触发这种更严格的“我的意思是可空性”模式?
    • (对于 System.Text.Json,默认情况下,通过属性省略显式 null 和隐式 null 都不是可空性错误)
    【解决方案2】:

    更新 如果您使用net5,请使用@langen 指出的现在提供的参数化构造函数支持。下面的其他内容仍然有用。

    原创 稍微另类的方法。 System.Text.Json 使用私有无参数构造函数似乎没有问题。所以你至少可以做到以下几点:

    public class Car
    {
        public string Name { get; set; }
        public int Year { get; set; }
    
        // For System.Text.Json deserialization only
        #pragma warning disable CS8618 // Non-nullable field is uninitialized.
        private Car() { }
        #pragma warning restore CS8618
    
        public Car(string name, int year)
        {
            Name = name
                ?? throw new ArgumentNullException(nameof(name));
            Year = year;
        }
    }
    

    好处是:

    • 从您自己的代码中初始化对象必须通过公共 ctor。
    • 您无需对每个属性都执行 = null!;

    使用 S.T.Json 和可为空的引用类型的缺点:

    • S.T.Json 仍然需要属性的设置器在反序列化期间实际设置值。我尝试使用私有对象,但不行,所以我们仍然无法获得不可变对象...

    【讨论】:

    • 仅供参考,System.Text.Json 和 Json.Net 都支持参数化构造函数,但它们不支持可为空的引用类型。使用您的示例代码,两个反序列化器都会很乐意将Name 属性设置为null,这是一个很大的问题。
    • 空值检查并加入 ctor 总是一个好主意,因为你不能真正信任别人,好点。
    • 在 ctor 中抛出异常会导致 ASP.net 返回 500,这不是很好。理想的行为是 ASP.net 返回一个 4XX 并告诉客户端出了什么问题。
    • 公平地说,最初的问题并不是关于模型绑定,这总是有点痛苦。 FWIW,ASP.NET Core 6 现在考虑模型绑定/验证中的可空性并自动返回 400 Bad Request。不确定是否将 ctor 投入使用会导致 500 仍然存在。
    【解决方案3】:

    另一种选择,对于那些想要处理具有有意义异常的缺失属性的人:

    using System;
    
    public class Car
    {
        private string? name;
        private int? year;
    
        public string Name
        {
            get => this.name ?? throw new InvalidOperationException($"{nameof(this.Name)} was not set.");
            set => this.name = value;
        }
    
        public int Year
        {
            get => this.year ?? throw new InvalidOperationException($"{nameof(this.Year)} was not set.");
            set => this.year = value;
        }
    }
    
    

    【讨论】:

      猜你喜欢
      • 2017-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多