diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/AnchorTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/AnchorTagHelper.cs
new file mode 100644
index 0000000000..55cbd258ce
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/AnchorTagHelper.cs
@@ -0,0 +1,156 @@
+// 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.Mvc.Rendering;
+using Microsoft.AspNet.Razor.Runtime.TagHelpers;
+
+namespace Microsoft.AspNet.Mvc.TagHelpers
+{
+ ///
+ /// implementation targeting <a> elements.
+ ///
+ [TagName("a")]
+ public class AnchorTagHelper : TagHelper
+ {
+ private const string RouteAttributePrefix = "route-";
+ private const string Href = "href";
+
+ [Activate]
+ private IHtmlGenerator Generator { get; set; }
+
+ ///
+ /// The name of the action method.
+ ///
+ /// Must be null if is non-null.
+ public string Action { get; set; }
+
+ ///
+ /// The name of the controller.
+ ///
+ /// Must be null if is non-null.
+ public string Controller { get; set; }
+
+ ///
+ /// The protocol for the URL, such as "http" or "https".
+ ///
+ public string Protocol { get; set; }
+
+ ///
+ /// The host name.
+ ///
+ public string Host { get; set; }
+
+ ///
+ /// The URL fragment name.
+ ///
+ public string Fragment { get; set; }
+
+ ///
+ /// Name of the route.
+ ///
+ ///
+ /// Must be null if or is non-null.
+ ///
+ public string Route { get; set; }
+
+ ///
+ /// Does nothing if user provides an "href" attribute. Throws an
+ /// if "href" attribute is provided and ,
+ /// , or are non-null.
+ public override void Process(TagHelperContext context, TagHelperOutput output)
+ {
+ var routePrefixedAttributes = output.FindPrefixedAttributes(RouteAttributePrefix);
+
+ // If there's an "href" on the tag it means it's being used as a normal anchor.
+ if (output.Attributes.ContainsKey(Href))
+ {
+ if (Action != null ||
+ Controller != null ||
+ Route != null ||
+ Protocol != null ||
+ Host != null ||
+ Fragment != null ||
+ routePrefixedAttributes.Any())
+ {
+ // User specified an href and one of the bound attributes; can't determine the href attribute.
+ throw new InvalidOperationException(
+ Resources.FormatAnchorTagHelper_CannotOverrideSpecifiedHref(
+ "",
+ nameof(Action).ToLowerInvariant(),
+ nameof(Controller).ToLowerInvariant(),
+ nameof(Route).ToLowerInvariant(),
+ nameof(Protocol).ToLowerInvariant(),
+ nameof(Host).ToLowerInvariant(),
+ nameof(Fragment).ToLowerInvariant(),
+ RouteAttributePrefix,
+ Href));
+ }
+ }
+ else
+ {
+ TagBuilder tagBuilder;
+ var routeValues = GetRouteValues(output, routePrefixedAttributes);
+
+ if (Route == null)
+ {
+ tagBuilder = Generator.GenerateActionLink(linkText: string.Empty,
+ actionName: Action,
+ controllerName: Controller,
+ protocol: Protocol,
+ hostname: Host,
+ fragment: Fragment,
+ routeValues: routeValues,
+ htmlAttributes: null);
+ }
+ else if (Action != null || Controller != null)
+ {
+ // Route and Action or Controller were specified. Can't determine the href attribute.
+ throw new InvalidOperationException(
+ Resources.FormatAnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified(
+ "",
+ nameof(Route).ToLowerInvariant(),
+ nameof(Action).ToLowerInvariant(),
+ nameof(Controller).ToLowerInvariant(),
+ Href));
+ }
+ else
+ {
+ tagBuilder = Generator.GenerateRouteLink(linkText: string.Empty,
+ routeName: Route,
+ protocol: Protocol,
+ hostName: Host,
+ fragment: Fragment,
+ routeValues: routeValues,
+ htmlAttributes: null);
+ }
+
+ if (tagBuilder != null)
+ {
+ output.MergeAttributes(tagBuilder);
+ }
+ }
+ }
+
+ // TODO: We will not need this method once https://github.com/aspnet/Razor/issues/89 is completed.
+ private static Dictionary GetRouteValues(
+ TagHelperOutput output, IEnumerable> routePrefixedAttributes)
+ {
+ Dictionary routeValues = null;
+ if (routePrefixedAttributes.Any())
+ {
+ // Prefixed values should be treated as bound attributes, remove them from the output.
+ output.RemoveRange(routePrefixedAttributes);
+
+ // Generator.GenerateForm does not accept a Dictionary for route values.
+ routeValues = routePrefixedAttributes.ToDictionary(
+ attribute => attribute.Key.Substring(RouteAttributePrefix.Length),
+ attribute => (object)attribute.Value);
+ }
+
+ return routeValues;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/Resources.Designer.cs
index 3f07276690..f326434f03 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/Resources.Designer.cs
@@ -10,6 +10,38 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNet.Mvc.TagHelpers.Resources", typeof(Resources).GetTypeInfo().Assembly);
+ ///
+ /// Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.
+ ///
+ internal static string AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified
+ {
+ get { return GetString("AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified"); }
+ }
+
+ ///
+ /// Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.
+ ///
+ internal static string FormatAnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified(object p0, object p1, object p2, object p3, object p4)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotDetermineHrefRouteActionOrControllerSpecified"), p0, p1, p2, p3, p4);
+ }
+
+ ///
+ /// Cannot determine an {8} for {0}. An {0} with a specified {8} must not have attributes starting with {7} or an {1}, {2}, {3}, {4}, {5} or {6} attribute.
+ ///
+ internal static string AnchorTagHelper_CannotOverrideSpecifiedHref
+ {
+ get { return GetString("AnchorTagHelper_CannotOverrideSpecifiedHref"); }
+ }
+
+ ///
+ /// Cannot determine an {8} for {0}. An {0} with a specified {8} must not have attributes starting with {7} or an {1}, {2}, {3}, {4}, {5} or {6} attribute.
+ ///
+ internal static string FormatAnchorTagHelper_CannotOverrideSpecifiedHref(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7, object p8)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("AnchorTagHelper_CannotOverrideSpecifiedHref"), p0, p1, p2, p3, p4, p5, p6, p7, p8);
+ }
+
///
/// Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.
///
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Resources.resx b/src/Microsoft.AspNet.Mvc.TagHelpers/Resources.resx
index 722a84ffe9..db5e382720 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Resources.resx
@@ -117,6 +117,12 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Cannot determine an {4} for {0}. An {0} with a specified {1} must not have an {2} or {3} attribute.
+
+
+ Cannot determine an {8} for {0}. An {0} with a specified {8} must not have attributes starting with {7} or an {1}, {2}, {3}, {4}, {5} or {6} attribute.
+
Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.
diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/AnchorTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/AnchorTagHelperTest.cs
new file mode 100644
index 0000000000..2e0fbc0367
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/AnchorTagHelperTest.cs
@@ -0,0 +1,235 @@
+// 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.Reflection;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Mvc.ModelBinding;
+using Microsoft.AspNet.Mvc.Razor;
+using Microsoft.AspNet.Mvc.Rendering;
+using Microsoft.AspNet.Razor.Runtime.TagHelpers;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.TagHelpers
+{
+ public class AnchorTagHelperTest
+ {
+ [Fact]
+ public async Task ProcessAsync_GeneratesExpectedOutput()
+ {
+ // Arrange
+ var metadataProvider = new DataAnnotationsModelMetadataProvider();
+ var anchorTagHelper = new AnchorTagHelper
+ {
+ Action = "index",
+ Controller = "home",
+ Fragment = "hello=world",
+ Host = "contoso.com",
+ Protocol = "http"
+ };
+
+ var tagHelperContext = new TagHelperContext(
+ allAttributes: new Dictionary
+ {
+ { "id", "myanchor" },
+ { "route-foo", "bar" },
+ { "action", "index" },
+ { "controller", "home" },
+ { "fragment", "hello=world" },
+ { "host", "contoso.com" },
+ { "protocol", "http" }
+ });
+ var output = new TagHelperOutput(
+ "a",
+ attributes: new Dictionary
+ {
+ { "id", "myanchor" },
+ { "route-foo", "bar" },
+ },
+ content: "Something");
+
+ var urlHelper = new Mock();
+ urlHelper
+ .Setup(mock => mock.Action(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny