-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Use PipeReader JsonSerializer overloads #62895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR updates ASP.NET Core's JSON deserialization to use the new PipeReader
overloads on JsonSerializer.DeserializeAsync
instead of the existing Stream
overloads for better performance. The changes include an AppContext switch (Microsoft.AspNetCore.UseStreamBasedJsonParsing
) to allow fallback to the old Stream
based overloads for backwards compatibility with custom JsonConverter
implementations that may not handle ReadOnlySequence
properly.
- Adds AppContext switch for backward compatibility with custom JsonConverter implementations
- Updates JSON deserialization to use PipeReader overloads by default for UTF-8 encoding
- Refactors resource management to only dispose transcoding streams when necessary
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
File | Description |
---|---|
src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs |
Updates MVC input formatter to use PipeReader JsonSerializer overloads with AppContext switch |
src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs |
Updates HTTP request JSON extensions to use PipeReader overloads with AppContext switch |
src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.BindAsync.cs |
Skips test that relies on Stream.Position reset behavior |
src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs |
Updates test assertion for broader exception type |
// Fallback to the stream-based overloads for JsonSerializer.DeserializeAsync | ||
// This is to give users with custom JsonConverter implementations the chance to update their | ||
// converters to support ReadOnlySequence<T> if needed while still keeping their apps working. | ||
_useStreamJsonOverload = AppContext.TryGetSwitch("Microsoft.AspNetCore.UseStreamBasedJsonParsing", out var isEnabled) && isEnabled; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AppContext switch name contains a typo or inconsistency. The switch is named 'UseStreamBasedJsonParsing' but the behavior is actually controlling whether to use Stream vs PipeReader overloads. Consider renaming to 'Microsoft.AspNetCore.Json.UseStreamOverloads' for clarity.
_useStreamJsonOverload = AppContext.TryGetSwitch("Microsoft.AspNetCore.UseStreamBasedJsonParsing", out var isEnabled) && isEnabled; | |
_useStreamJsonOverload = | |
(AppContext.TryGetSwitch("Microsoft.AspNetCore.Json.UseStreamOverloads", out var isNewEnabled) && isNewEnabled) || | |
(AppContext.TryGetSwitch("Microsoft.AspNetCore.UseStreamBasedJsonParsing", out var isOldEnabled) && isOldEnabled); |
Copilot uses AI. Check for mistakes.
// Fallback to the stream-based overloads for JsonSerializer.DeserializeAsync | ||
// This is to give users with custom JsonConverter implementations the chance to update their | ||
// converters to support ReadOnlySequence<T> if needed while still keeping their apps working. | ||
private static readonly bool _useStreamJsonOverload = AppContext.TryGetSwitch("Microsoft.AspNetCore.UseStreamBasedJsonParsing", out var isEnabled) && isEnabled; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same AppContext switch name issue exists here. Both classes use the same switch name but it should be consistent and clearly indicate its purpose of choosing between Stream and PipeReader overloads.
private static readonly bool _useStreamJsonOverload = AppContext.TryGetSwitch("Microsoft.AspNetCore.UseStreamBasedJsonParsing", out var isEnabled) && isEnabled; | |
private static readonly bool _useStreamJsonOverload = AppContext.TryGetSwitch("Microsoft.AspNetCore.Json.UseStreamOverPipeReader", out var isEnabled) && isEnabled; |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM assuming fix for skipped test is forthcoming.
|
||
try | ||
{ | ||
return await JsonSerializer.DeserializeAsync<TValue>(inputStream, options, cancellationToken); | ||
if (encoding == null || encoding.CodePage == Encoding.UTF8.CodePage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Is there anyway for us to dedupe this logic across the different extension methods? Maybe a helper that figures out what serialization invocation to use based on the context switch and return the associated task?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The difficulty here is that each of the HttpRequestJsonExtension methods use a different overload of DeserializeAsync.
We could in theory share the other checks, but I'm not sure it's much cleaner:
public static async ValueTask<object?> ReadFromJsonAsync(
this HttpRequest request,
Type type,
JsonSerializerContext context,
CancellationToken cancellationToken = default)
{
Stream? inputStream = null;
bool isTranscodedStream = false;
ValueTask<object?> deserializeTask;
try
{
(var pipeReader, inputStream, isTranscodedStream) = Shared(request);
if (pipeReader is not null)
{
deserializeTask = JsonSerializer.DeserializeAsync(pipeReader, type, context, cancellationToken);
}
else
{
deserializeTask = JsonSerializer.DeserializeAsync(inputStream, type, context, cancellationToken);
}
return await deserializeTask;
}
finally
{
if (isTranscodedStream)
{
await inputStream!.DisposeAsync();
}
}
}
private static (PipeReader?, Stream?, bool) Shared(HttpRequest request)
{
if (!request.HasJsonContentType(out var charset))
{
ThrowContentTypeError(request);
}
var encoding = GetEncodingFromCharset(charset);
if (encoding == null || encoding.CodePage == Encoding.UTF8.CodePage)
{
if (_useStreamJsonOverload)
{
return (null, request.Body, false);
}
return (request.BodyReader, null, false);
}
var inputStream = Encoding.CreateTranscodingStream(request.Body, encoding, Encoding.UTF8, leaveOpen: true);
return (null, inputStream, true);
}
/backport to main |
Started backporting to main: https://github.com/dotnet/aspnetcore/actions/runs/16512533424 |
woop! |
Use PipeReader JsonSerializer overloads
Description
Updates MVC and HttpRequestJsonExtensions (minimal APIs and user code) to use the new
PipeReader
overloads onJsonSerializer.DeserializeAsync
. Because theReadOnlySequence
property onUtf8JsonReader
is now being exercised, we're being cautious of customJsonConverter
implementations that don't properly handle that scenario, by providing an AppContext switch to go back to theStream
based overloads. We'll remove the switch in 11.0.Closes #60657
Customer Impact
Decreased buffer pool usage and byte copying. Also, finishes the work we started in 9.0 to integrate
System.IO.Pipelines
into Json as an alternative to usingStream
.Regression?
Risk
Change at the JSON layer has been well tested. Decent test coverage for the HTTP layer that makes use of JSON. We also have added an
AppContext
switch to go back to the previous code as we anticipate user code having bugs in customJsonConverter
implementations.Verification
Packaging changes reviewed?