-
Notifications
You must be signed in to change notification settings - Fork 9.8k
/
SystemTextJsonInputFormatter.cs
171 lines (145 loc) · 6.99 KB
/
SystemTextJsonInputFormatter.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// 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.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Formatters
{
/// <summary>
/// A <see cref="TextInputFormatter"/> for JSON content that uses <see cref="JsonSerializer"/>.
/// </summary>
public class SystemTextJsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
{
private readonly JsonOptions _jsonOptions;
private readonly ILogger<SystemTextJsonInputFormatter> _logger;
/// <summary>
/// Initializes a new instance of <see cref="SystemTextJsonInputFormatter"/>.
/// </summary>
/// <param name="options">The <see cref="JsonOptions"/>.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param>
public SystemTextJsonInputFormatter(
JsonOptions options,
ILogger<SystemTextJsonInputFormatter> logger)
{
SerializerOptions = options.JsonSerializerOptions;
_jsonOptions = options;
_logger = logger;
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
SupportedEncodings.Add(UTF16EncodingLittleEndian);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax);
}
/// <summary>
/// Gets the <see cref="JsonSerializerOptions"/> used to configure the <see cref="JsonSerializer"/>.
/// </summary>
/// <remarks>
/// A single instance of <see cref="SystemTextJsonInputFormatter"/> is used for all JSON formatting. Any
/// changes to the options will affect all input formatting.
/// </remarks>
public JsonSerializerOptions SerializerOptions { get; }
/// <inheritdoc />
InputFormatterExceptionPolicy IInputFormatterExceptionPolicy.ExceptionPolicy => InputFormatterExceptionPolicy.MalformedInputExceptions;
/// <inheritdoc />
public sealed override async Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context,
Encoding encoding)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (encoding == null)
{
throw new ArgumentNullException(nameof(encoding));
}
var httpContext = context.HttpContext;
var (inputStream, usesTranscodingStream) = GetInputStream(httpContext, encoding);
object? model;
try
{
model = await JsonSerializer.DeserializeAsync(inputStream, context.ModelType, SerializerOptions);
}
catch (JsonException jsonException)
{
var path = jsonException.Path ?? string.Empty;
var modelStateException = WrapExceptionForModelState(jsonException);
context.ModelState.TryAddModelError(path, modelStateException, context.Metadata);
Log.JsonInputException(_logger, jsonException);
return InputFormatterResult.Failure();
}
catch (Exception exception) when (exception is FormatException || exception is OverflowException)
{
// The code in System.Text.Json never throws these exceptions. However a custom converter could produce these errors for instance when
// parsing a value. These error messages are considered safe to report to users using ModelState.
context.ModelState.TryAddModelError(string.Empty, exception, context.Metadata);
Log.JsonInputException(_logger, exception);
return InputFormatterResult.Failure();
}
finally
{
if (usesTranscodingStream)
{
await inputStream.DisposeAsync();
}
}
if (model == null && !context.TreatEmptyInputAsDefaultValue)
{
// Some nonempty inputs might deserialize as null, for example whitespace,
// or the JSON-encoded value "null". The upstream BodyModelBinder needs to
// be notified that we don't regard this as a real input so it can register
// a model binding error.
return InputFormatterResult.NoValue();
}
else
{
Log.JsonInputSuccess(_logger, context.ModelType);
return InputFormatterResult.Success(model);
}
}
private Exception WrapExceptionForModelState(JsonException jsonException)
{
if (!_jsonOptions.AllowInputFormatterExceptionMessages)
{
// This app is not opted-in to System.Text.Json messages, return the original exception.
return jsonException;
}
// InputFormatterException specifies that the message is safe to return to a client, it will
// be added to model state.
return new InputFormatterException(jsonException.Message, jsonException);
}
private (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding encoding)
{
if (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 class Log
{
private static readonly Action<ILogger, string, Exception> _jsonInputFormatterException;
private static readonly Action<ILogger, string?, Exception?> _jsonInputSuccess;
static Log()
{
_jsonInputFormatterException = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(1, "SystemTextJsonInputException"),
"JSON input formatter threw an exception: {Message}");
_jsonInputSuccess = LoggerMessage.Define<string?>(
LogLevel.Debug,
new EventId(2, "SystemTextJsonInputSuccess"),
"JSON input formatter succeeded, deserializing to type '{TypeName}'");
}
public static void JsonInputException(ILogger logger, Exception exception)
=> _jsonInputFormatterException(logger, exception.Message, exception);
public static void JsonInputSuccess(ILogger logger, Type modelType)
=> _jsonInputSuccess(logger, modelType.FullName, null);
}
}
}