我可以让 Json.net 使用“主”构造函数反序列化 C# 9 记录类型,就好像它有 [JsonConstructor] 一样?

sol*_*ish 7 c# json json.net c#-9.0

在 .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)
    { }
}
Run Code Online (Sandbox Code Playgroud)

正如您所料,它们被序列化并发送到其他地方进行处理。发送者调用双参数构造函数并获得一个新的 Id,但反序列化器需要使用记录声明隐含的“主要”3 参数构造函数。我正在使用 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);
Run Code Online (Sandbox Code Playgroud)

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

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

但这会尝试将该属性应用于无效的类型。这是 C# 中的语法错误,但显然它适用于 F#

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

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

public record SomethingHappenedEvent(Guid Id, object TheThing, string WhatHappened) { }
Run Code Online (Sandbox Code Playgroud)

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

AAA*_*ddd 18

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

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

除此之外,您将该属性放在了错误的位置。简而言之,属性需要位于构造函数上。

疯狂设计的荒谬的例子:

给定

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

测试

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);
Run Code Online (Sandbox Code Playgroud)

输出

{
  "Id": "2905cfaf-d13d-4df1-af83-e4dcde20d44f"
}
True
Run Code Online (Sandbox Code Playgroud)

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

var json = JsonSerializer.Serialize(record);
var otherSideRecord = JsonSerializer.Deserialize<TestRecord>(json);
Run Code Online (Sandbox Code Playgroud)

  • 有趣的。这实际上在示例中有效,并且可能在许多现实案例中有效。但这有点奇怪。2 参数构造函数是反序列化器使用的*错误的构造函数,因此在其上使用该属性似乎很奇怪。事实证明这里实际上是可以的,因为 json.net *之后*调用了 Id setter,覆盖了接收端 Guid.NewGuid 生成的值。如果 2 参数构造函数由于某种原因实际上是不正确的,而不仅仅是多余的,那么这将会失败,但这显然是一个更狭窄的情况。 (3认同)

sol*_*ish 7

我在试验时遇到了另一种选择。我会把它留在这里以防它对某人有用。您可以将自定义构造函数转换为工厂方法。这样就只剩下一个构造函数供解串器查找。

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

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

var myEvent = SomethingHappenedEvent.Create("partyPeople", "gotDown");
Run Code Online (Sandbox Code Playgroud)

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