12

我们有一个 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"
    }
  }
}

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

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

4

2 回答 2

13

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

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

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

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

你会得到一个例外(这里是演示小提琴#1 ):

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是 a struct,并且该结构的默认值无法序列化。事实上,简单地做JsonSerializer.Serialize(new JsonElement());会引发同样的异常(这里是演示小提琴#2 )。(这与JObjectwhich 是默认值当然是 . 的引用类型形成对比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 });

演示小提琴#3在这里

或者,在.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; }
}

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

演示小提琴#4在这里

或者,您可以通过绑定所有 JSON 属性来简化数据模型,而不是像这样IdJsonExtensionData属性:

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

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

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

演示小提琴#5在这里

于 2020-01-14T10:11:30.627 回答
3

例外是正确的——对象的状态是无效的。Metaand元素不可为空,Relasionships但 JSON 字符串不包含它们。序列化的对象最终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 本身使用 Pipelines 读取输入流而不进行分配,反序列化有效负载并以反序列化对象作为参数调用方法,使用最少的分配。任何返回值都以相同的方式序列化。

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

操作代码应该只是:

[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 类型的内容。如果请求格式错误,它将返回 400。如果代码抛出,它将返回 500

于 2020-01-14T10:08:51.967 回答