Set model prefix for [ApiController]

Infers the 'empty' model prefix for complex types that are read from the value
providers. This gives us better defaults when using the parameter object
pattern with respect to swagger/API explorer.
This commit is contained in:
Ryan Nowak 2018-03-19 22:22:08 -07:00
parent 14429721d9
commit 1ff5bdca79
4 changed files with 159 additions and 10 deletions

View File

@ -64,6 +64,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
controllerModel.ApiExplorer.IsVisible = true;
}
if (isApiController)
{
InferBoundPropertyModelPrefixes(controllerModel);
}
var controllerHasSelectorModel = controllerModel.Selectors.Any(s => s.AttributeRouteModel != null);
foreach (var actionModel in controllerModel.Actions)
@ -79,6 +84,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
InferParameterBindingSources(actionModel);
InferParameterModelPrefixes(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 BindingSource InferBindingSourceForParameter(ParameterModel parameter)
{
@ -189,16 +240,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
else
{
ModelMetadata parameterMetadata;
if (_modelMetadataProvider is ModelMetadataProvider modelMetadataProvider)
{
parameterMetadata = modelMetadataProvider.GetMetadataForParameter(parameter.ParameterInfo);
}
else
{
parameterMetadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterInfo.ParameterType);
}
var parameterMetadata = GetParameterMetadata(parameter);
if (parameterMetadata != null)
{
var bindingSource = parameterMetadata.IsComplexType ?
@ -234,5 +276,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return parameterExistsInSomeRoute;
}
private ModelMetadata GetParameterMetadata(ParameterModel parameter)
{
if (_modelMetadataProvider is ModelMetadataProvider modelMetadataProvider)
{
return modelMetadataProvider.GetMetadataForParameter(parameter.ParameterInfo);
}
else
{
return _modelMetadataProvider.GetMetadataForType(parameter.ParameterInfo.ParameterType);
}
}
}
}

View File

@ -398,6 +398,38 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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]
public void AddMultipartFormDataConsumesAttribute_NoOpsIfBehaviorIsDisabled()
{
@ -483,6 +515,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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)
{
var context = GetContext(controllerType);
@ -622,5 +660,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
=> sourceType == typeof(string);
}
[ApiController]
private class ControllerWithBoundProperty
{
[FromQuery]
public TestModel TestProperty { get; set; }
public IActionResult SomeAction([FromQuery] TestModel test) => null;
}
}
}

View File

@ -124,5 +124,47 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(name, result.Name);
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);
}
}
}

View File

@ -61,5 +61,11 @@ namespace BasicWebSite
Email = email,
};
}
[HttpGet("[action]")]
public ActionResult<Contact> ActionWithInferredEmptyPrefix([FromQuery] Contact contact)
{
return contact;
}
}
}