Last active
February 29, 2024 17:47
-
-
Save davidfowl/058b607055257a52c9ea9185a3efff71 to your computer and use it in GitHub Desktop.
Peeking at a JSON token using a PipeReader in the ASP.NET Core request pipeline
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Buffers; | |
using System.Collections.Generic; | |
using System.IO.Pipelines; | |
using System.Linq; | |
using System.Text.Json; | |
using System.Text.Json.Serialization; | |
using System.Threading.Tasks; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
namespace WebApplication409 | |
{ | |
public class Startup | |
{ | |
// This method gets called by the runtime. Use this method to add services to the container. | |
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
} | |
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseRouting(); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapGet("/", async context => | |
{ | |
await context.Response.WriteAsync("Hello World!"); | |
}); | |
endpoints.MapPost("/json", async context => | |
{ | |
// Allow posting either an array or object | |
var token = await PeekJsonTokenType(context.Request.BodyReader); | |
switch (token) | |
{ | |
case JsonTokenType.StartObject: | |
var person = await JsonSerializer.DeserializeAsync<Person>(context.Request.BodyReader.AsStream()); | |
break; | |
case JsonTokenType.StartArray: | |
var people = await JsonSerializer.DeserializeAsync<Person[]>(context.Request.BodyReader.AsStream()); | |
break; | |
default: | |
break; | |
} | |
}); | |
}); | |
} | |
public class Person | |
{ | |
[JsonPropertyName("name")] | |
public string Name { get; set; } | |
[JsonPropertyName("age")] | |
public int Age { get; set; } | |
} | |
private static async ValueTask<JsonTokenType> PeekJsonTokenType(PipeReader reader) | |
{ | |
// Separate method so that we can use the ref struct | |
static bool DetermineTokenType(in ReadOnlySequence<byte> buffer, out JsonTokenType jsonToken) | |
{ | |
var jsonReader = new Utf8JsonReader(buffer); | |
if (jsonReader.Read()) | |
{ | |
jsonToken = jsonReader.TokenType; | |
return true; | |
} | |
jsonToken = JsonTokenType.None; | |
return false; | |
} | |
while (true) | |
{ | |
var result = await reader.ReadAsync(); | |
var buffer = result.Buffer; | |
if (DetermineTokenType(buffer, out var tokenType)) | |
{ | |
// Don't consume any of the buffer so we can re-parse it with the | |
// serializer | |
reader.AdvanceTo(buffer.Start, buffer.Start); | |
return tokenType; | |
} | |
else | |
{ | |
// We don't have enough to read a token, keep buffering | |
reader.AdvanceTo(buffer.Start, buffer.End); | |
} | |
// If there's no more data coming, then bail | |
if (result.IsCompleted) | |
{ | |
return JsonTokenType.None; | |
} | |
} | |
} | |
} | |
} |
Which then begs the question, how to differentiate between invalid input and there not being enough read to read a token. I guess the exception vs Read returning false is that differentiator.
There’s one thing I missed, quitting the loop if the body is complete. Let me add that.
Awesome, will pop that into our code base.
Thanks so much for all your help; means a lot! As I mentioned on the ticket, this really isn't my area of expertise, but I've learnt a lot thanks to you. Appreciate it :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks so much @davidfowl! One thing I noticed using
Utf8JsonReader.Read
is that its signature would have you believe it'll returnfalse
on a failure, but it seems to throw on certain invalid inputs. So a try-catch is necessary to fully cover any weird inputs.