Skip to content

Commit

Permalink
Allow for nulls to be stripped from the result. (#5627)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib authored Dec 20, 2022
1 parent d7286a0 commit 53e8c0c
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using HotChocolate.AspNetCore;
using HotChocolate.AspNetCore.Serialization;
using HotChocolate.Execution.Configuration;
using HotChocolate.Execution.Serialization;
using HotChocolate.Utilities;

// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -105,6 +106,28 @@ public static IServiceCollection AddHttpResponseFormatter(
return services;
}

/// <summary>
/// Adds the <see cref="DefaultHttpResponseFormatter"/> with specific formatter options
/// to the DI.
/// </summary>
/// <param name="services">
/// The <see cref="IServiceCollection"/>.
/// </param>
/// <param name="options">
/// The JSON result formatter options
/// </param>
/// <returns>
/// Returns the <see cref="IServiceCollection"/> so that configuration can be chained.
/// </returns>
public static IServiceCollection AddHttpResponseFormatter(
this IServiceCollection services,
JsonResultFormatterOptions options)
{
services.RemoveAll<IHttpResponseFormatter>();
services.AddSingleton<IHttpResponseFormatter>(new DefaultHttpResponseFormatter(options));
return services;
}

/// <summary>
/// Adds a custom HTTP response formatter to the DI.
/// </summary>
Expand All @@ -125,4 +148,29 @@ public static IServiceCollection AddHttpResponseFormatter<T>(
services.AddSingleton<IHttpResponseFormatter, T>();
return services;
}

/// <summary>
/// Adds a custom HTTP response formatter to the DI.
/// </summary>
/// <param name="services">
/// The <see cref="IServiceCollection"/>.
/// </param>
/// <param name="factory">
/// The service factory.
/// </param>
/// <typeparam name="T">
/// The type of the custom <see cref="IHttpResponseFormatter"/>.
/// </typeparam>
/// <returns>
/// Returns the <see cref="IServiceCollection"/> so that configuration can be chained.
/// </returns>
public static IServiceCollection AddHttpResponseFormatter<T>(
this IServiceCollection services,
Func<IServiceProvider, T> factory)
where T : class, IHttpResponseFormatter
{
services.RemoveAll<IHttpResponseFormatter>();
services.AddSingleton<IHttpResponseFormatter>(factory);
return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,21 @@ public class DefaultHttpResponseFormatter : IHttpResponseFormatter
public DefaultHttpResponseFormatter(
bool indented = false,
JavaScriptEncoder? encoder = null)
: this(new JsonResultFormatterOptions { Indented = indented, Encoder = encoder })
{
_jsonFormatter = new JsonResultFormatter(indented, encoder);
}

/// <summary>
/// Creates a new instance of <see cref="DefaultHttpResponseFormatter" />.
/// </summary>
/// <param name="options">
/// The JSON result formatter options
/// </param>
public DefaultHttpResponseFormatter(JsonResultFormatterOptions options)
{
_jsonFormatter = new JsonResultFormatter(options);
_multiPartFormatter = new MultiPartResultFormatter(_jsonFormatter);
_eventStreamResultFormatter = new EventStreamResultFormatter(indented, encoder);
_eventStreamResultFormatter = new EventStreamResultFormatter(options);
}

public GraphQLRequestFlags CreateRequestFlags(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ protected virtual TestServer CreateStarWarsServer(
endpoints.MapGraphQL("/arguments", "arguments");
endpoints.MapGraphQL("/upload", "upload");
endpoints.MapGraphQL("/starwars", "StarWars");
endpoints.MapGraphQL("/test", "test");
}));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
using System.Net;
using System.Net.Http.Json;
using CookieCrumble;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using HotChocolate.AspNetCore.Instrumentation;
using HotChocolate.AspNetCore.Serialization;
using HotChocolate.AspNetCore.Tests.Utilities;
using HotChocolate.Execution;
using HotChocolate.Execution.Serialization;
using Newtonsoft.Json;
using static HotChocolate.Execution.Serialization.JsonNullIgnoreCondition;

namespace HotChocolate.AspNetCore;

