Adding attribute routing
This commit is contained in:
parent
85cf199ef1
commit
e396f1b451
11
Mvc.sln
11
Mvc.sln
|
|
@ -237,6 +237,16 @@ Global
|
||||||
{42CDBF4A-E238-4C0F-A416-44588363EB4C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
{42CDBF4A-E238-4C0F-A416-44588363EB4C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
{42CDBF4A-E238-4C0F-A416-44588363EB4C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
{42CDBF4A-E238-4C0F-A416-44588363EB4C}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
{42CDBF4A-E238-4C0F-A416-44588363EB4C}.Release|x86.ActiveCfg = Release|Any CPU
|
{42CDBF4A-E238-4C0F-A416-44588363EB4C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -261,5 +271,6 @@ Global
|
||||||
{07C0E921-FCBB-458C-AC11-3D01CE68B16B} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
{07C0E921-FCBB-458C-AC11-3D01CE68B16B} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||||
{680D75ED-601F-4D86-B01B-1072D0C31B8C} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
{680D75ED-601F-4D86-B01B-1072D0C31B8C} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||||
{42CDBF4A-E238-4C0F-A416-44588363EB4C} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
{42CDBF4A-E238-4C0F-A416-44588363EB4C} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||||
|
{5C34562F-2861-4CD6-AF02-462A9A8D76EE} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,19 @@ using Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
namespace MvcSample.Web
|
namespace MvcSample.Web
|
||||||
{
|
{
|
||||||
|
[Route("api/REST")]
|
||||||
public class SimpleRest : Controller
|
public class SimpleRest : Controller
|
||||||
{
|
{
|
||||||
public string Get()
|
[HttpGet]
|
||||||
|
public string ThisIsAGetMethod()
|
||||||
{
|
{
|
||||||
return "Get method";
|
return "Get method";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("OtherThing")]
|
||||||
|
public string GetOtherThing()
|
||||||
|
{
|
||||||
|
return "Get other thing";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// 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.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
|
@ -12,6 +12,11 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
public List<RouteDataActionConstraint> RouteConstraints { get; set; }
|
public List<RouteDataActionConstraint> RouteConstraints { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The route template May be null if the action has no attribute routes.
|
||||||
|
/// </summary>
|
||||||
|
public string RouteTemplate { get; set; }
|
||||||
|
|
||||||
public List<HttpMethodConstraint> MethodConstraints { get; set; }
|
public List<HttpMethodConstraint> MethodConstraints { get; set; }
|
||||||
|
|
||||||
public List<IActionConstraint> DynamicConstraints { get; set; }
|
public List<IActionConstraint> DynamicConstraints { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,41 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies an action that only supports the HTTP GET method.
|
||||||
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
public sealed class HttpGetAttribute : Attribute, IActionHttpMethodProvider
|
public sealed class HttpGetAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
|
||||||
{
|
{
|
||||||
private static readonly IEnumerable<string> _supportedMethods = new string[] { "GET" };
|
private static readonly IEnumerable<string> _supportedMethods = new string[] { "GET" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="HttpGetAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public HttpGetAttribute()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="HttpGetAttribute"/> with the given route template.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="template">The route template. May not be null.</param>
|
||||||
|
public HttpGetAttribute([NotNull] string template)
|
||||||
|
{
|
||||||
|
Template = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IEnumerable<string> HttpMethods
|
public IEnumerable<string> HttpMethods
|
||||||
{
|
{
|
||||||
get { return _supportedMethods; }
|
get { return _supportedMethods; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Template { get; private set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +27,8 @@
|
||||||
<Compile Include="ActionDescriptor.cs" />
|
<Compile Include="ActionDescriptor.cs" />
|
||||||
<Compile Include="ActionDescriptorProviderContext.cs" />
|
<Compile Include="ActionDescriptorProviderContext.cs" />
|
||||||
<Compile Include="ActionDescriptorsCollection.cs" />
|
<Compile Include="ActionDescriptorsCollection.cs" />
|
||||||
|
<Compile Include="ReflectedActionDescriptor.cs" />
|
||||||
|
<Compile Include="ReflectedActionDescriptorProvider.cs" />
|
||||||
<Compile Include="ReflectedModelBuilder\IReflectedApplicationModelConvention.cs" />
|
<Compile Include="ReflectedModelBuilder\IReflectedApplicationModelConvention.cs" />
|
||||||
<Compile Include="ReflectedModelBuilder\ReflectedActionModel.cs" />
|
<Compile Include="ReflectedModelBuilder\ReflectedActionModel.cs" />
|
||||||
<Compile Include="ReflectedModelBuilder\ReflectedControllerModel.cs" />
|
<Compile Include="ReflectedModelBuilder\ReflectedControllerModel.cs" />
|
||||||
|
|
@ -74,7 +76,6 @@
|
||||||
<Compile Include="DefaultActionSelector.cs" />
|
<Compile Include="DefaultActionSelector.cs" />
|
||||||
<Compile Include="DefaultControllerAssemblyProvider.cs" />
|
<Compile Include="DefaultControllerAssemblyProvider.cs" />
|
||||||
<Compile Include="DefaultControllerFactory.cs" />
|
<Compile Include="DefaultControllerFactory.cs" />
|
||||||
<Compile Include="ReflectedModelBuilder\ReflectedParameterModel.cs" />
|
|
||||||
<Compile Include="Extensions\IEnumerableExtensions.cs" />
|
<Compile Include="Extensions\IEnumerableExtensions.cs" />
|
||||||
<Compile Include="Filters\FilterItemOrderComparer.cs" />
|
<Compile Include="Filters\FilterItemOrderComparer.cs" />
|
||||||
<Compile Include="Filters\TypeFilterAttribute.cs" />
|
<Compile Include="Filters\TypeFilterAttribute.cs" />
|
||||||
|
|
@ -152,11 +153,10 @@
|
||||||
<Compile Include="ParameterDescriptor.cs" />
|
<Compile Include="ParameterDescriptor.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Properties\Resources.Designer.cs" />
|
<Compile Include="Properties\Resources.Designer.cs" />
|
||||||
<Compile Include="ReflectedActionDescriptor.cs" />
|
|
||||||
<Compile Include="ReflectedActionDescriptorProvider.cs" />
|
|
||||||
<Compile Include="ReflectedActionExecutor.cs" />
|
<Compile Include="ReflectedActionExecutor.cs" />
|
||||||
<Compile Include="ReflectedActionInvoker.cs" />
|
<Compile Include="ReflectedActionInvoker.cs" />
|
||||||
<Compile Include="ReflectedActionInvokerProvider.cs" />
|
<Compile Include="ReflectedActionInvokerProvider.cs" />
|
||||||
|
<Compile Include="ReflectedModelBuilder\ReflectedParameterModel.cs" />
|
||||||
<Compile Include="Rendering\DynamicViewData.cs" />
|
<Compile Include="Rendering\DynamicViewData.cs" />
|
||||||
<Compile Include="Rendering\Expressions\CachedExpressionCompiler.cs" />
|
<Compile Include="Rendering\Expressions\CachedExpressionCompiler.cs" />
|
||||||
<Compile Include="Rendering\Expressions\ExpressionHelper.cs" />
|
<Compile Include="Rendering\Expressions\ExpressionHelper.cs" />
|
||||||
|
|
@ -203,10 +203,17 @@
|
||||||
<Compile Include="Rendering\SelectListItem.cs" />
|
<Compile Include="Rendering\SelectListItem.cs" />
|
||||||
<Compile Include="Rendering\UnobtrusiveValidationAttributesGenerator.cs" />
|
<Compile Include="Rendering\UnobtrusiveValidationAttributesGenerator.cs" />
|
||||||
<Compile Include="Rendering\ViewEngineResult.cs" />
|
<Compile Include="Rendering\ViewEngineResult.cs" />
|
||||||
|
<Compile Include="RouteAttribute.cs" />
|
||||||
<Compile Include="RouteConstraintAttribute.cs" />
|
<Compile Include="RouteConstraintAttribute.cs" />
|
||||||
<Compile Include="RouteDataActionConstraint.cs" />
|
<Compile Include="RouteDataActionConstraint.cs" />
|
||||||
<Compile Include="KnownRouteValueConstraint.cs" />
|
<Compile Include="KnownRouteValueConstraint.cs" />
|
||||||
<Compile Include="RouteKeyHandling.cs" />
|
<Compile Include="RouteKeyHandling.cs" />
|
||||||
|
<Compile Include="Routing\AttributeRoute.cs" />
|
||||||
|
<Compile Include="Routing\AttributeRouteEntry.cs" />
|
||||||
|
<Compile Include="Routing\AttributeRoutePrecedence.cs" />
|
||||||
|
<Compile Include="Routing\AttributeRouteTemplate.cs" />
|
||||||
|
<Compile Include="Routing\AttributeRouting.cs" />
|
||||||
|
<Compile Include="Routing\IRouteTemplateProvider.cs" />
|
||||||
<Compile Include="TemplateInfo.cs" />
|
<Compile Include="TemplateInfo.cs" />
|
||||||
<Compile Include="UrlHelper.cs" />
|
<Compile Include="UrlHelper.cs" />
|
||||||
<Compile Include="UrlHelperExtensions.cs" />
|
<Compile Include="UrlHelperExtensions.cs" />
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
#endif
|
#endif
|
||||||
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
|
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
using Microsoft.Framework.OptionsModel;
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
|
|
@ -20,16 +23,19 @@ namespace Microsoft.AspNet.Mvc
|
||||||
private readonly IActionDiscoveryConventions _conventions;
|
private readonly IActionDiscoveryConventions _conventions;
|
||||||
private readonly IEnumerable<IFilter> _globalFilters;
|
private readonly IEnumerable<IFilter> _globalFilters;
|
||||||
private readonly IEnumerable<IReflectedApplicationModelConvention> _modelConventions;
|
private readonly IEnumerable<IReflectedApplicationModelConvention> _modelConventions;
|
||||||
|
private readonly IInlineConstraintResolver _constraintResolver;
|
||||||
|
|
||||||
public ReflectedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider,
|
public ReflectedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider,
|
||||||
IActionDiscoveryConventions conventions,
|
IActionDiscoveryConventions conventions,
|
||||||
IEnumerable<IFilter> globalFilters,
|
IEnumerable<IFilter> globalFilters,
|
||||||
IOptionsAccessor<MvcOptions> optionsAccessor)
|
IOptionsAccessor<MvcOptions> optionsAccessor,
|
||||||
|
IInlineConstraintResolver constraintResolver)
|
||||||
{
|
{
|
||||||
_controllerAssemblyProvider = controllerAssemblyProvider;
|
_controllerAssemblyProvider = controllerAssemblyProvider;
|
||||||
_conventions = conventions;
|
_conventions = conventions;
|
||||||
_globalFilters = globalFilters ?? Enumerable.Empty<IFilter>();
|
_globalFilters = globalFilters ?? Enumerable.Empty<IFilter>();
|
||||||
_modelConventions = optionsAccessor.Options.ApplicationModelConventions;
|
_modelConventions = optionsAccessor.Options.ApplicationModelConventions;
|
||||||
|
_constraintResolver = constraintResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Order
|
public int Order
|
||||||
|
|
@ -106,6 +112,8 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
|
public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
|
||||||
{
|
{
|
||||||
|
var routeGroupsByTemplate = GetRouteGroupsByTemplate(model);
|
||||||
|
|
||||||
var actions = new List<ReflectedActionDescriptor>();
|
var actions = new List<ReflectedActionDescriptor>();
|
||||||
|
|
||||||
var removalConstraints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var removalConstraints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
@ -188,6 +196,45 @@ namespace Microsoft.AspNet.Mvc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (routeGroupsByTemplate.Any())
|
||||||
|
{
|
||||||
|
var templateText = AttributeRouteTemplate.Combine(
|
||||||
|
controller.RouteTemplate,
|
||||||
|
action.RouteTemplate);
|
||||||
|
|
||||||
|
if (templateText == null)
|
||||||
|
{
|
||||||
|
// A conventional routed action can't match any route group.
|
||||||
|
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||||
|
AttributeRouting.RouteGroupKey,
|
||||||
|
RouteKeyHandling.DenyKey));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// An attribute routed action will ignore conventional routed constraints.
|
||||||
|
actionDescriptor.RouteConstraints.Clear();
|
||||||
|
|
||||||
|
// TODO #738 - this currently has parity with what we did in MVC5 for the action
|
||||||
|
// route values. This needs to be reconsidered as part of #738.
|
||||||
|
var template = TemplateParser.Parse(templateText, _constraintResolver);
|
||||||
|
if (template.Parameters.Any(
|
||||||
|
p => p.IsParameter &&
|
||||||
|
string.Equals(p.Name, "action", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||||
|
"action",
|
||||||
|
action.ActionName));
|
||||||
|
}
|
||||||
|
|
||||||
|
var routeGroup = routeGroupsByTemplate[templateText];
|
||||||
|
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||||
|
AttributeRouting.RouteGroupKey,
|
||||||
|
routeGroup));
|
||||||
|
|
||||||
|
actionDescriptor.RouteTemplate = templateText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actionDescriptor.FilterDescriptors =
|
actionDescriptor.FilterDescriptors =
|
||||||
action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
|
action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
|
||||||
.Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
|
.Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
|
||||||
|
|
@ -214,5 +261,25 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Groups the set of all attribute routing templates and returns mapping of [template -> group].
|
||||||
|
private static Dictionary<string, string> GetRouteGroupsByTemplate(ReflectedApplicationModel model)
|
||||||
|
{
|
||||||
|
var groupsByTemplate = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var controller in model.Controllers)
|
||||||
|
{
|
||||||
|
foreach (var action in controller.Actions)
|
||||||
|
{
|
||||||
|
var template = AttributeRouteTemplate.Combine(controller.RouteTemplate, action.RouteTemplate);
|
||||||
|
if (template != null && !groupsByTemplate.ContainsKey(template))
|
||||||
|
{
|
||||||
|
groupsByTemplate.Add(template, "__route__" + template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupsByTemplate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
||||||
{
|
{
|
||||||
|
|
@ -19,6 +20,12 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
||||||
|
|
||||||
Filters = Attributes.OfType<IFilter>().ToList();
|
Filters = Attributes.OfType<IFilter>().ToList();
|
||||||
|
|
||||||
|
var routeTemplateAttribute = Attributes.OfType<IRouteTemplateProvider>().FirstOrDefault();
|
||||||
|
if (routeTemplateAttribute != null)
|
||||||
|
{
|
||||||
|
RouteTemplate = routeTemplateAttribute.Template;
|
||||||
|
}
|
||||||
|
|
||||||
HttpMethods = new List<string>();
|
HttpMethods = new List<string>();
|
||||||
Parameters = new List<ReflectedParameterModel>();
|
Parameters = new List<ReflectedParameterModel>();
|
||||||
}
|
}
|
||||||
|
|
@ -36,5 +43,7 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
||||||
public bool IsActionNameMatchRequired { get; set; }
|
public bool IsActionNameMatchRequired { get; set; }
|
||||||
|
|
||||||
public List<ReflectedParameterModel> Parameters { get; private set; }
|
public List<ReflectedParameterModel> Parameters { get; private set; }
|
||||||
|
|
||||||
|
public string RouteTemplate { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
||||||
{
|
{
|
||||||
|
|
@ -23,6 +24,12 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
||||||
Filters = Attributes.OfType<IFilter>().ToList();
|
Filters = Attributes.OfType<IFilter>().ToList();
|
||||||
RouteConstraints = Attributes.OfType<RouteConstraintAttribute>().ToList();
|
RouteConstraints = Attributes.OfType<RouteConstraintAttribute>().ToList();
|
||||||
|
|
||||||
|
var routeTemplateAttribute = Attributes.OfType<IRouteTemplateProvider>().FirstOrDefault();
|
||||||
|
if (routeTemplateAttribute != null)
|
||||||
|
{
|
||||||
|
RouteTemplate = routeTemplateAttribute.Template;
|
||||||
|
}
|
||||||
|
|
||||||
ControllerName = controllerType.Name.EndsWith("Controller", StringComparison.Ordinal)
|
ControllerName = controllerType.Name.EndsWith("Controller", StringComparison.Ordinal)
|
||||||
? controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length)
|
? controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length)
|
||||||
: controllerType.Name;
|
: controllerType.Name;
|
||||||
|
|
@ -39,5 +46,7 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
||||||
public List<IFilter> Filters { get; private set; }
|
public List<IFilter> Filters { get; private set; }
|
||||||
|
|
||||||
public List<RouteConstraintAttribute> RouteConstraints { get; private set; }
|
public List<RouteConstraintAttribute> RouteConstraints { get; private set; }
|
||||||
|
|
||||||
|
public string RouteTemplate { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 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 Microsoft.AspNet.Mvc.Routing;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies an attribute route on a controller.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class RouteAttribute : Attribute, IRouteTemplateProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="RouteAttribute"/> with the given route template.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="template">The route template. May not be null.</param>
|
||||||
|
public RouteAttribute([NotNull] string template)
|
||||||
|
{
|
||||||
|
Template = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Template { get; private set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IRouter"/> implementation for attribute routing.
|
||||||
|
/// </summary>
|
||||||
|
public class AttributeRoute : IRouter
|
||||||
|
{
|
||||||
|
private readonly IRouter _next;
|
||||||
|
private readonly TemplateRoute[] _routes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="AttributeRoute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="next">The next router. Invoked when a route entry matches.</param>
|
||||||
|
/// <param name="entries">The set of route entries.</param>
|
||||||
|
public AttributeRoute([NotNull] IRouter next, [NotNull] IEnumerable<AttributeRouteEntry> entries)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
|
||||||
|
// FOR RIGHT NOW - this is just an array of regular template routes. We'll follow up by implementing
|
||||||
|
// a good data-structure here.
|
||||||
|
_routes = entries.OrderBy(e => e.Precedence).Select(e => e.Route).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task RouteAsync([NotNull] RouteContext context)
|
||||||
|
{
|
||||||
|
foreach (var route in _routes)
|
||||||
|
{
|
||||||
|
await route.RouteAsync(context);
|
||||||
|
if (context.IsHandled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetVirtualPath([NotNull] VirtualPathContext context)
|
||||||
|
{
|
||||||
|
// Not implemented right now, but we don't want to throw here and block other routes from generating
|
||||||
|
// a link.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// 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 Microsoft.AspNet.Routing.Template;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to build an <see cref="AttributeRoute"/>. Represents an individual route that will be aggregated
|
||||||
|
/// into the <see cref="AttributeRoute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class AttributeRouteEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The precedence of the template.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Precedence { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="TemplateRoute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public TemplateRoute Route { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Computes precedence for an attribute route template.
|
||||||
|
/// </summary>
|
||||||
|
public static class AttributeRoutePrecedence
|
||||||
|
{
|
||||||
|
public static decimal Compute(Template template)
|
||||||
|
{
|
||||||
|
// Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
|
||||||
|
// and 4 results in a combined precedence of 2.14 (decimal).
|
||||||
|
var precedence = 0m;
|
||||||
|
|
||||||
|
for (var i = 0; i < template.Segments.Count; i++)
|
||||||
|
{
|
||||||
|
var segment = template.Segments[i];
|
||||||
|
|
||||||
|
var digit = ComputeDigit(segment);
|
||||||
|
Contract.Assert(digit >= 0 && digit < 10);
|
||||||
|
|
||||||
|
precedence += Decimal.Divide(digit, (decimal)Math.Pow(10, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return precedence;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Segments have the following order:
|
||||||
|
// 1 - Literal segments
|
||||||
|
// 2 - Constrained parameter segments / Multi-part segments
|
||||||
|
// 3 - Unconstrained parameter segments
|
||||||
|
// 4 - Constrained wildcard parameter segments
|
||||||
|
// 5 - Unconstrained wildcard parameter segments
|
||||||
|
private static int ComputeDigit(TemplateSegment segment)
|
||||||
|
{
|
||||||
|
if (segment.Parts.Count > 1)
|
||||||
|
{
|
||||||
|
// Multi-part segments should appear after literal segments but before parameter segments
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var part = segment.Parts[0];
|
||||||
|
// Literal segments always go first
|
||||||
|
if (part.IsLiteral)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Assert(part.IsParameter);
|
||||||
|
var digit = part.IsCatchAll ? 5 : 3;
|
||||||
|
|
||||||
|
// If there is a route constraint for the parameter, reduce order by 1
|
||||||
|
// Constrained parameters end up with order 2, Constrained catch alls end up with order 4
|
||||||
|
if (part.InlineConstraint != null)
|
||||||
|
{
|
||||||
|
digit--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return digit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// 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.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Functionality supporting route templates for attribute routes.
|
||||||
|
/// </summary>
|
||||||
|
public static class AttributeRouteTemplate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Combines attribute routing templates.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="left">The left template.</param>
|
||||||
|
/// <param name="right">The right template.</param>
|
||||||
|
/// <returns>A combined template.</returns>
|
||||||
|
public static string Combine(string left, string right)
|
||||||
|
{
|
||||||
|
if (left == null && right == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (left == null)
|
||||||
|
{
|
||||||
|
return right.Trim('/');
|
||||||
|
}
|
||||||
|
else if (right == null)
|
||||||
|
{
|
||||||
|
return left.Trim('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neither is null
|
||||||
|
var trimmedLeft = left.Trim('/');
|
||||||
|
var trimmedRight = right.Trim('/');
|
||||||
|
|
||||||
|
if (trimmedLeft == string.Empty)
|
||||||
|
{
|
||||||
|
return trimmedRight;
|
||||||
|
}
|
||||||
|
else if (trimmedRight == string.Empty)
|
||||||
|
{
|
||||||
|
return trimmedLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both templates contain some text.
|
||||||
|
return trimmedLeft + '/' + trimmedRight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
public static class AttributeRouting
|
||||||
|
{
|
||||||
|
// Key used by routing and action selection to match an attribute route entry to a
|
||||||
|
// group of action descriptors.
|
||||||
|
public static readonly string RouteGroupKey = "!__route_group";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an attribute route using the provided services and provided target router.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="target">The router to invoke when a route entry matches.</param>
|
||||||
|
/// <param name="services">The application services.</param>
|
||||||
|
/// <returns>An attribute route.</returns>
|
||||||
|
public static IRouter CreateAttributeMegaRoute([NotNull] IRouter target, [NotNull] IServiceProvider services)
|
||||||
|
{
|
||||||
|
var actions = GetActionDescriptors(services);
|
||||||
|
|
||||||
|
// We're creating one AttributeRouteEntry per group, so we need to identify the distinct set of
|
||||||
|
// groups. It's guaranteed that all members of the group have the same template and precedence,
|
||||||
|
// so we only need to hang on to a single instance of the template.
|
||||||
|
var routeTemplatesByGroup = GroupTemplatesByGroupId(actions);
|
||||||
|
|
||||||
|
var inlineConstraintResolver = services.GetService<IInlineConstraintResolver>();
|
||||||
|
|
||||||
|
var entries = new List<AttributeRouteEntry>();
|
||||||
|
foreach (var routeGroup in routeTemplatesByGroup)
|
||||||
|
{
|
||||||
|
var routeGroupId = routeGroup.Key;
|
||||||
|
var template = routeGroup.Value;
|
||||||
|
|
||||||
|
var parsedTemplate = TemplateParser.Parse(template, inlineConstraintResolver);
|
||||||
|
var precedence = AttributeRoutePrecedence.Compute(parsedTemplate);
|
||||||
|
|
||||||
|
entries.Add(new AttributeRouteEntry()
|
||||||
|
{
|
||||||
|
Precedence = precedence,
|
||||||
|
Route = new TemplateRoute(
|
||||||
|
target,
|
||||||
|
template,
|
||||||
|
defaults: new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ RouteGroupKey, routeGroupId },
|
||||||
|
},
|
||||||
|
constraints: null,
|
||||||
|
inlineConstraintResolver: inlineConstraintResolver),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AttributeRoute(target, entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<ActionDescriptor> GetActionDescriptors(IServiceProvider services)
|
||||||
|
{
|
||||||
|
var actionDescriptorProvider = services.GetService<IActionDescriptorsCollectionProvider>();
|
||||||
|
|
||||||
|
var actionDescriptorsCollection = actionDescriptorProvider.ActionDescriptors;
|
||||||
|
return actionDescriptorsCollection.Items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> GroupTemplatesByGroupId(IReadOnlyList<ActionDescriptor> actions)
|
||||||
|
{
|
||||||
|
var routeTemplatesByGroup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var action in actions.Where(a => a.RouteTemplate != null))
|
||||||
|
{
|
||||||
|
var constraint = action.RouteConstraints
|
||||||
|
.Where(c => c.RouteKey == AttributeRouting.RouteGroupKey)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (constraint == null ||
|
||||||
|
constraint.KeyHandling != RouteKeyHandling.RequireKey ||
|
||||||
|
constraint.RouteValue == null)
|
||||||
|
{
|
||||||
|
// This is unlikely to happen by default, but could happen through extensibility. Just ignore it.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var routeGroup = constraint.RouteValue;
|
||||||
|
if (!routeTemplatesByGroup.ContainsKey(routeGroup))
|
||||||
|
{
|
||||||
|
routeTemplatesByGroup.Add(routeGroup, action.RouteTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return routeTemplatesByGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// 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.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for attributes which can supply a route template for attribute routing.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRouteTemplateProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The route template. May be null.
|
||||||
|
/// </summary>
|
||||||
|
string Template { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNet.Mvc;
|
using Microsoft.AspNet.Mvc;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Builder
|
namespace Microsoft.AspNet.Builder
|
||||||
|
|
@ -29,6 +30,10 @@ namespace Microsoft.AspNet.Builder
|
||||||
ServiceProvider = app.ApplicationServices
|
ServiceProvider = app.ApplicationServices
|
||||||
};
|
};
|
||||||
|
|
||||||
|
routes.Routes.Add(AttributeRouting.CreateAttributeMegaRoute(
|
||||||
|
routes.DefaultHandler,
|
||||||
|
app.ApplicationServices));
|
||||||
|
|
||||||
configureRoutes(routes);
|
configureRoutes(routes);
|
||||||
|
|
||||||
return app.UseRouter(routes.Build());
|
return app.UseRouter(routes.Build());
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,8 @@ namespace Microsoft.AspNet.Mvc.Test
|
||||||
controllerAssemblyProvider.Object,
|
controllerAssemblyProvider.Object,
|
||||||
actionDiscoveryConventions,
|
actionDiscoveryConventions,
|
||||||
null,
|
null,
|
||||||
new MockMvcOptionsAccessor());
|
new MockMvcOptionsAccessor(),
|
||||||
|
Mock.Of<IInlineConstraintResolver>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpContext GetHttpContext(string httpMethod)
|
private static HttpContext GetHttpContext(string httpMethod)
|
||||||
|
|
@ -327,4 +328,4 @@ namespace Microsoft.AspNet.Mvc.Test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.Design;
|
using System.ComponentModel.Design;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
|
|
@ -187,7 +188,8 @@ namespace Microsoft.AspNet.Mvc.Test
|
||||||
controllerAssemblyProvider.Object,
|
controllerAssemblyProvider.Object,
|
||||||
actionDiscoveryConventions,
|
actionDiscoveryConventions,
|
||||||
null,
|
null,
|
||||||
new MockMvcOptionsAccessor());
|
new MockMvcOptionsAccessor(),
|
||||||
|
Mock.Of<IInlineConstraintResolver>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpContext GetHttpContext(string httpMethod)
|
private static HttpContext GetHttpContext(string httpMethod)
|
||||||
|
|
@ -307,4 +309,4 @@ namespace Microsoft.AspNet.Mvc.Test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@
|
||||||
<Compile Include="Rendering\ViewContextTests.cs" />
|
<Compile Include="Rendering\ViewContextTests.cs" />
|
||||||
<Compile Include="Rendering\ViewDataOfTTest.cs" />
|
<Compile Include="Rendering\ViewDataOfTTest.cs" />
|
||||||
<Compile Include="KnownRouteValueConstraintTests.cs" />
|
<Compile Include="KnownRouteValueConstraintTests.cs" />
|
||||||
|
<Compile Include="Routing\AttributeRoutePrecedenceTests.cs" />
|
||||||
|
<Compile Include="Routing\AttributeRouteTemplateTests.cs" />
|
||||||
<Compile Include="TestController.cs" />
|
<Compile Include="TestController.cs" />
|
||||||
<Compile Include="TypeHelperTest.cs" />
|
<Compile Include="TypeHelperTest.cs" />
|
||||||
<Compile Include="StaticActionDiscoveryConventions.cs" />
|
<Compile Include="StaticActionDiscoveryConventions.cs" />
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -297,36 +298,38 @@ namespace Microsoft.AspNet.Mvc.Test
|
||||||
TypeInfo controllerTypeInfo,
|
TypeInfo controllerTypeInfo,
|
||||||
IEnumerable<IFilter> filters = null)
|
IEnumerable<IFilter> filters = null)
|
||||||
{
|
{
|
||||||
|
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfo);
|
||||||
|
|
||||||
var assemblyProvider = new Mock<IControllerAssemblyProvider>();
|
var assemblyProvider = new Mock<IControllerAssemblyProvider>();
|
||||||
assemblyProvider
|
assemblyProvider
|
||||||
.SetupGet(ap => ap.CandidateAssemblies)
|
.SetupGet(ap => ap.CandidateAssemblies)
|
||||||
.Returns(new Assembly[] { controllerTypeInfo.Assembly });
|
.Returns(new Assembly[] { controllerTypeInfo.Assembly });
|
||||||
|
|
||||||
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfo);
|
|
||||||
|
|
||||||
var provider = new ReflectedActionDescriptorProvider(
|
var provider = new ReflectedActionDescriptorProvider(
|
||||||
assemblyProvider.Object,
|
assemblyProvider.Object,
|
||||||
conventions,
|
conventions,
|
||||||
filters,
|
filters,
|
||||||
new MockMvcOptionsAccessor());
|
new MockMvcOptionsAccessor(),
|
||||||
|
Mock.Of<IInlineConstraintResolver>());
|
||||||
|
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<ActionDescriptor> GetDescriptors(params TypeInfo[] controllerTypeInfos)
|
private IEnumerable<ActionDescriptor> GetDescriptors(params TypeInfo[] controllerTypeInfos)
|
||||||
{
|
{
|
||||||
|
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfos);
|
||||||
|
|
||||||
var assemblyProvider = new Mock<IControllerAssemblyProvider>();
|
var assemblyProvider = new Mock<IControllerAssemblyProvider>();
|
||||||
assemblyProvider
|
assemblyProvider
|
||||||
.SetupGet(ap => ap.CandidateAssemblies)
|
.SetupGet(ap => ap.CandidateAssemblies)
|
||||||
.Returns(controllerTypeInfos.Select(cti => cti.Assembly).Distinct());
|
.Returns(controllerTypeInfos.Select(cti => cti.Assembly).Distinct());
|
||||||
|
|
||||||
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfos);
|
|
||||||
|
|
||||||
var provider = new ReflectedActionDescriptorProvider(
|
var provider = new ReflectedActionDescriptorProvider(
|
||||||
assemblyProvider.Object,
|
assemblyProvider.Object,
|
||||||
conventions,
|
conventions,
|
||||||
null,
|
null,
|
||||||
new MockMvcOptionsAccessor());
|
new MockMvcOptionsAccessor(),
|
||||||
|
null);
|
||||||
|
|
||||||
return provider.GetDescriptors();
|
return provider.GetDescriptors();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#if NET45
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
using Moq;
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
public class AttributeRoutePrecedenceTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Employees/{id}", "Employees/{employeeId}")]
|
||||||
|
[InlineData("abc", "def")]
|
||||||
|
[InlineData("{x:alpha}", "{x:int}")]
|
||||||
|
public void Compute_IsEqual(string xTemplate, string yTemplate)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var xPrededence = Compute(xTemplate);
|
||||||
|
var yPrededence = Compute(yTemplate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(xPrededence, yPrededence);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("abc", "a{x}")]
|
||||||
|
[InlineData("abc", "{x}c")]
|
||||||
|
[InlineData("abc", "{x:int}")]
|
||||||
|
[InlineData("abc", "{x}")]
|
||||||
|
[InlineData("abc", "{*x}")]
|
||||||
|
[InlineData("{x:int}", "{x}")]
|
||||||
|
[InlineData("{x:int}", "{*x}")]
|
||||||
|
[InlineData("a{x}", "{x}")]
|
||||||
|
[InlineData("{x}c", "{x}")]
|
||||||
|
[InlineData("a{x}", "{*x}")]
|
||||||
|
[InlineData("{x}c", "{*x}")]
|
||||||
|
[InlineData("{x}", "{*x}")]
|
||||||
|
[InlineData("{*x:maxlength(10)}", "{*x}")]
|
||||||
|
[InlineData("abc/def", "abc/{x:int}")]
|
||||||
|
[InlineData("abc/def", "abc/{x}")]
|
||||||
|
[InlineData("abc/def", "abc/{*x}")]
|
||||||
|
[InlineData("abc/{x:int}", "abc/{x}")]
|
||||||
|
[InlineData("abc/{x:int}", "abc/{*x}")]
|
||||||
|
[InlineData("abc/{x}", "abc/{*x}")]
|
||||||
|
[InlineData("{x}/{y:int}", "{x}/{y}")]
|
||||||
|
public void Compute_IsLessThan(string xTemplate, string yTemplate)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var xPrededence = Compute(xTemplate);
|
||||||
|
var yPrededence = Compute(yTemplate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(xPrededence < yPrededence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal Compute(string template)
|
||||||
|
{
|
||||||
|
var options = new Mock<IOptionsAccessor<RouteOptions>>();
|
||||||
|
options.SetupGet(o => o.Options).Returns(new RouteOptions());
|
||||||
|
|
||||||
|
var constraintResolver = new DefaultInlineConstraintResolver(
|
||||||
|
Mock.Of<IServiceProvider>(),
|
||||||
|
options.Object);
|
||||||
|
|
||||||
|
var parsed = TemplateParser.Parse(template, constraintResolver);
|
||||||
|
return AttributeRoutePrecedence.Compute(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
// 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 Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
public class AttributeRouteTemplateTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, null, null)]
|
||||||
|
[InlineData("", null, "")]
|
||||||
|
[InlineData(null, "", "")]
|
||||||
|
[InlineData("/", null, "")]
|
||||||
|
[InlineData(null, "/", "")]
|
||||||
|
[InlineData("/", "", "")]
|
||||||
|
[InlineData("", "/", "")]
|
||||||
|
[InlineData("/", "/", "")]
|
||||||
|
[InlineData("/", "/", "")]
|
||||||
|
public void Combine_EmptyTemplates(string left, string right, string expected)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var combined = AttributeRouteTemplate.Combine(left, right);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("home", null, "home")]
|
||||||
|
[InlineData("home", "", "home")]
|
||||||
|
[InlineData("/home/", "/", "home")]
|
||||||
|
[InlineData(null, "GetEmployees", "GetEmployees")]
|
||||||
|
[InlineData("/", "GetEmployees", "GetEmployees")]
|
||||||
|
[InlineData("", "/GetEmployees/{id}/", "GetEmployees/{id}")]
|
||||||
|
public void Combine_OneTemplateHasValue(string left, string right, string expected)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var combined = AttributeRouteTemplate.Combine(left, right);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("home", "About", "home/About")]
|
||||||
|
[InlineData("home/", "/About", "home/About")]
|
||||||
|
[InlineData("/home/{action}", "{id}", "home/{action}/{id}")]
|
||||||
|
public void Combine_BothTemplatesHasValue(string left, string right, string expected)
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var combined = AttributeRouteTemplate.Combine(left, right);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, combined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,4 +43,4 @@
|
||||||
<Compile Include="TestApplicationEnvironment.cs" />
|
<Compile Include="TestApplicationEnvironment.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -134,6 +134,155 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.Equal(404, response.StatusCode);
|
Assert.Equal(404, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_IsReachable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/Store/Shop/Products");
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("/Store/Shop/Products", result.ExpectedUrls);
|
||||||
|
Assert.Equal("Store", result.Controller);
|
||||||
|
Assert.Equal("ListProducts", result.Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The url would be /Store/ListProducts with conventional routes
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_IsNotReachableWithTraditionalRoute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/Store/ListProducts");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(404, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's two actions at this URL - but attribute routes go in the route table
|
||||||
|
// first.
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_TriedBeforeConventionRouting()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/Home/About");
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("/Home/About", result.ExpectedUrls);
|
||||||
|
Assert.Equal("Store", result.Controller);
|
||||||
|
Assert.Equal("About", result.Action);
|
||||||
|
|
||||||
|
// A convention-routed action would have values for action and controller.
|
||||||
|
Assert.None(
|
||||||
|
result.RouteValues,
|
||||||
|
(kvp) => string.Equals(kvp.Key, "action", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
Assert.None(
|
||||||
|
result.RouteValues,
|
||||||
|
(kvp) => string.Equals(kvp.Key, "controller", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_ControllerLevelRoute_WithActionParameter_IsReachable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/Blog/Edit/5");
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("/Blog/Edit/5", result.ExpectedUrls);
|
||||||
|
Assert.Equal("Blog", result.Controller);
|
||||||
|
Assert.Equal("Edit", result.Action);
|
||||||
|
|
||||||
|
// This route is parameterized on {action}, but not controller.
|
||||||
|
Assert.Contains(
|
||||||
|
new KeyValuePair<string, object>("action", "Edit"),
|
||||||
|
result.RouteValues);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
new KeyValuePair<string, object>("postId", "5"),
|
||||||
|
result.RouteValues);
|
||||||
|
|
||||||
|
Assert.None(
|
||||||
|
result.RouteValues,
|
||||||
|
(kvp) => string.Equals(kvp.Key, "controller", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's no [HttpGet] on the action here.
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_ControllerLevelRoute_IsReachable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/api/Employee");
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("/api/Employee", result.ExpectedUrls);
|
||||||
|
Assert.Equal("Employee", result.Controller);
|
||||||
|
Assert.Equal("List", result.Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// There's an [HttpGet] with its own template on the action here.
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_ControllerLevelRoute_CombinedWithActionRoute_IsReachable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/api/Employee/5/Boss");
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("/api/Employee/5/Boss", result.ExpectedUrls);
|
||||||
|
Assert.Equal("Employee", result.Controller);
|
||||||
|
Assert.Equal("GetBoss", result.Action);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
new KeyValuePair<string, object>("id", "5"),
|
||||||
|
result.RouteValues);
|
||||||
|
}
|
||||||
|
|
||||||
// See TestResponseGenerator in RoutingWebSite for the code that generates this data.
|
// See TestResponseGenerator in RoutingWebSite for the code that generates this data.
|
||||||
private class RoutingResult
|
private class RoutingResult
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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 Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
|
namespace RoutingWebSite
|
||||||
|
{
|
||||||
|
// This controller contains actions mapped with a single controller-level route.
|
||||||
|
[Route("Blog/{action=ShowPosts}/{postId?}")]
|
||||||
|
public class BlogController
|
||||||
|
{
|
||||||
|
private readonly TestResponseGenerator _generator;
|
||||||
|
|
||||||
|
public BlogController(TestResponseGenerator generator)
|
||||||
|
{
|
||||||
|
_generator = generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult ShowPosts()
|
||||||
|
{
|
||||||
|
return _generator.Generate("/Blog", "/Blog/ShowPosts");
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult Edit(int postId)
|
||||||
|
{
|
||||||
|
return _generator.Generate("/Blog/Edit/" + postId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
using Microsoft.AspNet.Mvc;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace RoutingWebSite
|
||||||
|
{
|
||||||
|
// This controller combines routes on the controller with routes on actions in a REST + navigation property
|
||||||
|
// style.
|
||||||
|
[Route("api/Employee")]
|
||||||
|
public class EmployeeController : Controller
|
||||||
|
{
|
||||||
|
private readonly TestResponseGenerator _generator;
|
||||||
|
|
||||||
|
public EmployeeController(TestResponseGenerator generator)
|
||||||
|
{
|
||||||
|
_generator = generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult List()
|
||||||
|
{
|
||||||
|
return _generator.Generate("/api/Employee");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public IActionResult Get(int id)
|
||||||
|
{
|
||||||
|
return _generator.Generate("/api/Employee/" + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/Boss")]
|
||||||
|
public IActionResult GetBoss(int id)
|
||||||
|
{
|
||||||
|
return _generator.Generate("/api/Employee/" + id + "/Boss");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ namespace RoutingWebSite
|
||||||
|
|
||||||
public IActionResult About()
|
public IActionResult About()
|
||||||
{
|
{
|
||||||
|
// There are no urls that reach this action - it's hidden by an attribute route.
|
||||||
return _generator.Generate();
|
return _generator.Generate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// 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 Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
|
namespace RoutingWebSite
|
||||||
|
{
|
||||||
|
// This controller contains only actions with individual attribute routes.
|
||||||
|
public class StoreController : Controller
|
||||||
|
{
|
||||||
|
private readonly TestResponseGenerator _generator;
|
||||||
|
|
||||||
|
public StoreController(TestResponseGenerator generator)
|
||||||
|
{
|
||||||
|
_generator = generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("Store/Shop/Products")]
|
||||||
|
public IActionResult ListProducts()
|
||||||
|
{
|
||||||
|
return _generator.Generate("/Store/Shop/Products");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intentionally designed to conflict with HomeController#About.
|
||||||
|
[HttpGet("Home/About")]
|
||||||
|
public IActionResult About()
|
||||||
|
{
|
||||||
|
return _generator.Generate("/Home/About");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Areas\Travel\FlightController.cs" />
|
<Compile Include="Areas\Travel\FlightController.cs" />
|
||||||
|
<Compile Include="Controllers\BlogController.cs" />
|
||||||
|
<Compile Include="Controllers\EmployeeController.cs" />
|
||||||
<Compile Include="Controllers\HomeController.cs" />
|
<Compile Include="Controllers\HomeController.cs" />
|
||||||
|
<Compile Include="Controllers\StoreController.cs" />
|
||||||
<Compile Include="Startup.cs" />
|
<Compile Include="Startup.cs" />
|
||||||
<Compile Include="TestResponseGenerator.cs" />
|
<Compile Include="TestResponseGenerator.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue