Операция недопустима из-за текущего состояния объекта (System.Text.Json)

У нас есть API, который просто отправляет входящие документы JSON в шину сообщений, назначив каждому GUID. Мы обновляем.Net Core 2.2 до 3.1 и стремились заменить NewtonSoft новымSystem.Text.Json библиотека.

Мы десериализуем входящий документ, назначаем 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 они былиJObjectс.

После добавления GUID он появляется в payload при проверке объекта в точке останова, но я подозреваю, что проблема связана с другими элементами объекта, доступными только для чтения, или с чем-то подобным.

2 ответа

Решение

Ваша проблема может быть воспроизведена на следующем более минимальном примере. Определите следующую модель:

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 это struct, и значение по умолчанию для этой структуры не может быть сериализовано. Фактически, просто делаяJsonSerializer.Serialize(new JsonElement());кидает то же исключение (демо скрипка #2 здесь). (Это контрастирует сJObject который является ссылочным типом, значение по умолчанию, конечно же, null.)

Итак, какие у вас есть варианты? Вы могли бы сделать все своиJsonElement свойства могут иметь значение NULL и установить 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 здесь.

Или вы можете упростить свою модель данных, связав все свойства JSON, кроме Id к JsonExtensionData свойство так:

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

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

Такой подход избавляет от необходимости вручную устанавливать IgnoreNullValues при повторной сериализации, и, таким образом, ASP.NET Core будет правильно повторно сериализовать модель автоматически.

Демо скрипка №4 здесь.

Исключение верно - состояние объекта недопустимое. ВMeta а также Relasionshipsэлементы не допускают значения NULL, но строка 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 использует конвейеры для чтения входного потока без выделения, десериализует полезную нагрузку и вызывает метод с десериализованным объектом в качестве параметра, используя минимальные выделения. Все возвращаемые значения сериализуются таким же образом.

Хотя код вопроса выделяет много - он кэширует ввод в StreamReader, затем вся полезная нагрузка кэшируется в payloadString а затем снова, как payloadобъект. Обратный процесс также использует временные строки. Этот код занимает как минимум вдвое больше оперативной памяти, чем было бы использовано 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, если код выдает

Другие вопросы по тегам