[Fixes #731] Add unit tests for ReflectedActionDescriptorProvider

1. Added tests that cover parameters in actions.
2. Added tests that cover building the reflected application model.
3. Added tests that cover attribute routed action constraints and default values.
4. Added tests that cover conventionally routed action constraints and default values.
5. Refactored and cleaned up ReflectedActionDescriptorProvider. All the refactors consist
   of extracting blocks of code to separate methods to better display the flow when building
   the action descriptors.
This commit is contained in:
jacalvar 2014-08-30 23:33:10 -07:00
parent 8fd7cd51e2
commit 9345afeed2
2 changed files with 639 additions and 193 deletions

View File

@ -112,12 +112,6 @@ namespace Microsoft.AspNet.Mvc
return applicationModel; return applicationModel;
} }
private bool HasConstraint(List<RouteDataActionConstraint> constraints, string routeKey)
{
return constraints.Any(
rc => string.Equals(rc.RouteKey, routeKey, StringComparison.OrdinalIgnoreCase));
}
public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model) public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
{ {
var actions = new List<ReflectedActionDescriptor>(); var actions = new List<ReflectedActionDescriptor>();
@ -132,162 +126,32 @@ namespace Microsoft.AspNet.Mvc
var controllerDescriptor = new ControllerDescriptor(controller.ControllerType); var controllerDescriptor = new ControllerDescriptor(controller.ControllerType);
foreach (var action in controller.Actions) foreach (var action in controller.Actions)
{ {
var parameterDescriptors = new List<ParameterDescriptor>(); var actionDescriptor = CreateActionDescriptor(
foreach (var parameter in action.Parameters) action,
{ controller,
var isFromBody = parameter.Attributes.OfType<FromBodyAttribute>().Any(); controllerDescriptor,
model.Filters);
parameterDescriptors.Add(new ParameterDescriptor() AddActionConstraints(actionDescriptor, action, controller);
{ AddControllerRouteConstraints(actionDescriptor, controller.RouteConstraints, removalConstraints);
Name = parameter.ParameterName,
IsOptional = parameter.IsOptional,
ParameterBindingInfo = isFromBody if (IsAttributeRoutedAction(actionDescriptor))
? null
: new ParameterBindingInfo(
parameter.ParameterName,
parameter.ParameterInfo.ParameterType),
BodyParameterInfo = isFromBody
? new BodyParameterInfo(parameter.ParameterInfo.ParameterType)
: null
});
}
var combinedRoute = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(
controller.AttributeRouteModel,
action.AttributeRouteModel);
var attributeRouteInfo = combinedRoute == null ? null : new AttributeRouteInfo()
{
Template = combinedRoute.Template,
Order = combinedRoute.Order ?? DefaultAttributeRouteOrder,
Name = combinedRoute.Name,
};
var actionDescriptor = new ReflectedActionDescriptor()
{
Name = action.ActionName,
ControllerDescriptor = controllerDescriptor,
MethodInfo = action.ActionMethod,
Parameters = parameterDescriptors,
RouteConstraints = new List<RouteDataActionConstraint>(),
AttributeRouteInfo = attributeRouteInfo
};
actionDescriptor.DisplayName = string.Format(
"{0}.{1}",
action.ActionMethod.DeclaringType.FullName,
action.ActionMethod.Name);
var httpMethods = action.HttpMethods;
if (httpMethods != null && httpMethods.Count > 0)
{
actionDescriptor.MethodConstraints = new List<HttpMethodConstraint>()
{
new HttpMethodConstraint(httpMethods)
};
}
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"controller",
controller.ControllerName));
if (action.IsActionNameMatchRequired)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
action.ActionName));
}
else
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
RouteKeyHandling.DenyKey));
}
foreach (var constraintAttribute in controller.RouteConstraints)
{
if (constraintAttribute.BlockNonAttributedActions)
{
removalConstraints.Add(constraintAttribute.RouteKey);
}
// Skip duplicates
if (!HasConstraint(actionDescriptor.RouteConstraints, constraintAttribute.RouteKey))
{
if (constraintAttribute.RouteValue == null)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteKeyHandling));
}
else
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteValue));
}
}
}
if (actionDescriptor.AttributeRouteInfo != null &&
actionDescriptor.AttributeRouteInfo.Template != null)
{ {
hasAttributeRoutes = true; hasAttributeRoutes = true;
// An attribute routed action will ignore conventional routed constraints. We still // An attribute routed action will ignore conventional routed constraints. We still
// want to provide these values as ambient values. // want to provide these values as ambient values for link generation.
foreach (var constraint in actionDescriptor.RouteConstraints) AddConstraintsAsDefaultRouteValues(actionDescriptor);
{
// We don't need to do anything with attribute routing for 'catch all' behavior. Order
// and predecedence of attribute routes allow this kind of behavior.
if (constraint.KeyHandling == RouteKeyHandling.RequireKey ||
constraint.KeyHandling == RouteKeyHandling.DenyKey)
{
actionDescriptor.RouteValueDefaults.Add(constraint.RouteKey, constraint.RouteValue);
}
}
// Replaces tokens like [controller]/[action] in the route template with the actual values // Replaces tokens like [controller]/[action] in the route template with the actual values
// for this action. // for this action.
var templateText = actionDescriptor.AttributeRouteInfo.Template; ReplaceAttributeRouteTokens(actionDescriptor, routeTemplateErrors);
try
{
templateText = ReflectedAttributeRouteModel.ReplaceTokens(
templateText,
actionDescriptor.RouteValueDefaults);
}
catch (InvalidOperationException ex)
{
var message = Resources.FormatAttributeRoute_IndividualErrorMessage(
actionDescriptor.DisplayName,
Environment.NewLine,
ex.Message);
routeTemplateErrors.Add(message); // Attribute routed actions will ignore conventional routed constraints. Instead they have
} // a single route constraint "RouteGroup" associated with it.
ReplaceRouteConstraints(actionDescriptor);
actionDescriptor.AttributeRouteInfo.Template = templateText;
var routeGroupValue = GetRouteGroupValue(
actionDescriptor.AttributeRouteInfo.Order,
templateText);
var routeConstraints = new List<RouteDataActionConstraint>();
routeConstraints.Add(new RouteDataActionConstraint(
AttributeRouting.RouteGroupKey,
routeGroupValue));
actionDescriptor.RouteConstraints = routeConstraints;
} }
actionDescriptor.FilterDescriptors =
action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
.Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
.Concat(model.Filters.Select(f => new FilterDescriptor(f, FilterScope.Global)))
.OrderBy(d => d, FilterDescriptorOrderComparer.Comparer)
.ToList();
actions.Add(actionDescriptor); actions.Add(actionDescriptor);
} }
} }
@ -297,11 +161,10 @@ namespace Microsoft.AspNet.Mvc
foreach (var actionDescriptor in actions) foreach (var actionDescriptor in actions)
{ {
if (actionDescriptor.AttributeRouteInfo == null || if (!IsAttributeRoutedAction(actionDescriptor))
actionDescriptor.AttributeRouteInfo.Template == null)
{ {
// Any any attribute routes are in use, then non-attribute-routed ADs can't be selected // Any attribute routes are in use, then non-attribute-routed action descriptors can't be
// when a route group returned by the route. // selected when a route group returned by the route.
if (hasAttributeRoutes) if (hasAttributeRoutes)
{ {
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
@ -309,15 +172,11 @@ namespace Microsoft.AspNet.Mvc
RouteKeyHandling.DenyKey)); RouteKeyHandling.DenyKey));
} }
foreach (var key in removalConstraints) // Add a route constraint with DenyKey for each constraint in the set to all the
{ // actions that don't have that constraint. For example, if a controller defines
if (!HasConstraint(actionDescriptor.RouteConstraints, key)) // an area constraint, all actions that don't belong to an area must have a route
{ // constraint that prevents them from matching an incomming request.
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint( AddRemovalConstraints(actionDescriptor, removalConstraints);
key,
RouteKeyHandling.DenyKey));
}
}
} }
else else
{ {
@ -326,18 +185,7 @@ namespace Microsoft.AspNet.Mvc
{ {
// Build a map of attribute route name to action descriptors to ensure that all // Build a map of attribute route name to action descriptors to ensure that all
// attribute routes with a given name have the same template. // attribute routes with a given name have the same template.
IList<ActionDescriptor> namedActionGroup; AddActionToNamedGroup(actionsByRouteName, attributeRouteInfo.Name, actionDescriptor);
if (actionsByRouteName.TryGetValue(attributeRouteInfo.Name, out namedActionGroup))
{
namedActionGroup.Add(actionDescriptor);
}
else
{
namedActionGroup = new List<ActionDescriptor>();
namedActionGroup.Add(actionDescriptor);
actionsByRouteName.Add(attributeRouteInfo.Name, namedActionGroup);
}
} }
// We still want to add a 'null' for any constraint with DenyKey so that link generation // We still want to add a 'null' for any constraint with DenyKey so that link generation
@ -379,7 +227,251 @@ namespace Microsoft.AspNet.Mvc
return actions; return actions;
} }
private static IList<string> AddErrorNumbers(IList<string> namedRoutedErrors) private static ReflectedActionDescriptor CreateActionDescriptor(ReflectedActionModel action,
ReflectedControllerModel controller,
ControllerDescriptor controllerDescriptor,
IEnumerable<IFilter> globalFilters)
{
var parameterDescriptors = new List<ParameterDescriptor>();
foreach (var parameter in action.Parameters)
{
var isFromBody = parameter.Attributes.OfType<FromBodyAttribute>().Any();
var paramDescriptor = new ParameterDescriptor()
{
Name = parameter.ParameterName,
IsOptional = parameter.IsOptional
};
if (isFromBody)
{
paramDescriptor.BodyParameterInfo = new BodyParameterInfo(
parameter.ParameterInfo.ParameterType);
}
else
{
paramDescriptor.ParameterBindingInfo = new ParameterBindingInfo(
parameter.ParameterName,
parameter.ParameterInfo.ParameterType);
}
parameterDescriptors.Add(paramDescriptor);
}
var attributeRouteInfo = CreateAttributeRouteInfo(action, controller);
var actionDescriptor = new ReflectedActionDescriptor()
{
Name = action.ActionName,
ControllerDescriptor = controllerDescriptor,
MethodInfo = action.ActionMethod,
Parameters = parameterDescriptors,
RouteConstraints = new List<RouteDataActionConstraint>(),
AttributeRouteInfo = attributeRouteInfo
};
actionDescriptor.DisplayName = string.Format(
"{0}.{1}",
action.ActionMethod.DeclaringType.FullName,
action.ActionMethod.Name);
actionDescriptor.FilterDescriptors =
action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
.Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
.Concat(globalFilters.Select(f => new FilterDescriptor(f, FilterScope.Global)))
.OrderBy(d => d, FilterDescriptorOrderComparer.Comparer)
.ToList();
return actionDescriptor;
}
private static AttributeRouteInfo CreateAttributeRouteInfo(
ReflectedActionModel action,
ReflectedControllerModel controller)
{
var combinedRoute = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(
controller.AttributeRouteModel,
action.AttributeRouteModel);
if (combinedRoute == null)
{
return null;
}
else
{
return new AttributeRouteInfo()
{
Template = combinedRoute.Template,
Order = combinedRoute.Order ?? DefaultAttributeRouteOrder,
Name = combinedRoute.Name,
};
}
}
private static void AddActionConstraints(
ReflectedActionDescriptor actionDescriptor,
ReflectedActionModel action,
ReflectedControllerModel controller)
{
var httpMethods = action.HttpMethods;
if (httpMethods != null && httpMethods.Count > 0)
{
actionDescriptor.MethodConstraints = new List<HttpMethodConstraint>()
{
new HttpMethodConstraint(httpMethods)
};
}
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"controller",
controller.ControllerName));
if (action.IsActionNameMatchRequired)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
action.ActionName));
}
else
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
RouteKeyHandling.DenyKey));
}
}
private static void AddControllerRouteConstraints(
ReflectedActionDescriptor actionDescriptor,
IList<RouteConstraintAttribute> routeconstraints,
ISet<string> removalConstraints)
{
// Apply all the constraints defined on the controller (for example, [Area]) to the actions
// in that controller. Also keep track of all the constraints that require preventing actions
// without the constraint to match. For example, actions without an [Area] attribute on their
// controller should not match when a value has been given for area when matching a url or
// generating a link.
foreach (var constraintAttribute in routeconstraints)
{
if (constraintAttribute.BlockNonAttributedActions)
{
removalConstraints.Add(constraintAttribute.RouteKey);
}
// Skip duplicates
if (!HasConstraint(actionDescriptor.RouteConstraints, constraintAttribute.RouteKey))
{
if (constraintAttribute.RouteValue == null)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteKeyHandling));
}
else
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteValue));
}
}
}
}
private static bool HasConstraint(List<RouteDataActionConstraint> constraints, string routeKey)
{
return constraints.Any(
rc => string.Equals(rc.RouteKey, routeKey, StringComparison.OrdinalIgnoreCase));
}
private static void ReplaceRouteConstraints(ReflectedActionDescriptor actionDescriptor)
{
var routeGroupValue = GetRouteGroupValue(
actionDescriptor.AttributeRouteInfo.Order,
actionDescriptor.AttributeRouteInfo.Template);
var routeConstraints = new List<RouteDataActionConstraint>();
routeConstraints.Add(new RouteDataActionConstraint(
AttributeRouting.RouteGroupKey,
routeGroupValue));
actionDescriptor.RouteConstraints = routeConstraints;
}
private static void ReplaceAttributeRouteTokens(
ReflectedActionDescriptor actionDescriptor,
IList<string> routeTemplateErrors)
{
try
{
actionDescriptor.AttributeRouteInfo.Template = ReflectedAttributeRouteModel.ReplaceTokens(
actionDescriptor.AttributeRouteInfo.Template,
actionDescriptor.RouteValueDefaults);
}
catch (InvalidOperationException ex)
{
var message = Resources.FormatAttributeRoute_IndividualErrorMessage(
actionDescriptor.DisplayName,
Environment.NewLine,
ex.Message);
routeTemplateErrors.Add(message);
}
}
private static void AddConstraintsAsDefaultRouteValues(ReflectedActionDescriptor actionDescriptor)
{
foreach (var constraint in actionDescriptor.RouteConstraints)
{
// We don't need to do anything with attribute routing for 'catch all' behavior. Order
// and predecedence of attribute routes allow this kind of behavior.
if (constraint.KeyHandling == RouteKeyHandling.RequireKey ||
constraint.KeyHandling == RouteKeyHandling.DenyKey)
{
actionDescriptor.RouteValueDefaults.Add(constraint.RouteKey, constraint.RouteValue);
}
}
}
private static void AddRemovalConstraints(
ReflectedActionDescriptor actionDescriptor,
ISet<string> removalConstraints)
{
foreach (var key in removalConstraints)
{
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
key,
RouteKeyHandling.DenyKey));
}
}
}
private static void AddActionToNamedGroup(
IDictionary<string, IList<ActionDescriptor>> actionsByRouteName,
string routeName,
ReflectedActionDescriptor actionDescriptor)
{
IList<ActionDescriptor> namedActionGroup;
if (actionsByRouteName.TryGetValue(routeName, out namedActionGroup))
{
namedActionGroup.Add(actionDescriptor);
}
else
{
namedActionGroup = new List<ActionDescriptor>();
namedActionGroup.Add(actionDescriptor);
actionsByRouteName.Add(routeName, namedActionGroup);
}
}
private static bool IsAttributeRoutedAction(ReflectedActionDescriptor actionDescriptor)
{
return actionDescriptor.AttributeRouteInfo != null &&
actionDescriptor.AttributeRouteInfo.Template != null;
}
private static IList<string> AddErrorNumbers(
IList<string> namedRoutedErrors)
{ {
return namedRoutedErrors return namedRoutedErrors
.Select((nre, i) => .Select((nre, i) =>

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.Test
var actionNames = descriptors.Select(ad => ad.Name); var actionNames = descriptors.Select(ad => ad.Name);
// Assert // Assert
Assert.Equal(new[] { "GetPerson", "ListPeople", }, actionNames); Assert.Equal(new[] { "GetPerson", "ShowPeople", }, actionNames);
} }
[Fact] [Fact]
@ -58,21 +58,244 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(FilterScope.Action, filter3.Scope); Assert.Equal(FilterScope.Action, filter3.Scope);
} }
[Fact] [Theory]
public void GetDescriptors_AddsHttpMethodConstraints() [InlineData(typeof(HttpMethodController), nameof(HttpMethodController.OnlyPost), "POST")]
[InlineData(typeof(AttributeRoutedHttpMethodController), nameof(AttributeRoutedHttpMethodController.PutOrPatch), "PUT,PATCH")]
public void GetDescriptors_AddsHttpMethodConstraints(Type controllerType, string actionName, string expectedMethods)
{ {
// Arrange // Arrange
var provider = GetProvider(typeof(HttpMethodController).GetTypeInfo()); var provider = GetProvider(controllerType.GetTypeInfo());
// Act // Act
var descriptors = provider.GetDescriptors(); var descriptors = provider.GetDescriptors();
var descriptor = Assert.Single(descriptors); var descriptor = Assert.Single(descriptors);
// Assert // Assert
Assert.Equal("OnlyPost", descriptor.Name); Assert.Equal(actionName, descriptor.Name);
Assert.Single(descriptor.MethodConstraints); Assert.Single(descriptor.MethodConstraints);
Assert.Equal(new string[] { "POST" }, descriptor.MethodConstraints[0].HttpMethods); Assert.Equal(expectedMethods.Split(','), descriptor.MethodConstraints[0].HttpMethods);
}
[Fact]
public void GetDescriptors_AddsParameters_ToActionDescriptor()
{
// Arrange & Act
var descriptors = GetDescriptors(
typeof(ActionParametersController).GetTypeInfo());
// Assert
var main = Assert.Single(descriptors,
d => d.Name.Equals(nameof(ActionParametersController.RequiredInt)));
Assert.NotNull(main.Parameters);
var id = Assert.Single(main.Parameters);
Assert.Equal("id", id.Name);
Assert.False(id.IsOptional);
Assert.Null(id.BodyParameterInfo);
Assert.NotNull(id.ParameterBindingInfo);
Assert.Equal("id", id.ParameterBindingInfo.Prefix);
Assert.Equal(typeof(int), id.ParameterBindingInfo.ParameterType);
}
[Fact]
public void GetDescriptors_AddsMultipleParameters_ToActionDescriptor()
{
// Arrange & Act
var descriptors = GetDescriptors(
typeof(ActionParametersController).GetTypeInfo());
// Assert
var main = Assert.Single(descriptors,
d => d.Name.Equals(nameof(ActionParametersController.MultipleParameters)));
Assert.NotNull(main.Parameters);
var id = Assert.Single(main.Parameters, p => p.Name == "id");
Assert.Equal("id", id.Name);
Assert.False(id.IsOptional);
Assert.Null(id.BodyParameterInfo);
Assert.NotNull(id.ParameterBindingInfo);
Assert.Equal("id", id.ParameterBindingInfo.Prefix);
Assert.Equal(typeof(int), id.ParameterBindingInfo.ParameterType);
var entity = Assert.Single(main.Parameters, p => p.Name == "entity");
Assert.Equal("entity", entity.Name);
Assert.False(entity.IsOptional);
Assert.Null(entity.ParameterBindingInfo);
Assert.NotNull(entity.BodyParameterInfo);
Assert.Equal(typeof(TestActionParameter), entity.BodyParameterInfo.ParameterType);
}
[Fact]
public void GetDescriptors_AddsMultipleParametersWithDifferentCasing_ToActionDescriptor()
{
// Arrange & Act
var descriptors = GetDescriptors(
typeof(ActionParametersController).GetTypeInfo());
// Assert
var main = Assert.Single(descriptors,
d => d.Name.Equals(nameof(ActionParametersController.DifferentCasing)));
Assert.NotNull(main.Parameters);
var id = Assert.Single(main.Parameters, p => p.Name == "id");
Assert.Equal("id", id.Name);
Assert.False(id.IsOptional);
Assert.Null(id.BodyParameterInfo);
Assert.NotNull(id.ParameterBindingInfo);
Assert.Equal("id", id.ParameterBindingInfo.Prefix);
Assert.Equal(typeof(int), id.ParameterBindingInfo.ParameterType);
var upperCaseId = Assert.Single(main.Parameters, p => p.Name == "ID");
Assert.Equal("ID", upperCaseId.Name);
Assert.False(upperCaseId.IsOptional);
Assert.Null(upperCaseId.BodyParameterInfo);
Assert.NotNull(upperCaseId.ParameterBindingInfo);
Assert.Equal("ID", upperCaseId.ParameterBindingInfo.Prefix);
Assert.Equal(typeof(int), upperCaseId.ParameterBindingInfo.ParameterType);
var pascalCaseId = Assert.Single(main.Parameters, p => p.Name == "Id");
Assert.Equal("Id", pascalCaseId.Name);
Assert.False(pascalCaseId.IsOptional);
Assert.Null(pascalCaseId.BodyParameterInfo);
Assert.NotNull(pascalCaseId.ParameterBindingInfo);
Assert.Equal("Id", pascalCaseId.ParameterBindingInfo.Prefix);
Assert.Equal(typeof(int), pascalCaseId.ParameterBindingInfo.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.BodyParameterInfo);
Assert.NotNull(id.ParameterBindingInfo);
Assert.Equal("id", id.ParameterBindingInfo.Prefix);
Assert.Equal(parameterType, id.ParameterBindingInfo.ParameterType);
}
[Fact]
public void GetDescriptors_AddsParameters_DetectsFromBodyParameters()
{
// Arrange & Act
var actionName = nameof(ActionParametersController.FromBodyParameter);
var descriptors = GetDescriptors(
typeof(ActionParametersController).GetTypeInfo());
// Assert
var fromBody = Assert.Single(descriptors,
d => d.Name.Equals(actionName));
Assert.NotNull(fromBody.Parameters);
var entity = Assert.Single(fromBody.Parameters);
Assert.Equal("entity", entity.Name);
Assert.False(entity.IsOptional);
Assert.NotNull(entity.BodyParameterInfo);
Assert.Equal(typeof(TestActionParameter), entity.BodyParameterInfo.ParameterType);
Assert.Null(entity.ParameterBindingInfo);
}
[Fact]
public void GetDescriptors_AddsParameters_DoesNotDetectParameterFromBody_IfNoFromBodyAttribute()
{
// Arrange & Act
var actionName = nameof(ActionParametersController.NotFromBodyParameter);
var descriptors = GetDescriptors(
typeof(ActionParametersController).GetTypeInfo());
// Assert
var notFromBody = Assert.Single(descriptors,
d => d.Name.Equals(actionName));
Assert.NotNull(notFromBody.Parameters);
var entity = Assert.Single(notFromBody.Parameters);
Assert.Equal("entity", entity.Name);
Assert.False(entity.IsOptional);
Assert.Null(entity.BodyParameterInfo);
Assert.NotNull(entity.ParameterBindingInfo);
Assert.Equal("entity", entity.ParameterBindingInfo.Prefix);
Assert.Equal(typeof(TestActionParameter), entity.ParameterBindingInfo.ParameterType);
}
[Fact]
public void GetDescriptors_AddsControllerAndActionConstraints_ToConventionallyRoutedActions()
{
// Arrange & Act
var descriptors = GetDescriptors(
typeof(ConventionallyRoutedController).GetTypeInfo());
// Assert
var action = Assert.Single(descriptors);
Assert.NotNull(action.RouteConstraints);
var controller = Assert.Single(action.RouteConstraints,
rc => rc.RouteKey.Equals("controller"));
Assert.Equal(RouteKeyHandling.RequireKey, controller.KeyHandling);
Assert.Equal("ConventionallyRouted", controller.RouteValue);
var actionConstraint = Assert.Single(action.RouteConstraints,
rc => rc.RouteKey.Equals("action"));
Assert.Equal(RouteKeyHandling.RequireKey, actionConstraint.KeyHandling);
Assert.Equal(nameof(ConventionallyRoutedController.ConventionalAction), actionConstraint.RouteValue);
}
[Fact]
public void GetDescriptors_AddsControllerAndActionDefaults_ToAttributeRoutedActions()
{
// Arrange & Act
var descriptors = GetDescriptors(
typeof(AttributeRoutedController).GetTypeInfo());
// Assert
var action = Assert.Single(descriptors);
var routeconstraint = Assert.Single(action.RouteConstraints);
Assert.Equal(RouteKeyHandling.RequireKey, routeconstraint.KeyHandling);
Assert.Equal(AttributeRouting.RouteGroupKey, routeconstraint.RouteKey);
var controller = Assert.Single(action.RouteValueDefaults,
rc => rc.Key.Equals("controller"));
Assert.Equal("AttributeRouted", controller.Value);
var actionConstraint = Assert.Single(action.RouteValueDefaults,
rc => rc.Key.Equals("action"));
Assert.Equal(nameof(AttributeRoutedController.AttributeRoutedAction), actionConstraint.Value);
} }
[Fact] [Fact]
@ -110,6 +333,12 @@ namespace Microsoft.AspNet.Mvc.Test
c => c =>
c.RouteKey == "action" && c.RouteKey == "action" &&
c.RouteValue == "Edit"); c.RouteValue == "Edit");
Assert.Single(
descriptorWithConstraint.RouteConstraints,
c =>
c.RouteKey == "key" &&
c.RouteValue == "value" &&
c.KeyHandling == RouteKeyHandling.RequireKey);
Assert.Equal(3, descriptorWithoutConstraint.RouteConstraints.Count); Assert.Equal(3, descriptorWithoutConstraint.RouteConstraints.Count);
Assert.Single( Assert.Single(
@ -122,6 +351,12 @@ namespace Microsoft.AspNet.Mvc.Test
c => c =>
c.RouteKey == "action" && c.RouteKey == "action" &&
c.RouteValue == "OnlyPost"); c.RouteValue == "OnlyPost");
Assert.Single(
descriptorWithoutConstraint.RouteConstraints,
c =>
c.RouteKey == "key" &&
c.RouteValue == null &&
c.KeyHandling == RouteKeyHandling.DenyKey);
} }
[Fact] [Fact]
@ -158,6 +393,12 @@ namespace Microsoft.AspNet.Mvc.Test
c => c =>
c.RouteKey == "action" && c.RouteKey == "action" &&
c.RouteValue == "Create"); c.RouteValue == "Create");
Assert.Single(
descriptorWithConstraint.RouteConstraints,
c =>
c.RouteKey == "key" &&
c.RouteValue == "value" &&
c.KeyHandling == RouteKeyHandling.RequireKey);
Assert.Equal(2, descriptorWithoutConstraint.RouteConstraints.Count); Assert.Equal(2, descriptorWithoutConstraint.RouteConstraints.Count);
Assert.Single( Assert.Single(
@ -190,6 +431,66 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Same(filter, Assert.Single(filters)); Assert.Same(filter, Assert.Single(filters));
} }
[Fact]
public void BuildModel_CreatesReflectedControllerModels_ForAllControllers()
{
// Arrange
var provider = GetProvider(
typeof(ConventionallyRoutedController).GetTypeInfo(),
typeof(AttributeRoutedController).GetTypeInfo(),
typeof(EmptyController).GetTypeInfo(),
typeof(NonActionAttributeController).GetTypeInfo());
// Act
var model = provider.BuildModel();
// Assert
Assert.NotNull(model);
Assert.Equal(4, model.Controllers.Count);
var conventional = Assert.Single(model.Controllers,
c => c.ControllerName == "ConventionallyRouted");
Assert.Null(conventional.AttributeRouteModel);
Assert.Single(conventional.Actions);
var attributeRouted = Assert.Single(model.Controllers,
c => c.ControllerName == "AttributeRouted");
Assert.Single(attributeRouted.Actions);
Assert.NotNull(attributeRouted.AttributeRouteModel);
var empty = Assert.Single(model.Controllers,
c => c.ControllerName == "Empty");
Assert.Empty(empty.Actions);
var nonAction = Assert.Single(model.Controllers,
c => c.ControllerName == "NonActionAttribute");
Assert.Empty(nonAction.Actions);
}
[Fact]
public void BuildModel_CreatesReflectedActionDescriptors_ForValidActions()
{
// Arrange
var provider = GetProvider(
typeof(PersonController).GetTypeInfo());
// Act
var model = provider.BuildModel();
// Assert
var controller = Assert.Single(model.Controllers);
Assert.Equal(2, controller.Actions.Count);
var getPerson = Assert.Single(controller.Actions, a => a.ActionName == "GetPerson");
Assert.Empty(getPerson.HttpMethods);
Assert.True(getPerson.IsActionNameMatchRequired);
var showPeople = Assert.Single(controller.Actions, a => a.ActionName == "ShowPeople");
Assert.Empty(showPeople.HttpMethods);
Assert.True(showPeople.IsActionNameMatchRequired);
}
[Fact] [Fact]
public void GetDescriptor_SetsDisplayName() public void GetDescriptor_SetsDisplayName()
{ {
@ -340,7 +641,7 @@ namespace Microsoft.AspNet.Mvc.Test
{ {
// Arrange // Arrange
var provider = GetProvider( var provider = GetProvider(
typeof(MixedAttributeRouteController).GetTypeInfo(), typeof(ConventionalAndAttributeRoutedActionsWithAreaController).GetTypeInfo(),
typeof(ConstrainedController).GetTypeInfo()); typeof(ConstrainedController).GetTypeInfo());
// Act // Act
@ -364,7 +665,7 @@ namespace Microsoft.AspNet.Mvc.Test
{ {
// Arrange // Arrange
var provider = GetProvider( var provider = GetProvider(
typeof(MixedAttributeRouteController).GetTypeInfo(), typeof(ConventionalAndAttributeRoutedActionsWithAreaController).GetTypeInfo(),
typeof(ConstrainedController).GetTypeInfo()); typeof(ConstrainedController).GetTypeInfo());
// Act // Act
@ -385,7 +686,7 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(5, indexAction.RouteValueDefaults.Count); Assert.Equal(5, indexAction.RouteValueDefaults.Count);
var controllerDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("controller", StringComparison.OrdinalIgnoreCase)); var controllerDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("controller", StringComparison.OrdinalIgnoreCase));
Assert.Equal("MixedAttributeRoute", controllerDefault.Value); Assert.Equal("ConventionalAndAttributeRoutedActionsWithArea", controllerDefault.Value);
var actionDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("action", StringComparison.OrdinalIgnoreCase)); var actionDefault = Assert.Single(indexAction.RouteValueDefaults, rd => rd.Key.Equals("action", StringComparison.OrdinalIgnoreCase));
Assert.Equal("Index", actionDefault.Value); Assert.Equal("Index", actionDefault.Value);
@ -520,11 +821,19 @@ namespace Microsoft.AspNet.Mvc.Test
} }
} }
[Route("Products")]
private class AttributeRoutedHttpMethodController
{
[AcceptVerbs("PUT", "PATCH")]
public void PutOrPatch() { }
}
private class PersonController private class PersonController
{ {
public void GetPerson() public void GetPerson()
{ } { }
[ActionName("ShowPeople")]
public void ListPeople() public void ListPeople()
{ } { }
@ -533,7 +842,7 @@ namespace Microsoft.AspNet.Mvc.Test
{ } { }
} }
public class MyRouteConstraintAttribute : RouteConstraintAttribute private class MyRouteConstraintAttribute : RouteConstraintAttribute
{ {
public MyRouteConstraintAttribute(bool blockNonAttributedActions) public MyRouteConstraintAttribute(bool blockNonAttributedActions)
: base("key", "value", blockNonAttributedActions) : base("key", "value", blockNonAttributedActions)
@ -541,7 +850,7 @@ namespace Microsoft.AspNet.Mvc.Test
} }
} }
public class MySecondRouteConstraintAttribute : RouteConstraintAttribute private class MySecondRouteConstraintAttribute : RouteConstraintAttribute
{ {
public MySecondRouteConstraintAttribute(bool blockNonAttributedActions) public MySecondRouteConstraintAttribute(bool blockNonAttributedActions)
: base("second", "value", blockNonAttributedActions) : base("second", "value", blockNonAttributedActions)
@ -549,7 +858,7 @@ namespace Microsoft.AspNet.Mvc.Test
} }
} }
[MyRouteConstraintAttribute(blockNonAttributedActions: true)] [MyRouteConstraint(blockNonAttributedActions: true)]
private class BlockNonAttributedActionsController private class BlockNonAttributedActionsController
{ {
public void Edit() public void Edit()
@ -557,7 +866,7 @@ namespace Microsoft.AspNet.Mvc.Test
} }
} }
[MyRouteConstraintAttribute(blockNonAttributedActions: false)] [MyRouteConstraint(blockNonAttributedActions: false)]
private class DontBlockNonAttributedActionsController private class DontBlockNonAttributedActionsController
{ {
public void Create() public void Create()
@ -623,7 +932,7 @@ namespace Microsoft.AspNet.Mvc.Test
} }
[Area("Home")] [Area("Home")]
private class MixedAttributeRouteController private class ConventionalAndAttributeRoutedActionsWithAreaController
{ {
[HttpGet("Index")] [HttpGet("Index")]
public void Index() { } public void Index() { }
@ -635,7 +944,7 @@ namespace Microsoft.AspNet.Mvc.Test
} }
[Route("Products", Name = "Products")] [Route("Products", Name = "Products")]
public class SameNameDifferentTemplatesController private class SameNameDifferentTemplatesController
{ {
[HttpGet] [HttpGet]
public void Get() { } public void Get() { }
@ -668,7 +977,7 @@ namespace Microsoft.AspNet.Mvc.Test
public void PatchItems() { } public void PatchItems() { }
} }
public class DifferentCasingsAttributeRouteNamesController private class DifferentCasingsAttributeRouteNamesController
{ {
[HttpGet("{id}", Name = "Products")] [HttpGet("{id}", Name = "Products")]
public void Get() { } public void Get() { }
@ -689,5 +998,50 @@ namespace Microsoft.AspNet.Mvc.Test
{ {
public void ConstrainedNonAttributedAction() { } public void ConstrainedNonAttributedAction() { }
} }
private class ActionParametersController
{
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) { }
public void MultipleParameters(int id, [FromBody] TestActionParameter entity) { }
public void DifferentCasing(int id, int ID, int Id) { }
}
private class ConventionallyRoutedController
{
public void ConventionalAction() { }
}
[Route("api")]
private class AttributeRoutedController()
{
[HttpGet("AttributeRoute")]
public void AttributeRoutedAction() { }
}
private class EmptyController
{
}
private class NonActionAttributeController
{
[NonAction]
public void Action() { }
}
private class TestActionParameter
{
public int Id { get; set; }
public int Name { get; set; }
}
} }
} }