Adding attribute routing

This commit is contained in:
Ryan Nowak 2014-06-10 14:16:13 -07:00
parent 85cf199ef1
commit e396f1b451
29 changed files with 892 additions and 18 deletions

11
Mvc.sln
View File

@ -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.Build.0 = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -261,5 +271,6 @@ Global
{07C0E921-FCBB-458C-AC11-3D01CE68B16B} = {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}
{5C34562F-2861-4CD6-AF02-462A9A8D76EE} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
EndGlobal

View File

@ -2,11 +2,19 @@ using Microsoft.AspNet.Mvc;
namespace MvcSample.Web
{
[Route("api/REST")]
public class SimpleRest : Controller
{
public string Get()
[HttpGet]
public string ThisIsAGetMethod()
{
return "Get method";
}
[HttpGet("OtherThing")]
public string GetOtherThing()
{
return "Get other thing";
}
}
}

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
@ -12,6 +12,11 @@ namespace Microsoft.AspNet.Mvc
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<IActionConstraint> DynamicConstraints { get; set; }

View File

@ -3,17 +3,41 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Identifies an action that only supports the HTTP GET method.
/// </summary>
[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" };
/// <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
{
get { return _supportedMethods; }
}
/// <inheritdoc />
public string Template { get; private set; }
}
}

View File

@ -27,6 +27,8 @@
<Compile Include="ActionDescriptor.cs" />
<Compile Include="ActionDescriptorProviderContext.cs" />
<Compile Include="ActionDescriptorsCollection.cs" />
<Compile Include="ReflectedActionDescriptor.cs" />
<Compile Include="ReflectedActionDescriptorProvider.cs" />
<Compile Include="ReflectedModelBuilder\IReflectedApplicationModelConvention.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedActionModel.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedControllerModel.cs" />
@ -74,7 +76,6 @@
<Compile Include="DefaultActionSelector.cs" />
<Compile Include="DefaultControllerAssemblyProvider.cs" />
<Compile Include="DefaultControllerFactory.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedParameterModel.cs" />
<Compile Include="Extensions\IEnumerableExtensions.cs" />
<Compile Include="Filters\FilterItemOrderComparer.cs" />
<Compile Include="Filters\TypeFilterAttribute.cs" />
@ -152,11 +153,10 @@
<Compile Include="ParameterDescriptor.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs" />
<Compile Include="ReflectedActionDescriptor.cs" />
<Compile Include="ReflectedActionDescriptorProvider.cs" />
<Compile Include="ReflectedActionExecutor.cs" />
<Compile Include="ReflectedActionInvoker.cs" />
<Compile Include="ReflectedActionInvokerProvider.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedParameterModel.cs" />
<Compile Include="Rendering\DynamicViewData.cs" />
<Compile Include="Rendering\Expressions\CachedExpressionCompiler.cs" />
<Compile Include="Rendering\Expressions\ExpressionHelper.cs" />
@ -203,10 +203,17 @@
<Compile Include="Rendering\SelectListItem.cs" />
<Compile Include="Rendering\UnobtrusiveValidationAttributesGenerator.cs" />
<Compile Include="Rendering\ViewEngineResult.cs" />
<Compile Include="RouteAttribute.cs" />
<Compile Include="RouteConstraintAttribute.cs" />
<Compile Include="RouteDataActionConstraint.cs" />
<Compile Include="KnownRouteValueConstraint.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="UrlHelper.cs" />
<Compile Include="UrlHelperExtensions.cs" />

View File

@ -8,6 +8,9 @@ using System.Linq;
using System.Reflection;
#endif
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc
@ -20,16 +23,19 @@ namespace Microsoft.AspNet.Mvc
private readonly IActionDiscoveryConventions _conventions;
private readonly IEnumerable<IFilter> _globalFilters;
private readonly IEnumerable<IReflectedApplicationModelConvention> _modelConventions;
private readonly IInlineConstraintResolver _constraintResolver;
public ReflectedActionDescriptorProvider(IControllerAssemblyProvider controllerAssemblyProvider,
IActionDiscoveryConventions conventions,
IEnumerable<IFilter> globalFilters,
IOptionsAccessor<MvcOptions> optionsAccessor)
IOptionsAccessor<MvcOptions> optionsAccessor,
IInlineConstraintResolver constraintResolver)
{
_controllerAssemblyProvider = controllerAssemblyProvider;
_conventions = conventions;
_globalFilters = globalFilters ?? Enumerable.Empty<IFilter>();
_modelConventions = optionsAccessor.Options.ApplicationModelConventions;
_constraintResolver = constraintResolver;
}
public int Order
@ -106,6 +112,8 @@ namespace Microsoft.AspNet.Mvc
public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
{
var routeGroupsByTemplate = GetRouteGroupsByTemplate(model);
var actions = new List<ReflectedActionDescriptor>();
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 =
action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
.Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
@ -214,5 +261,25 @@ namespace Microsoft.AspNet.Mvc
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;
}
}
}

View File

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
@ -19,6 +20,12 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
Filters = Attributes.OfType<IFilter>().ToList();
var routeTemplateAttribute = Attributes.OfType<IRouteTemplateProvider>().FirstOrDefault();
if (routeTemplateAttribute != null)
{
RouteTemplate = routeTemplateAttribute.Template;
}
HttpMethods = new List<string>();
Parameters = new List<ReflectedParameterModel>();
}
@ -36,5 +43,7 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
public bool IsActionNameMatchRequired { get; set; }
public List<ReflectedParameterModel> Parameters { get; private set; }
public string RouteTemplate { get; set; }
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
@ -23,6 +24,12 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
Filters = Attributes.OfType<IFilter>().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)
? controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length)
: controllerType.Name;
@ -39,5 +46,7 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
public List<IFilter> Filters { get; private set; }
public List<RouteConstraintAttribute> RouteConstraints { get; private set; }
public string RouteTemplate { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -3,6 +3,7 @@
using System;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Builder
@ -29,6 +30,10 @@ namespace Microsoft.AspNet.Builder
ServiceProvider = app.ApplicationServices
};
routes.Routes.Add(AttributeRouting.CreateAttributeMegaRoute(
routes.DefaultHandler,
app.ApplicationServices));
configureRoutes(routes);
return app.UseRouter(routes.Build());

View File

@ -220,7 +220,8 @@ namespace Microsoft.AspNet.Mvc.Test
controllerAssemblyProvider.Object,
actionDiscoveryConventions,
null,
new MockMvcOptionsAccessor());
new MockMvcOptionsAccessor(),
Mock.Of<IInlineConstraintResolver>());
}
private static HttpContext GetHttpContext(string httpMethod)
@ -327,4 +328,4 @@ namespace Microsoft.AspNet.Mvc.Test
}
}
#endif
#endif

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Routing;
@ -187,7 +188,8 @@ namespace Microsoft.AspNet.Mvc.Test
controllerAssemblyProvider.Object,
actionDiscoveryConventions,
null,
new MockMvcOptionsAccessor());
new MockMvcOptionsAccessor(),
Mock.Of<IInlineConstraintResolver>());
}
private static HttpContext GetHttpContext(string httpMethod)
@ -307,4 +309,4 @@ namespace Microsoft.AspNet.Mvc.Test
}
}
#endif
#endif

View File

@ -67,6 +67,8 @@
<Compile Include="Rendering\ViewContextTests.cs" />
<Compile Include="Rendering\ViewDataOfTTest.cs" />
<Compile Include="KnownRouteValueConstraintTests.cs" />
<Compile Include="Routing\AttributeRoutePrecedenceTests.cs" />
<Compile Include="Routing\AttributeRouteTemplateTests.cs" />
<Compile Include="TestController.cs" />
<Compile Include="TypeHelperTest.cs" />
<Compile Include="StaticActionDiscoveryConventions.cs" />

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Routing;
using Moq;
using Xunit;
@ -297,36 +298,38 @@ namespace Microsoft.AspNet.Mvc.Test
TypeInfo controllerTypeInfo,
IEnumerable<IFilter> filters = null)
{
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfo);
var assemblyProvider = new Mock<IControllerAssemblyProvider>();
assemblyProvider
.SetupGet(ap => ap.CandidateAssemblies)
.Returns(new Assembly[] { controllerTypeInfo.Assembly });
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfo);
var provider = new ReflectedActionDescriptorProvider(
assemblyProvider.Object,
conventions,
filters,
new MockMvcOptionsAccessor());
new MockMvcOptionsAccessor(),
Mock.Of<IInlineConstraintResolver>());
return provider;
}
private IEnumerable<ActionDescriptor> GetDescriptors(params TypeInfo[] controllerTypeInfos)
{
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfos);
var assemblyProvider = new Mock<IControllerAssemblyProvider>();
assemblyProvider
.SetupGet(ap => ap.CandidateAssemblies)
.Returns(controllerTypeInfos.Select(cti => cti.Assembly).Distinct());
var conventions = new StaticActionDiscoveryConventions(controllerTypeInfos);
var provider = new ReflectedActionDescriptorProvider(
assemblyProvider.Object,
conventions,
null,
new MockMvcOptionsAccessor());
new MockMvcOptionsAccessor(),
null);
return provider.GetDescriptors();
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -43,4 +43,4 @@
<Compile Include="TestApplicationEnvironment.cs" />
</ItemGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
</Project>

View File

@ -134,6 +134,155 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
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.
private class RoutingResult
{

View File

@ -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);
}
}
}

View File

@ -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");
}
}
}

View File

@ -22,6 +22,7 @@ namespace RoutingWebSite
public IActionResult About()
{
// There are no urls that reach this action - it's hidden by an attribute route.
return _generator.Generate();
}
}

View File

@ -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");
}
}
}

View File

@ -29,7 +29,10 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Areas\Travel\FlightController.cs" />
<Compile Include="Controllers\BlogController.cs" />
<Compile Include="Controllers\EmployeeController.cs" />
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Controllers\StoreController.cs" />
<Compile Include="Startup.cs" />
<Compile Include="TestResponseGenerator.cs" />
</ItemGroup>