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

У нас есть 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 s.

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

Ответы [ 2 ]

3 голосов
/ 14 января 2020

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

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

Затем попытайтесь десериализовать и повторно сериализовать пустой объект JSON следующим образом:

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

И вы получите исключение (demo fiddle # 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 обнуляемыми и установить 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 здесь .

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

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

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

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

Демонстрационная скрипка # 4 здесь .

2 голосов
/ 14 января 2020

Исключение верно - состояние объекта недопустимо. Элементы Meta и Relasionships не обнуляются, но строка 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 });

Решение real хотя было бы избавиться от всего этого кода. Это затрудняет использование System.Text. Json. Сам по себе ASP. NET Ядро использует конвейеры для чтения входного потока без выделения , десериализации полезной нагрузки и вызова метода с десериализованным объектом в качестве параметра, используя минимальные выделения. Любые возвращаемые значения сериализуются таким же образом.

Код вопроса хотя и выделяет много - он кэширует входные данные в 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 Само ядро ​​- ASP. NET Ядро отклонит любое POST это не имеет правильный тип MIME. Он вернет 400, если запрос плохо отформатирован. Он вернет 500, если код выбрасывает

...