From f3ebc05a67f56796d99e26b1b3eacc5290fb8f87 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Sat, 6 Jun 2020 15:29:13 -0700 Subject: [PATCH 1/3] Add support for optional FromBody parameters Fixes https://github.com/dotnet/aspnetcore/issues/6878 --- ....AspNetCore.Mvc.Abstractions.netcoreapp.cs | 2 + .../ApiExplorer/ApiParameterDescription.cs | 5 +++ .../src/ModelBinding/BindingInfo.cs | 20 +++++++++ .../IAllowEmptyInputInBodyModelBinding.cs | 10 +++++ .../src/DefaultApiDescriptionProvider.cs | 10 +++-- .../test/DefaultApiDescriptionProviderTest.cs | 45 ++++++++++++++++--- ...icrosoft.AspNetCore.Mvc.Core.netcoreapp.cs | 5 ++- src/Mvc/Mvc.Core/src/FromBodyAttribute.cs | 20 ++++++++- .../ModelBinding/Binders/BodyModelBinder.cs | 13 +++--- .../Binders/BodyModelBinderProvider.cs | 21 ++++++--- .../Binders/BodyModelBinderTests.cs | 7 ++- .../InputFormatterTests.cs | 34 ++++++++++++-- .../Controllers/HomeController.cs | 8 ++++ 13 files changed, 172 insertions(+), 28 deletions(-) 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 f60d50cb4014..07fa85a62563 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 @@ public ApiDescriptionProviderContext(System.Collections.Generic.IReadOnlyList 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 cfa796ab7589..4f116b61603e 100644 --- a/src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs +++ b/src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs @@ -38,6 +38,7 @@ public BindingInfo(BindingInfo other) BinderType = other.BinderType; PropertyFilterProvider = other.PropertyFilterProvider; RequestPredicate = other.RequestPredicate; + AllowEmptyInputInBodyModelBinding = other.AllowEmptyInputInBodyModelBinding; } /// @@ -87,6 +88,12 @@ public Type BinderType /// public Func RequestPredicate { get; set; } + /// + /// Gets or sets the flag which decides whether body model binding should treat empty + /// input as valid. + /// + public bool? AllowEmptyInputInBodyModelBinding { get; set; } + /// /// Constructs a new instance of from the given . /// @@ -160,6 +167,16 @@ public static BindingInfo GetBindingInfo(IEnumerable attributes) } } + foreach (var allowEmptyInputInModelBinding in attributes.OfType()) + { + isBindingInfoPresent = true; + if (allowEmptyInputInModelBinding.AllowEmptyInputInBodyModelBinding != null) + { + bindingInfo.AllowEmptyInputInBodyModelBinding = allowEmptyInputInModelBinding.AllowEmptyInputInBodyModelBinding; + break; + } + } + return isBindingInfoPresent ? bindingInfo : null; } @@ -235,6 +252,9 @@ public bool TryApplyBindingInfo(ModelMetadata modelMetadata) 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/IAllowEmptyInputInBodyModelBinding.cs b/src/Mvc/Mvc.Abstractions/src/ModelBinding/IAllowEmptyInputInBodyModelBinding.cs new file mode 100644 index 000000000000..b04e0add89e3 --- /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 IAllowEmptyInputInBodyModelBinding + { + public bool? AllowEmptyInputInBodyModelBinding { get; } + } +} diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index d44d801bd988..5b57b854b1f5 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -222,7 +222,7 @@ private IList GetParameters(ApiParameterContext context ProcessRouteParameters(context); // Set IsRequired=true - ProcessIsRequired(context); + ProcessIsRequired(context, _mvcOptions); // Set DefaultValue ProcessParameterDefaultValue(context); @@ -273,13 +273,13 @@ private void ProcessRouteParameters(ApiParameterContext context) } } - 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; + parameter.IsRequired = !(parameter.BindingInfo?.AllowEmptyInputInBodyModelBinding ?? mvcOptions.AllowEmptyInputInBodyModelBinding); } if (parameter.ModelMetadata != null && parameter.ModelMetadata.IsBindingRequired) @@ -466,6 +466,8 @@ private class ApiParameterDescriptionContext public string PropertyName { get; set; } + public BindingInfo BindingInfo { get; set; } + public static ApiParameterDescriptionContext GetContext( ModelMetadata metadata, BindingInfo bindingInfo, @@ -478,6 +480,7 @@ public static ApiParameterDescriptionContext GetContext( BinderModelName = bindingInfo?.BinderModelName, BindingSource = bindingInfo?.BindingSource, PropertyName = propertyName ?? metadata.Name, + BindingInfo = bindingInfo, }; } } @@ -607,6 +610,7 @@ private ApiParameterDescription CreateResult( 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 121b21d47a89..327f1df05e60 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -1725,12 +1725,47 @@ public void ProcessIsRequired_SetsTrue_ForFromBodyParameters() 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_IfAllowEmptyInputInBodyModelBinding_IsSetInBindingInfo() + { + // Arrange + var description = new ApiParameterDescription + { + Source = BindingSource.Body, + BindingInfo = new BindingInfo + { + AllowEmptyInputInBodyModelBinding = true, + } + }; + 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 @@ public void ProcessIsRequired_SetsTrue_ForParameterDescriptorsWithBindRequired() 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 @@ public void ProcessIsRequired_SetsTrue_ForRequiredRouteParameterDescriptors() var context = GetApiParameterContext(description); // Act - DefaultApiDescriptionProvider.ProcessIsRequired(context); + DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions()); // Assert Assert.True(description.IsRequired); @@ -1779,7 +1814,7 @@ public void ProcessIsRequired_DoesNotSetToTrue_ByDefault() var context = GetApiParameterContext(description); // Act - DefaultApiDescriptionProvider.ProcessIsRequired(context); + DefaultApiDescriptionProvider.ProcessIsRequired(context, new MvcOptions()); // Assert Assert.False(description.IsRequired); @@ -1798,7 +1833,7 @@ public void ProcessIsRequired_DoesNotSetToTrue_ForParameterDescriptorsWithValida 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 d4672abb31dc..70aeae70e9e6 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 @@ -744,6 +744,7 @@ public FormatFilterAttribute() { } public partial class FromBodyAttribute : System.Attribute, Microsoft.AspNetCore.Mvc.ModelBinding.IBindingSourceMetadata { public FromBodyAttribute() { } + public bool AllowEmptyInputInBodyModelBinding { get { throw null; } set { } } public Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource BindingSource { get { throw null; } } } [System.AttributeUsageAttribute(System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] @@ -2794,8 +2795,8 @@ public partial class BodyModelBinderProvider : Microsoft.AspNetCore.Mvc.ModelBin { public BodyModelBinderProvider(System.Collections.Generic.IList formatters, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpRequestStreamReaderFactory readerFactory) { } public BodyModelBinderProvider(System.Collections.Generic.IList formatters, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpRequestStreamReaderFactory readerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } - public BodyModelBinderProvider(System.Collections.Generic.IList formatters, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpRequestStreamReaderFactory readerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Mvc.MvcOptions options) { } - public Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext context) { throw null; } + public BodyModelBinderProvider(System.Collections.Generic.IList formatters, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpRequestStreamReaderFactory readerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Mvc.MvcOptions? options) { } + public Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext context) { throw null; } } public partial class ByteArrayModelBinder : Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder { diff --git a/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs b/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs index 157d1cdd3d8e..59324af39341 100644 --- a/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs +++ b/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs @@ -10,9 +10,27 @@ 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, IAllowEmptyInputInBodyModelBinding { + private bool? _allowEmptyInputInBodyModelBinding; + /// public BindingSource BindingSource => BindingSource.Body; + + /// + /// Gets or sets the flag which decides whether body model binding (for example, on an + /// action method parameter with ) should treat empty + /// input as valid. by default. + /// + /// + /// When configured, takes precedence over . + /// + public bool AllowEmptyInputInBodyModelBinding + { + get => _allowEmptyInputInBodyModelBinding ?? false; + set => _allowEmptyInputInBodyModelBinding = value; + } + + bool? IAllowEmptyInputInBodyModelBinding.AllowEmptyInputInBodyModelBinding => _allowEmptyInputInBodyModelBinding; } } diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs index 5b036b844dd7..c675fb88acf4 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs @@ -80,17 +80,18 @@ public BodyModelBinder( throw new ArgumentNullException(nameof(readerFactory)); } - _formatters = formatters; - _readerFactory = readerFactory.CreateReader; - if (loggerFactory != null) { - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory?.CreateLogger(); } _options = options; + _formatters = formatters; + _readerFactory = readerFactory.CreateReader; } + internal bool AllowEmptyInputInBodyModelBinding { get; set; } + /// public async Task BindModelAsync(ModelBindingContext bindingContext) { @@ -116,15 +117,13 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) var httpContext = bindingContext.HttpContext; - var allowEmptyInputInModelBinding = _options?.AllowEmptyInputInBodyModelBinding == true; - var formatterContext = new InputFormatterContext( httpContext, modelBindingKey, bindingContext.ModelState, bindingContext.ModelMetadata, _readerFactory, - allowEmptyInputInModelBinding); + AllowEmptyInputInBodyModelBinding); 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 c2110c793e79..1265f8148c6c 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs @@ -1,12 +1,15 @@ // 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. +#nullable enable + using System; using System.Collections.Generic; 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 { @@ -18,7 +21,7 @@ public class BodyModelBinderProvider : IModelBinderProvider private readonly IList _formatters; private readonly IHttpRequestStreamReaderFactory _readerFactory; private readonly ILoggerFactory _loggerFactory; - private readonly MvcOptions _options; + private readonly MvcOptions? _options; /// /// Creates a new . @@ -26,7 +29,7 @@ public class BodyModelBinderProvider : IModelBinderProvider /// The list of . /// The . public BodyModelBinderProvider(IList formatters, IHttpRequestStreamReaderFactory readerFactory) - : this(formatters, readerFactory, loggerFactory: null) + : this(formatters, readerFactory, loggerFactory: NullLoggerFactory.Instance) { } @@ -52,7 +55,7 @@ public BodyModelBinderProvider( IList formatters, IHttpRequestStreamReaderFactory readerFactory, ILoggerFactory loggerFactory, - MvcOptions options) + MvcOptions? options) { if (formatters == null) { @@ -71,7 +74,7 @@ public BodyModelBinderProvider( } /// - public IModelBinder GetBinder(ModelBinderProviderContext context) + public IModelBinder? GetBinder(ModelBinderProviderContext context) { if (context == null) { @@ -89,7 +92,15 @@ public IModelBinder GetBinder(ModelBinderProviderContext context) typeof(IInputFormatter).FullName)); } - return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options); + var allowEmptyInputInBodyModelBinding = + context?.BindingInfo.AllowEmptyInputInBodyModelBinding + ?? _options?.AllowEmptyInputInBodyModelBinding + ?? false; + + return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options) + { + AllowEmptyInputInBodyModelBinding = allowEmptyInputInBodyModelBinding, + }; } return null; diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs index ffdf5733d86e..a7212f732b06 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 @@ private static DefaultModelBindingContext GetBindingContext( 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.AllowEmptyInputInBodyModelBinding = 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 699c1010f1e5..0283623d1af3 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 @@ public InputFormatterTests(MvcTestFixture fixture) 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 @@ public async Task ValidationUsesModelMetadataFromActualModelType_ForInputFormatt 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 8f46f635716b..f2532b2ad3b2 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/HomeController.cs +++ b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/HomeController.cs @@ -34,5 +34,13 @@ public DummyClass GetDerivedDummyClass(int sampleInput) SampleIntInDerived = 50 }; } + + [HttpPost] + public IActionResult DefaultBody([FromBody] DummyClass dummy) + => ModelState.IsValid ? Ok() : ValidationProblem(); + + [HttpPost] + public IActionResult OptionalBody([FromBody(AllowEmptyInputInBodyModelBinding = true)] DummyClass dummy) + => ModelState.IsValid ? Ok() : ValidationProblem(); } } \ No newline at end of file From 0d0691bb2ed0a2db6452899a52ddab1ec880b2eb Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 10 Jun 2020 10:24:05 -0700 Subject: [PATCH 2/3] Fixup nullable --- .../Microsoft.AspNetCore.Mvc.Core.netcoreapp.cs | 4 ++-- .../Binders/BodyModelBinderProvider.cs | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) 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 70aeae70e9e6..aa2a45cc3e1b 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 @@ -2795,8 +2795,8 @@ public partial class BodyModelBinderProvider : Microsoft.AspNetCore.Mvc.ModelBin { public BodyModelBinderProvider(System.Collections.Generic.IList formatters, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpRequestStreamReaderFactory readerFactory) { } public BodyModelBinderProvider(System.Collections.Generic.IList formatters, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpRequestStreamReaderFactory readerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } - public BodyModelBinderProvider(System.Collections.Generic.IList formatters, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpRequestStreamReaderFactory readerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Mvc.MvcOptions? options) { } - public Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder? GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext context) { throw null; } + public BodyModelBinderProvider(System.Collections.Generic.IList formatters, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpRequestStreamReaderFactory readerFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Mvc.MvcOptions options) { } + public Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder GetBinder(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBinderProviderContext context) { throw null; } } public partial class ByteArrayModelBinder : Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinder { diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs index 1265f8148c6c..945025fa8e88 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs @@ -1,8 +1,6 @@ // 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. -#nullable enable - using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Core; @@ -21,7 +19,7 @@ public class BodyModelBinderProvider : IModelBinderProvider private readonly IList _formatters; private readonly IHttpRequestStreamReaderFactory _readerFactory; private readonly ILoggerFactory _loggerFactory; - private readonly MvcOptions? _options; + private readonly MvcOptions _options; /// /// Creates a new . @@ -55,7 +53,7 @@ public BodyModelBinderProvider( IList formatters, IHttpRequestStreamReaderFactory readerFactory, ILoggerFactory loggerFactory, - MvcOptions? options) + MvcOptions options) { if (formatters == null) { @@ -74,7 +72,7 @@ public BodyModelBinderProvider( } /// - public IModelBinder? GetBinder(ModelBinderProviderContext context) + public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context == null) { @@ -93,9 +91,9 @@ public BodyModelBinderProvider( } var allowEmptyInputInBodyModelBinding = - context?.BindingInfo.AllowEmptyInputInBodyModelBinding - ?? _options?.AllowEmptyInputInBodyModelBinding - ?? false; + context?.BindingInfo.AllowEmptyInputInBodyModelBinding ?? + _options?.AllowEmptyInputInBodyModelBinding ?? + false; return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options) { From 7d2a94e4e152a6bb43ccd4e16db87fe4b46cf672 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 22 Jun 2020 09:25:43 -0700 Subject: [PATCH 3/3] Changes per API review --- ....AspNetCore.Mvc.Abstractions.netcoreapp.cs | 8 +++- .../src/ModelBinding/BindingInfo.cs | 16 +++---- .../src/ModelBinding/EmptyBodyBehavior.cs | 27 +++++++++++ .../IAllowEmptyInputInBodyModelBinding.cs | 4 +- .../src/DefaultApiDescriptionProvider.cs | 9 +++- .../test/DefaultApiDescriptionProviderTest.cs | 4 +- ...icrosoft.AspNetCore.Mvc.Core.netcoreapp.cs | 2 +- src/Mvc/Mvc.Core/src/FromBodyAttribute.cs | 20 +++----- .../ModelBinding/Binders/BodyModelBinder.cs | 11 +++-- .../Binders/BodyModelBinderProvider.cs | 17 +++++-- .../Binders/BodyModelBinderProviderTest.cs | 47 +++++++++++++++++++ .../Binders/BodyModelBinderTests.cs | 2 +- .../Controllers/HomeController.cs | 3 +- 13 files changed, 127 insertions(+), 43 deletions(-) create mode 100644 src/Mvc/Mvc.Abstractions/src/ModelBinding/EmptyBodyBehavior.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 07fa85a62563..84ca15ad39f9 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 @@ -460,10 +460,10 @@ public partial class BindingInfo { public BindingInfo() { } public BindingInfo(Microsoft.AspNetCore.Mvc.ModelBinding.BindingInfo other) { } - public bool? AllowEmptyInputInBodyModelBinding { [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 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; } @@ -502,6 +502,12 @@ public partial class CompositeBindingSource : Microsoft.AspNetCore.Mvc.ModelBind 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/ModelBinding/BindingInfo.cs b/src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs index 4f116b61603e..38fbc894a968 100644 --- a/src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs +++ b/src/Mvc/Mvc.Abstractions/src/ModelBinding/BindingInfo.cs @@ -38,7 +38,7 @@ public BindingInfo(BindingInfo other) BinderType = other.BinderType; PropertyFilterProvider = other.PropertyFilterProvider; RequestPredicate = other.RequestPredicate; - AllowEmptyInputInBodyModelBinding = other.AllowEmptyInputInBodyModelBinding; + EmptyBodyBehavior = other.EmptyBodyBehavior; } /// @@ -89,10 +89,9 @@ public Type BinderType public Func RequestPredicate { get; set; } /// - /// Gets or sets the flag which decides whether body model binding should treat empty - /// input as valid. + /// Gets or sets the value which decides if empty bodies are treated as valid inputs. /// - public bool? AllowEmptyInputInBodyModelBinding { get; set; } + public EmptyBodyBehavior EmptyBodyBehavior { get; set; } /// /// Constructs a new instance of from the given . @@ -167,14 +166,11 @@ public static BindingInfo GetBindingInfo(IEnumerable attributes) } } - foreach (var allowEmptyInputInModelBinding in attributes.OfType()) + foreach (var configureEmptyBodyBehavior in attributes.OfType()) { isBindingInfoPresent = true; - if (allowEmptyInputInModelBinding.AllowEmptyInputInBodyModelBinding != null) - { - bindingInfo.AllowEmptyInputInBodyModelBinding = allowEmptyInputInModelBinding.AllowEmptyInputInBodyModelBinding; - break; - } + bindingInfo.EmptyBodyBehavior = configureEmptyBodyBehavior.EmptyBodyBehavior; + break; } return isBindingInfoPresent ? bindingInfo : null; 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 000000000000..4e3a062d5eaf --- /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 index b04e0add89e3..3184da3e5bc9 100644 --- a/src/Mvc/Mvc.Abstractions/src/ModelBinding/IAllowEmptyInputInBodyModelBinding.cs +++ b/src/Mvc/Mvc.Abstractions/src/ModelBinding/IAllowEmptyInputInBodyModelBinding.cs @@ -3,8 +3,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { - internal interface IAllowEmptyInputInBodyModelBinding + internal interface IConfigureEmptyBodyBehavior { - public bool? AllowEmptyInputInBodyModelBinding { get; } + public EmptyBodyBehavior EmptyBodyBehavior { get; } } } diff --git a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs index 5b57b854b1f5..bbc1f86ef118 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs @@ -279,7 +279,14 @@ internal static void ProcessIsRequired(ApiParameterContext context, MvcOptions m { if (parameter.Source == BindingSource.Body) { - parameter.IsRequired = !(parameter.BindingInfo?.AllowEmptyInputInBodyModelBinding ?? mvcOptions.AllowEmptyInputInBodyModelBinding); + 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) diff --git a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs index 327f1df05e60..332a9d2cfc3a 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs @@ -1746,7 +1746,7 @@ public void ProcessIsRequired_SetsFalse_IfAllowEmptyInputInBodyModelBinding_IsSe } [Fact] - public void ProcessIsRequired_SetsFalse_IfAllowEmptyInputInBodyModelBinding_IsSetInBindingInfo() + public void ProcessIsRequired_SetsFalse_IfEmptyBodyBehaviorIsAllowedInBindingInfo() { // Arrange var description = new ApiParameterDescription @@ -1754,7 +1754,7 @@ public void ProcessIsRequired_SetsFalse_IfAllowEmptyInputInBodyModelBinding_IsSe Source = BindingSource.Body, BindingInfo = new BindingInfo { - AllowEmptyInputInBodyModelBinding = true, + EmptyBodyBehavior = EmptyBodyBehavior.Allow, } }; var context = GetApiParameterContext(description); 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 aa2a45cc3e1b..0e3186b36f81 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 @@ -744,8 +744,8 @@ public FormatFilterAttribute() { } public partial class FromBodyAttribute : System.Attribute, Microsoft.AspNetCore.Mvc.ModelBinding.IBindingSourceMetadata { public FromBodyAttribute() { } - public bool AllowEmptyInputInBodyModelBinding { get { throw null; } set { } } 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 59324af39341..9894666c7de9 100644 --- a/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs +++ b/src/Mvc/Mvc.Core/src/FromBodyAttribute.cs @@ -10,27 +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, IAllowEmptyInputInBodyModelBinding + public class FromBodyAttribute : Attribute, IBindingSourceMetadata, IConfigureEmptyBodyBehavior { - private bool? _allowEmptyInputInBodyModelBinding; - /// public BindingSource BindingSource => BindingSource.Body; /// - /// Gets or sets the flag which decides whether body model binding (for example, on an - /// action method parameter with ) should treat empty - /// input as valid. by default. + /// Gets or sets a value which decides whether body model binding should treat empty + /// input as valid. /// /// - /// When configured, takes precedence over . + /// The default behavior is to use framework defaults as configured by . + /// Specifying or will override the framework defaults. /// - public bool AllowEmptyInputInBodyModelBinding - { - get => _allowEmptyInputInBodyModelBinding ?? false; - set => _allowEmptyInputInBodyModelBinding = value; - } - - bool? IAllowEmptyInputInBodyModelBinding.AllowEmptyInputInBodyModelBinding => _allowEmptyInputInBodyModelBinding; + 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 c675fb88acf4..6b21cf751d75 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs @@ -80,17 +80,18 @@ public BodyModelBinder( throw new ArgumentNullException(nameof(readerFactory)); } + _formatters = formatters; + _readerFactory = readerFactory.CreateReader; + if (loggerFactory != null) { - _logger = loggerFactory?.CreateLogger(); + _logger = loggerFactory.CreateLogger(); } _options = options; - _formatters = formatters; - _readerFactory = readerFactory.CreateReader; } - internal bool AllowEmptyInputInBodyModelBinding { get; set; } + internal bool AllowEmptyBody { get; set; } /// public async Task BindModelAsync(ModelBindingContext bindingContext) @@ -123,7 +124,7 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) bindingContext.ModelState, bindingContext.ModelMetadata, _readerFactory, - AllowEmptyInputInBodyModelBinding); + 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 945025fa8e88..cf3c583e9b69 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinderProvider.cs @@ -90,18 +90,25 @@ public IModelBinder GetBinder(ModelBinderProviderContext context) typeof(IInputFormatter).FullName)); } - var allowEmptyInputInBodyModelBinding = - context?.BindingInfo.AllowEmptyInputInBodyModelBinding ?? - _options?.AllowEmptyInputInBodyModelBinding ?? - false; + var treatEmptyInputAsDefaultValue = CalculateAllowEmptyBody(context.BindingInfo.EmptyBodyBehavior, _options); return new BodyModelBinder(_formatters, _readerFactory, _loggerFactory, _options) { - AllowEmptyInputInBodyModelBinding = allowEmptyInputInBodyModelBinding, + 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 22a6d69a6686..148af531be1a 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 @@ public void GetBinder_DoesNotThrowNullReferenceException() 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 a7212f732b06..6aaf585aecd7 100644 --- a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs @@ -659,7 +659,7 @@ private static BodyModelBinder CreateBinder(IList formatters, b { var options = new MvcOptions(); var binder = CreateBinder(formatters, options); - binder.AllowEmptyInputInBodyModelBinding = treatEmptyInputAsDefaultValueOption; + binder.AllowEmptyBody = treatEmptyInputAsDefaultValueOption; return binder; } diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/HomeController.cs b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/HomeController.cs index f2532b2ad3b2..4258cac15331 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 { @@ -40,7 +41,7 @@ public IActionResult DefaultBody([FromBody] DummyClass dummy) => ModelState.IsValid ? Ok() : ValidationProblem(); [HttpPost] - public IActionResult OptionalBody([FromBody(AllowEmptyInputInBodyModelBinding = true)] DummyClass dummy) + public IActionResult OptionalBody([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] DummyClass dummy) => ModelState.IsValid ? Ok() : ValidationProblem(); } } \ No newline at end of file