Skip to content

Instantly share code, notes, and snippets.

@martincostello
Last active November 28, 2021 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save martincostello/0def8df05fc51becb36914cb2bbf0c77 to your computer and use it in GitHub Desktop.
Save martincostello/0def8df05fc51becb36914cb2bbf0c77 to your computer and use it in GitHub Desktop.
Extensions for using the JSON source generator with Minimal APIs
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace Microsoft.AspNetCore.Http;
/// <summary>
/// Provides extension methods for writing a JSON serialized value to the HTTP response.
/// </summary>
/// <remarks>
/// Based on https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs.
/// </remarks>
public static class HttpResponseJsonExtensions
{
private const string JsonContentTypeWithCharset = "application/json; charset=utf-8";
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// <c>application/json; charset=utf-8</c>.
/// </summary>
/// <typeparam name="TValue">The type of object to write.</typeparam>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="context">The serializer context to use when serializing the value.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync<TValue>(
this HttpResponse response,
TValue value,
JsonSerializerContext? context,
CancellationToken cancellationToken = default)
{
return response.WriteAsJsonAsync(value, context, contentType: null, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// the specified content-type.
/// </summary>
/// <typeparam name="TValue">The type of object to write.</typeparam>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="context">The serializer context to use when serializing the value.</param>
/// <param name="contentType">The content-type to set on the response.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync<TValue>(
this HttpResponse response,
TValue value,
JsonSerializerContext? context,
string? contentType,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(response);
context ??= ResolveSerializerContext(response.HttpContext);
response.ContentType = contentType ?? JsonContentTypeWithCharset;
// If no user-provided token, pass the RequestAborted token and ignore OperationCanceledException
if (!cancellationToken.CanBeCanceled)
{
return WriteAsJsonAsyncSlow(response.Body, value, typeof(TValue), context, response.HttpContext.RequestAborted);
}
return JsonSerializer.SerializeAsync(response.Body, value, typeof(TValue), context, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// <c>application/json; charset=utf-8</c>.
/// </summary>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="type">The type of object to write.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync(
this HttpResponse response,
object? value,
Type type,
CancellationToken cancellationToken = default)
{
return response.WriteAsJsonAsync(value, type, context: null, contentType: null, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// <c>application/json; charset=utf-8</c>.
/// </summary>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="type">The type of object to write.</param>
/// <param name="context">The serializer context to use when serializing the value.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync(
this HttpResponse response,
object? value,
Type type,
JsonSerializerContext? context,
CancellationToken cancellationToken = default)
{
return response.WriteAsJsonAsync(value, type, context, contentType: null, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// <c>application/json; charset=utf-8</c>.
/// </summary>
/// <typeparam name="T">The type of the value to write as JSON.</typeparam>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="jsonTypeInfo">The JSON type metadata to use when serializing the value.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync<T>(
this HttpResponse response,
T value,
JsonTypeInfo<T> jsonTypeInfo,
CancellationToken cancellationToken = default)
{
return response.WriteAsJsonAsync(value, jsonTypeInfo, contentType: null, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// the specified content-type.
/// </summary>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="type">The type of object to write.</param>
/// <param name="context">The serializer context to use when serializing the value.</param>
/// <param name="contentType">The content-type to set on the response.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync(
this HttpResponse response,
object? value,
Type type,
JsonSerializerContext? context,
string? contentType,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(response);
ArgumentNullException.ThrowIfNull(type);
context ??= ResolveSerializerContext(response.HttpContext);
response.ContentType = contentType ?? JsonContentTypeWithCharset;
// If no-user provided token, pass the RequestAborted token and ignore OperationCanceledException
if (!cancellationToken.CanBeCanceled)
{
return WriteAsJsonAsyncSlow(response.Body, value, type, context, response.HttpContext.RequestAborted);
}
return JsonSerializer.SerializeAsync(response.Body, value, type, context, cancellationToken);
}
/// <summary>
/// Write the specified value as JSON to the response body. The response content-type will be set to
/// the specified content-type.
/// </summary>
/// <typeparam name="T">The type of the value to write as JSON.</typeparam>
/// <param name="response">The response to write JSON to.</param>
/// <param name="value">The value to write as JSON.</param>
/// <param name="jsonTypeInfo">The JSON type metadata to use when serializing the value.</param>
/// <param name="contentType">The content-type to set on the response.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task WriteAsJsonAsync<T>(
this HttpResponse response,
T value,
JsonTypeInfo<T> jsonTypeInfo,
string? contentType,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(response);
response.ContentType = contentType ?? JsonContentTypeWithCharset;
// If no-user provided token, pass the RequestAborted token and ignore OperationCanceledException
if (!cancellationToken.CanBeCanceled)
{
return WriteAsJsonAsyncSlow(response.Body, value, jsonTypeInfo, response.HttpContext.RequestAborted);
}
return JsonSerializer.SerializeAsync(response.Body, value, jsonTypeInfo, cancellationToken);
}
private static JsonSerializerContext ResolveSerializerContext(HttpContext httpContext)
{
// This line assumes the application has registered an instance of JsonSerializerContext
// with the IServiceCollection as a singleton. Remove this and make this a required parameter
// (instead of being optional) if you do not want to do this in your application and want
// to pass it to the Results.Extensions.Json() method(s) explicitly from your endpoint(s).
return httpContext.RequestServices?.GetRequiredService<JsonSerializerContext>()!;
}
private static async Task WriteAsJsonAsyncSlow<T>(
Stream body,
T value,
JsonTypeInfo<T> jsonTypeInfo,
CancellationToken cancellationToken)
{
try
{
await JsonSerializer.SerializeAsync(body, value, jsonTypeInfo, cancellationToken);
}
catch (OperationCanceledException)
{
// Ignore
}
}
private static async Task WriteAsJsonAsyncSlow(
Stream body,
object? value,
Type inputType,
JsonSerializerContext context,
CancellationToken cancellationToken)
{
try
{
await JsonSerializer.SerializeAsync(body, value, inputType, context, cancellationToken);
}
catch (OperationCanceledException)
{
// Ignore
}
}
}
using System.Text.Json.Serialization;
namespace Microsoft.AspNetCore.Http.Result;
/// <summary>
/// A result which formats the given object as JSON.
/// </summary>
/// <remarks>
/// Based on https://raw.githubusercontent.com/dotnet/aspnetcore/main/src/Http/Http.Results/src/JsonResult.cs.
/// </remarks>
internal sealed partial class JsonResult : IResult
{
/// <summary>
/// Gets the <see cref="Net.Http.Headers.MediaTypeHeaderValue"/> representing the Content-Type header of the response.
/// </summary>
public string? ContentType { get; init; }
/// <summary>
/// Gets the type of the input value to be formatted.
/// </summary>
public Type InputType { get; init; } = default!;
/// <summary>
/// Gets the serializer context.
/// </summary>
public JsonSerializerContext? JsonSerializerContext { get; init; }
/// <summary>
/// Gets the HTTP status code.
/// </summary>
public int? StatusCode { get; init; }
/// <summary>
/// Gets the value to be formatted.
/// </summary>
public object? Value { get; init; }
/// <inheritdoc />
Task IResult.ExecuteAsync(HttpContext httpContext)
{
var logger = httpContext.RequestServices.GetRequiredService<ILogger<JsonResult>>();
Log.JsonResultExecuting(logger, Value);
if (StatusCode is int statusCode)
{
httpContext.Response.StatusCode = statusCode;
}
return httpContext.Response.WriteAsJsonAsync(Value, InputType, JsonSerializerContext, ContentType);
}
private static partial class Log
{
public static void JsonResultExecuting(ILogger logger, object? value)
{
if (logger.IsEnabled(LogLevel.Information))
{
string? type = value == null ? "null" : value.GetType().FullName!;
JsonResultExecuting(logger, type);
}
}
[LoggerMessage(
1,
LogLevel.Information,
"Executing JsonResult, writing value of type '{Type}'.",
EventName = "JsonResultExecuting",
SkipEnabledCheck = true)]
private static partial void JsonResultExecuting(ILogger logger, string type);
}
}
using System.Text.Json.Serialization.Metadata;
namespace Microsoft.AspNetCore.Http.Result;
/// <summary>
/// A result which formats the given object as JSON.
/// </summary>
/// <typeparam name="T">
/// The type of the object to format as JSON.
/// </typeparam>
/// <remarks>
/// Based on https://raw.githubusercontent.com/dotnet/aspnetcore/main/src/Http/Http.Results/src/JsonResult.cs.
/// </remarks>
internal sealed partial class JsonResult<T> : IResult
{
/// <summary>
/// Gets the <see cref="Net.Http.Headers.MediaTypeHeaderValue"/> representing the Content-Type header of the response.
/// </summary>
public string? ContentType { get; init; }
/// <summary>
/// Gets the JSON type information to use.
/// </summary>
public JsonTypeInfo<T> JsonTypeInfo { get; init; } = default!;
/// <summary>
/// Gets the HTTP status code.
/// </summary>
public int? StatusCode { get; init; }
/// <summary>
/// Gets the value to be formatted.
/// </summary>
public T Value { get; init; } = default!;
/// <inheritdoc />
Task IResult.ExecuteAsync(HttpContext httpContext)
{
var logger = httpContext.RequestServices.GetRequiredService<ILogger<JsonResult<T>>>();
Log.JsonResultExecuting(logger, Value);
if (StatusCode is int statusCode)
{
httpContext.Response.StatusCode = statusCode;
}
return httpContext.Response.WriteAsJsonAsync(Value, JsonTypeInfo, ContentType);
}
private static partial class Log
{
public static void JsonResultExecuting(ILogger logger, object? value)
{
if (logger.IsEnabled(LogLevel.Information))
{
string? type = value == null ? "null" : value.GetType().FullName!;
JsonResultExecuting(logger, type);
}
}
[LoggerMessage(
1,
LogLevel.Information,
"Executing JsonResult, writing value of type '{Type}'.",
EventName = "JsonResultExecuting",
SkipEnabledCheck = true)]
private static partial void JsonResultExecuting(ILogger logger, string type);
}
}
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.Http.Result;
namespace Microsoft.AspNetCore.Http;
/// <summary>
/// A class containing extensions for <see cref="IResult"/> that use the JSON source generator.
/// </summary>
public static class ResultExtensions
{
/// <summary>
/// Creates a <see cref="IResult"/> that serializes the specified <paramref name="value"/> object to JSON.
/// </summary>
/// <typeparam name="T">The type of the value to write as JSON.</typeparam>
/// <param name="extensions">The <see cref="IResultExtensions"/> being extended.</param>
/// <param name="value">The object to write as JSON.</param>
/// <param name="context">The serializer context to use when serializing the value.</param>
/// <param name="contentType">The content-type to set on the response.</param>
/// <param name="statusCode">The status code to set on the response.</param>
/// <returns>
/// The created <see cref="JsonResult{Type}"/> that serializes the specified
/// <paramref name="value"/> as JSON format for the response.</returns>
/// <remarks>
/// Callers should cache an instance of serializer settings to avoid recreating cached data with each call.
/// </remarks>
public static IResult Json<T>(
this IResultExtensions extensions,
T? value,
JsonSerializerContext? context = null,
string? contentType = null,
int? statusCode = null)
{
ArgumentNullException.ThrowIfNull(extensions);
return new JsonResult
{
ContentType = contentType,
InputType = typeof(T),
JsonSerializerContext = context,
StatusCode = statusCode,
Value = value,
};
}
/// <summary>
/// Creates a <see cref="IResult"/> that serializes the specified <paramref name="value"/> object to JSON.
/// </summary>
/// <param name="extensions">The <see cref="IResultExtensions"/> being extended.</param>
/// <param name="value">The object to write as JSON.</param>
/// <param name="inputType">The type of the value to write as JSON.</param>
/// <param name="context">The serializer context to use when serializing the value.</param>
/// <param name="contentType">The content-type to set on the response.</param>
/// <param name="statusCode">The status code to set on the response.</param>
/// <returns>
/// The created <see cref="JsonResult{Type}"/> that serializes the specified
/// <paramref name="value"/> as JSON format for the response.</returns>
/// <remarks>
/// Callers should cache an instance of serializer settings to avoid recreating cached data with each call.
/// </remarks>
public static IResult Json(
this IResultExtensions extensions,
object? value,
Type inputType,
JsonSerializerContext? context = null,
string? contentType = null,
int? statusCode = null)
{
ArgumentNullException.ThrowIfNull(extensions);
ArgumentNullException.ThrowIfNull(inputType);
return new JsonResult
{
ContentType = contentType,
InputType = inputType,
JsonSerializerContext = context,
StatusCode = statusCode,
Value = value,
};
}
/// <summary>
/// Creates a <see cref="IResult"/> that serializes the specified <paramref name="value"/> object to JSON.
/// </summary>
/// <typeparam name="T">The type of the value to write as JSON.</typeparam>
/// <param name="extensions">The <see cref="IResultExtensions"/> being extended.</param>
/// <param name="value">The object to write as JSON.</param>
/// <param name="jsonTypeInfo">The JSON type metadata to use when serializing the value.</param>
/// <param name="contentType">The content-type to set on the response.</param>
/// <param name="statusCode">The status code to set on the response.</param>
/// <returns>
/// The created <see cref="JsonResult{Type}"/> that serializes the specified
/// <paramref name="value"/> as JSON format for the response.</returns>
/// <remarks>
/// Callers should cache an instance of serializer settings to avoid recreating cached data with each call.
/// </remarks>
public static IResult Json<T>(
this IResultExtensions extensions,
T value,
JsonTypeInfo<T> jsonTypeInfo,
string? contentType = null,
int? statusCode = null)
{
ArgumentNullException.ThrowIfNull(extensions);
ArgumentNullException.ThrowIfNull(value);
ArgumentNullException.ThrowIfNull(jsonTypeInfo);
return new JsonResult<T>
{
ContentType = contentType,
JsonTypeInfo = jsonTypeInfo,
StatusCode = statusCode,
Value = value,
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment