diff --git a/samples/Samples.Server/Samples.Server.csproj b/samples/Samples.Server/Samples.Server.csproj index 6935a5c9..0ce1365b 100644 --- a/samples/Samples.Server/Samples.Server.csproj +++ b/samples/Samples.Server/Samples.Server.csproj @@ -17,6 +17,7 @@ + diff --git a/samples/Samples.Server/Startup.cs b/samples/Samples.Server/Startup.cs index 6ad87a2d..76c4d047 100644 --- a/samples/Samples.Server/Startup.cs +++ b/samples/Samples.Server/Startup.cs @@ -34,7 +34,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); - MicrosoftDI.GraphQLBuilderExtensions.AddGraphQL(services) + var graphql = MicrosoftDI.GraphQLBuilderExtensions.AddGraphQL(services) .AddSubscriptionDocumentExecuter() .AddServer(true) .AddSchema() @@ -44,12 +44,22 @@ public void ConfigureServices(IServiceCollection services) var logger = options.RequestServices.GetRequiredService>(); options.UnhandledExceptionDelegate = ctx => logger.LogError("{Error} occurred", ctx.OriginalException.Message); }) + .AddSystemTextJson() .AddErrorInfoProvider() .Configure(opt => opt.ExposeExceptionStackTrace = Environment.IsDevelopment()) .AddWebSockets() .AddDataLoader() .AddGraphTypes(typeof(ChatSchema).Assembly); + + if (Configuration["serializer:type"] == "NewtonsoftJson") + { + graphql.AddNewtonsoftJson(); + } + else + { + graphql.AddSystemTextJson(); + } } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/samples/Samples.Server/appsettings.json b/samples/Samples.Server/appsettings.json index 19b8c152..865aada4 100644 --- a/samples/Samples.Server/appsettings.json +++ b/samples/Samples.Server/appsettings.json @@ -1,4 +1,4 @@ -{ +{ "Logging": { "IncludeScopes": false, "Debug": { @@ -10,6 +10,10 @@ "LogLevel": { "Default": "Debug" } + }, + "serializer": { + // SystemTextJson or NewtonsoftJson + "type": "SystemTextJson" } } } diff --git a/src/Transports.AspNetCore.NewtonsoftJson/BuferringDocumentWriter.cs b/src/Transports.AspNetCore.NewtonsoftJson/BuferringDocumentWriter.cs new file mode 100644 index 00000000..d3ac8b76 --- /dev/null +++ b/src/Transports.AspNetCore.NewtonsoftJson/BuferringDocumentWriter.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using GraphQL.Execution; +using GraphQL.NewtonsoftJson; +using Microsoft.AspNetCore.WebUtilities; +using Newtonsoft.Json; + +namespace GraphQL.Server.Transports.AspNetCore.NewtonsoftJson +{ + public sealed class BufferingDocumentWriter : IDocumentWriter + { + private readonly DocumentWriter _documentWriter; + + public BufferingDocumentWriter(Action action, IErrorInfoProvider errorInfoProvider) + { + _documentWriter = new DocumentWriter(action, errorInfoProvider); + } + + public async Task WriteAsync(Stream stream, T value, CancellationToken cancellationToken = default) + { + await using (var bufferStream = new FileBufferingWriteStream()) + { + await _documentWriter.WriteAsync(bufferStream, value, cancellationToken); + + await bufferStream.DrainBufferAsync(stream, cancellationToken); + } + } + } +} diff --git a/src/Transports.AspNetCore.NewtonsoftJson/GraphQLBuilderNewtonsoftJsonExtensions.cs b/src/Transports.AspNetCore.NewtonsoftJson/GraphQLBuilderNewtonsoftJsonExtensions.cs index 1ad3f610..399886d2 100644 --- a/src/Transports.AspNetCore.NewtonsoftJson/GraphQLBuilderNewtonsoftJsonExtensions.cs +++ b/src/Transports.AspNetCore.NewtonsoftJson/GraphQLBuilderNewtonsoftJsonExtensions.cs @@ -1,6 +1,5 @@ using System; using GraphQL.Execution; -using GraphQL.NewtonsoftJson; using GraphQL.Server.Transports.AspNetCore; using GraphQL.Server.Transports.AspNetCore.NewtonsoftJson; using Microsoft.Extensions.DependencyInjection; @@ -31,7 +30,7 @@ public static IGraphQLBuilder AddNewtonsoftJson(this IGraphQLBuilder builder, Action configureSerializerSettings = null) { builder.Services.AddSingleton(p => new GraphQLRequestDeserializer(configureDeserializerSettings ?? (_ => { }))); - builder.Services.Replace(ServiceDescriptor.Singleton(p => new DocumentWriter(configureSerializerSettings ?? (_ => { }), p.GetService() ?? new ErrorInfoProvider()))); + builder.Services.Replace(ServiceDescriptor.Singleton(p => new BufferingDocumentWriter(configureSerializerSettings ?? (_ => { }), p.GetService() ?? new ErrorInfoProvider()))); return builder; } @@ -55,7 +54,7 @@ public static DI.IGraphQLBuilder AddNewtonsoftJson(this DI.IGraphQLBuilder build Action configureSerializerSettings = null) { builder.Register(p => new GraphQLRequestDeserializer(configureDeserializerSettings ?? (_ => { })), DI.ServiceLifetime.Singleton); - NewtonsoftJson.GraphQLBuilderExtensions.AddNewtonsoftJson(builder, configureSerializerSettings); + builder.AddDocumentWriter(p => new BufferingDocumentWriter(configureSerializerSettings ?? (_ => { }), p.GetService() ?? new ErrorInfoProvider())); return builder; } diff --git a/src/Transports.AspNetCore.NewtonsoftJson/GraphQLRequestDeserializer.cs b/src/Transports.AspNetCore.NewtonsoftJson/GraphQLRequestDeserializer.cs index 17b03c3f..164357d1 100644 --- a/src/Transports.AspNetCore.NewtonsoftJson/GraphQLRequestDeserializer.cs +++ b/src/Transports.AspNetCore.NewtonsoftJson/GraphQLRequestDeserializer.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using GraphQL.NewtonsoftJson; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; using Newtonsoft.Json; namespace GraphQL.Server.Transports.AspNetCore.NewtonsoftJson @@ -14,6 +15,9 @@ namespace GraphQL.Server.Transports.AspNetCore.NewtonsoftJson /// public class GraphQLRequestDeserializer : IGraphQLRequestDeserializer { + // https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.NewtonsoftJson/src/MvcNewtonsoftJsonOptions.cs + private const int MemoryBufferThreshold = 1024 * 30; + private readonly JsonSerializer _serializer; public GraphQLRequestDeserializer(Action configure) @@ -31,50 +35,57 @@ public GraphQLRequestDeserializer(Action configure) _serializer = JsonSerializer.Create(settings); // it's thread safe https://stackoverflow.com/questions/36186276/is-the-json-net-jsonserializer-threadsafe } - public Task DeserializeFromJsonBodyAsync(HttpRequest httpRequest, CancellationToken cancellationToken = default) + public async Task DeserializeFromJsonBodyAsync(HttpRequest httpRequest, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - // Do not explicitly or implicitly (via using, etc.) call dispose because StreamReader will dispose inner stream. - // This leads to the inability to use the stream further by other consumers/middlewares of the request processing - // pipeline. In fact, it is absolutely not dangerous not to dispose StreamReader as it does not perform any useful - // work except for the disposing inner stream. - var reader = new StreamReader(httpRequest.Body); - - var result = new GraphQLRequestDeserializationResult { IsSuccessful = true }; - - using (var jsonReader = new JsonTextReader(reader) { CloseInput = false }) + // From: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs + await using (var readStream = new FileBufferingReadStream(httpRequest.Body, MemoryBufferThreshold)) { - int firstChar = reader.Peek(); + await readStream.DrainAsync(cancellationToken); + readStream.Seek(0L, SeekOrigin.Begin); - cancellationToken.ThrowIfCancellationRequested(); + // Do not explicitly or implicitly (via using, etc.) call dispose because StreamReader will dispose inner stream. + // This leads to the inability to use the stream further by other consumers/middlewares of the request processing + // pipeline. In fact, it is absolutely not dangerous not to dispose StreamReader as it does not perform any useful + // work except for the disposing inner stream. + var reader = new StreamReader(readStream); - try + var result = new GraphQLRequestDeserializationResult { IsSuccessful = true }; + + using (var jsonReader = new JsonTextReader(reader) { CloseInput = false }) { - switch (firstChar) + int firstChar = reader.Peek(); + + cancellationToken.ThrowIfCancellationRequested(); + + try { - case '{': - result.Single = ToGraphQLRequest(_serializer.Deserialize(jsonReader)); - break; - case '[': - result.Batch = _serializer.Deserialize(jsonReader) - .Select(ToGraphQLRequest) - .ToArray(); - break; - default: - result.IsSuccessful = false; - result.Exception = GraphQLRequestDeserializationException.InvalidFirstChar(); - break; + switch (firstChar) + { + case '{': + result.Single = ToGraphQLRequest(_serializer.Deserialize(jsonReader)); + break; + case '[': + result.Batch = _serializer.Deserialize(jsonReader) + .Select(ToGraphQLRequest) + .ToArray(); + break; + default: + result.IsSuccessful = false; + result.Exception = GraphQLRequestDeserializationException.InvalidFirstChar(); + break; + } + } + catch (JsonException e) + { + result.IsSuccessful = false; + result.Exception = new GraphQLRequestDeserializationException(e); } } - catch (JsonException e) - { - result.IsSuccessful = false; - result.Exception = new GraphQLRequestDeserializationException(e); - } - } - return Task.FromResult(result); + return result; + } } public Inputs DeserializeInputsFromJson(string json) => json?.ToInputs();