Merge branch 'release/2.1' into dev
This commit is contained in:
commit
db6aed7f36
|
|
@ -64,6 +64,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
controllerModel.ApiExplorer.IsVisible = true;
|
controllerModel.ApiExplorer.IsVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isApiController)
|
||||||
|
{
|
||||||
|
InferBoundPropertyModelPrefixes(controllerModel);
|
||||||
|
}
|
||||||
|
|
||||||
var controllerHasSelectorModel = controllerModel.Selectors.Any(s => s.AttributeRouteModel != null);
|
var controllerHasSelectorModel = controllerModel.Selectors.Any(s => s.AttributeRouteModel != null);
|
||||||
|
|
||||||
foreach (var actionModel in controllerModel.Actions)
|
foreach (var actionModel in controllerModel.Actions)
|
||||||
|
|
@ -79,6 +84,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
InferParameterBindingSources(actionModel);
|
InferParameterBindingSources(actionModel);
|
||||||
|
|
||||||
|
InferParameterModelPrefixes(actionModel);
|
||||||
|
|
||||||
AddMultipartFormDataConsumesAttribute(actionModel);
|
AddMultipartFormDataConsumesAttribute(actionModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -179,6 +186,50 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For any complex types that are bound from value providers, set the prefix
|
||||||
|
// to the empty prefix by default. This makes binding much more predictable
|
||||||
|
// and describable via ApiExplorer
|
||||||
|
|
||||||
|
// internal for testing
|
||||||
|
internal void InferBoundPropertyModelPrefixes(ControllerModel controllerModel)
|
||||||
|
{
|
||||||
|
foreach (var property in controllerModel.ControllerProperties)
|
||||||
|
{
|
||||||
|
if (property.BindingInfo != null &&
|
||||||
|
property.BindingInfo.BinderModelName == null &&
|
||||||
|
property.BindingInfo.BindingSource != null &&
|
||||||
|
!property.BindingInfo.BindingSource.IsGreedy)
|
||||||
|
{
|
||||||
|
var metadata = _modelMetadataProvider.GetMetadataForProperty(
|
||||||
|
controllerModel.ControllerType,
|
||||||
|
property.PropertyInfo.Name);
|
||||||
|
if (metadata.IsComplexType)
|
||||||
|
{
|
||||||
|
property.BindingInfo.BinderModelName = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal for testing
|
||||||
|
internal void InferParameterModelPrefixes(ActionModel actionModel)
|
||||||
|
{
|
||||||
|
foreach (var parameter in actionModel.Parameters)
|
||||||
|
{
|
||||||
|
if (parameter.BindingInfo != null &&
|
||||||
|
parameter.BindingInfo.BinderModelName == null &&
|
||||||
|
parameter.BindingInfo.BindingSource != null &&
|
||||||
|
!parameter.BindingInfo.BindingSource.IsGreedy)
|
||||||
|
{
|
||||||
|
var metadata = GetParameterMetadata(parameter);
|
||||||
|
if (metadata.IsComplexType)
|
||||||
|
{
|
||||||
|
parameter.BindingInfo.BinderModelName = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Internal for unit testing.
|
// Internal for unit testing.
|
||||||
internal BindingSource InferBindingSourceForParameter(ParameterModel parameter)
|
internal BindingSource InferBindingSourceForParameter(ParameterModel parameter)
|
||||||
{
|
{
|
||||||
|
|
@ -189,16 +240,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ModelMetadata parameterMetadata;
|
var parameterMetadata = GetParameterMetadata(parameter);
|
||||||
if (_modelMetadataProvider is ModelMetadataProvider modelMetadataProvider)
|
|
||||||
{
|
|
||||||
parameterMetadata = modelMetadataProvider.GetMetadataForParameter(parameter.ParameterInfo);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
parameterMetadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterInfo.ParameterType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterMetadata != null)
|
if (parameterMetadata != null)
|
||||||
{
|
{
|
||||||
var bindingSource = parameterMetadata.IsComplexType ?
|
var bindingSource = parameterMetadata.IsComplexType ?
|
||||||
|
|
@ -234,5 +276,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
||||||
return parameterExistsInSomeRoute;
|
return parameterExistsInSomeRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ModelMetadata GetParameterMetadata(ParameterModel parameter)
|
||||||
|
{
|
||||||
|
if (_modelMetadataProvider is ModelMetadataProvider modelMetadataProvider)
|
||||||
|
{
|
||||||
|
return modelMetadataProvider.GetMetadataForParameter(parameter.ParameterInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _modelMetadataProvider.GetMetadataForType(parameter.ParameterInfo.ParameterType);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,38 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
Assert.Same(BindingSource.Query, result);
|
Assert.Same(BindingSource.Query, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferBoundPropertyModelPrefixes_SetsModelPrefix_ForComplexTypeFromValueProvider()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var controller = GetControllerModel(typeof(ControllerWithBoundProperty));
|
||||||
|
|
||||||
|
var provider = GetProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.InferBoundPropertyModelPrefixes(controller);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var property = Assert.Single(controller.ControllerProperties);
|
||||||
|
Assert.Equal(string.Empty, property.BindingInfo.BinderModelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InferParameterModelPrefixes_SetsModelPrefix_ForComplexTypeFromValueProvider()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var action = GetActionModel(typeof(ControllerWithBoundProperty), nameof(ControllerWithBoundProperty.SomeAction));
|
||||||
|
|
||||||
|
var provider = GetProvider();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.InferParameterModelPrefixes(action);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var parameter = Assert.Single(action.Parameters);
|
||||||
|
Assert.Equal(string.Empty, parameter.BindingInfo.BinderModelName);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AddMultipartFormDataConsumesAttribute_NoOpsIfBehaviorIsDisabled()
|
public void AddMultipartFormDataConsumesAttribute_NoOpsIfBehaviorIsDisabled()
|
||||||
{
|
{
|
||||||
|
|
@ -483,6 +515,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ControllerModel GetControllerModel(Type controllerType)
|
||||||
|
{
|
||||||
|
var context = GetContext(controllerType);
|
||||||
|
return Assert.Single(context.Result.Controllers);
|
||||||
|
}
|
||||||
|
|
||||||
private static ActionModel GetActionModel(Type controllerType, string actionName)
|
private static ActionModel GetActionModel(Type controllerType, string actionName)
|
||||||
{
|
{
|
||||||
var context = GetContext(controllerType);
|
var context = GetContext(controllerType);
|
||||||
|
|
@ -622,5 +660,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||||
=> sourceType == typeof(string);
|
=> sourceType == typeof(string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
private class ControllerWithBoundProperty
|
||||||
|
{
|
||||||
|
[FromQuery]
|
||||||
|
public TestModel TestProperty { get; set; }
|
||||||
|
|
||||||
|
public IActionResult SomeAction([FromQuery] TestModel test) => null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,5 +124,47 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal(name, result.Name);
|
Assert.Equal(name, result.Name);
|
||||||
Assert.Equal(email, result.Email);
|
Assert.Equal(email, result.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ActionsWithApiBehavior_InferEmptyPrefixForComplexValueProviderModel_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var id = 31;
|
||||||
|
var name = "test_user";
|
||||||
|
var email = "email@test.com";
|
||||||
|
var url = $"/contact/ActionWithInferredEmptyPrefix?name={name}&contactid={id}&email={email}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetAsync(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
var result = await response.Content.ReadAsAsync<Contact>();
|
||||||
|
Assert.Equal(id, result.ContactId);
|
||||||
|
Assert.Equal(name, result.Name);
|
||||||
|
Assert.Equal(email, result.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ActionsWithApiBehavior_InferEmptyPrefixForComplexValueProviderModel_Ignored()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var id = 31;
|
||||||
|
var name = "test_user";
|
||||||
|
var email = "email@test.com";
|
||||||
|
var url = $"/contact/ActionWithInferredEmptyPrefix?contact.name={name}&contact.contactid={id}&contact.email={email}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetAsync(url);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
var result = await response.Content.ReadAsAsync<Contact>();
|
||||||
|
Assert.Equal(0, result.ContactId);
|
||||||
|
Assert.Null(result.Name);
|
||||||
|
Assert.Null(result.Email);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,5 +61,11 @@ namespace BasicWebSite
|
||||||
Email = email,
|
Email = email,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("[action]")]
|
||||||
|
public ActionResult<Contact> ActionWithInferredEmptyPrefix([FromQuery] Contact contact)
|
||||||
|
{
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue