Skip to content

Commit 4c7e313

Browse files
authored
Add JSON extension methods to request and response (#21731)
1 parent 648c15d commit 4c7e313

14 files changed

+1065
-1
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Project>
2+
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
3+
4+
<PropertyGroup>
5+
<Nullable>annotations</Nullable>
6+
</PropertyGroup>
7+
</Project>

src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
</PropertyGroup>
66
<ItemGroup Condition="'$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)'">
77
<Compile Include="Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs" />
8+
<Compile Include="../src/Properties/AssemblyInfo.cs" />
89
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
910
<Reference Include="Microsoft.Net.Http.Headers" />
1011
<Reference Include="Microsoft.Extensions.FileProviders.Abstractions" />

src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ public static partial class HttpContextServerVariableExtensions
1313
{
1414
public static string GetServerVariable(this Microsoft.AspNetCore.Http.HttpContext context, string variableName) { throw null; }
1515
}
16+
public static partial class HttpRequestExtensions
17+
{
18+
public static bool HasJsonContentType(this Microsoft.AspNetCore.Http.HttpRequest request) { throw null; }
19+
}
1620
public static partial class ResponseExtensions
1721
{
1822
public static void Clear(this Microsoft.AspNetCore.Http.HttpResponse response) { }
@@ -127,3 +131,29 @@ public void Set(string name, object value) { }
127131
public void SetList<T>(string name, System.Collections.Generic.IList<T> values) { }
128132
}
129133
}
134+
namespace Microsoft.AspNetCore.Http.Json
135+
{
136+
public static partial class HttpRequestJsonExtensions
137+
{
138+
[System.Diagnostics.DebuggerStepThroughAttribute]
139+
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; }
140+
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; }
141+
[System.Diagnostics.DebuggerStepThroughAttribute]
142+
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; }
143+
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; }
144+
}
145+
public static partial class HttpResponseJsonExtensions
146+
{
147+
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; }
148+
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; }
149+
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; }
150+
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; }
151+
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; }
152+
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; }
153+
}
154+
public partial class JsonOptions
155+
{
156+
public JsonOptions() { }
157+
public System.Text.Json.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
158+
}
159+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Extensions.Primitives;
6+
using Microsoft.Net.Http.Headers;
7+
8+
#nullable enable
9+
10+
namespace Microsoft.AspNetCore.Http
11+
{
12+
public static class HttpRequestExtensions
13+
{
14+
/// <summary>
15+
/// Checks the Content-Type header for JSON types.
16+
/// </summary>
17+
/// <returns>true if the Content-Type header represents a JSON content type; otherwise, false.</returns>
18+
public static bool HasJsonContentType(this HttpRequest request)
19+
{
20+
return request.HasJsonContentType(out _);
21+
}
22+
23+
internal static bool HasJsonContentType(this HttpRequest request, out StringSegment charset)
24+
{
25+
if (request == null)
26+
{
27+
throw new ArgumentNullException(nameof(request));
28+
}
29+
30+
if (!MediaTypeHeaderValue.TryParse(request.ContentType, out var mt))
31+
{
32+
charset = StringSegment.Empty;
33+
return false;
34+
}
35+
36+
// Matches application/json
37+
if (mt.MediaType.Equals(JsonConstants.JsonContentType, StringComparison.OrdinalIgnoreCase))
38+
{
39+
charset = mt.Charset;
40+
return true;
41+
}
42+
43+
// Matches +json, e.g. application/ld+json
44+
if (mt.Suffix.Equals("json", StringComparison.OrdinalIgnoreCase))
45+
{
46+
charset = mt.Charset;
47+
return true;
48+
}
49+
50+
charset = StringSegment.Empty;
51+
return false;
52+
}
53+
}
54+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Text;
7+
using System.Text.Json;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Options;
12+
using Microsoft.Extensions.Primitives;
13+
14+
#nullable enable
15+
16+
namespace Microsoft.AspNetCore.Http.Json
17+
{
18+
public static class HttpRequestJsonExtensions
19+
{
20+
/// <summary>
21+
/// Read JSON from the request and deserialize to the specified type.
22+
/// If the request's content-type is not a known JSON type then an error will be thrown.
23+
/// </summary>
24+
/// <typeparam name="TValue">The type of object to read.</typeparam>
25+
/// <param name="request">The request to read from.</param>
26+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
27+
/// <returns>The task object representing the asynchronous operation.</returns>
28+
public static ValueTask<TValue> ReadFromJsonAsync<TValue>(
29+
this HttpRequest request,
30+
CancellationToken cancellationToken = default)
31+
{
32+
return request.ReadFromJsonAsync<TValue>(options: null, cancellationToken);
33+
}
34+
35+
/// <summary>
36+
/// Read JSON from the request and deserialize to the specified type.
37+
/// If the request's content-type is not a known JSON type then an error will be thrown.
38+
/// </summary>
39+
/// <typeparam name="TValue">The type of object to read.</typeparam>
40+
/// <param name="request">The request to read from.</param>
41+
/// <param name="options">The serializer options use when deserializing the content.</param>
42+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
43+
/// <returns>The task object representing the asynchronous operation.</returns>
44+
public static async ValueTask<TValue> ReadFromJsonAsync<TValue>(
45+
this HttpRequest request,
46+
JsonSerializerOptions? options,
47+
CancellationToken cancellationToken = default)
48+
{
49+
if (request == null)
50+
{
51+
throw new ArgumentNullException(nameof(request));
52+
}
53+
54+
if (!request.HasJsonContentType(out var charset))
55+
{
56+
throw CreateContentTypeError(request);
57+
}
58+
59+
options ??= ResolveSerializerOptions(request.HttpContext);
60+
61+
var encoding = GetEncodingFromCharset(charset);
62+
var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding);
63+
64+
try
65+
{
66+
return await JsonSerializer.DeserializeAsync<TValue>(inputStream, options, cancellationToken);
67+
}
68+
finally
69+
{
70+
if (usesTranscodingStream)
71+
{
72+
await inputStream.DisposeAsync();
73+
}
74+
}
75+
}
76+
77+
/// <summary>
78+
/// Read JSON from the request and deserialize to the specified type.
79+
/// If the request's content-type is not a known JSON type then an error will be thrown.
80+
/// </summary>
81+
/// <param name="request">The request to read from.</param>
82+
/// <param name="type">The type of object to read.</param>
83+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
84+
/// <returns>The task object representing the asynchronous operation.</returns>
85+
public static ValueTask<object?> ReadFromJsonAsync(
86+
this HttpRequest request,
87+
Type type,
88+
CancellationToken cancellationToken = default)
89+
{
90+
return request.ReadFromJsonAsync(type, options: null, cancellationToken);
91+
}
92+
93+
/// <summary>
94+
/// Read JSON from the request and deserialize to the specified type.
95+
/// If the request's content-type is not a known JSON type then an error will be thrown.
96+
/// </summary>
97+
/// <param name="request">The request to read from.</param>
98+
/// <param name="type">The type of object to read.</param>
99+
/// <param name="options">The serializer options use when deserializing the content.</param>
100+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the operation.</param>
101+
/// <returns>The task object representing the asynchronous operation.</returns>
102+
public static async ValueTask<object?> ReadFromJsonAsync(
103+
this HttpRequest request,
104+
Type type,
105+
JsonSerializerOptions? options,
106+
CancellationToken cancellationToken = default)
107+
{
108+
if (request == null)
109+
{
110+
throw new ArgumentNullException(nameof(request));
111+
}
112+
if (type == null)
113+
{
114+
throw new ArgumentNullException(nameof(type));
115+
}
116+
117+
if (!request.HasJsonContentType(out var charset))
118+
{
119+
throw CreateContentTypeError(request);
120+
}
121+
122+
options ??= ResolveSerializerOptions(request.HttpContext);
123+
124+
var encoding = GetEncodingFromCharset(charset);
125+
var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding);
126+
127+
try
128+
{
129+
return await JsonSerializer.DeserializeAsync(inputStream, type, options, cancellationToken);
130+
}
131+
finally
132+
{
133+
if (usesTranscodingStream)
134+
{
135+
await inputStream.DisposeAsync();
136+
}
137+
}
138+
}
139+
140+
private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext)
141+
{
142+
// Attempt to resolve options from DI then fallback to default options
143+
return httpContext.RequestServices?.GetService<IOptions<JsonOptions>>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions;
144+
}
145+
146+
private static InvalidOperationException CreateContentTypeError(HttpRequest request)
147+
{
148+
return new InvalidOperationException($"Unable to read the request as JSON because the request content type '{request.ContentType}' is not a known JSON content type.");
149+
}
150+
151+
private static (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding? encoding)
152+
{
153+
if (encoding == null || encoding.CodePage == Encoding.UTF8.CodePage)
154+
{
155+
return (httpContext.Request.Body, false);
156+
}
157+
158+
var inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true);
159+
return (inputStream, true);
160+
}
161+
162+
private static Encoding? GetEncodingFromCharset(StringSegment charset)
163+
{
164+
if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase))
165+
{
166+
// This is an optimization for utf-8 that prevents the Substring caused by
167+
// charset.Value
168+
return Encoding.UTF8;
169+
}
170+
171+
try
172+
{
173+
// charset.Value might be an invalid encoding name as in charset=invalid.
174+
return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null;
175+
}
176+
catch (Exception ex)
177+
{
178+
throw new InvalidOperationException($"Unable to read the request as JSON because the request content type charset '{charset}' is not a known encoding.", ex);
179+
}
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)