由于对象的当前状态 (System.Text.Json),操作无效

Operation is not valid due to the current state of the object (System.Text.Json)

我们有一个 API,它只是将传入的 JSON 文档发布到消息总线,并为每个文档分配了一个 GUID。我们正在从 .Net Core 2.2 升级到 3.1,目标是用新的 System.Text.Json 库替换 NewtonSoft。

我们反序列化传入文档,将 GUID 分配给其中一个字段,然后在发送到消息总线之前重新序列化。不幸的是,重新序列化失败,异常 Operation is not valid due to the current state of the object.

这是一个显示问题的控制器:-

using System;
using System.Net;
using Project.Models;
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Text;
using System.Text.Json;

namespace Project.Controllers
{
    [Route("api/test")]
    public class TestController : Controller
    {
        private const string JSONAPIMIMETYPE = "application/vnd.api+json";

        public TestController()
        {
        }

        [HttpPost("{eventType}")]
        public async System.Threading.Tasks.Task<IActionResult> ProcessEventAsync([FromRoute] string eventType)
        {
            try
            {
                JsonApiMessage payload;

                using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) {
                    string payloadString = await reader.ReadToEndAsync();

                    try {
                        payload = JsonSerializer.Deserialize<JsonApiMessage>(payloadString);
                    }
                    catch (Exception ex) {
                        return StatusCode((int)HttpStatusCode.BadRequest);
                    }
                }

                if ( ! Request.ContentType.Contains(JSONAPIMIMETYPE) )
                {
                    return StatusCode((int)HttpStatusCode.UnsupportedMediaType);
                }

                Guid messageID = Guid.NewGuid();
                payload.Data.Id = messageID.ToString();

                // we would send the message here but for this test, just reserialise it
                string reserialisedPayload = JsonSerializer.Serialize(payload);

                Request.HttpContext.Response.ContentType = JSONAPIMIMETYPE;
                return Accepted(payload);
            }
            catch (Exception ex) 
            {
                return StatusCode((int)HttpStatusCode.InternalServerError);
            }
        }
    }
}

JsonApiMessage对象定义如下:-

using System.Text.Json;
using System.Text.Json.Serialization;

namespace Project.Models
{
    public class JsonApiMessage
    {
        [JsonPropertyName("data")]
        public JsonApiData Data { get; set; }

        [JsonPropertyName("included")]
        public JsonApiData[] Included { get; set; }
    }

    public class JsonApiData
    {
        [JsonPropertyName("type")]
        public string Type { get; set; }

        [JsonPropertyName("id")]
        public string Id { get; set; }

        [JsonPropertyName("attributes")]
        public JsonElement Attributes { get; set; }

        [JsonPropertyName("meta")]
        public JsonElement Meta { get; set; }

        [JsonPropertyName("relationships")]
        public JsonElement Relationships { get; set; }
    }
}

调用示例如下所示:-

POST http://localhost:5000/api/test/event
Content-Type: application/vnd.api+json; charset=UTF-8

{
  "data": {
    "type": "test",
    "attributes": {
      "source": "postman",
      "instance": "jg",
      "level": "INFO",
      "message": "If this comes back with an ID, the API is probably working"
    }
  }
}

当我在 Visual Studio 的断点处检查 payload 的内容时,它在顶层看起来不错,但 JsonElement 位看起来不透明,所以我不知道如果它们被正确解析。它们的结构可能不同,所以我们只关心它们是否有效 JSON。在旧的 NewtonSoft 版本中,它们是 JObjects。

添加 GUID 后,在断点处检查时它出现在 payload 对象中,但我怀疑该问题与对象中的其他元素为只读或类似情况有关.

异常是对的——对象的状态是无效的。 MetaRelasionships 元素不可为空,但 JSON 字符串不包含它们。 de 序列化对象在那些无法序列化的属性中以 Undefined 值结束。

    [JsonPropertyName("meta")]
    public JsonElement? Meta { get; set; }

    [JsonPropertyName("relationships")]
    public JsonElement? Relationships { get; set; }

快速解决方法是将这些属性更改为 JsonElement?。这将允许正确的反序列化和序列化。默认情况下,缺少的元素将作为空值发出:

"meta": null,
"relationships": null

要忽略它们,请添加 IgnoreNullValues =true 选项:

