diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs index 2bfe03d14b97..4efbc86c5f2f 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs @@ -8,6 +8,7 @@ using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -143,6 +144,17 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) if (formatter == null) { + if (AllowEmptyBody) + { + var hasBody = httpContext.Features.Get()?.CanHaveBody; + hasBody ??= httpContext.Request.ContentLength is not null && httpContext.Request.ContentLength == 0; + if (hasBody == false) + { + bindingContext.Result = ModelBindingResult.Success(model: null); + return; + } + } + _logger.NoInputFormatterSelected(formatterContext); var message = Resources.FormatUnsupportedContentType(httpContext.Request.ContentType); diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs index 76ceb024c10f..a63505a7ccb7 100644 --- a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs @@ -195,6 +195,62 @@ public async Task BindModel_PassesAllowEmptyInputOptionViaContext(bool treatEmpt Times.Once); } + [Fact] + public async Task BindModel_SetsModelIfAllowEmpty() + { + // Arrange + var mockInputFormatter = new Mock(); + mockInputFormatter.Setup(f => f.CanRead(It.IsAny())) + .Returns(false); + var inputFormatter = mockInputFormatter.Object; + + var provider = new TestModelMetadataProvider(); + provider.ForType().BindingDetails(d => d.BindingSource = BindingSource.Body); + + var bindingContext = GetBindingContext( + typeof(Person), + metadataProvider: provider); + bindingContext.BinderModelName = "custom"; + + var binder = CreateBinder(new[] { inputFormatter }, treatEmptyInputAsDefaultValueOption : true); + + // Act + await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(bindingContext.Result.IsModelSet); + Assert.Null(bindingContext.Result.Model); + Assert.True(bindingContext.ModelState.IsValid); + } + + [Fact] + public async Task BindModel_FailsIfNotAllowEmpty() + { + // Arrange + var mockInputFormatter = new Mock(); + mockInputFormatter.Setup(f => f.CanRead(It.IsAny())) + .Returns(false); + var inputFormatter = mockInputFormatter.Object; + + var provider = new TestModelMetadataProvider(); + provider.ForType().BindingDetails(d => d.BindingSource = BindingSource.Body); + + var bindingContext = GetBindingContext( + typeof(Person), + metadataProvider: provider); + bindingContext.BinderModelName = "custom"; + + var binder = CreateBinder(new[] { inputFormatter }, treatEmptyInputAsDefaultValueOption: false); + + // Act + await binder.BindModelAsync(bindingContext); + + // Assert + Assert.False(bindingContext.ModelState.IsValid); + Assert.Single(bindingContext.ModelState[bindingContext.BinderModelName].Errors); + Assert.Equal("Unsupported content type ''.", bindingContext.ModelState[bindingContext.BinderModelName].Errors[0].Exception.Message); + } + // Throwing InputFormatterException [Fact] public async Task BindModel_CustomFormatter_ThrowingInputFormatterException_AddsErrorToModelState() diff --git a/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs b/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs index 984cf3d98185..2911d85a82ed 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs @@ -188,6 +188,20 @@ public async Task BodyIsRequiredByDefault() }); } + [Fact] + public async Task BodyIsRequiredByDefaultFailsWithEmptyBody() + { + var content = new ByteArrayContent(Array.Empty()); + Assert.Null(content.Headers.ContentType); + Assert.Equal(0, content.Headers.ContentLength); + + // Act + var response = await Client.PostAsync($"Home/{nameof(HomeController.DefaultBody)}", content); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType); + } + [Fact] public async Task OptionalFromBodyWorks() { @@ -197,4 +211,19 @@ public async Task OptionalFromBodyWorks() // Assert await response.AssertStatusCodeAsync(HttpStatusCode.OK); } + + [Fact] + public async Task OptionalFromBodyWorksWithEmptyRequest() + { + // Arrange + var content = new ByteArrayContent(Array.Empty()); + Assert.Null(content.Headers.ContentType); + Assert.Equal(0, content.Headers.ContentLength); + + // Act + var response = await Client.PostAsync($"Home/{nameof(HomeController.OptionalBody)}", content); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } }