Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Http/Http.Extensions/ref/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />

<PropertyGroup>
<Nullable>annotations</Nullable>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
<Compile Include="Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs" />
<Compile Include="../src/Properties/AssemblyInfo.cs" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.Net.Http.Headers" />
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public static partial class HttpContextServerVariableExtensions
{
public static string GetServerVariable(this Microsoft.AspNetCore.Http.HttpContext context, string variableName) { throw null; }
}
public static partial class HttpRequestExtensions
{
public static bool HasJsonContentType(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; }
}
public static partial class ResponseExtensions
{
public static void Clear(this Microsoft.AspNetCore.Http.HttpResponse response) { }
Expand Down Expand Up @@ -127,3 +131,29 @@ public void Set(string name, object value) { }
public void SetList<T>(string name, System.Collections.Generic.IList<T> values) { }
}
}
namespace Microsoft.AspNetCore.Http.Json
{
public static partial class HttpRequestJsonExtensions
{
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.ValueTask<object?> ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest request, System.Type type, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.ValueTask<object?> ReadFromJsonAsync(this Microsoft.AspNetCore.Http.HttpRequest request, System.Type type, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.ValueTask<TValue> ReadFromJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpRequest request, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.ValueTask<TValue> ReadFromJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
public static partial class HttpResponseJsonExtensions
{
public static System.Threading.Tasks.Task WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse response, object? value, System.Type type, System.Text.Json.JsonSerializerOptions? options, string? contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse response, object? value, System.Type type, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync(this Microsoft.AspNetCore.Http.HttpResponse response, object? value, System.Type type, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpResponse response, [System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value, System.Text.Json.JsonSerializerOptions? options, string? contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpResponse response, [System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value, System.Text.Json.JsonSerializerOptions? options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAsJsonAsync<TValue>(this Microsoft.AspNetCore.Http.HttpResponse response, [System.Diagnostics.CodeAnalysis.AllowNullAttribute] TValue value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
public partial class JsonOptions
{
public JsonOptions() { }
public System.Text.Json.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
}
}
54 changes: 54 additions & 0 deletions src/Http/Http.Extensions/src/HttpRequestExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

#nullable enable

namespace Microsoft.AspNetCore.Http
{
public static class HttpRequestExtensions
{
/// <summary>
/// Checks the Content-Type header for JSON types.
/// </summary>
/// <returns>true if the Content-Type header represents a JSON content type; otherwise, false.</returns>
public static bool HasJsonContentType(this HttpRequest request)
{
return request.HasJsonContentType(out _);
}

internal static bool HasJsonContentType(this HttpRequest request, out StringSegment charset)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}

if (!MediaTypeHeaderValue.TryParse(request.ContentType, out var mt))
{
charset = StringSegment.Empty;
return false;
}

// Matches application/json
if (mt.MediaType.Equals(JsonConstants.JsonContentType, StringComparison.OrdinalIgnoreCase))
{
charset = mt.Charset;
return true;
}

// Matches +json, e.g. application/ld+json
if (mt.Suffix.Equals("json", StringComparison.OrdinalIgnoreCase))
{
charset = mt.Charset;
return true;
}

charset = StringSegment.Empty;
return false;
}
}
}
182 changes: 182 additions & 0 deletions src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

#nullable enable

namespace Microsoft.AspNetCore.Http.Json
{
public static class HttpRequestJsonExtensions
{
/// <summary>
/// Read JSON from the request and deserialize to the specified type.
/// If the request's content-type is not a known JSON type then an error will be thrown.
/// </summary>
/// <typeparam name="TValue">The type of object to read.</typeparam>
/// <param name="request">The request to read from.</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 ValueTask<TValue> ReadFromJsonAsync<TValue>(
this HttpRequest request,
CancellationToken cancellationToken = default)
{
return request.ReadFromJsonAsync<TValue>(options: null, cancellationToken);
}

/// <summary>
/// Read JSON from the request and deserialize to the specified type.
/// If the request's content-type is not a known JSON type then an error will be thrown.
/// </summary>
/// <typeparam name="TValue">The type of object to read.</typeparam>
/// <param name="request">The request to read from.</param>
/// <param name="options">The serializer options use when deserializing the content.</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 async ValueTask<TValue> ReadFromJsonAsync<TValue>(
this HttpRequest request,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}

if (!request.HasJsonContentType(out var charset))
{
throw CreateContentTypeError(request);
}

options ??= ResolveSerializerOptions(request.HttpContext);

var encoding = GetEncodingFromCharset(charset);
var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding);

try
{
return await JsonSerializer.DeserializeAsync<TValue>(inputStream, options, cancellationToken);
}
finally
{
if (usesTranscodingStream)
{
await inputStream.DisposeAsync();
}
}
}

/// <summary>
/// Read JSON from the request and deserialize to the specified type.
/// If the request's content-type is not a known JSON type then an error will be thrown.
/// </summary>
/// <param name="request">The request to read from.</param>
/// <param name="type">The type of object to read.</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 ValueTask<object?> ReadFromJsonAsync(
this HttpRequest request,
Type type,
CancellationToken cancellationToken = default)
{
return request.ReadFromJsonAsync(type, options: null, cancellationToken);
}

/// <summary>
/// Read JSON from the request and deserialize to the specified type.
/// If the request's content-type is not a known JSON type then an error will be thrown.
/// </summary>
/// <param name="request">The request to read from.</param>
/// <param name="type">The type of object to read.</param>
/// <param name="options">The serializer options use when deserializing the content.</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 async ValueTask<object?> ReadFromJsonAsync(
this HttpRequest request,
Type type,
JsonSerializerOptions? options,
CancellationToken cancellationToken = default)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}

if (!request.HasJsonContentType(out var charset))
{
throw CreateContentTypeError(request);
}

options ??= ResolveSerializerOptions(request.HttpContext);

var encoding = GetEncodingFromCharset(charset);
var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding);

try
{
return await JsonSerializer.DeserializeAsync(inputStream, type, options, cancellationToken);
}
finally
{
if (usesTranscodingStream)
{
await inputStream.DisposeAsync();
}
}
}

private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext)
{
// Attempt to resolve options from DI then fallback to default options
return httpContext.RequestServices?.GetService<IOptions<JsonOptions>>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions;
}

private static InvalidOperationException CreateContentTypeError(HttpRequest request)
{
return new InvalidOperationException($"Unable to read the request as JSON because the request content type '{request.ContentType}' is not a known JSON content type.");
}

private static (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding? encoding)
{
if (encoding == null || encoding.CodePage == Encoding.UTF8.CodePage)
{
return (httpContext.Request.Body, false);
}

var inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true);
return (inputStream, true);
}

private static Encoding? GetEncodingFromCharset(StringSegment charset)
{
if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase))
{
// This is an optimization for utf-8 that prevents the Substring caused by
// charset.Value
return Encoding.UTF8;
}

try
{
// charset.Value might be an invalid encoding name as in charset=invalid.
return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Unable to read the request as JSON because the request content type charset '{charset}' is not a known encoding.", ex);
}
}
}
}
Loading