Make [FromBody] treat empty request bodies as invalid (#4750)
This commit is contained in:
parent
1e7972bd8f
commit
90acd055fe
|
|
@ -36,6 +36,36 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
ModelStateDictionary modelState,
|
||||
ModelMetadata metadata,
|
||||
Func<Stream, Encoding, TextReader> readerFactory)
|
||||
: this(httpContext, modelName, modelState, metadata, readerFactory, treatEmptyInputAsDefaultValue: false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="InputFormatterContext"/>.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">
|
||||
/// The <see cref="Http.HttpContext"/> for the current operation.
|
||||
/// </param>
|
||||
/// <param name="modelName">The name of the model.</param>
|
||||
/// <param name="modelState">
|
||||
/// The <see cref="ModelStateDictionary"/> for recording errors.
|
||||
/// </param>
|
||||
/// <param name="metadata">
|
||||
/// The <see cref="ModelMetadata"/> of the model to deserialize.
|
||||
/// </param>
|
||||
/// <param name="readerFactory">
|
||||
/// A delegate which can create a <see cref="TextReader"/> for the request body.
|
||||
/// </param>
|
||||
/// <param name="treatEmptyInputAsDefaultValue">
|
||||
/// A value for the <see cref="TreatEmptyInputAsDefaultValue"/> property.
|
||||
/// </param>
|
||||
public InputFormatterContext(
|
||||
HttpContext httpContext,
|
||||
string modelName,
|
||||
ModelStateDictionary modelState,
|
||||
ModelMetadata metadata,
|
||||
Func<Stream, Encoding, TextReader> readerFactory,
|
||||
bool treatEmptyInputAsDefaultValue)
|
||||
{
|
||||
if (httpContext == null)
|
||||
{
|
||||
|
|
@ -67,9 +97,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
ModelState = modelState;
|
||||
Metadata = metadata;
|
||||
ReaderFactory = readerFactory;
|
||||
TreatEmptyInputAsDefaultValue = treatEmptyInputAsDefaultValue;
|
||||
ModelType = metadata.ModelType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a flag to indicate whether the input formatter should allow no value to be provided.
|
||||
/// If <see langword="false"/>, the input formatter should handle empty input by returning
|
||||
/// <see cref="InputFormatterResult.NoValueAsync()"/>. If <see langword="true"/>, the input
|
||||
/// formatter should handle empty input by returning the default value for the type
|
||||
/// <see cref="ModelType"/>.
|
||||
/// </summary>
|
||||
public bool TreatEmptyInputAsDefaultValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Http.HttpContext"/> associated with the current operation.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -10,17 +10,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// </summary>
|
||||
public class InputFormatterResult
|
||||
{
|
||||
private static readonly InputFormatterResult _failure = new InputFormatterResult();
|
||||
private static readonly InputFormatterResult _failure = new InputFormatterResult(hasError: true);
|
||||
private static readonly InputFormatterResult _noValue = new InputFormatterResult(hasError: false);
|
||||
private static readonly Task<InputFormatterResult> _failureAsync = Task.FromResult(_failure);
|
||||
private static readonly Task<InputFormatterResult> _noValueAsync = Task.FromResult(_noValue);
|
||||
|
||||
private InputFormatterResult()
|
||||
private InputFormatterResult(bool hasError)
|
||||
{
|
||||
HasError = true;
|
||||
HasError = hasError;
|
||||
}
|
||||
|
||||
private InputFormatterResult(object model)
|
||||
{
|
||||
Model = model;
|
||||
IsModelSet = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -28,6 +31,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
/// </summary>
|
||||
public bool HasError { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an indication whether a value for the <see cref="Model"/> property was supplied.
|
||||
/// </summary>
|
||||
public bool IsModelSet { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the deserialized <see cref="object"/>.
|
||||
/// </summary>
|
||||
|
|
@ -89,5 +97,31 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
{
|
||||
return Task.FromResult(Success(model));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an <see cref="InputFormatterResult"/> indicating the <see cref="IInputFormatter.ReadAsync"/>
|
||||
/// operation produced no value.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An <see cref="InputFormatterResult"/> indicating the <see cref="IInputFormatter.ReadAsync"/>
|
||||
/// operation produced no value.
|
||||
/// </returns>
|
||||
public static InputFormatterResult NoValue()
|
||||
{
|
||||
return _noValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="Task"/> that on completion provides an <see cref="InputFormatterResult"/> indicating
|
||||
/// the <see cref="IInputFormatter.ReadAsync"/> operation produced no value.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="Task"/> that on completion provides an <see cref="InputFormatterResult"/> indicating the
|
||||
/// <see cref="IInputFormatter.ReadAsync"/> operation produced no value.
|
||||
/// </returns>
|
||||
public static Task<InputFormatterResult> NoValueAsync()
|
||||
{
|
||||
return _noValueAsync;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
/// <value>Default <see cref="string"/> is "A value is required.".</value>
|
||||
Func<string> MissingKeyOrValueAccessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message the model binding system adds when no value is provided for the request body,
|
||||
/// but a value is required.
|
||||
/// </summary>
|
||||
/// <value>Default <see cref="string"/> is "A non-empty request body is required.".</value>
|
||||
Func<string> MissingRequestBodyRequiredValueAccessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message the model binding system adds when a <c>null</c> value is bound to a
|
||||
/// non-<see cref="Nullable"/> property.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{
|
||||
"OldTypeId": "public interface Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.IModelBindingMessageProvider",
|
||||
"NewTypeId": "public interface Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.IModelBindingMessageProvider",
|
||||
"NewMemberId": "System.Func<System.String> get_MissingRequestBodyRequiredValueAccessor()",
|
||||
"Kind": "Addition"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
{
|
||||
"OldTypeId": "public interface Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.IModelBindingMessageProvider",
|
||||
"NewTypeId": "public interface Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.IModelBindingMessageProvider",
|
||||
"NewMemberId": "System.Func<System.String> get_MissingRequestBodyRequiredValueAccessor()",
|
||||
"Kind": "Addition"
|
||||
}
|
||||
]
|
||||
|
|
@ -105,7 +105,12 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
var request = context.HttpContext.Request;
|
||||
if (request.ContentLength == 0)
|
||||
{
|
||||
return InputFormatterResult.SuccessAsync(GetDefaultValueForType(context.ModelType));
|
||||
if (context.TreatEmptyInputAsDefaultValue)
|
||||
{
|
||||
return InputFormatterResult.SuccessAsync(GetDefaultValueForType(context.ModelType));
|
||||
}
|
||||
|
||||
return InputFormatterResult.NoValueAsync();
|
||||
}
|
||||
|
||||
return ReadRequestBodyAsync(context);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
// Set up ModelBinding
|
||||
options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider());
|
||||
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
|
||||
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory));
|
||||
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory, options));
|
||||
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
|
||||
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
|
||||
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
private readonly IList<IInputFormatter> _formatters;
|
||||
private readonly Func<Stream, Encoding, TextReader> _readerFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly MvcOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="BodyModelBinder"/>.
|
||||
|
|
@ -45,7 +46,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
/// instances for reading the request body.
|
||||
/// </param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public BodyModelBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory, ILoggerFactory loggerFactory)
|
||||
public BodyModelBinder(
|
||||
IList<IInputFormatter> formatters,
|
||||
IHttpRequestStreamReaderFactory readerFactory,
|
||||
ILoggerFactory loggerFactory)
|
||||
: this(formatters, readerFactory, loggerFactory, options: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="BodyModelBinder"/>.
|
||||
/// </summary>
|
||||
/// <param name="formatters">The list of <see cref="IInputFormatter"/>.</param>
|
||||
/// <param name="readerFactory">
|
||||
/// The <see cref="IHttpRequestStreamReaderFactory"/>, used to create <see cref="System.IO.TextReader"/>
|
||||
/// instances for reading the request body.
|
||||
/// </param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="options">The <see cref="MvcOptions"/>.</param>
|
||||
public BodyModelBinder(
|
||||
IList<IInputFormatter> formatters,
|
||||
IHttpRequestStreamReaderFactory readerFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
MvcOptions options)
|
||||
{
|
||||
if (formatters == null)
|
||||
{
|
||||
|
|
@ -64,6 +87,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
{
|
||||
_logger = loggerFactory.CreateLogger<BodyModelBinder>();
|
||||
}
|
||||
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -89,12 +114,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
|
||||
var httpContext = bindingContext.HttpContext;
|
||||
|
||||
var allowEmptyInputInModelBinding = _options?.AllowEmptyInputInBodyModelBinding == true;
|
||||
|
||||
var formatterContext = new InputFormatterContext(
|
||||
httpContext,
|
||||
modelBindingKey,
|
||||
bindingContext.ModelState,
|
||||
bindingContext.ModelMetadata,
|
||||
_readerFactory);
|
||||
_readerFactory,
|
||||
allowEmptyInputInModelBinding);
|
||||
|
||||
var formatter = (IInputFormatter)null;
|
||||
for (var i = 0; i < _formatters.Count; i++)
|
||||
|
|
@ -132,7 +160,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
return;
|
||||
}
|
||||
|
||||
bindingContext.Result = ModelBindingResult.Success(model);
|
||||
if (result.IsModelSet)
|
||||
{
|
||||
bindingContext.Result = ModelBindingResult.Success(model);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the input formatter gives a "no value" result, that's always a model state error,
|
||||
// because BodyModelBinder implicitly regards input as being required for model binding.
|
||||
// If instead the input formatter wants to treat the input as optional, it must do so by
|
||||
// returning InputFormatterResult.Success(defaultForModelType), because input formatters
|
||||
// are responsible for choosing a default value for the model type.
|
||||
var message = bindingContext
|
||||
.ModelMetadata
|
||||
.ModelBindingMessageProvider
|
||||
.MissingRequestBodyRequiredValueAccessor();
|
||||
bindingContext.ModelState.AddModelError(modelBindingKey, message);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
private readonly IList<IInputFormatter> _formatters;
|
||||
private readonly IHttpRequestStreamReaderFactory _readerFactory;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly MvcOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="BodyModelBinderProvider"/>.
|
||||
|
|
@ -36,6 +37,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
/// <param name="readerFactory">The <see cref="IHttpRequestStreamReaderFactory"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
public BodyModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory, ILoggerFactory loggerFactory)
|
||||
: this(formatters, readerFactory, loggerFactory, options: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="BodyModelBinderProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="formatters">The list of <see cref="IInputFormatter"/>.</param>
|
||||
/// <param name="readerFactory">The <see cref="IHttpRequestStreamReaderFactory"/>.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="options">The <see cref="MvcOptions"/>.</param>
|
||||
public BodyModelBinderProvider(
|
||||
IList<IInputFormatter> formatters,
|
||||
IHttpRequestStreamReaderFactory readerFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
MvcOptions options)
|
||||
{
|
||||
if (formatters == null)
|
||||
{
|
||||
|
|
@ -50,6 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
_formatters = formatters;
|
||||
_readerFactory = readerFactory;
|
||||
_loggerFactory = loggerFactory;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -71,7 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
typeof(IInputFormatter).FullName));
|
||||
}
|
||||
|
||||
return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory);
|
||||
return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
{
|
||||
private Func<string, string> _missingBindRequiredValueAccessor;
|
||||
private Func<string> _missingKeyOrValueAccessor;
|
||||
private Func<string> _missingRequestBodyRequiredValueAccessor;
|
||||
private Func<string, string> _valueMustNotBeNullAccessor;
|
||||
private Func<string, string, string> _attemptedValueIsInvalidAccessor;
|
||||
private Func<string, string> _unknownValueIsInvalidAccessor;
|
||||
|
|
@ -26,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
{
|
||||
MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember;
|
||||
MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent;
|
||||
MissingRequestBodyRequiredValueAccessor = Resources.FormatModelBinding_MissingRequestBodyRequiredMember;
|
||||
ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid;
|
||||
AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid;
|
||||
UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid;
|
||||
|
|
@ -47,6 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
|
||||
MissingBindRequiredValueAccessor = originalProvider.MissingBindRequiredValueAccessor;
|
||||
MissingKeyOrValueAccessor = originalProvider.MissingKeyOrValueAccessor;
|
||||
MissingRequestBodyRequiredValueAccessor = originalProvider.MissingRequestBodyRequiredValueAccessor;
|
||||
ValueMustNotBeNullAccessor = originalProvider.ValueMustNotBeNullAccessor;
|
||||
AttemptedValueIsInvalidAccessor = originalProvider.AttemptedValueIsInvalidAccessor;
|
||||
UnknownValueIsInvalidAccessor = originalProvider.UnknownValueIsInvalidAccessor;
|
||||
|
|
@ -90,6 +93,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Func<string> MissingRequestBodyRequiredValueAccessor
|
||||
{
|
||||
get
|
||||
{
|
||||
return _missingRequestBodyRequiredValueAccessor;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
_missingRequestBodyRequiredValueAccessor = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Func<string, string> ValueMustNotBeNullAccessor
|
||||
{
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
ValueProviderFactories = new List<IValueProviderFactory>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the flag which decides whether body model binding (for example, on an
|
||||
/// action method parameter with <see cref="FromBodyAttribute"/>) should treat empty
|
||||
/// input as valid. <see langword="false"/> by default.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// When <see langword="false"/>, actions that model bind the request body (for example,
|
||||
/// using <see cref="FromBodyAttribute"/>) will register an error in the
|
||||
/// <see cref="ModelStateDictionary"/> if the incoming request body is empty.
|
||||
/// </example>
|
||||
public bool AllowEmptyInputInBodyModelBinding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Dictionary of CacheProfile Names, <see cref="CacheProfile"/> which are pre-defined settings for
|
||||
/// response caching.
|
||||
|
|
|
|||
|
|
@ -808,6 +808,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
internal static string FormatModelBinding_MissingBindRequiredMember(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_MissingBindRequiredMember"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// A non-empty request body is required.
|
||||
/// </summary>
|
||||
internal static string ModelBinding_MissingRequestBodyRequiredMember
|
||||
{
|
||||
get => GetString("ModelBinding_MissingRequestBodyRequiredMember");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A non-empty request body is required.
|
||||
/// </summary>
|
||||
internal static string FormatModelBinding_MissingRequestBodyRequiredMember()
|
||||
=> GetString("ModelBinding_MissingRequestBodyRequiredMember");
|
||||
|
||||
/// <summary>
|
||||
/// The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -296,6 +296,9 @@
|
|||
</data>
|
||||
<data name="ModelBinding_MissingBindRequiredMember" xml:space="preserve">
|
||||
<value>A value for the '{0}' property was not provided.</value>
|
||||
</data>
|
||||
<data name="ModelBinding_MissingRequestBodyRequiredMember" xml:space="preserve">
|
||||
<value>A non-empty request body is required.</value>
|
||||
</data>
|
||||
<data name="ValueProviderResult_NoConverterExists" xml:space="preserve">
|
||||
<value>The parameter conversion from type '{0}' to type '{1}' failed because no type converter can convert between these types.</value>
|
||||
|
|
|
|||
|
|
@ -158,7 +158,18 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
if (successful)
|
||||
{
|
||||
return InputFormatterResult.SuccessAsync(model);
|
||||
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.NoValueAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
return InputFormatterResult.SuccessAsync(model);
|
||||
}
|
||||
}
|
||||
|
||||
return InputFormatterResult.FailureAsync();
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
var formatter = new BadConfigurationFormatter();
|
||||
var context = new InputFormatterContext(
|
||||
new DefaultHttpContext(),
|
||||
"",
|
||||
string.Empty,
|
||||
new ModelStateDictionary(),
|
||||
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
|
||||
(s, e) => new StreamReader(s, e));
|
||||
|
|
@ -410,6 +410,31 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
() => formatter.GetSupportedContentTypes("application/json", typeof(object)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, true)]
|
||||
[InlineData(false, false)]
|
||||
public async Task ReadAsync_WithEmptyRequest_ReturnsNoValueResultWhenExpected(bool allowEmptyInputValue, bool expectedIsModelSet)
|
||||
{
|
||||
// Arrange
|
||||
var formatter = new TestFormatter();
|
||||
var context = new InputFormatterContext(
|
||||
new DefaultHttpContext(),
|
||||
string.Empty,
|
||||
new ModelStateDictionary(),
|
||||
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
|
||||
(s, e) => new StreamReader(s, e),
|
||||
allowEmptyInputValue);
|
||||
context.HttpContext.Request.ContentLength = 0;
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
Assert.Null(result.Model);
|
||||
Assert.Equal(expectedIsModelSet, result.IsModelSet);
|
||||
}
|
||||
|
||||
private class BadConfigurationFormatter : InputFormatter
|
||||
{
|
||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -115,6 +116,79 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
Assert.False(bindingContext.Result.IsModelSet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindModel_NoValueResult_SetsModelStateError()
|
||||
{
|
||||
// Arrange
|
||||
var mockInputFormatter = new Mock<IInputFormatter>();
|
||||
mockInputFormatter.Setup(f => f.CanRead(It.IsAny<InputFormatterContext>()))
|
||||
.Returns(true);
|
||||
mockInputFormatter.Setup(o => o.ReadAsync(It.IsAny<InputFormatterContext>()))
|
||||
.Returns(InputFormatterResult.NoValueAsync());
|
||||
var inputFormatter = mockInputFormatter.Object;
|
||||
|
||||
var provider = new TestModelMetadataProvider();
|
||||
provider.ForType<Person>().BindingDetails(d =>
|
||||
{
|
||||
d.BindingSource = BindingSource.Body;
|
||||
d.ModelBindingMessageProvider.MissingRequestBodyRequiredValueAccessor =
|
||||
() => "Customized error message";
|
||||
});
|
||||
|
||||
var bindingContext = GetBindingContext(
|
||||
typeof(Person),
|
||||
metadataProvider: provider);
|
||||
bindingContext.BinderModelName = "custom";
|
||||
|
||||
var binder = CreateBinder(new[] { inputFormatter });
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.Null(bindingContext.Result.Model);
|
||||
Assert.False(bindingContext.Result.IsModelSet);
|
||||
Assert.False(bindingContext.ModelState.IsValid);
|
||||
|
||||
// Key is the bindermodelname because this was a top-level binding.
|
||||
var entry = Assert.Single(bindingContext.ModelState);
|
||||
Assert.Equal("custom", entry.Key);
|
||||
Assert.Equal("Customized error message", entry.Value.Errors.Single().ErrorMessage);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task BindModel_PassesAllowEmptyInputOptionViaContext(bool treatEmptyInputAsDefaultValueOption)
|
||||
{
|
||||
// Arrange
|
||||
var mockInputFormatter = new Mock<IInputFormatter>();
|
||||
mockInputFormatter.Setup(f => f.CanRead(It.IsAny<InputFormatterContext>()))
|
||||
.Returns(true);
|
||||
mockInputFormatter.Setup(o => o.ReadAsync(It.IsAny<InputFormatterContext>()))
|
||||
.Returns(InputFormatterResult.NoValueAsync())
|
||||
.Verifiable();
|
||||
var inputFormatter = mockInputFormatter.Object;
|
||||
|
||||
var provider = new TestModelMetadataProvider();
|
||||
provider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
|
||||
|
||||
var bindingContext = GetBindingContext(
|
||||
typeof(Person),
|
||||
metadataProvider: provider);
|
||||
bindingContext.BinderModelName = "custom";
|
||||
|
||||
var binder = CreateBinder(new[] { inputFormatter }, treatEmptyInputAsDefaultValueOption);
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
mockInputFormatter.Verify(formatter => formatter.ReadAsync(
|
||||
It.Is<InputFormatterContext>(ctx => ctx.TreatEmptyInputAsDefaultValue == treatEmptyInputAsDefaultValueOption)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CustomFormatterDeserializationException_AddedToModelState()
|
||||
{
|
||||
|
|
@ -316,11 +390,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
return bindingContext;
|
||||
}
|
||||
|
||||
private static BodyModelBinder CreateBinder(IList<IInputFormatter> formatters)
|
||||
private static BodyModelBinder CreateBinder(IList<IInputFormatter> formatters, bool treatEmptyInputAsDefaultValueOption = false)
|
||||
{
|
||||
var sink = new TestSink();
|
||||
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
|
||||
return new BodyModelBinder(formatters, new TestHttpRequestStreamReaderFactory(), loggerFactory);
|
||||
var options = new MvcOptions { AllowEmptyInputInBodyModelBinding = treatEmptyInputAsDefaultValueOption };
|
||||
return new BodyModelBinder(formatters, new TestHttpRequestStreamReaderFactory(), loggerFactory, options);
|
||||
}
|
||||
|
||||
private class Person
|
||||
|
|
|
|||
|
|
@ -342,6 +342,40 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
Assert.IsType<TooManyModelErrorsException>(error.Exception);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("null", true, true)]
|
||||
[InlineData("null", false, false)]
|
||||
[InlineData(" ", true, true)]
|
||||
[InlineData(" ", false, false)]
|
||||
public async Task ReadAsync_WithInputThatDeserializesToNull_SetsModelOnlyIfAllowingEmptyInput(string content, bool allowEmptyInput, bool expectedIsModelSet)
|
||||
{
|
||||
// Arrange
|
||||
var logger = GetLogger();
|
||||
var formatter =
|
||||
new JsonInputFormatter(logger, _serializerSettings, ArrayPool<char>.Shared, _objectPoolProvider);
|
||||
var contentBytes = Encoding.UTF8.GetBytes(content);
|
||||
|
||||
var modelState = new ModelStateDictionary();
|
||||
var httpContext = GetHttpContext(contentBytes);
|
||||
var provider = new EmptyModelMetadataProvider();
|
||||
var metadata = provider.GetMetadataForType(typeof(object));
|
||||
var context = new InputFormatterContext(
|
||||
httpContext,
|
||||
modelName: string.Empty,
|
||||
modelState: modelState,
|
||||
metadata: metadata,
|
||||
readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader,
|
||||
treatEmptyInputAsDefaultValue: allowEmptyInput);
|
||||
|
||||
// Act
|
||||
var result = await formatter.ReadAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.HasError);
|
||||
Assert.Equal(expectedIsModelSet, result.IsModelSet);
|
||||
Assert.Null(result.Model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_UsesSerializerSettings()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -79,6 +79,24 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(expectedSampleIntValue.ToString(), responseBody);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("application/json", "")]
|
||||
[InlineData("application/json", " ")]
|
||||
public async Task JsonInputFormatter_ReturnsBadRequest_ForEmptyRequestBody(
|
||||
string requestContentType,
|
||||
string jsonInput)
|
||||
{
|
||||
// Arrange
|
||||
var content = new StringContent(jsonInput, Encoding.UTF8, requestContentType);
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\"I'm a JSON string!\"")]
|
||||
[InlineData("true")]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using System.Text;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||
|
|
@ -356,10 +357,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FromBodyAndRequiredOnProperty_EmptyBody_AddsModelStateError()
|
||||
public async Task FromBodyAllowingEmptyInputAndRequiredOnProperty_EmptyBody_AddsModelStateError()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "Parameter1",
|
||||
|
|
@ -381,6 +381,11 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
var addressRequired = ValidationAttributeUtil.GetRequiredErrorMessage("Address");
|
||||
|
||||
var optionsAccessor = testContext.GetService<IOptions<MvcOptions>>();
|
||||
optionsAccessor.Value.AllowEmptyInputInBodyModelBinding = true;
|
||||
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(optionsAccessor.Value);
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
|
|
@ -396,10 +401,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FromBodyOnActionParameter_EmptyBody_BindsToNullValue()
|
||||
public async Task FromBodyAllowingEmptyInputOnActionParameter_EmptyBody_BindsToNullValue()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "Parameter1",
|
||||
|
|
@ -421,6 +425,11 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
var httpContext = testContext.HttpContext;
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
var optionsAccessor = testContext.GetService<IOptions<MvcOptions>>();
|
||||
optionsAccessor.Value.AllowEmptyInputInBodyModelBinding = true;
|
||||
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(optionsAccessor.Value);
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
|
|
@ -584,6 +593,63 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.NotEmpty(error.Exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(true, true)]
|
||||
public async Task FromBodyWithEmptyBody_JsonFormatterAddsModelErrorWhenExpected(
|
||||
bool allowEmptyInputInBodyModelBindingSetting, bool expectedModelStateIsValid)
|
||||
{
|
||||
// Arrange
|
||||
var parameter = new ParameterDescriptor
|
||||
{
|
||||
Name = "Parameter1",
|
||||
BindingInfo = new BindingInfo
|
||||
{
|
||||
BinderModelName = "CustomParameter",
|
||||
},
|
||||
ParameterType = typeof(Person5)
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
request =>
|
||||
{
|
||||
request.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty));
|
||||
request.ContentType = "application/json";
|
||||
});
|
||||
|
||||
var optionsAccessor = testContext.GetService<IOptions<MvcOptions>>();
|
||||
optionsAccessor.Value.AllowEmptyInputInBodyModelBinding = allowEmptyInputInBodyModelBindingSetting;
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(optionsAccessor.Value);
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(modelBindingResult.IsModelSet);
|
||||
var boundPerson = Assert.IsType<Person5>(modelBindingResult.Model);
|
||||
Assert.NotNull(boundPerson);
|
||||
|
||||
if (expectedModelStateIsValid)
|
||||
{
|
||||
Assert.True(modelState.IsValid);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.False(modelState.IsValid);
|
||||
var entry = Assert.Single(modelState);
|
||||
Assert.Equal("CustomParameter.Address", entry.Key);
|
||||
var street = entry.Value;
|
||||
Assert.Equal(ModelValidationState.Invalid, street.ValidationState);
|
||||
var error = Assert.Single(street.Errors);
|
||||
|
||||
// Since the message doesn't come from DataAnnotations, we don't have a way to get the
|
||||
// exact string, so just check it's nonempty.
|
||||
Assert.NotEmpty(error.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private class Person2
|
||||
{
|
||||
[FromBody]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http.Internal;
|
|||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -134,7 +135,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
public async Task MutableObjectModelBinder_BindsNestedPOCO_WithBodyModelBinder_WithPrefix_NoBodyData()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "parameter",
|
||||
|
|
@ -148,9 +148,14 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
request.ContentType = "application/json";
|
||||
});
|
||||
|
||||
var optionsAccessor = testContext.GetService<IOptions<MvcOptions>>();
|
||||
optionsAccessor.Value.AllowEmptyInputInBodyModelBinding = true;
|
||||
|
||||
var modelState = testContext.ModelState;
|
||||
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
|
||||
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(optionsAccessor.Value);
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(testContext, valueProvider, parameter);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
{
|
||||
public class ExcludeBindingMetadataProviderIntegrationTest
|
||||
{
|
||||
[Fact]
|
||||
[Fact(Skip = "See issue #6110")]
|
||||
public async Task BindParameter_WithTypeProperty_IsNotBound()
|
||||
{
|
||||
// Arrange
|
||||
|
|
|
|||
|
|
@ -8,5 +8,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
public class ModelBindingTestContext : ControllerContext
|
||||
{
|
||||
public IModelMetadataProvider MetadataProvider { get; set; }
|
||||
|
||||
public T GetService<T>()
|
||||
{
|
||||
return (T)HttpContext.RequestServices.GetService(typeof(T));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,16 +53,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
else
|
||||
{
|
||||
var metadataProvider = TestModelMetadataProvider.CreateProvider(options.ModelMetadataDetailsProviders);
|
||||
return GetParameterBinder(metadataProvider, binderProvider);
|
||||
return GetParameterBinder(metadataProvider, binderProvider, options);
|
||||
}
|
||||
}
|
||||
|
||||
public static ParameterBinder GetParameterBinder(
|
||||
IModelMetadataProvider metadataProvider,
|
||||
IModelBinderProvider binderProvider = null)
|
||||
IModelBinderProvider binderProvider = null,
|
||||
MvcOptions mvcOptions = null)
|
||||
{
|
||||
var services = GetServices();
|
||||
var options = services.GetRequiredService<IOptions<MvcOptions>>();
|
||||
var options = mvcOptions != null
|
||||
? Options.Create(mvcOptions)
|
||||
: services.GetRequiredService<IOptions<MvcOptions>>();
|
||||
|
||||
if (binderProvider != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ namespace FormatterWebSite.Controllers
|
|||
[HttpPost]
|
||||
public IActionResult ReturnInput([FromBody]DummyClass dummyObject)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return Content(dummyObject.SampleInt.ToString());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue