From f7d2fac8a2d2818e4bfcb36e92b69c465161d2dc Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 22 Jun 2020 12:11:55 -0700 Subject: [PATCH] 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 --- ....AspNetCore.Mvc.Abstractions.netcoreapp.cs | 8 ++++ .../ApiExplorer/ApiParameterDescription.cs | 5 ++ .../src/ModelBinding/BindingInfo.cs | 16 +++++++ .../src/ModelBinding/EmptyBodyBehavior.cs | 27 +++++++++++ .../IAllowEmptyInputInBodyModelBinding.cs | 10 ++++ .../src/DefaultApiDescriptionProvider.cs | 17 +++++-- .../test/DefaultApiDescriptionProviderTest.cs | 45 ++++++++++++++++-- ...icrosoft.AspNetCore.Mvc.Core.netcoreapp.cs | 1 + src/Mvc/Mvc.Core/src/FromBodyAttribute.cs | 12 ++++- .../ModelBinding/Binders/BodyModelBinder.cs | 6 +-- .../Binders/BodyModelBinderProvider.cs | 20 +++++++- .../Binders/BodyModelBinderProviderTest.cs | 47 +++++++++++++++++++ .../Binders/BodyModelBinderTests.cs | 7 ++- .../InputFormatterTests.cs | 34 ++++++++++++-- .../Controllers/HomeController.cs | 9 ++++ 15 files changed, 245 insertions(+), 19 deletions(-) create mode 100644 src/Mvc/Mvc.Abstractions/src/ModelBinding/EmptyBodyBehavior.cs create mode 100644 src/Mvc/Mvc.Abstractions/src/ModelBinding/IAllowEmptyInputInBodyModelBinding.cs diff --git a/src/Mvc/Mvc.Abstractions/ref/Microsoft.AspNetCore.Mvc.Abstractions.netcoreapp.cs b/src/Mvc/Mvc.Abstractions/ref/Microsoft.AspNetCore.Mvc.Abstractions.netcoreapp.cs index f60d50cb40..84ca15ad39 100644 --- a/src/Mvc/Mvc.Abstractions/ref/Microsoft.AspNetCore.Mvc.Abstractions.netcoreapp.cs +++ b/src/Mvc/Mvc.Abstractions/ref/Microsoft.AspNetCore.Mvc.Abstractions.netcoreapp.cs @@ -160,6 +160,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer public partial class 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 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 { } } @@ -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 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.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 System.Func 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 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 static Microsoft.AspNetCore.Mvc.ModelBinding.CompositeBindingSource Create(System.Collections.Generic.IEnumerable bindingSources, string displayName) { throw null; } } + public enum EmptyBodyBehavior + { + Default = 0, + Allow = 1, + Disallow = 2, + } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct EnumGroupAndName { diff --git a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiParameterDescription.cs b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiParameterDescription.cs index f020c6361a..17f499c1f0 100644 --- a/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiParameterDescription.cs +++ b/src/Mvc/Mvc.Abstractions/src/ApiExplorer/ApiParameterDescription.cs @@ -32,6 +32,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer /// public BindingSource Source { get; set; } + /// + /// Gets or sets the . + /// + public BindingInfo BindingInfo { get; set; } + /// /// Gets or sets the parameter type. /// diff --git a/src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs b/src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs index cfa796ab75..38fbc894a9 100644 --- a/src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs +++ b/src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs @@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding BinderType = other.BinderType; PropertyFilterProvider = other.PropertyFilterProvider; RequestPredicate = other.RequestPredicate; + EmptyBodyBehavior = other.EmptyBodyBehavior; } /// @@ -87,6 +88,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding /// public Func RequestPredicate { get; set; } + /// + /// Gets or sets the value which decides if empty bodies are treated as valid inputs. + /// + public EmptyBodyBehavior EmptyBodyBehavior { get; set; } + /// /// Constructs a new instance of from the given . /// @@ -160,6 +166,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } } + foreach (var configureEmptyBodyBehavior in attributes.OfType()) + { + isBindingInfoPresent = true; + bindingInfo.EmptyBodyBehavior = configureEmptyBodyBehavior.EmptyBodyBehavior; + break; + } + return isBindingInfoPresent ? bindingInfo : null; } @@ -235,6 +248,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding PropertyFilterProvider = modelMetadata.PropertyFilterProvider; } + // There isn't a ModelMetadata feature to configure AllowEmptyInputInBodyModelBinding, + // so nothing to infer from it. + return isBindingInfoPresent; } diff --git a/src/Mvc/Mvc.Abstractions/src/ModelBinding/EmptyBodyBehavior.cs b/src/Mvc/Mvc.Abstractions/src/ModelBinding/EmptyBodyBehavior.cs new file mode 100644 index 0000000000..4e3a062d5e --- /dev/null +++ b/src/Mvc/Mvc.Abstractions/src/ModelBinding/EmptyBodyBehavior.cs @@ -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 +{ + /// + /// Determines the behavior for processing empty bodies during input formatting. + /// + public enum EmptyBodyBehavior + { + /// + /// Uses the framework default behavior for processing empty bodies. + /// This is typically configured using MvcOptions.AllowEmptyInputInBodyModelBinding + /// + Default, + + /// + /// Empty bodies are treated as valid inputs. + /// + Allow, + + /// + /// Empty bodies are treated as invalid inputs. + /// + Disallow, + } +} diff --git a/src/Mvc/Mvc.Abstractions/src/ModelBinding/IAllowEmptyInputInBodyModelBinding.cs b/src/Mvc/Mvc.Abstractions/src/ModelBinding/IAllowEmptyInputInBodyModelBinding.cs new file mode 100644 index 0000000000..3184da3e5b --- /dev/null +++ b/src/Mvc/Mvc.Abstractions/src/ModelBinding/IAllowEmptyInputInBodyModelBinding.cs @@ -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; } + } +} diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index d44d801bd9..bbc1f86ef1 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -222,7 +222,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer ProcessRouteParameters(context); // Set IsRequired=true - ProcessIsRequired(context); + ProcessIsRequired(context, _mvcOptions); // Set DefaultValue 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) { 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) @@ -466,6 +473,8 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer public string PropertyName { get; set; } + public BindingInfo BindingInfo { get; set; } + public static ApiParameterDescriptionContext GetContext( ModelMetadata metadata, BindingInfo bindingInfo, @@ -478,6 +487,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer BinderModelName = bindingInfo?.BinderModelName, BindingSource = bindingInfo?.BindingSource, PropertyName = propertyName ?? metadata.Name, + BindingInfo = bindingInfo, }; } } @@ -607,6 +617,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer Source = source, Type = bindingContext.ModelMetadata.ModelType, ParameterDescriptor = Parameter, + BindingInfo = bindingContext.BindingInfo }; } diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index 121b21d47a..332a9d2cfc 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -1725,12 +1725,47 @@ namespace Microsoft.AspNetCore.Mvc.Description var context = GetApiParameterContext(description); // Act - DefaultApiDescriptionProvider.ProcessIsRequired(context); + DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions()); // Assert 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] public void ProcessIsRequired_SetsTrue_ForParameterDescriptorsWithBindRequired() { @@ -1747,7 +1782,7 @@ namespace Microsoft.AspNetCore.Mvc.Description description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name)); // Act - DefaultApiDescriptionProvider.ProcessIsRequired(context); + DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions()); // Assert Assert.True(description.IsRequired); @@ -1765,7 +1800,7 @@ namespace Microsoft.AspNetCore.Mvc.Description var context = GetApiParameterContext(description); // Act - DefaultApiDescriptionProvider.ProcessIsRequired(context); + DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions()); // Assert Assert.True(description.IsRequired); @@ -1779,7 +1814,7 @@ namespace Microsoft.AspNetCore.Mvc.Description var context = GetApiParameterContext(description); // Act - DefaultApiDescriptionProvider.ProcessIsRequired(context); + DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions()); // Assert Assert.False(description.IsRequired); @@ -1798,7 +1833,7 @@ namespace Microsoft.AspNetCore.Mvc.Description description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name)); // Act - DefaultApiDescriptionProvider.ProcessIsRequired(context); + DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions()); // Assert Assert.False(description.IsRequired); diff --git a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp.cs b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp.cs index d4672abb31..0e3186b36f 100644 --- a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp.cs +++ b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp.cs @@ -745,6 +745,7 @@ namespace Microsoft.AspNetCore.Mvc { public FromBodyAttribute() { } 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)] public partial class FromFormAttribute : System.Attribute, Microsoft.AspNetCore.Mvc.ModelBinding.IBindingSourceMetadata, Microsoft.AspNetCore.Mvc.ModelBinding.IModelNameProvider diff --git a/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs b/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs index 157d1cdd3d..9894666c7d 100644 --- a/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs +++ b/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs @@ -10,9 +10,19 @@ namespace Microsoft.AspNetCore.Mvc /// Specifies that a parameter or property should be bound using the request body. /// [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class FromBodyAttribute : Attribute, IBindingSourceMetadata + public class FromBodyAttribute : Attribute, IBindingSourceMetadata, IConfigureEmptyBodyBehavior { /// public BindingSource BindingSource => BindingSource.Body; + + /// + /// Gets or sets a value which decides whether body model binding should treat empty + /// input as valid. + /// + /// + /// The default behavior is to use framework defaults as configured by . + /// Specifying or will override the framework defaults. + /// + public EmptyBodyBehavior EmptyBodyBehavior { get; set; } } } diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs index 5b036b844d..6b21cf751d 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs @@ -91,6 +91,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders _options = options; } + internal bool AllowEmptyBody { get; set; } + /// public async Task BindModelAsync(ModelBindingContext bindingContext) { @@ -116,15 +118,13 @@ 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, - allowEmptyInputInModelBinding); + AllowEmptyBody); var formatter = (IInputFormatter)null; for (var i = 0; i < _formatters.Count; i++) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs index c2110c793e..cf3c583e9b 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders { @@ -26,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders /// The list of . /// The . public BodyModelBinderProvider(IList 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)); } - 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; } + + internal static bool CalculateAllowEmptyBody(EmptyBodyBehavior emptyBodyBehavior, MvcOptions options) + { + if (emptyBodyBehavior == EmptyBodyBehavior.Default) + { + return options?.AllowEmptyInputInBodyModelBinding ?? false; + } + + return emptyBodyBehavior == EmptyBodyBehavior.Allow; + } } } diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderProviderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderProviderTest.cs index 22a6d69a66..148af531be 100644 --- a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderProviderTest.cs +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderProviderTest.cs @@ -86,6 +86,53 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders 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) { var sink = new TestSink(); diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs index ffdf5733d8..6aaf585aec 100644 --- a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs @@ -657,8 +657,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders private static BodyModelBinder CreateBinder(IList formatters, bool treatEmptyInputAsDefaultValueOption = false) { - var options = new MvcOptions { AllowEmptyInputInBodyModelBinding = treatEmptyInputAsDefaultValueOption }; - return CreateBinder(formatters, options); + var options = new MvcOptions(); + var binder = CreateBinder(formatters, options); + binder.AllowEmptyBody = treatEmptyInputAsDefaultValueOption; + + return binder; } private static BodyModelBinder CreateBinder(IList formatters, MvcOptions mvcOptions) diff --git a/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs b/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs index 699c1010f1..0283623d1a 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs @@ -3,8 +3,10 @@ using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Text; using System.Threading.Tasks; +using FormatterWebSite.Controllers; using FormatterWebSite.Models; using Microsoft.AspNetCore.Testing; using Newtonsoft.Json; @@ -22,9 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public HttpClient Client { get; } - [ConditionalFact] - // Mono issue - https://github.com/aspnet/External/issues/18 - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [Fact] public async Task CheckIfXmlInputFormatterIsBeingCalled() { // Arrange @@ -168,5 +168,33 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("The DerivedProperty field is required.", value.First); }); } + + [Fact] + public async Task BodyIsRequiredByDefault() + { + // Act + var response = await Client.PostAsJsonAsync($"Home/{nameof(HomeController.DefaultBody)}", value: null); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest); + var problemDetails = await response.Content.ReadFromJsonAsync(); + 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($"Home/{nameof(HomeController.OptionalBody)}", value: null); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } } } diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/HomeController.cs b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/HomeController.cs index 8f46f63571..4258cac153 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/HomeController.cs +++ b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/HomeController.cs @@ -2,6 +2,7 @@ // 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.ModelBinding; namespace FormatterWebSite.Controllers { @@ -34,5 +35,13 @@ namespace FormatterWebSite.Controllers 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(); } } \ No newline at end of file