Remove ParameterModel.IsOptional

The ParameterModel and ParameterDescriptor have had a notion of
optionality for a while now, even though all parameters are treated as
'optional' in MVC.

This change removes these settings. Optionality for overloading in webapi
compat shim is reimplemented via a new binder metadata.
This commit is contained in:
Ryan Nowak 2015-01-05 11:18:16 -08:00
parent fb21b736ee
commit 0a473b0600
17 changed files with 107 additions and 77 deletions

View File

@ -317,7 +317,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
parameterModel.BinderMetadata = attributes.OfType<IBinderMetadata>().FirstOrDefault(); parameterModel.BinderMetadata = attributes.OfType<IBinderMetadata>().FirstOrDefault();
parameterModel.ParameterName = parameterInfo.Name; parameterModel.ParameterName = parameterInfo.Name;
parameterModel.IsOptional = parameterInfo.HasDefaultValue;
return parameterModel; return parameterModel;
} }

View File

@ -22,7 +22,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
Action = other.Action; Action = other.Action;
Attributes = new List<object>(other.Attributes); Attributes = new List<object>(other.Attributes);
BinderMetadata = other.BinderMetadata; BinderMetadata = other.BinderMetadata;
IsOptional = other.IsOptional;
ParameterInfo = other.ParameterInfo; ParameterInfo = other.ParameterInfo;
ParameterName = other.ParameterName; ParameterName = other.ParameterName;
} }
@ -33,8 +32,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
public IBinderMetadata BinderMetadata { get; set; } public IBinderMetadata BinderMetadata { get; set; }
public bool IsOptional { get; set; }
public ParameterInfo ParameterInfo { get; private set; } public ParameterInfo ParameterInfo { get; private set; }
public string ParameterName { get; set; } public string ParameterName { get; set; }

View File

@ -275,7 +275,6 @@ namespace Microsoft.AspNet.Mvc
var parameterDescriptor = new ParameterDescriptor() var parameterDescriptor = new ParameterDescriptor()
{ {
BinderMetadata = parameter.BinderMetadata, BinderMetadata = parameter.BinderMetadata,
IsOptional = parameter.IsOptional,
Name = parameter.ParameterName, Name = parameter.ParameterName,
ParameterType = parameter.ParameterInfo.ParameterType, ParameterType = parameter.ParameterInfo.ParameterType,
}; };

View File

@ -263,7 +263,6 @@ namespace Microsoft.AspNet.Mvc.Description
{ {
var resourceParameter = new ApiParameterDescription var resourceParameter = new ApiParameterDescription
{ {
IsOptional = parameter.IsOptional,
Name = parameter.Name, Name = parameter.Name,
ParameterDescriptor = parameter, ParameterDescriptor = parameter,
Type = parameter.ParameterType, Type = parameter.ParameterType,
@ -288,7 +287,7 @@ namespace Microsoft.AspNet.Mvc.Description
var resourceParameter = new ApiParameterDescription var resourceParameter = new ApiParameterDescription
{ {
Source = ApiParameterSource.Path, Source = ApiParameterSource.Path,
IsOptional = parameter.IsOptional && IsOptionalParameter(templateParameter), IsOptional = IsOptionalParameter(templateParameter),
Name = parameter.Name, Name = parameter.Name,
ParameterDescriptor = parameter, ParameterDescriptor = parameter,
Constraints = GetConstraints(_constraintResolver, templateParameter.InlineConstraints), Constraints = GetConstraints(_constraintResolver, templateParameter.InlineConstraints),

View File

@ -10,8 +10,6 @@ namespace Microsoft.AspNet.Mvc
{ {
public string Name { get; set; } public string Name { get; set; }
public bool IsOptional { get; set; }
public Type ParameterType { get; set; } public Type ParameterType { get; set; }
public IBinderMetadata BinderMetadata { get; set; } public IBinderMetadata BinderMetadata { get; set; }

View File

@ -1,6 +1,7 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq; using System.Linq;
using System.Web.Http; using System.Web.Http;
using Microsoft.AspNet.Mvc.ApplicationModels; using Microsoft.AspNet.Mvc.ApplicationModels;
@ -46,6 +47,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
// Complex types are by-default from the body. // Complex types are by-default from the body.
parameter.BinderMetadata = new FromBodyAttribute(); parameter.BinderMetadata = new FromBodyAttribute();
} }
// If the parameter has a default value, we want to consider it as optional parameter by default.
var optionalMetadata = parameter.BinderMetadata as FromUriAttribute;
if (parameter.ParameterInfo.HasDefaultValue && optionalMetadata != null)
{
optionalMetadata.IsOptional = true;
}
} }
} }
} }