public class HttpPostMiddlewareTests : ServerTestBase
{
private static readonly Uri _url = new("http://localhost:5000/graphql");

public HttpPostMiddlewareTests(TestServerFactory serverFactory)
: base(serverFactory)
{
Expand Down Expand Up @@ -1141,6 +1147,122 @@ await server.PostAsync(new ClientQueryRequest
result.MatchSnapshot();
}

[Fact]
public async Task Strip_Null_Values_Variant_1()
{
// arrange
var server = CreateStarWarsServer(
configureServices: s => s.AddHttpResponseFormatter(
_ => new DefaultHttpResponseFormatter(new() { NullIgnoreCondition = Fields })));
var client = server.CreateClient();

// act
using var request = new HttpRequestMessage(HttpMethod.Post, _url)
{
Content = JsonContent.Create(
new ClientQueryRequest
{
Query = "{ __schema { description } }"
})
};

using var response = await client.SendAsync(request);

// assert
// expected response content-type: application/json
// expected status code: 200
Snapshot
.Create()
.Add(response)
.MatchInline(
@"Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
-------------------------->
{""data"":{""__schema"":{}}}");
}

[Fact]
public async Task Strip_Null_Values_Variant_2()
{
// arrange
var server = CreateStarWarsServer(
configureServices: s => s.AddHttpResponseFormatter(
new JsonResultFormatterOptions { NullIgnoreCondition = Fields }));
var client = server.CreateClient();

// act
using var request = new HttpRequestMessage(HttpMethod.Post, _url)
{
Content = JsonContent.Create(
new ClientQueryRequest
{
Query = "{ __schema { description } }"
})
};

using var response = await client.SendAsync(request);

// assert
// expected response content-type: application/json
// expected status code: 200
Snapshot
.Create()
.Add(response)
.MatchInline(
@"Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
-------------------------->
{""data"":{""__schema"":{}}}");
}

[Fact]
public async Task Strip_Null_Elements()
{
// arrange
var url = new Uri("http://localhost:5000/test");

var server = CreateStarWarsServer(
configureServices: s => s
.AddGraphQLServer("test")
.AddQueryType<NullListQuery>()
.Services
.AddHttpResponseFormatter(new JsonResultFormatterOptions
{
NullIgnoreCondition = Lists
}));
var client = server.CreateClient();

// act
using var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = JsonContent.Create(
new ClientQueryRequest
{
Query = "{ nullValues }"
})
};

using var response = await client.SendAsync(request);

// assert
// expected response content-type: application/json
// expected status code: 200
Snapshot
.Create()
.Add(response)
.MatchInline(
@"Headers:
Content-Type: application/graphql-response+json; charset=utf-8
-------------------------->
Status Code: OK
-------------------------->
{""data"":{""nullValues"":[""abc""]}}");
}

public class ErrorRequestInterceptor : DefaultHttpRequestInterceptor
{
public override ValueTask OnCreateAsync(
Expand All @@ -1163,4 +1285,9 @@ public override IDisposable ExecuteHttpRequest(HttpContext context, HttpRequestK
return EmptyScope;
}
}

public class NullListQuery
{
public List<string?> NullValues => new() { null, "abc", null };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ namespace HotChocolate;

public static class ExecutionResultExtensions
{
private static readonly JsonResultFormatter _formatter = new(false);
private static readonly JsonResultFormatter _formatterIndented = new(true);
private static readonly JsonResultFormatter _formatter = new(new() { Indented = false});
private static readonly JsonResultFormatter _formatterIndented = new(new() { Indented = true});

public static void WriteTo(
this IQueryResult result,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,13 @@ private static readonly byte[] _completeEvent
/// <summary>
/// Creates a new instance of <see cref="EventStreamResultFormatter" />.
/// </summary>
/// <param name="indented">
/// Defines whether the underlying <see cref="Utf8JsonWriter"/>
/// should pretty print the JSON which includes:
/// indenting nested JSON tokens, adding new lines, and adding
/// white space between property names and values.
/// By default, the JSON is written without any extra white space.
/// <param name="options">
/// The JSON result formatter options
/// </param>
/// <param name="encoder">
/// Gets or sets the encoder to use when escaping strings, or null to use the default encoder.
/// </param>
public EventStreamResultFormatter(
bool indented = false,
JavaScriptEncoder? encoder = null)
public EventStreamResultFormatter(JsonResultFormatterOptions options)
{
_options = new JsonWriterOptions { Indented = indented, Encoder = encoder };
_payloadFormatter = new JsonResultFormatter(indented, encoder);
_options = options.CreateWriterOptions();
_payloadFormatter = new JsonResultFormatter(options);
}

/// <inheritdoc cref="IExecutionResultFormatter.FormatAsync(IExecutionResult, Stream, CancellationToken)" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace HotChocolate.Execution.Serialization;

/// <summary>
/// Specifies when null values are ignored.
/// </summary>
public enum JsonNullIgnoreCondition
{
/// <summary>
/// No null values are ignore.
/// </summary>
Default = 0,

/// <summary>
/// Fields that have a null value are ignored.
/// </summary>
Fields = 1,

/// <summary>
/// Null elements in lists are ignored.
/// </summary>
Lists = 2,

/// <summary>
/// Fields that have a null value and null elements in lists are ignored.
/// </summary>
All = 4
}
Loading

0 comments on commit 53e8c0c

Please sign in to comment.