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.ParameterName = parameterInfo.Name;
parameterModel.IsOptional = parameterInfo.HasDefaultValue;
return parameterModel;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
// 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.
using System;
using System.Linq;
using System.Web.Http;
using Microsoft.AspNet.Mvc.ApplicationModels;
@ -46,6 +47,13 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
// Complex types are by-default from the body.
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.
if ((parameter.BinderMetadata is IRouteDataValueProviderMetadata ||
parameter.BinderMetadata is IQueryValueProviderMetadata) &&
!parameter.IsOptional &&
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 prefix = nameProvider?.Name ?? parameter.Name;

View File

@ -12,10 +12,13 @@ namespace System.Web.Http
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class FromUriAttribute :
Attribute,
IOptionalBinderMetadata,
IQueryValueProviderMetadata,
IRouteDataValueProviderMetadata,
IModelNameProvider
{
public bool IsOptional { get; set; }
/// <inheritdoc />
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.BinderMetadata = (IBinderMetadata)parameter.Attributes[0];
parameter.IsOptional = true;
parameter.ParameterName = "id";
// Act

View File

@ -109,7 +109,6 @@ namespace Microsoft.AspNet.Mvc.Test
var id = Assert.Single(main.Parameters);
Assert.Equal("id", id.Name);
Assert.False(id.IsOptional);
Assert.Null(id.BinderMetadata);
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");
Assert.Equal("id", id.Name);
Assert.False(id.IsOptional);
Assert.Null(id.BinderMetadata);
Assert.Equal(typeof(int), id.ParameterType);
var entity = Assert.Single(main.Parameters, p => p.Name == "entity");
Assert.Equal("entity", entity.Name);
Assert.False(entity.IsOptional);
Assert.IsType<FromBodyAttribute>(entity.BinderMetadata);
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");
Assert.Equal("id", id.Name);
Assert.False(id.IsOptional);
Assert.Null(id.BinderMetadata);
Assert.Equal(typeof(int), id.ParameterType);
var upperCaseId = Assert.Single(main.Parameters, p => p.Name == "ID");
Assert.Equal("ID", upperCaseId.Name);
Assert.False(upperCaseId.IsOptional);
Assert.Null(upperCaseId.BinderMetadata);
Assert.Equal(typeof(int), upperCaseId.ParameterType);
var pascalCaseId = Assert.Single(main.Parameters, p => p.Name == "Id");
Assert.Equal("Id", pascalCaseId.Name);
Assert.False(pascalCaseId.IsOptional);
Assert.Null(id.BinderMetadata);
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]
public void GetDescriptors_AddsParameters_DetectsFromBodyParameters()
{
@ -216,7 +186,6 @@ namespace Microsoft.AspNet.Mvc.Test
var entity = Assert.Single(fromBody.Parameters);
Assert.Equal("entity", entity.Name);
Assert.False(entity.IsOptional);
Assert.IsType<FromBodyAttribute>(entity.BinderMetadata);
Assert.Equal(typeof(TestActionParameter), entity.ParameterType);
}
@ -238,7 +207,6 @@ namespace Microsoft.AspNet.Mvc.Test
var entity = Assert.Single(notFromBody.Parameters);
Assert.Equal("entity", entity.Name);
Assert.False(entity.IsOptional);
Assert.Null(entity.BinderMetadata);
Assert.Equal(typeof(TestActionParameter), entity.ParameterType);
}
@ -1710,10 +1678,6 @@ namespace Microsoft.AspNet.Mvc.Test
{
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 NotFromBodyParameter(TestActionParameter entity) { }

View File

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

View File

@ -324,7 +324,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("RangeRouteConstraint", Assert.Single(month.ConstraintTypes));
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("IntRouteConstraint", Assert.Single(day.ConstraintTypes));

View File

@ -57,13 +57,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ParameterModel/GetParameterIsOptional");
var response = await client.GetAsync("http://localhost/ParameterModel/GetParameterMetadata");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
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)
{
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()
{
var assemblyProvider = new Mock<IAssemblyProvider>();
@ -455,5 +486,18 @@ namespace System.Web.Http.TestControllers
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

View File

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

View File

@ -4,25 +4,31 @@
using System;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.ApplicationModels;
using Microsoft.AspNet.Mvc.ModelBinding;
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 string GetParameterIsOptional([Optional] int? id)
public string GetParameterMetadata([Cool] int? id)
{
var actionDescriptor = (ControllerActionDescriptor)ActionContext.ActionDescriptor;
return actionDescriptor.Parameters[0].IsOptional.ToString();
return ActionContext.ActionDescriptor.Parameters[0].BinderMetadata.GetType().Name;
}
private class OptionalAttribute : Attribute, IParameterModelConvention
private class CoolAttribute : Attribute, IParameterModelConvention
{
public void Apply(ParameterModel model)
{
model.IsOptional = true;
model.BinderMetadata = new CoolMetadata();
}
}
private class CoolMetadata : IBinderMetadata
{
}
}
}