From e5732c5ea063331a19e0658c9abf31c7c61800da Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Mon, 23 May 2016 21:17:08 -0700 Subject: [PATCH] Use type metadata for `ModelBinderProviderContext.BindingInfo` - #4652 - previously ignored for top-level models - `ModelBinderProviderContext.BindingInfo` is now never `null` - similarly, use type metadata (as well as parameter info) for `ModelBindingContext.BinderModelName` - previously ignored when overridden in `ControllerArgumentBinder` --- .../ModelBinderProviderContext.cs | 2 +- .../Internal/ControllerArgumentBinder.cs | 5 +- .../Binders/BinderTypeModelBinderProvider.cs | 2 +- .../Binders/BodyModelBinderProvider.cs | 2 +- .../Binders/HeaderModelBinderProvider.cs | 2 +- .../Binders/ServicesModelBinderProvider.cs | 2 +- .../ModelBinding/ModelBinderFactory.cs | 9 +- .../TestModelBinderProviderContext.cs | 10 +- ...nderTypeBasedModelBinderIntegrationTest.cs | 49 +++++ .../BodyValidationIntegrationTests.cs | 124 ++++++++++-- .../GenericModelBinderIntegrationTest.cs | 2 +- ...MutableObjectModelBinderIntegrationTest.cs | 181 ++++++++++++++++++ .../ServicesModelBinderIntegrationTest.cs | 78 ++++++++ 13 files changed, 445 insertions(+), 23 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBinderProviderContext.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBinderProviderContext.cs index aa7f4a91a7..2d01d6acb9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBinderProviderContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelBinderProviderContext.cs @@ -16,7 +16,7 @@ public abstract class ModelBinderProviderContext public abstract IModelBinder CreateBinder(ModelMetadata metadata); /// - /// Gets the . May be null. + /// Gets the . /// public abstract BindingInfo BindingInfo { get; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerArgumentBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerArgumentBinder.cs index 196d19209d..741e5a6b47 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerArgumentBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerArgumentBinder.cs @@ -163,10 +163,11 @@ public async Task BindModelAsync( parameter.BindingInfo, parameter.Name); - if (parameter.BindingInfo?.BinderModelName != null) + var parameterModelName = parameter.BindingInfo?.BinderModelName ?? metadata.BinderModelName; + if (parameterModelName != null) { // The name was set explicitly, always use that as the prefix. - modelBindingContext.ModelName = parameter.BindingInfo.BinderModelName; + modelBindingContext.ModelName = parameterModelName; } else if (modelBindingContext.ValueProvider.ContainsPrefix(parameter.Name)) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinderProvider.cs index 22a0c1bac7..aa56c0edcb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinderProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BinderTypeModelBinderProvider.cs @@ -19,7 +19,7 @@ public IModelBinder GetBinder(ModelBinderProviderContext context) throw new ArgumentNullException(nameof(context)); } - if (context.BindingInfo?.BinderType != null) + if (context.BindingInfo.BinderType != null) { return new BinderTypeModelBinder(context.BindingInfo.BinderType); } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs index ac2818c33f..0658cc71d3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs @@ -45,7 +45,7 @@ public IModelBinder GetBinder(ModelBinderProviderContext context) throw new ArgumentNullException(nameof(context)); } - if (context.BindingInfo?.BindingSource != null && + if (context.BindingInfo.BindingSource != null && context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body)) { return new BodyModelBinder(_formatters, _readerFactory); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinderProvider.cs index 8da75e8da2..ce10e5ebb8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinderProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/HeaderModelBinderProvider.cs @@ -18,7 +18,7 @@ public IModelBinder GetBinder(ModelBinderProviderContext context) throw new ArgumentNullException(nameof(context)); } - if (context.BindingInfo?.BindingSource != null && + if (context.BindingInfo.BindingSource != null && context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header)) { // We only support strings and collections of strings. Some cases can fail diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinderProvider.cs index c543a043d2..2dea775a0a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinderProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ServicesModelBinderProvider.cs @@ -18,7 +18,7 @@ public IModelBinder GetBinder(ModelBinderProviderContext context) throw new ArgumentNullException(nameof(context)); } - if (context.BindingInfo?.BindingSource != null && + if (context.BindingInfo.BindingSource != null && context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Services)) { return new ServicesModelBinder(); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs index e17538b719..fd244e742d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs @@ -152,7 +152,14 @@ public DefaultModelBinderProviderContext( { _factory = factory; Metadata = factoryContext.Metadata; - BindingInfo = factoryContext.BindingInfo; + BindingInfo = new BindingInfo + { + BinderModelName = factoryContext.BindingInfo?.BinderModelName ?? Metadata.BinderModelName, + BinderType = factoryContext.BindingInfo?.BinderType ?? Metadata.BinderType, + BindingSource = factoryContext.BindingInfo?.BindingSource ?? Metadata.BindingSource, + PropertyFilterProvider = + factoryContext.BindingInfo?.PropertyFilterProvider ?? Metadata.PropertyFilterProvider, + }; MetadataProvider = _factory._metadataProvider; Stack = new List>(); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/TestModelBinderProviderContext.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/TestModelBinderProviderContext.cs index 1478ebb3fc..d8b963e798 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/TestModelBinderProviderContext.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/TestModelBinderProviderContext.cs @@ -11,7 +11,7 @@ public class TestModelBinderProviderContext : ModelBinderProviderContext // Has to be internal because TestModelMetadataProvider is 'shared' code. internal static readonly TestModelMetadataProvider CachedMetadataProvider = new TestModelMetadataProvider(); - private readonly List> _binderCreators = + private readonly List> _binderCreators = new List>(); public TestModelBinderProviderContext(Type modelType) @@ -31,7 +31,13 @@ public TestModelBinderProviderContext(Type modelType) public TestModelBinderProviderContext(ModelMetadata metadata, BindingInfo bindingInfo) { Metadata = metadata; - BindingInfo = bindingInfo; + BindingInfo = bindingInfo ?? new BindingInfo + { + BinderModelName = metadata.BinderModelName, + BinderType = metadata.BinderType, + BindingSource = metadata.BindingSource, + PropertyFilterProvider = metadata.PropertyFilterProvider, + }; MetadataProvider = CachedMetadataProvider; } diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs index 9d14d55d7c..e20fa6e815 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BinderTypeBasedModelBinderIntegrationTest.cs @@ -134,6 +134,55 @@ private class Address public string Street { get; set; } } + public static TheoryData NullAndEmptyBindingInfo + { + get + { + return new TheoryData + { + null, + new BindingInfo(), + }; + } + } + + [Theory] + [MemberData(nameof(NullAndEmptyBindingInfo))] + public async Task BinderTypeOnParameterType_WithData_EmptyPrefix_GetsBound(BindingInfo bindingInfo) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor + { + Name = "Parameter1", + BindingInfo = bindingInfo, + ParameterType = typeof(Address), + }; + + var testContext = ModelBindingTestHelper.GetTestContext(); + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var address = Assert.IsType
(modelBindingResult.Model); + Assert.Equal("SomeStreet", address.Street); + + // ModelState + Assert.True(modelState.IsValid); + var kvp = Assert.Single(modelState); + Assert.Equal("Street", kvp.Key); + var entry = kvp.Value; + Assert.NotNull(entry); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + Assert.NotNull(entry.RawValue); // Value is set by test model binder, no need to validate it. + } + [Fact] public async Task BindProperty_WithData_EmptyPrefix_GetsBound() { diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs index fda4489d54..6845f77e5f 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs @@ -1,6 +1,7 @@ // 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. +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; @@ -15,18 +16,6 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests { public class BodyValidationIntegrationTests { - private class Person - { - [FromBody] - [Required] - public Address Address { get; set; } - } - - private class Address - { - public string Street { get; set; } - } - [Fact] public async Task ModelMetadataTypeAttribute_ValidBaseClass_NoModelStateErrors() { @@ -354,6 +343,18 @@ public async Task ModelMetadataTypeAttribute_InvalidClassAttributeOnBaseClassPro Assert.Equal("Product must be made in the USA if it is not named.", modelStateErrors[""]); } + private class Person + { + [FromBody] + [Required] + public Address Address { get; set; } + } + + private class Address + { + public string Street { get; set; } + } + [Fact] public async Task FromBodyAndRequiredOnProperty_EmptyBody_AddsModelStateError() { @@ -690,6 +691,105 @@ public async Task FromBodyOnProperty_Succeeds_IgnoresRequiredOnValueTypeSubPrope Assert.Empty(modelState); } + private class Person6 + { + public Address6 Address { get; set; } + } + + private class Address6 + { + public string Street { get; set; } + } + + [Theory] + [MemberData( + nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo), + MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))] + public async Task FromBodyOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo) + { + // Arrange + var inputText = "{ \"Street\" : \"someStreet\" }"; + + // Similar to a custom IBindingSourceMetadata implementation or [ModelBinder] subclass. + var metadataProvider = new TestModelMetadataProvider(); + metadataProvider + .ForProperty(nameof(Person6.Address)) + .BindingDetails(binding => binding.BindingSource = BindingSource.Body); + + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); + var parameter = new ParameterDescriptor + { + Name = "parameter-name", + BindingInfo = bindingInfo, + ParameterType = typeof(Person6), + }; + + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(inputText)); + request.ContentType = "application/json"; + }); + testContext.MetadataProvider = metadataProvider; + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + var person = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(person.Address); + Assert.Equal("someStreet", person.Address.Street, StringComparer.Ordinal); + + Assert.True(modelState.IsValid); + Assert.Empty(modelState); + } + + [Theory] + [MemberData( + nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo), + MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))] + public async Task FromBodyOnParameterType_WithData_Succeeds(BindingInfo bindingInfo) + { + // Arrange + var inputText = "{ \"Street\" : \"someStreet\" }"; + + // Similar to a custom IBindingSourceMetadata implementation or [ModelBinder] subclass. + var metadataProvider = new TestModelMetadataProvider(); + metadataProvider + .ForType() + .BindingDetails(binding => binding.BindingSource = BindingSource.Body); + + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); + var parameter = new ParameterDescriptor + { + Name = "parameter-name", + BindingInfo = bindingInfo, + ParameterType = typeof(Address6), + }; + + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(inputText)); + request.ContentType = "application/json"; + }); + testContext.MetadataProvider = metadataProvider; + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + var address = Assert.IsType(modelBindingResult.Model); + Assert.Equal("someStreet", address.Street, StringComparer.Ordinal); + + Assert.True(modelState.IsValid); + Assert.Empty(modelState); + } + private Dictionary CreateValidationDictionary(ModelStateDictionary modelState) { var result = new Dictionary(); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs index f1c6fa3fcd..cd7adce382 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/GenericModelBinderIntegrationTest.cs @@ -154,7 +154,7 @@ private class AddressBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { - var allowedBindingSource = context.BindingInfo?.BindingSource; + var allowedBindingSource = context.BindingInfo.BindingSource; if (allowedBindingSource?.CanAcceptDataFrom(BindAddressAttribute.Source) == true) { // Binding Sources are opt-in. This model either didn't specify one or specified something diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs index 32099b28c4..e622534d8f 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/MutableObjectModelBinderIntegrationTest.cs @@ -1932,6 +1932,187 @@ public async Task MutableObjectModelBinder_BindsPOCO_TypeConvertedPropertyWithEm Assert.False(modelState.IsValid); } + private class Person12 + { + public Address12 Address { get; set; } + } + + [ModelBinder(Name = "HomeAddress")] + private class Address12 + { + public string Street { get; set; } + } + + [Theory] + [MemberData( + nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo), + MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))] + public async Task ModelNameOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor + { + Name = "parameter-name", + BindingInfo = bindingInfo, + ParameterType = typeof(Person12), + }; + + var testContext = ModelBindingTestHelper.GetTestContext( + request => request.QueryString = new QueryString("?HomeAddress.Street=someStreet")); + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + var person = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(person.Address); + Assert.Equal("someStreet", person.Address.Street, StringComparer.Ordinal); + + Assert.True(modelState.IsValid); + var kvp = Assert.Single(modelState); + Assert.Equal("HomeAddress.Street", kvp.Key); + var entry = kvp.Value; + Assert.NotNull(entry); + Assert.Empty(entry.Errors); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + } + + [Theory] + [MemberData( + nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo), + MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))] + public async Task ModelNameOnParameterType_WithData_Succeeds(BindingInfo bindingInfo) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor + { + Name = "parameter-name", + BindingInfo = bindingInfo, + ParameterType = typeof(Address12), + }; + + var testContext = ModelBindingTestHelper.GetTestContext( + request => request.QueryString = new QueryString("?HomeAddress.Street=someStreet")); + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + var address = Assert.IsType(modelBindingResult.Model); + Assert.Equal("someStreet", address.Street, StringComparer.Ordinal); + + Assert.True(modelState.IsValid); + var kvp = Assert.Single(modelState); + Assert.Equal("HomeAddress.Street", kvp.Key); + var entry = kvp.Value; + Assert.NotNull(entry); + Assert.Empty(entry.Errors); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + } + + private class Person13 + { + public Address13 Address { get; set; } + } + + [Bind("Street")] + private class Address13 + { + public int Number { get; set; } + + public string Street { get; set; } + + public string City { get; set; } + + public string State { get; set; } + } + + [Theory] + [MemberData( + nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo), + MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))] + public async Task BindAttributeOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor + { + Name = "parameter-name", + BindingInfo = bindingInfo, + ParameterType = typeof(Person13), + }; + + var testContext = ModelBindingTestHelper.GetTestContext( + request => request.QueryString = new QueryString( + "?Address.Number=23&Address.Street=someStreet&Address.City=Redmond&Address.State=WA")); + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + var person = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(person.Address); + Assert.Null(person.Address.City); + Assert.Equal(0, person.Address.Number); + Assert.Null(person.Address.State); + Assert.Equal("someStreet", person.Address.Street, StringComparer.Ordinal); + + Assert.True(modelState.IsValid); + var kvp = Assert.Single(modelState); + Assert.Equal("Address.Street", kvp.Key); + var entry = kvp.Value; + Assert.NotNull(entry); + Assert.Empty(entry.Errors); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + } + + [Theory] + [MemberData( + nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo), + MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))] + public async Task BindAttributeOnParameterType_WithData_Succeeds(BindingInfo bindingInfo) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor + { + Name = "parameter-name", + BindingInfo = bindingInfo, + ParameterType = typeof(Address13), + }; + + var testContext = ModelBindingTestHelper.GetTestContext( + request => request.QueryString = new QueryString("?Number=23&Street=someStreet&City=Redmond&State=WA")); + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + var address = Assert.IsType(modelBindingResult.Model); + Assert.Null(address.City); + Assert.Equal(0, address.Number); + Assert.Null(address.State); + Assert.Equal("someStreet", address.Street, StringComparer.Ordinal); + + Assert.True(modelState.IsValid); + var kvp = Assert.Single(modelState); + Assert.Equal("Street", kvp.Key); + var entry = kvp.Value; + Assert.NotNull(entry); + Assert.Empty(entry.Errors); + Assert.Equal(ModelValidationState.Valid, entry.ValidationState); + } + private static void SetJsonBodyContent(HttpRequest request, string content) { var stream = new MemoryStream(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(content)); diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs index 2f467e96ea..f83865ebb5 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs @@ -183,5 +183,83 @@ public async Task BindParameterFromService_NoService_Throws() () => argumentBinder.BindModelAsync(parameter, testContext)); Assert.Contains(typeof(IActionResult).FullName, exception.Message); } + + private class Person + { + public JsonOutputFormatter Service { get; set; } + } + + [Theory] + [MemberData( + nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo), + MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))] + public async Task FromServicesOnPropertyType_WithData_Succeeds(BindingInfo bindingInfo) + { + // Arrange + // Similar to a custom IBindingSourceMetadata implementation or [ModelBinder] subclass on a custom service. + var metadataProvider = new TestModelMetadataProvider(); + metadataProvider + .ForProperty(nameof(Person.Service)) + .BindingDetails(binding => binding.BindingSource = BindingSource.Services); + + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); + var parameter = new ParameterDescriptor + { + Name = "parameter-name", + BindingInfo = bindingInfo, + ParameterType = typeof(Person), + }; + + var testContext = ModelBindingTestHelper.GetTestContext(); + testContext.MetadataProvider = metadataProvider; + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + var person = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(person.Service); + + Assert.True(modelState.IsValid); + Assert.Empty(modelState); + } + + [Theory] + [MemberData( + nameof(BinderTypeBasedModelBinderIntegrationTest.NullAndEmptyBindingInfo), + MemberType = typeof(BinderTypeBasedModelBinderIntegrationTest))] + public async Task FromserviesOnParameterType_WithData_Succeeds(BindingInfo bindingInfo) + { + // Arrange + // Similar to a custom IBindingSourceMetadata implementation or [ModelBinder] subclass on a custom service. + var metadataProvider = new TestModelMetadataProvider(); + metadataProvider + .ForType() + .BindingDetails(binding => binding.BindingSource = BindingSource.Services); + + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(metadataProvider); + var parameter = new ParameterDescriptor + { + Name = "parameter-name", + BindingInfo = bindingInfo, + ParameterType = typeof(JsonOutputFormatter), + }; + + var testContext = ModelBindingTestHelper.GetTestContext(); + testContext.MetadataProvider = metadataProvider; + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, testContext); + + // Assert + Assert.True(modelBindingResult.IsModelSet); + Assert.IsType(modelBindingResult.Model); + + Assert.True(modelState.IsValid); + Assert.Empty(modelState); + } } } \ No newline at end of file