var newJson = JsonSerializer.Serialize(payload, new JsonSerializerOptions 
                           { WriteIndented = true,IgnoreNullValues =true });

真正的 解决方案是删除所有代码。它 妨碍 使用 System.Text.Json。独自一人,ASP.NET Core 使用管道读取输入流 而无需 分配,反序列化有效负载并使用反序列化对象作为参数调用方法,使用最少的分配。任何 return 值都以相同的方式序列化。

问题的代码虽然分配了很多 - 它在 StreamReader 中缓存输入,然后将整个有效负载缓存在 payloadString 中,然后再次缓存为 payload 对象。反向过程也使用临时字符串。此代码占用的内存 至少 是 ASP.NET 核心使用内存的两倍。

操作代码应该是:

[HttpPost("{eventType}")]
public async Task<IActionResult> ProcessEventAsync([FromRoute] string eventType,
                                                   MyApiData payload)
{
    Guid messageID = Guid.NewGuid();
    payload.Data.Id = messageID.ToString();

    return Accepted(payload);
}

其中 MyApiData 是一个强类型对象。 Json例子的形状对应于:

public class Attributes
{
    public string source { get; set; }
    public string instance { get; set; }
    public string level { get; set; }
    public string message { get; set; }
}

public class Data
{
    public string type { get; set; }
    public Attributes attributes { get; set; }
}

public class MyApiData
{
    public Data data { get; set; }
    public Data[] included {get;set;}
}

所有其他检查均由 ASP.NET Core 本身执行 - ASP.NET Core 将拒绝任何 POST 没有正确 MIME 类型的内容。如果请求格式错误,它将 return 返回 400。如果代码抛出 return 500

您的问题可以通过以下更小的示例重现。定义以下模型:

public class JsonApiMessage
{
    public JsonElement data { get; set; }
}

然后尝试反序列化并重新序列化一个空的 JSON 对象,如下所示:

var payload = JsonSerializer.Deserialize<JsonApiMessage>("{}");
var newJson = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });

你会得到一个异常(演示fiddle #1 here):

System.InvalidOperationException: Operation is not valid due to the current state of the object.
   at System.Text.Json.JsonElement.WriteTo(Utf8JsonWriter writer)
   at System.Text.Json.Serialization.Converters.JsonConverterJsonElement.Write(Utf8JsonWriter writer, JsonElement value, JsonSerializerOptions options)

问题似乎是 JsonElement is a struct, and the default value for this struct can't be serialized. In fact, simply doing JsonSerializer.Serialize(new JsonElement()); throws the same exception (demo fiddle #2 here)。 (这与 JObject 形成对比,后者是一个默认值当然是 null 的引用类型。)

那么,您有哪些选择?您可以使所有 JsonElement 属性都可以为空,并在重新序列化时设置 IgnoreNullValues = true

public class JsonApiData
{
    [JsonPropertyName("type")]
    public string Type { get; set; }

    [JsonPropertyName("id")]
    public string Id { get; set; }

    [JsonPropertyName("attributes")]
    public JsonElement? Attributes { get; set; }

    [JsonPropertyName("meta")]
    public JsonElement? Meta { get; set; }

    [JsonPropertyName("relationships")]
    public JsonElement? Relationships { get; set; }
}

然后:

var reserialisedPayload  = JsonSerializer.Serialize(payload, new JsonSerializerOptions { IgnoreNullValues = true });

演示 fiddle #3 here.

或者,在 .NET 5 或更高版本 中,您可以将所有 JsonElement 属性标记为 [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]:

public class JsonApiData
{
    // Remainder unchanged

    [JsonPropertyName("attributes")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public JsonElement Attributes { get; set; }

    [JsonPropertyName("meta")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public JsonElement Meta { get; set; }

    [JsonPropertyName("relationships")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public JsonElement Relationships { get; set; }
}

这样做会导致在序列化过程中跳过未初始化的元素,而无需修改序列化选项。

演示 fiddle #4 here.

或者,您可以通过将 Id 以外的所有 JSON 属性绑定到 JsonExtensionData 属性 来简化您的数据模型,如下所示:

public class JsonApiData
{
    [JsonPropertyName("id")]
    public string Id { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement> ExtensionData { get; set; }
}

这种方法避免了在重新序列化时手动设置 IgnoreNullValues,因此 ASP.NET Core 将自动正确地重新序列化模型。

演示 fiddle #5 here.