View File

@ -93,9 +93,17 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
// We only consider parameters that are bound from the URL. // We only consider parameters that are bound from the URL.
if ((parameter.BinderMetadata is IRouteDataValueProviderMetadata || if ((parameter.BinderMetadata is IRouteDataValueProviderMetadata ||
parameter.BinderMetadata is IQueryValueProviderMetadata) && parameter.BinderMetadata is IQueryValueProviderMetadata) &&
!parameter.IsOptional &&
ValueProviderResult.CanConvertFromString(parameter.ParameterType)) ValueProviderResult.CanConvertFromString(parameter.ParameterType))
{ {
var optionalMetadata = parameter.BinderMetadata as IOptionalBinderMetadata;
if (optionalMetadata == null || optionalMetadata.IsOptional)
{
// Optional parameters are ignored in overloading. If a parameter doesn't specify that it's
// required then treat it as optional (MVC default). WebAPI parameters will all by-default
// specify themselves as required unless they have a default value.
continue;
}
var nameProvider = parameter.BinderMetadata as IModelNameProvider; var nameProvider = parameter.BinderMetadata as IModelNameProvider;
var prefix = nameProvider?.Name ?? parameter.Name; var prefix = nameProvider?.Name ?? parameter.Name;

View File

@ -12,10 +12,13 @@ namespace System.Web.Http
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromUriAttribute : public class FromUriAttribute :
Attribute, Attribute,
IOptionalBinderMetadata,
IQueryValueProviderMetadata, IQueryValueProviderMetadata,
IRouteDataValueProviderMetadata, IRouteDataValueProviderMetadata,
IModelNameProvider IModelNameProvider
{ {
public bool IsOptional { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public string Name { get; set; } public string Name { get; set; }
} }

View File

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// An <see cref="IBinderMetadata"/> that designates an optional parameter for the purposes
/// of WebAPI action overloading. Optional parameters do not participate in overloading, and
/// do not have to have a value for the action to be selected.
///
/// This has no impact when used without WebAPI action overloading.
/// </summary>
public interface IOptionalBinderMetadata : IBinderMetadata
{
bool IsOptional { get; }
}
}

View File

@ -19,7 +19,6 @@ namespace Microsoft.AspNet.Mvc.ApplicationModels
parameter.Action = new ActionModel(typeof(TestController).GetMethod("Edit"), new List<object>()); parameter.Action = new ActionModel(typeof(TestController).GetMethod("Edit"), new List<object>());
parameter.BinderMetadata = (IBinderMetadata)parameter.Attributes[0]; parameter.BinderMetadata = (IBinderMetadata)parameter.Attributes[0];
parameter.IsOptional = true;
parameter.ParameterName = "id"; parameter.ParameterName = "id";
// Act // Act

View File

@ -109,7 +109,6 @@ namespace Microsoft.AspNet.Mvc.Test
var id = Assert.Single(main.Parameters); var id = Assert.Single(main.Parameters);
Assert.Equal("id", id.Name); Assert.Equal("id", id.Name);
Assert.False(id.IsOptional);
Assert.Null(id.BinderMetadata); Assert.Null(id.BinderMetadata);
Assert.Equal(typeof(int), id.ParameterType); Assert.Equal(typeof(int), id.ParameterType);
} }
@ -129,14 +128,12 @@ namespace Microsoft.AspNet.Mvc.Test
var id = Assert.Single(main.Parameters, p => p.Name == "id"); var id = Assert.Single(main.Parameters, p => p.Name == "id");
Assert.Equal("id", id.Name); Assert.Equal("id", id.Name);
Assert.False(id.IsOptional);
Assert.Null(id.BinderMetadata); Assert.Null(id.BinderMetadata);
Assert.Equal(typeof(int), id.ParameterType); Assert.Equal(typeof(int), id.ParameterType);
var entity = Assert.Single(main.Parameters, p => p.Name == "entity"); var entity = Assert.Single(main.Parameters, p => p.Name == "entity");
Assert.Equal("entity", entity.Name); Assert.Equal("entity", entity.Name);
Assert.False(entity.IsOptional);
Assert.IsType<FromBodyAttribute>(entity.BinderMetadata); Assert.IsType<FromBodyAttribute>(entity.BinderMetadata);
Assert.Equal(typeof(TestActionParameter), entity.ParameterType); Assert.Equal(typeof(TestActionParameter), entity.ParameterType);
} }
@ -156,49 +153,22 @@ namespace Microsoft.AspNet.Mvc.Test
var id = Assert.Single(main.Parameters, p => p.Name == "id"); var id = Assert.Single(main.Parameters, p => p.Name == "id");
Assert.Equal("id", id.Name); Assert.Equal("id", id.Name);
Assert.False(id.IsOptional);
Assert.Null(id.BinderMetadata); Assert.Null(id.BinderMetadata);
Assert.Equal(typeof(int), id.ParameterType); Assert.Equal(typeof(int), id.ParameterType);
var upperCaseId = Assert.Single(main.Parameters, p => p.Name == "ID"); var upperCaseId = Assert.Single(main.Parameters, p => p.Name == "ID");
Assert.Equal("ID", upperCaseId.Name); Assert.Equal("ID", upperCaseId.Name);
Assert.False(upperCaseId.IsOptional);
Assert.Null(upperCaseId.BinderMetadata); Assert.Null(upperCaseId.BinderMetadata);
Assert.Equal(typeof(int), upperCaseId.ParameterType); Assert.Equal(typeof(int), upperCaseId.ParameterType);
var pascalCaseId = Assert.Single(main.Parameters, p => p.Name == "Id"); var pascalCaseId = Assert.Single(main.Parameters, p => p.Name == "Id");
Assert.Equal("Id", pascalCaseId.Name); Assert.Equal("Id", pascalCaseId.Name);
Assert.False(pascalCaseId.IsOptional);
Assert.Null(id.BinderMetadata); Assert.Null(id.BinderMetadata);
Assert.Equal(typeof(int), pascalCaseId.ParameterType); Assert.Equal(typeof(int), pascalCaseId.ParameterType);
} }
[Theory]
[InlineData(nameof(ActionParametersController.OptionalInt), typeof(Nullable<int>))]
[InlineData(nameof(ActionParametersController.OptionalChar), typeof(char))]
public void GetDescriptors_AddsParametersWithDefaultValues_AsOptionalParameters(
string actionName,
Type parameterType)
{
// Arrange & Act
var descriptors = GetDescriptors(
typeof(ActionParametersController).GetTypeInfo());
// Assert
var optional = Assert.Single(descriptors,
d => d.Name.Equals(actionName));
Assert.NotNull(optional.Parameters);
var id = Assert.Single(optional.Parameters);
Assert.Equal("id", id.Name);
Assert.True(id.IsOptional);
Assert.Null(id.BinderMetadata);
Assert.Equal(parameterType, id.ParameterType);
}
[Fact] [Fact]
public void GetDescriptors_AddsParameters_DetectsFromBodyParameters() public void GetDescriptors_AddsParameters_DetectsFromBodyParameters()
{ {
@ -216,7 +186,6 @@ namespace Microsoft.AspNet.Mvc.Test
var entity = Assert.Single(fromBody.Parameters); var entity = Assert.Single(fromBody.Parameters);
Assert.Equal("entity", entity.Name); Assert.Equal("entity", entity.Name);
Assert.False(entity.IsOptional);
Assert.IsType<FromBodyAttribute>(entity.BinderMetadata); Assert.IsType<FromBodyAttribute>(entity.BinderMetadata);
Assert.Equal(typeof(TestActionParameter), entity.ParameterType); Assert.Equal(typeof(TestActionParameter), entity.ParameterType);
} }
@ -238,7 +207,6 @@ namespace Microsoft.AspNet.Mvc.Test
var entity = Assert.Single(notFromBody.Parameters); var entity = Assert.Single(notFromBody.Parameters);
Assert.Equal("entity", entity.Name); Assert.Equal("entity", entity.Name);
Assert.False(entity.IsOptional);
Assert.Null(entity.BinderMetadata); Assert.Null(entity.BinderMetadata);
Assert.Equal(typeof(TestActionParameter), entity.ParameterType); Assert.Equal(typeof(TestActionParameter), entity.ParameterType);
} }
@ -1710,10 +1678,6 @@ namespace Microsoft.AspNet.Mvc.Test
{ {
public void RequiredInt(int id) { } public void RequiredInt(int id) { }
public void OptionalInt(int? id = 5) { }
public void OptionalChar(char id = 'c') { }
public void FromBodyParameter([FromBody] TestActionParameter entity) { } public void FromBodyParameter([FromBody] TestActionParameter entity) { }
public void NotFromBodyParameter(TestActionParameter entity) { } public void NotFromBodyParameter(TestActionParameter entity) { }

View File

@ -121,7 +121,6 @@ namespace Microsoft.AspNet.Mvc.Description
new ParameterDescriptor() new ParameterDescriptor()
{ {
Name = "id", Name = "id",
IsOptional = true,
ParameterType = typeof(int), ParameterType = typeof(int),
}, },
new ParameterDescriptor() new ParameterDescriptor()
@ -141,7 +140,7 @@ namespace Microsoft.AspNet.Mvc.Description
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "id"); var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "id");
Assert.NotNull(id.ModelMetadata); Assert.NotNull(id.ModelMetadata);
Assert.True(id.IsOptional); Assert.False(id.IsOptional);
Assert.Same(action.Parameters[0], id.ParameterDescriptor); Assert.Same(action.Parameters[0], id.ParameterDescriptor);
Assert.Equal(ApiParameterSource.Query, id.Source); Assert.Equal(ApiParameterSource.Query, id.Source);
Assert.Equal(typeof(int), id.Type); Assert.Equal(typeof(int), id.Type);
@ -224,7 +223,6 @@ namespace Microsoft.AspNet.Mvc.Description
var parameterDescriptor = new ParameterDescriptor var parameterDescriptor = new ParameterDescriptor
{ {
Name = "id", Name = "id",
IsOptional = true,
ParameterType = typeof(int), ParameterType = typeof(int),
}; };
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor }; action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
@ -280,7 +278,6 @@ namespace Microsoft.AspNet.Mvc.Description
{ {
BinderMetadata = new FromBodyAttribute(), BinderMetadata = new FromBodyAttribute(),
Name = "id", Name = "id",
IsOptional = false,
ParameterType = typeof(int), ParameterType = typeof(int),
}; };
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor }; action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
@ -317,15 +314,11 @@ namespace Microsoft.AspNet.Mvc.Description
} }
[Theory] [Theory]
[InlineData("api/products/{id}", false, false)] [InlineData("api/products/{id}", false)]
[InlineData("api/products/{id}", true, false)] [InlineData("api/products/{id?}", true)]
[InlineData("api/products/{id?}", false, false)] [InlineData("api/products/{id=5}", true)]
[InlineData("api/products/{id?}", true, true)] public void GetApiDescription_ParameterFromPathAndDescriptor_IsOptionalIfRouteParameterIsOptional(
[InlineData("api/products/{id=5}", false, false)]
[InlineData("api/products/{id=5}", true, true)]
public void GetApiDescription_ParameterFromPathAndDescriptor_IsOptionalOnly_IfBothAreOptional(
string template, string template,
bool isDescriptorParameterOptional,
bool expectedOptional) bool expectedOptional)
{ {
// Arrange // Arrange
@ -335,7 +328,6 @@ namespace Microsoft.AspNet.Mvc.Description
var parameterDescriptor = new ParameterDescriptor var parameterDescriptor = new ParameterDescriptor
{ {
Name = "id", Name = "id",
IsOptional = isDescriptorParameterOptional,
ParameterType = typeof(int), ParameterType = typeof(int),
}; };
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor }; action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };

View File

@ -324,7 +324,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("RangeRouteConstraint", Assert.Single(month.ConstraintTypes)); Assert.Equal("RangeRouteConstraint", Assert.Single(month.ConstraintTypes));
var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day"); var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day");
Assert.False(day.IsOptional); Assert.True(day.IsOptional);
Assert.Equal("Path", day.Source); Assert.Equal("Path", day.Source);
Assert.Equal("IntRouteConstraint", Assert.Single(day.ConstraintTypes)); Assert.Equal("IntRouteConstraint", Assert.Single(day.ConstraintTypes));

View File

@ -57,13 +57,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient(); var client = server.CreateClient();
// Act // Act
var response = await client.GetAsync("http://localhost/ParameterModel/GetParameterIsOptional"); var response = await client.GetAsync("http://localhost/ParameterModel/GetParameterMetadata");
// Assert // Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync(); var body = await response.Content.ReadAsStringAsync();
Assert.Equal("True", body); Assert.Equal("CoolMetadata", body);
} }
} }

View File

@ -277,7 +277,8 @@ namespace System.Web.Http
foreach (var action in actions) foreach (var action in actions)
{ {
var parameter = Assert.Single(action.Parameters); var parameter = Assert.Single(action.Parameters);
Assert.IsType<FromUriAttribute>(parameter.BinderMetadata); var metadata = Assert.IsType<FromUriAttribute>(parameter.BinderMetadata);
Assert.False(metadata.IsOptional);
} }
} }
@ -335,6 +336,36 @@ namespace System.Web.Http
} }
} }
[Theory]
[InlineData(nameof(TestControllers.EventsController.GetWithId))]
[InlineData(nameof(TestControllers.EventsController.GetWithEmployee))]
public void GetActions_Parameters_ImplicitOptional(string name)
{
// Arrange
var provider = CreateProvider();
// Act
var context = new ActionDescriptorProviderContext();
provider.Invoke(context);
var results = context.Results.Cast<ControllerActionDescriptor>();
// Assert
var controllerType = typeof(TestControllers.EventsController).GetTypeInfo();
var actions = results
.Where(ad => ad.ControllerTypeInfo == controllerType)
.Where(ad => ad.Name == name)
.ToArray();
Assert.NotEmpty(actions);
foreach (var action in actions)
{
var parameter = Assert.Single(action.Parameters);
var metadata = Assert.IsType<FromUriAttribute>(parameter.BinderMetadata);
Assert.True(metadata.IsOptional);
}
}
private INestedProviderManager<ActionDescriptorProviderContext> CreateProvider() private INestedProviderManager<ActionDescriptorProviderContext> CreateProvider()
{ {
var assemblyProvider = new Mock<IAssemblyProvider>(); var assemblyProvider = new Mock<IAssemblyProvider>();
@ -455,5 +486,18 @@ namespace System.Web.Http.TestControllers
public class Employee public class Employee
{ {
} }
public class EventsController : ApiController
{
public IActionResult GetWithId(int id = 0)
{
return null;
}
public IActionResult GetWithEmployee([FromUri] Employee e = null)
{
return null;
}
}
} }
#endif #endif

View File

@ -165,9 +165,8 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
}, },
new ParameterDescriptor() new ParameterDescriptor()
{ {
BinderMetadata = new FromUriAttribute(), BinderMetadata = new FromUriAttribute() { IsOptional = true },
Name = "quantity", Name = "quantity",
IsOptional = true,
ParameterType = typeof(int), ParameterType = typeof(int),
}, },
}; };
@ -307,9 +306,8 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
}, },
new ParameterDescriptor() new ParameterDescriptor()
{ {
BinderMetadata = new FromUriAttribute(), BinderMetadata = new FromUriAttribute() { IsOptional = true },
Name = "quantity", Name = "quantity",
IsOptional = true,
ParameterType = typeof(int), ParameterType = typeof(int),
}, },
}; };
@ -430,8 +428,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
}, },
new ParameterDescriptor() new ParameterDescriptor()
{ {
BinderMetadata = new FromUriAttribute(), BinderMetadata = new FromUriAttribute() { IsOptional = true },
IsOptional = true,
Name = "quantity", Name = "quantity",
ParameterType = typeof(int), ParameterType = typeof(int),
}, },

View File

@ -4,25 +4,31 @@
using System; using System;
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ApplicationModels; using Microsoft.AspNet.Mvc.ApplicationModels;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace ApplicationModelWebSite namespace ApplicationModelWebSite
{ {
// This controller uses an reflected model attribute to change a parameter to optional. // This controller uses an reflected model attribute to change a parameter's binder metadata.
//
// This could be accomplished by simply making an attribute that implements IBinderMetadata, but
// this is part of a test for IParameterModelConvention.
public class ParameterModelController : Controller public class ParameterModelController : Controller
{ {
public string GetParameterIsOptional([Optional] int? id) public string GetParameterMetadata([Cool] int? id)
{ {
var actionDescriptor = (ControllerActionDescriptor)ActionContext.ActionDescriptor; return ActionContext.ActionDescriptor.Parameters[0].BinderMetadata.GetType().Name;
return actionDescriptor.Parameters[0].IsOptional.ToString();
} }
private class OptionalAttribute : Attribute, IParameterModelConvention private class CoolAttribute : Attribute, IParameterModelConvention
{ {
public void Apply(ParameterModel model) public void Apply(ParameterModel model)
{ {
model.IsOptional = true; model.BinderMetadata = new CoolMetadata();
} }
} }
private class CoolMetadata : IBinderMetadata
{
}
} }
} }