Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Last active February 29, 2024 17:47
Show Gist options
  • Save davidfowl/058b607055257a52c9ea9185a3efff71 to your computer and use it in GitHub Desktop.
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
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;
}
}
}
}
}
@benmccallum
Copy link

benmccallum commented Jan 21, 2020

Thanks so much @davidfowl! One thing I noticed using Utf8JsonReader.Read is that its signature would have you believe it'll return false on a failure, but it seems to throw on certain invalid inputs. So a try-catch is necessary to fully cover any weird inputs.

@benmccallum
Copy link

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.

@davidfowl
Copy link
Author

There’s one thing I missed, quitting the loop if the body is complete. Let me add that.

@benmccallum
Copy link

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