Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Adding attribute routing #720

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Mvc.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
10 changes: 9 additions & 1 deletion samples/MvcSample.Web/SimpleRest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@

namespace MvcSample.Web
{
[Route("api/REST")]
public class SimpleRest : Controller
{
public string Get()
[HttpGet]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean at this point (prior to removing Get as a constraint)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It means that the controller-level [Route(...)] is used without appending anything - in this case it's api/REST. I wanted the sample include this since it's the obvious way to code a rest convention

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per discussion will update this to use a different action name.

public string ThisIsAGetMethod()
{
return "Get method";
}

[HttpGet("OtherThing")]
public string GetOtherThing()
{
return "Get other thing";
}
}
}
7 changes: 6 additions & 1 deletion src/Microsoft.AspNet.Mvc.Core/ActionDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -12,6 +12,11 @@ public class ActionDescriptor

public List<RouteDataActionConstraint> RouteConstraints { get; set; }

/// <summary>
/// The <see cref="RouteInfo"/>. May be null if the action has no attribute routes.
/// </summary>
public RouteInfo RouteInfo { get; set; }

public List<HttpMethodConstraint> MethodConstraints { get; set; }

public List<IActionConstraint> DynamicConstraints { get; set; }
Expand Down
26 changes: 25 additions & 1 deletion src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During initial implementations of attribute routing in Web API, we had the templates on the verb attributes, which made the route constrained to that particular verb...when CORS feature came into picture this was problematic as now the user had to add HttpOptions verb explicitly on all the actions where he wanted to support CORS...so just FYI as a scenario to watch out for...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might also want to consider that we are adding two constraints with one attribute, will I be able to specify just [HttpGet] to indicate that only get is supported and that that won't plug in this as an attribute route?

If that's the case, when you have an attribute route with a route prefix on the controller, what's the behaviour when you just say [HttpGet], will it mark it as an attribute route? (maybe you want to hit that action through convention routing) or will it not, (in which case you will need to do something like [HttpGet("")] which seems unnatural.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good feedback. We already have a conclusion from the previous design meeting. Here's the truth table.

[Route("api/Foo")] + [HttpGet("bar")] = api/Foo/bar
[Route("api/Foo")] + [HttpGet] = api/Foo
[Route("api/Foo")] + (nothing on the action) = api/Foo
(nothing on the controller) + [HttpGet("bar")] = bar
(nothing on the controller) + [HttpGet] = (not attribute routed)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also @javiercn there is no 'route prefix'. The [Route] attribute only goes on classes/controllers, and defines the WHOLE controller as attribute routed. If there is an attribute on an individual method/action, then its template is appended.

}
}
14 changes: 11 additions & 3 deletions src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down Expand Up @@ -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" />
Expand Down Expand Up @@ -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" />
Expand Down Expand Up @@ -203,10 +203,18 @@
<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="Routing\RouteInfo.cs" />
<Compile Include="TemplateInfo.cs" />
<Compile Include="UrlHelper.cs" />
<Compile Include="UrlHelperExtensions.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
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
Expand All @@ -20,16 +23,19 @@ public class ReflectedActionDescriptorProvider : IActionDescriptorProvider
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
Expand Down Expand Up @@ -106,6 +112,8 @@ private bool HasConstraint(List<RouteDataActionConstraint> constraints, string r

public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
{
var routeGroupsByTemplate = GetRouteGroupsByTemplate(model);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this closer to where it's first used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👎 this is part of three statements that initialize state for the loop, and it doesn't belong inside the loop.

As discussed, if you think this code can be substantially simplified, then go ahead when you add features to it.


var actions = new List<ReflectedActionDescriptor>();

var removalConstraints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Expand Down Expand Up @@ -188,6 +196,50 @@ public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
}
}

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();

var template = TemplateParser.Parse(templateText, _constraintResolver);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the template is going to get parsed more than once in this case, and there is some duplicity. Shouldn't we just use the created attribute route in this case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to leave this as an exercise for @javiercn. The AttributeRoute class is going to see some significant changes and will end up using the parsed-template. The reason that RouteInfo.Template exists is so that can happen.


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.RouteInfo = new RouteInfo()
{
Precedence = AttributeRoutePrecedence.Compute(template),
Template = template,
TemplateText = templateText,
RouteGroup = routeGroup,
};
}
}

actionDescriptor.FilterDescriptors =
action.Filters.Select(f => new FilterDescriptor(f, FilterScope.Action))
.Concat(controller.Filters.Select(f => new FilterDescriptor(f, FilterScope.Controller)))
Expand All @@ -214,5 +266,25 @@ public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.Routing;

namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
Expand All @@ -19,6 +20,12 @@ public ReflectedActionModel([NotNull] MethodInfo actionMethod)

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>();
}
Expand All @@ -36,5 +43,7 @@ public ReflectedActionModel([NotNull] MethodInfo actionMethod)
public bool IsActionNameMatchRequired { get; set; }

public List<ReflectedParameterModel> Parameters { get; private set; }

public string RouteTemplate { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.Routing;

namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
{
Expand All @@ -23,6 +24,12 @@ public ReflectedControllerModel([NotNull] TypeInfo controllerType)
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;
Expand All @@ -39,5 +46,7 @@ public ReflectedControllerModel([NotNull] TypeInfo controllerType)
public List<IFilter> Filters { get; private set; }

public List<RouteConstraintAttribute> RouteConstraints { get; private set; }

public string RouteTemplate { get; set; }
}
}
27 changes: 27 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/RouteAttribute.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Loading