Add support for optional FromBody parameters (#22634)
* Add support for optional FromBody parameters Fixes https://github.com/dotnet/aspnetcore/issues/6878 * Fixup nullable * Changes per API review
This commit is contained in:
parent
c2bfbf5a04
commit
f7d2fac8a2
|
|
@ -160,6 +160,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
public partial class ApiParameterDescription
|
public partial class ApiParameterDescription
|
||||||
{
|
{
|
||||||
public ApiParameterDescription() { }
|
public ApiParameterDescription() { }
|
||||||
|
public Microsoft.AspNetCore.Mvc.ModelBinding.BindingInfo BindingInfo { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public object DefaultValue { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public object DefaultValue { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public bool IsRequired { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public bool IsRequired { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata ModelMetadata { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata ModelMetadata { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
|
|
@ -462,6 +463,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
public string BinderModelName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public string BinderModelName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public System.Type BinderType { get { throw null; } set { } }
|
public System.Type BinderType { get { throw null; } set { } }
|
||||||
public Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource BindingSource { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource BindingSource { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
|
public Microsoft.AspNetCore.Mvc.ModelBinding.EmptyBodyBehavior EmptyBodyBehavior { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public Microsoft.AspNetCore.Mvc.ModelBinding.IPropertyFilterProvider PropertyFilterProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public Microsoft.AspNetCore.Mvc.ModelBinding.IPropertyFilterProvider PropertyFilterProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public System.Func<Microsoft.AspNetCore.Mvc.ActionContext, bool> RequestPredicate { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public System.Func<Microsoft.AspNetCore.Mvc.ActionContext, bool> RequestPredicate { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public static Microsoft.AspNetCore.Mvc.ModelBinding.BindingInfo GetBindingInfo(System.Collections.Generic.IEnumerable<object> attributes) { throw null; }
|
public static Microsoft.AspNetCore.Mvc.ModelBinding.BindingInfo GetBindingInfo(System.Collections.Generic.IEnumerable<object> attributes) { throw null; }
|
||||||
|
|
@ -500,6 +502,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
public override bool CanAcceptDataFrom(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource bindingSource) { throw null; }
|
public override bool CanAcceptDataFrom(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource bindingSource) { throw null; }
|
||||||
public static Microsoft.AspNetCore.Mvc.ModelBinding.CompositeBindingSource Create(System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource> bindingSources, string displayName) { throw null; }
|
public static Microsoft.AspNetCore.Mvc.ModelBinding.CompositeBindingSource Create(System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource> bindingSources, string displayName) { throw null; }
|
||||||
}
|
}
|
||||||
|
public enum EmptyBodyBehavior
|
||||||
|
{
|
||||||
|
Default = 0,
|
||||||
|
Allow = 1,
|
||||||
|
Disallow = 2,
|
||||||
|
}
|
||||||
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||||
public readonly partial struct EnumGroupAndName
|
public readonly partial struct EnumGroupAndName
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public BindingSource Source { get; set; }
|
public BindingSource Source { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="BindingInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
public BindingInfo BindingInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the parameter type.
|
/// Gets or sets the parameter type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
BinderType = other.BinderType;
|
BinderType = other.BinderType;
|
||||||
PropertyFilterProvider = other.PropertyFilterProvider;
|
PropertyFilterProvider = other.PropertyFilterProvider;
|
||||||
RequestPredicate = other.RequestPredicate;
|
RequestPredicate = other.RequestPredicate;
|
||||||
|
EmptyBodyBehavior = other.EmptyBodyBehavior;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -87,6 +88,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<ActionContext, bool> RequestPredicate { get; set; }
|
public Func<ActionContext, bool> RequestPredicate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the value which decides if empty bodies are treated as valid inputs.
|
||||||
|
/// </summary>
|
||||||
|
public EmptyBodyBehavior EmptyBodyBehavior { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs a new instance of <see cref="BindingInfo"/> from the given <paramref name="attributes"/>.
|
/// Constructs a new instance of <see cref="BindingInfo"/> from the given <paramref name="attributes"/>.
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -160,6 +166,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var configureEmptyBodyBehavior in attributes.OfType<IConfigureEmptyBodyBehavior>())
|
||||||
|
{
|
||||||
|
isBindingInfoPresent = true;
|
||||||
|
bindingInfo.EmptyBodyBehavior = configureEmptyBodyBehavior.EmptyBodyBehavior;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return isBindingInfoPresent ? bindingInfo : null;
|
return isBindingInfoPresent ? bindingInfo : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,6 +248,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
PropertyFilterProvider = modelMetadata.PropertyFilterProvider;
|
PropertyFilterProvider = modelMetadata.PropertyFilterProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// There isn't a ModelMetadata feature to configure AllowEmptyInputInBodyModelBinding,
|
||||||
|
// so nothing to infer from it.
|
||||||
|
|
||||||
return isBindingInfoPresent;
|
return isBindingInfoPresent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determines the behavior for processing empty bodies during input formatting.
|
||||||
|
/// </summary>
|
||||||
|
public enum EmptyBodyBehavior
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Uses the framework default behavior for processing empty bodies.
|
||||||
|
/// This is typically configured using <c>MvcOptions.AllowEmptyInputInBodyModelBinding</c>
|
||||||
|
/// </summary>
|
||||||
|
Default,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Empty bodies are treated as valid inputs.
|
||||||
|
/// </summary>
|
||||||
|
Allow,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Empty bodies are treated as invalid inputs.
|
||||||
|
/// </summary>
|
||||||
|
Disallow,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
|
{
|
||||||
|
internal interface IConfigureEmptyBodyBehavior
|
||||||
|
{
|
||||||
|
public EmptyBodyBehavior EmptyBodyBehavior { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -222,7 +222,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
ProcessRouteParameters(context);
|
ProcessRouteParameters(context);
|
||||||
|
|
||||||
// Set IsRequired=true
|
// Set IsRequired=true
|
||||||
ProcessIsRequired(context);
|
ProcessIsRequired(context, _mvcOptions);
|
||||||
|
|
||||||
// Set DefaultValue
|
// Set DefaultValue
|
||||||
ProcessParameterDefaultValue(context);
|
ProcessParameterDefaultValue(context);
|
||||||
|
|
@ -273,13 +273,20 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void ProcessIsRequired(ApiParameterContext context)
|
internal static void ProcessIsRequired(ApiParameterContext context, MvcOptions mvcOptions)
|
||||||
{
|
{
|
||||||
foreach (var parameter in context.Results)
|
foreach (var parameter in context.Results)
|
||||||
{
|
{
|
||||||
if (parameter.Source == BindingSource.Body)
|
if (parameter.Source == BindingSource.Body)
|
||||||
{
|
{
|
||||||
parameter.IsRequired = true;
|
if (parameter.BindingInfo == null || parameter.BindingInfo.EmptyBodyBehavior == EmptyBodyBehavior.Default)
|
||||||
|
{
|
||||||
|
parameter.IsRequired = !mvcOptions.AllowEmptyInputInBodyModelBinding;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parameter.IsRequired = !(parameter.BindingInfo.EmptyBodyBehavior == EmptyBodyBehavior.Allow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parameter.ModelMetadata != null && parameter.ModelMetadata.IsBindingRequired)
|
if (parameter.ModelMetadata != null && parameter.ModelMetadata.IsBindingRequired)
|
||||||
|
|
@ -466,6 +473,8 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
|
|
||||||
public string PropertyName { get; set; }
|
public string PropertyName { get; set; }
|
||||||
|
|
||||||
|
public BindingInfo BindingInfo { get; set; }
|
||||||
|
|
||||||
public static ApiParameterDescriptionContext GetContext(
|
public static ApiParameterDescriptionContext GetContext(
|
||||||
ModelMetadata metadata,
|
ModelMetadata metadata,
|
||||||
BindingInfo bindingInfo,
|
BindingInfo bindingInfo,
|
||||||
|
|
@ -478,6 +487,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
BinderModelName = bindingInfo?.BinderModelName,
|
BinderModelName = bindingInfo?.BinderModelName,
|
||||||
BindingSource = bindingInfo?.BindingSource,
|
BindingSource = bindingInfo?.BindingSource,
|
||||||
PropertyName = propertyName ?? metadata.Name,
|
PropertyName = propertyName ?? metadata.Name,
|
||||||
|
BindingInfo = bindingInfo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -607,6 +617,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
Source = source,
|
Source = source,
|
||||||
Type = bindingContext.ModelMetadata.ModelType,
|
Type = bindingContext.ModelMetadata.ModelType,
|
||||||
ParameterDescriptor = Parameter,
|
ParameterDescriptor = Parameter,
|
||||||
|
BindingInfo = bindingContext.BindingInfo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1725,12 +1725,47 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
||||||
var context = GetApiParameterContext(description);
|
var context = GetApiParameterContext(description);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(description.IsRequired);
|
Assert.True(description.IsRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessIsRequired_SetsFalse_IfAllowEmptyInputInBodyModelBinding_IsSetInMvcOptions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var description = new ApiParameterDescription { Source = BindingSource.Body, };
|
||||||
|
var context = GetApiParameterContext(description);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions { AllowEmptyInputInBodyModelBinding = true });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(description.IsRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessIsRequired_SetsFalse_IfEmptyBodyBehaviorIsAllowedInBindingInfo()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var description = new ApiParameterDescription
|
||||||
|
{
|
||||||
|
Source = BindingSource.Body,
|
||||||
|
BindingInfo = new BindingInfo
|
||||||
|
{
|
||||||
|
EmptyBodyBehavior = EmptyBodyBehavior.Allow,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var context = GetApiParameterContext(description);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(description.IsRequired);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProcessIsRequired_SetsTrue_ForParameterDescriptorsWithBindRequired()
|
public void ProcessIsRequired_SetsTrue_ForParameterDescriptorsWithBindRequired()
|
||||||
{
|
{
|
||||||
|
|
@ -1747,7 +1782,7 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
||||||
description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name));
|
description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(description.IsRequired);
|
Assert.True(description.IsRequired);
|
||||||
|
|
@ -1765,7 +1800,7 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
||||||
var context = GetApiParameterContext(description);
|
var context = GetApiParameterContext(description);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(description.IsRequired);
|
Assert.True(description.IsRequired);
|
||||||
|
|
@ -1779,7 +1814,7 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
||||||
var context = GetApiParameterContext(description);
|
var context = GetApiParameterContext(description);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(description.IsRequired);
|
Assert.False(description.IsRequired);
|
||||||
|
|
@ -1798,7 +1833,7 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
||||||
description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name));
|
description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(description.IsRequired);
|
Assert.False(description.IsRequired);
|
||||||
|
|
|
||||||
|
|
@ -745,6 +745,7 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
{
|
{
|
||||||
public FromBodyAttribute() { }
|
public FromBodyAttribute() { }
|
||||||
public Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource BindingSource { get { throw null; } }
|
public Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource BindingSource { get { throw null; } }
|
||||||
|
public Microsoft.AspNetCore.Mvc.ModelBinding.EmptyBodyBehavior EmptyBodyBehavior { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
}
|
}
|
||||||
[System.AttributeUsageAttribute(System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
|
[System.AttributeUsageAttribute(System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
|
||||||
public partial class FromFormAttribute : System.Attribute, Microsoft.AspNetCore.Mvc.ModelBinding.IBindingSourceMetadata, Microsoft.AspNetCore.Mvc.ModelBinding.IModelNameProvider
|
public partial class FromFormAttribute : System.Attribute, Microsoft.AspNetCore.Mvc.ModelBinding.IBindingSourceMetadata, Microsoft.AspNetCore.Mvc.ModelBinding.IModelNameProvider
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,19 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
/// Specifies that a parameter or property should be bound using the request body.
|
/// Specifies that a parameter or property should be bound using the request body.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||||
public class FromBodyAttribute : Attribute, IBindingSourceMetadata
|
public class FromBodyAttribute : Attribute, IBindingSourceMetadata, IConfigureEmptyBodyBehavior
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public BindingSource BindingSource => BindingSource.Body;
|
public BindingSource BindingSource => BindingSource.Body;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value which decides whether body model binding should treat empty
|
||||||
|
/// input as valid.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The default behavior is to use framework defaults as configured by <see cref="MvcOptions.AllowEmptyInputInBodyModelBinding"/>.
|
||||||
|
/// Specifying <see cref="EmptyBodyBehavior.Allow"/> or <see cref="EmptyBodyBehavior.Disallow" /> will override the framework defaults.
|
||||||
|
/// </remarks>
|
||||||
|
public EmptyBodyBehavior EmptyBodyBehavior { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
_options = options;
|
_options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool AllowEmptyBody { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
{
|
{
|
||||||
|
|
@ -116,15 +118,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
|
||||||
var httpContext = bindingContext.HttpContext;
|
var httpContext = bindingContext.HttpContext;
|
||||||
|
|
||||||
var allowEmptyInputInModelBinding = _options?.AllowEmptyInputInBodyModelBinding == true;
|
|
||||||
|
|
||||||
var formatterContext = new InputFormatterContext(
|
var formatterContext = new InputFormatterContext(
|
||||||
httpContext,
|
httpContext,
|
||||||
modelBindingKey,
|
modelBindingKey,
|
||||||
bindingContext.ModelState,
|
bindingContext.ModelState,
|
||||||
bindingContext.ModelMetadata,
|
bindingContext.ModelMetadata,
|
||||||
_readerFactory,
|
_readerFactory,
|
||||||
allowEmptyInputInModelBinding);
|
AllowEmptyBody);
|
||||||
|
|
||||||
var formatter = (IInputFormatter)null;
|
var formatter = (IInputFormatter)null;
|
||||||
for (var i = 0; i < _formatters.Count; i++)
|
for (var i = 0; i < _formatters.Count; i++)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.Core;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
{
|
{
|
||||||
|
|
@ -26,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
/// <param name="formatters">The list of <see cref="IInputFormatter"/>.</param>
|
/// <param name="formatters">The list of <see cref="IInputFormatter"/>.</param>
|
||||||
/// <param name="readerFactory">The <see cref="IHttpRequestStreamReaderFactory"/>.</param>
|
/// <param name="readerFactory">The <see cref="IHttpRequestStreamReaderFactory"/>.</param>
|
||||||
public BodyModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
|
public BodyModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
|
||||||
: this(formatters, readerFactory, loggerFactory: null)
|
: this(formatters, readerFactory, loggerFactory: NullLoggerFactory.Instance)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,10 +90,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
typeof(IInputFormatter).FullName));
|
typeof(IInputFormatter).FullName));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options);
|
var treatEmptyInputAsDefaultValue = CalculateAllowEmptyBody(context.BindingInfo.EmptyBodyBehavior, _options);
|
||||||
|
|
||||||
|
return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options)
|
||||||
|
{
|
||||||
|
AllowEmptyBody = treatEmptyInputAsDefaultValue,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool CalculateAllowEmptyBody(EmptyBodyBehavior emptyBodyBehavior, MvcOptions options)
|
||||||
|
{
|
||||||
|
if (emptyBodyBehavior == EmptyBodyBehavior.Default)
|
||||||
|
{
|
||||||
|
return options?.AllowEmptyInputInBodyModelBinding ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyBodyBehavior == EmptyBodyBehavior.Allow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,53 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
provider.GetBinder(context);
|
provider.GetBinder(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculateAllowEmptyBody_EmptyBodyBehaviorIsDefaultValue_UsesMvcOptions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new MvcOptions { AllowEmptyInputInBodyModelBinding = true };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var allowEmpty = BodyModelBinderProvider.CalculateAllowEmptyBody(EmptyBodyBehavior.Default, options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(allowEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculateAllowEmptyBody_EmptyBodyBehaviorIsDefaultValue_DefaultsToFalseWhenOptionsIsUnavailable()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var allowEmpty = BodyModelBinderProvider.CalculateAllowEmptyBody(EmptyBodyBehavior.Default, options: null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(allowEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculateAllowEmptyBody_EmptyBodyBehaviorIsAllow()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var allowEmpty = BodyModelBinderProvider.CalculateAllowEmptyBody(EmptyBodyBehavior.Allow, options: new MvcOptions());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(allowEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CalculateAllowEmptyBody_EmptyBodyBehaviorIsDisallowed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
// MvcOptions.AllowEmptyInputInBodyModelBinding should be ignored if EmptyBodyBehavior disallows it
|
||||||
|
var options = new MvcOptions { AllowEmptyInputInBodyModelBinding = true };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var allowEmpty = BodyModelBinderProvider.CalculateAllowEmptyBody(EmptyBodyBehavior.Disallow, options);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(allowEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
private static BodyModelBinderProvider CreateProvider(params IInputFormatter[] formatters)
|
private static BodyModelBinderProvider CreateProvider(params IInputFormatter[] formatters)
|
||||||
{
|
{
|
||||||
var sink = new TestSink();
|
var sink = new TestSink();
|
||||||
|
|
|
||||||
|
|
@ -657,8 +657,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
|
||||||
private static BodyModelBinder CreateBinder(IList<IInputFormatter> formatters, bool treatEmptyInputAsDefaultValueOption = false)
|
private static BodyModelBinder CreateBinder(IList<IInputFormatter> formatters, bool treatEmptyInputAsDefaultValueOption = false)
|
||||||
{
|
{
|
||||||
var options = new MvcOptions { AllowEmptyInputInBodyModelBinding = treatEmptyInputAsDefaultValueOption };
|
var options = new MvcOptions();
|
||||||
return CreateBinder(formatters, options);
|
var binder = CreateBinder(formatters, options);
|
||||||
|
binder.AllowEmptyBody = treatEmptyInputAsDefaultValueOption;
|
||||||
|
|
||||||
|
return binder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BodyModelBinder CreateBinder(IList<IInputFormatter> formatters, MvcOptions mvcOptions)
|
private static BodyModelBinder CreateBinder(IList<IInputFormatter> formatters, MvcOptions mvcOptions)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@
|
||||||
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using FormatterWebSite.Controllers;
|
||||||
using FormatterWebSite.Models;
|
using FormatterWebSite.Models;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
@ -22,9 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
public HttpClient Client { get; }
|
public HttpClient Client { get; }
|
||||||
|
|
||||||
[ConditionalFact]
|
[Fact]
|
||||||
// Mono issue - https://github.com/aspnet/External/issues/18
|
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
|
||||||
public async Task CheckIfXmlInputFormatterIsBeingCalled()
|
public async Task CheckIfXmlInputFormatterIsBeingCalled()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -168,5 +168,33 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("The DerivedProperty field is required.", value.First);
|
Assert.Equal("The DerivedProperty field is required.", value.First);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BodyIsRequiredByDefault()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await Client.PostAsJsonAsync<object>($"Home/{nameof(HomeController.DefaultBody)}", value: null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
|
||||||
|
var problemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||||
|
Assert.Collection(
|
||||||
|
problemDetails.Errors,
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Empty(kvp.Key);
|
||||||
|
Assert.Equal("A non-empty request body is required.", Assert.Single(kvp.Value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OptionalFromBodyWorks()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await Client.PostAsJsonAsync<object>($"Home/{nameof(HomeController.OptionalBody)}", value: null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
namespace FormatterWebSite.Controllers
|
namespace FormatterWebSite.Controllers
|
||||||
{
|
{
|
||||||
|
|
@ -34,5 +35,13 @@ namespace FormatterWebSite.Controllers
|
||||||
SampleIntInDerived = 50
|
SampleIntInDerived = 50
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult DefaultBody([FromBody] DummyClass dummy)
|
||||||
|
=> ModelState.IsValid ? Ok() : ValidationProblem();
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult OptionalBody([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] DummyClass dummy)
|
||||||
|
=> ModelState.IsValid ? Ok() : ValidationProblem();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue