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.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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
</ItemGroup>
|
||||
<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);
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
// There are no urls that reach this action - it's hidden by an attribute route.
|
||||
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>
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue