我可以使用 "primary" 构造函数使 Json.net 反序列化 C# 9 记录类型,就像它具有 [JsonConstructor] 一样吗?

Can I make Json.net deserialize a C# 9 record type with the "primary" constructor, as if it had [JsonConstructor]?

在 .NET 5.0 上使用 C# 9,我有一堆记录类型,如下所示:

public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened)
{
    public SomethingHappenedEvent(object theThing, string whatHappened)
        : this(Guid.NewGuid(), theThing, whatHappened)
    { }
}

如您所料,它们会被序列化并发送到其他地方进行处理。发件人调用双参数构造函数并获得新的 Id,但反序列化器需要使用记录声明隐含的“主要”三参数构造函数。 我正在使用 Newtonsoft Json.NET,我当然希望它能起作用:

        var record = new SomethingHappenedEvent("roof", "caught fire");
        var json = JsonConvert.SerializeObject(record);
        var otherSideRecord = JsonConvert.DeserializeObject<SomethingHappenedEvent>(json);
        Assert.AreEqual(record, otherSideRecord);

当然不是。它抛出 JsonSerializationException。它找不到正确的构造函数,因为有两个构造函数,既不是默认的零参数构造函数,也没有用 JsonConstructorAttribute 标记。 我的问题真的是“我有什么选择来获得类似的东西?”。这会很棒:

[JsonConstructor]
public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened)
{
    public SomethingHappenedEvent(object theThing, string whatHappened)
        : this(Guid.NewGuid(), theThing, whatHappened)
    { }
}

但是那试图将属性应用于无效的类型。 这是 C# 中的语法错误,尽管 .

public record SomethingHappenedEvent
[JsonConstructor]
    (Guid Id, object TheThing, string WhatHappened)
{
    public SomethingHappenedEvent(object theThing, string whatHappened)
        : this(Guid.NewGuid(), theThing, whatHappened)
    { }
}

我目前的解决方案是将这些类型保留为普通 类 并使用所有额外的样板。我也知道我可以省略自定义构造函数并让我的调用者生成 ID。这是可行的,因为 json.net 只能找到一个构造函数。这当然很简洁!但我不喜欢在所有调用站点重复代码,即使在这种情况下它很小。

public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened) { }

FWIW 听起来 System.Text.Json 具有相同的限制。

首先,您只需在创建自己的构造函数时执行此操作。这是因为在实例化时它不知道要使用哪个。

其次,请注意(默认情况下)反序列化器将使用 属性 和构造函数名称,并覆盖您在实际构造函数类型中省略的名称。此外,构造函数中的每个参数都必须在反序列化时绑定到对象 属性 或字段。如果您不注意前者,可能会导致细微的错误,但这不仅限于 records.

除此之外,您的属性放错了地方。简而言之,属性需要在 constructor.

胡乱编造的荒谬示例:

给定

public record TestRecord(Guid Id)
{
   [JsonConstructor]
   public TestRecord(object theThing, string whatHappened) : this(Guid.NewGuid())
   {
   }
}

测试

var record = new TestRecord(Guid.NewGuid());
var json = JsonConvert.SerializeObject(record,Formatting.Indented);
Console.WriteLine(json);
var otherSideRecord = JsonConvert.DeserializeObject<TestRecord>(json);

// note this paradoxically still works, because it has overwritten the ID
Console.WriteLine(record == otherSideRecord);

输出

{
  "Id": "2905cfaf-d13d-4df1-af83-e4dcde20d44f"
}
True

请注意,该属性也适用于 Text.Json

var json = JsonSerializer.Serialize(record);
var otherSideRecord = JsonSerializer.Deserialize<TestRecord>(json);

我在试验时运行跨过另一个选项。我会把它留在这里以防它对某人有用。您可以将自定义构造函数转换为工厂方法。这只剩下一个构造函数供反序列化器查找。

public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened)
{
    public static SomethingHappenedEvent Create(object theThing, string whatHappened)
        => new(Guid.NewGuid(), theThing, whatHappened);
}

它改变了调用站点的语法。只需调用 Create 而不是 new:

var myEvent = SomethingHappenedEvent.Create("partyPeople", "gotDown");

当然,您可以将静态工厂方法推送到单独的工厂对象 - 如果您的对象构造具有更丰富的依赖关系,这可能会有用。

您可以使用属性添加默认构造函数。 Json.Net 将填充属性。

如果需要,可以将其设为私有,这样 class 的其他用户 none 就可以错误地使用它。 Json.Net 仍然会找到它:

public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened)
{
    [JsonConstructor]
    private SomethingHappenedEvent()
    { }

    public SomethingHappenedEvent(object theThing, string whatHappened)
        : this(Guid.NewGuid(), theThing, whatHappened)
    { }
}