Last active
November 28, 2021 09:26
-
-
Save martincostello/0def8df05fc51becb36914cb2bbf0c77 to your computer and use it in GitHub Desktop.
Extensions for using the JSON source generator with Minimal APIs
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.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 | |
} | |
} | |
} |
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.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); | |
} | |
} |
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.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); | |
} | |
} |
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.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