Обеспечьте использование Utf8JsonReader.ValueSequence в целях тестирования

Я написал свой собственный BigIntegerConverter для сериализации/десериализации JSON (.Net System.Text.Json). В методе Read я проверил, используется ли ValueSequence.

...
string stringValue;
if (reader.HasValueSequence)
{
    stringValue = Encoding.UTF8.GetString(reader.ValueSequence);
}
else
{
    stringValue = Encoding.UTF8.GetString(reader.ValueSpan);
}

if (BigInteger.TryParse(stringValue, CultureInfo.InvariantCulture, out var result))
{
    return result;
}
...

Теперь я хочу протестировать этот код, но пока могу добраться только до дерева else. Основываясь на документации, я предположил, что ValueSequence будет использоваться, если данные станут достаточно большими. Однако я уже тестирую BigIntegers размером с BigInteger.Pow(new BigInteger(long.MaxValue), 1234); и до сих пор не могу использовать ValueSequence.

Я что-то пропустил? Есть ли способ обеспечить использование ValueSqeuence в целях тестирования?

Мой тестовый пример выглядит так

[Theory]
[MemberData(nameof(GetNotNullTestData))]
public void Read_EntityWithNotNullableBigInteger(string name, BigInteger expected, string value)
{
    // Arrange
    var json = $$"""{"Name":"{{name}}","NotNullableValue":{{value}}}""";
    // Act
    var result = JsonSerializer.Deserialize<NotNullableBigIntegerEntity>(json, _options);
    // Assert
    Assert.NotNull(result);
    Assert.Equal(name, result.Name);
    Assert.Equal(expected, result.NotNullableValue);
}

С уважением Майкл

Из документации «Для входных данных в ReadOnlySpan<byte> это всегда возвращает false. Для входных данных в ReadOnlySequence<byte> это возвращает true только в том случае, если значение токена охватывает более одного сегмента и, следовательно, не может быть представлено. как пролет». Я не совсем понимаю, что они имеют в виду под значениями токенов, но кто-то, вероятно, знает.

Ralf 05.07.2024 15:56

Вероятно, зависит от способа буферизации базового потока. Как вы вводите JSON?

Charlieface 05.07.2024 15:56

@Charlieface Я отредактировал вопрос и добавил метод тестирования.

Iaman Swtrse 05.07.2024 16:43

При десериализации строки, вероятно, всегда будет использоваться один диапазон, почему бы и нет? Теперь попробуйте это с FileStream.

Charlieface 05.07.2024 19:21
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
4
75
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Utf8JsonReader имеет конструктор , который принимает ReadOnlySequence<byte> , поэтому вы можете взять строку JSON, закодировать ее в массив байтов UTF8, разбить ее на небольшие фрагменты, а затем преобразовать эту последовательность фрагментов в a ReadOnlySequence<byte> с помощью ReadOnlySequenceFactory из этого ответа в Десериализовать очень большой json из фрагментированного массива строк с помощью system.text.json.

Сначала представим следующий фабричный класс:

// From this answer https://stackoverflow.com/a/61087772 to https://stackoverflow.com/questions/61079767/deserialize-very-large-json-from-a-chunked-array-of-strings-using-system-text-js
public static class ReadOnlySequenceFactory
{
    public static ReadOnlySequence<T> AsSequence<T>(this IEnumerable<T []> buffers) => ReadOnlyMemorySegment<T>.Create(buffers.Select(a => new ReadOnlyMemory<T>(a)));
    public static ReadOnlySequence<T> AsSequence<T>(this IEnumerable<ReadOnlyMemory<T>> buffers) => ReadOnlyMemorySegment<T>.Create(buffers);

    // There is no public concrete implementation of ReadOnlySequenceSegment<T> so we must create one ourselves.
    // This is modeled on https://github.com/dotnet/runtime/blob/v5.0.18/src/libraries/System.Text.Json/tests/BufferFactory.cs
    // by https://github.com/ahsonkhan
    class ReadOnlyMemorySegment<T> : ReadOnlySequenceSegment<T>
    {
        public static ReadOnlySequence<T> Create(IEnumerable<ReadOnlyMemory<T>> buffers)
        {
            ReadOnlyMemorySegment<T>? first = null;
            ReadOnlyMemorySegment<T>? current = null;
            foreach (var buffer in buffers)
            {
                var next = new ReadOnlyMemorySegment<T> { Memory = buffer };
                if (first == null)
                    first = next;
                else
                {
                    current!.Next = next;
                    next.RunningIndex = current.RunningIndex + current.Memory.Length;
                }
                current = next;
            }
            if (first == null)
                first = current = new ();

            return new ReadOnlySequence<T>(first, 0, current!, current!.Memory.Length);
        }
    }
}

Затем напишите свой конвертер, например. следующее:

public class BigIntegerConverter : JsonConverter<BigInteger>
{
    // The actual implementation seems to be in INumberBase<TSelf>.TryParse() so I had to do this to call the method:
    static bool TryParse<TSelf>(ReadOnlySpan<byte> utf8Text, out TSelf? value) where TSelf : IUtf8SpanParsable<TSelf>, new() => 
        TSelf.TryParse(utf8Text, NumberFormatInfo.InvariantInfo, out value);

    public override BigInteger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.Number)
            throw new JsonException(string.Format("Found token {0} but expected token {1}", reader.TokenType, JsonTokenType.Number ));
        var utf8Text = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
        if (TryParse<BigInteger>(utf8Text, out var value))
            return value;
        throw new JsonException();
    }

    public override void Write(Utf8JsonWriter writer, BigInteger value, JsonSerializerOptions options) =>
        writer.WriteRawValue(value.ToString(NumberFormatInfo.InvariantInfo), false);
}

Теперь вы сможете написать свой тестовый метод следующим образом, разбивая входной JSON на небольшие фрагменты:

public record NotNullableBigIntegerEntity(string Name, BigInteger NotNullableValue);

JsonSerializerOptions _options = new()
{
    Converters = { new BigIntegerConverter() },
};

public void Read_EntityWithNotNullableBigInteger(string name, BigInteger expected, string value)
{
    int byteChunkSize = 3;
    
    // Arrange
    var json = $$"""{"Name":"{{name}}","NotNullableValue":{{value}}}""";
    // Break into chunks
    var utf8json = Encoding.UTF8.GetBytes(json);
    var sequence = utf8json.Chunk(byteChunkSize).AsSequence();
    // Act
    var reader = new Utf8JsonReader(sequence);
    var result = JsonSerializer.Deserialize<NotNullableBigIntegerEntity>(ref reader, _options);
    // Assert
    Assert.NotNull(result);
    Assert.Equal(name, result?.Name);
    Assert.Equal(expected, result?.NotNullableValue);
}

Примечания:

  • BigInteger реализует IUtf8SpanParsable<BigInteger>, который позволяет осуществлять прямой анализ диапазонов байтов в кодировке UTF8. Таким образом, нет необходимости вызывать Encoding.UTF8.GetString() для создания строки UTF16, соответствующей текущему значению.

  • Основываясь на документации, я предположил, что ValueSequence будет использоваться, если данные станут достаточно большими. -- Может быть, а может быть, и нет. MSFT, похоже, намеревался использовать ReadOnlySequence<byte> при асинхронной десериализации из некоторого потока запросов или ответов, но вы выполняете синхронную десериализацию из строки в памяти. Решение MSFT разбить эту строку на один диапазон байтов UTF8 или на несколько последовательностей байтов UTF8 — это деталь реализации, которую они не разглашают.

    (Когда я проверяю источник ссылки, кажется, что текущий код преобразует входящую строку в однобайтовый диапазон.)

Демо-рабочий пример здесь.

Я должен разобраться в этом. Но первая попытка просто скопировать и вставить ваше решение даже не вызывает метод .Read класса Converter.

Iaman Swtrse 08.07.2024 15:50

@IamanSwtrse - добавьте минимально воспроизводимый пример . Я изменил BigIntegerConverter().Read(), добавив Console.WriteLine(), и он действительно вызывается, см. dotnetfiddle.net/Kre0Qy.

dbc 08.07.2024 18:28

Нашел его через 2 часа. Я нашел опечатку в названии свойства... :( Теперь все работает как часы. Спасибо.

Iaman Swtrse 08.07.2024 21:46

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