diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOutputExtensions.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOutputExtensions.cs
new file mode 100644
index 0000000000..9b6ef20c2d
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/TagHelperOutputExtensions.cs
@@ -0,0 +1,118 @@
+// 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
+{
+ ///
+ /// Utility related extensions for .
+ ///
+ public static class TagHelperOutputExtensions
+ {
+ ///
+ /// Copies a user-provided attribute from 's
+ /// to 's
+ /// .
+ ///
+ /// The this method extends.
+ /// The name of the bound attribute.
+ /// The .
+ /// Only copies the attribute if 's
+ /// does not contain an attribute with the given
+ ///
+ public static void CopyHtmlAttribute(this TagHelperOutput tagHelperOutput,
+ string attributeName,
+ TagHelperContext context)
+ {
+ // We look for the original attribute so we can restore the exact attribute name the user typed.
+ var entry = context.AllAttributes.First(attribute =>
+ attribute.Key.Equals(attributeName, StringComparison.OrdinalIgnoreCase));
+
+ if (!tagHelperOutput.Attributes.ContainsKey(entry.Key))
+ {
+ tagHelperOutput.Attributes.Add(entry.Key, entry.Value.ToString());
+ }
+ }
+
+ ///
+ /// Returns all attributes from 's
+ /// that have the given .
+ ///
+ /// The this method extends.
+ /// A prefix to look for.
+ /// s with
+ /// starting with the given .
+ public static IEnumerable> FindPrefixedAttributes(
+ this TagHelperOutput tagHelperOutput, string prefix)
+ {
+ // TODO: We will not need this method once https://github.com/aspnet/Razor/issues/89 is completed.
+
+ // We're only interested in HTML attributes that have the desired prefix.
+ var prefixedAttributes = tagHelperOutput.Attributes
+ .Where(attribute => attribute.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+
+ return prefixedAttributes;
+ }
+
+ ///
+ /// Merges the given into the .
+ ///
+ /// The this method extends.
+ /// The to merge.
+ /// 's has the given
+ /// s appended to it. This is to ensure
+ /// multiple s running on the same HTML tag don't overwrite each other; therefore,
+ /// this method may not be appropriate for all scenarios.
+ public static void Merge(this TagHelperOutput tagHelperOutput, TagBuilder tagBuilder)
+ {
+ tagHelperOutput.TagName = tagBuilder.TagName;
+ tagHelperOutput.Content += tagBuilder.InnerHtml;
+
+ MergeAttributes(tagHelperOutput, tagBuilder);
+ }
+
+ ///
+ /// Merges the given 's into the
+ /// .
+ ///
+ /// The this method extends.
+ /// The to merge attributes from.
+ /// Existing on the given
+ /// are not overriden; "class" attributes are merged with spaces.
+ public static void MergeAttributes(this TagHelperOutput tagHelperOutput, TagBuilder tagBuilder)
+ {
+ foreach (var attribute in tagBuilder.Attributes)
+ {
+ if (!tagHelperOutput.Attributes.ContainsKey(attribute.Key))
+ {
+ tagHelperOutput.Attributes.Add(attribute.Key, attribute.Value);
+ }
+ else if (attribute.Key.Equals("class", StringComparison.Ordinal))
+ {
+ tagHelperOutput.Attributes["class"] += " " + attribute.Value;
+ }
+ }
+ }
+
+ ///
+ /// Removes the given from 's
+ /// .
+ ///
+ /// The this method extends.
+ /// Attributes to remove.
+ public static void RemoveRange(
+ this TagHelperOutput tagHelperOutput, IEnumerable> attributes)
+ {
+ foreach (var attribute in attributes)
+ {
+ tagHelperOutput.Attributes.Remove(attribute);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs
new file mode 100644
index 0000000000..19378c4585
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/TagHelperOutputExtensionsTest.cs
@@ -0,0 +1,283 @@
+// 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;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.TagHelpers
+{
+ public class TagHelperOutputExtensionsTest
+ {
+ [Theory]
+ [InlineData("hello", "world")]
+ [InlineData("HeLlO", "wOrLd")]
+ public void CopyHtmlAttribute_CopiesOriginalAttributes(string attributeName, string attributeValue)
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary(),
+ content: string.Empty);
+ var tagHelperContext = new TagHelperContext(
+ new Dictionary(StringComparer.Ordinal)
+ {
+ { attributeName, attributeValue }
+ });
+ var expectedAttribute = new KeyValuePair(attributeName, attributeValue);
+
+ // Act
+ tagHelperOutput.CopyHtmlAttribute("hello", tagHelperContext);
+
+ // Assert
+ var attribute = Assert.Single(tagHelperOutput.Attributes);
+ Assert.Equal(expectedAttribute, attribute);
+ }
+
+ [Fact]
+ public void CopyHtmlAttribute_DoesNotOverrideAttributes()
+ {
+ // Arrange
+ var attributeName = "hello";
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary()
+ {
+ { attributeName, "world2" }
+ },
+ content: string.Empty);
+ var expectedAttribute = new KeyValuePair(attributeName, "world2");
+ var tagHelperContext = new TagHelperContext(
+ new Dictionary(StringComparer.Ordinal)
+ {
+ { attributeName, "world" }
+ });
+
+ // Act
+ tagHelperOutput.CopyHtmlAttribute(attributeName, tagHelperContext);
+
+ // Assert
+ var attribute = Assert.Single(tagHelperOutput.Attributes);
+ Assert.Equal(expectedAttribute, attribute);
+ }
+
+ [Fact]
+ public void RemoveRange_RemovesProvidedAttributes()
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary()
+ {
+ { "route-Hello", "World" },
+ { "Route-I", "Am" }
+ },
+ content: string.Empty);
+ var expectedAttribute = new KeyValuePair("type", "btn");
+ tagHelperOutput.Attributes.Add(expectedAttribute);
+ var attributes = tagHelperOutput.FindPrefixedAttributes("route-");
+
+ // Act
+ tagHelperOutput.RemoveRange(attributes);
+
+ // Assert
+ var attribute = Assert.Single(tagHelperOutput.Attributes);
+ Assert.Equal(expectedAttribute, attribute);
+ }
+
+ [Fact]
+ public void FindPrefixedAttributes_ReturnsEmpty_AttributeListIfNoAttributesPrefixed()
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary()
+ {
+ { "routeHello", "World" },
+ { "Routee-I", "Am" }
+ },
+ content: string.Empty);
+
+ // Act
+ var attributes = tagHelperOutput.FindPrefixedAttributes("route-");
+
+ // Assert
+ Assert.Empty(attributes);
+ var attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("routeHello"));
+ Assert.Equal(attribute.Value, "World");
+ attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("Routee-I"));
+ Assert.Equal(attribute.Value, "Am");
+ }
+
+ [Fact]
+ public void MergeAttributes_DoesNotReplace_TagHelperOutputAttributeValues()
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary(),
+ content: string.Empty);
+ var expectedAttribute = new KeyValuePair("type", "btn");
+ tagHelperOutput.Attributes.Add(expectedAttribute);
+
+ var tagBuilder = new TagBuilder("p");
+ tagBuilder.Attributes.Add("type", "hello");
+
+ // Act
+ tagHelperOutput.MergeAttributes(tagBuilder);
+
+ // Assert
+ var attribute = Assert.Single(tagHelperOutput.Attributes);
+ Assert.Equal(expectedAttribute, attribute);
+ }
+
+ [Fact]
+ public void MergeAttributes_AppendsClass_TagHelperOutputAttributeValues()
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary(),
+ content: string.Empty);
+ tagHelperOutput.Attributes.Add("class", "Hello");
+
+ var tagBuilder = new TagBuilder("p");
+ tagBuilder.Attributes.Add("class", "btn");
+
+ var expectedAttribute = new KeyValuePair("class", "Hello btn");
+
+ // Act
+ tagHelperOutput.MergeAttributes(tagBuilder);
+
+ // Assert
+ var attribute = Assert.Single(tagHelperOutput.Attributes);
+ Assert.Equal(expectedAttribute, attribute);
+ }
+
+ [Fact]
+ public void MergeAttributes_DoesNotEncode_TagHelperOutputAttributeValues()
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary(),
+ content: string.Empty);
+
+ var tagBuilder = new TagBuilder("p");
+ var expectedAttribute = new KeyValuePair("visible", "val < 3");
+ tagBuilder.Attributes.Add(expectedAttribute);
+
+ // Act
+ tagHelperOutput.MergeAttributes(tagBuilder);
+
+ // Assert
+ var attribute = Assert.Single(tagHelperOutput.Attributes);
+ Assert.Equal(expectedAttribute, attribute);
+ }
+
+ [Fact]
+ public void MergeAttributes_CopiesMultiple_TagHelperOutputAttributeValues()
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary(),
+ content: string.Empty);
+
+ var tagBuilder = new TagBuilder("p");
+ var expectedAttribute1 = new KeyValuePair("class", "btn");
+ var expectedAttribute2 = new KeyValuePair("class2", "btn");
+ tagBuilder.Attributes.Add(expectedAttribute1);
+ tagBuilder.Attributes.Add(expectedAttribute2);
+
+ // Act
+ tagHelperOutput.MergeAttributes(tagBuilder);
+
+ // Assert
+ Assert.Equal(2, tagHelperOutput.Attributes.Count);
+ var attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("class"));
+ Assert.Equal(expectedAttribute1.Value, attribute.Value);
+ attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("class2"));
+ Assert.Equal(expectedAttribute2.Value, attribute.Value);
+ }
+
+ [Fact]
+ public void MergeAttributes_Maintains_TagHelperOutputAttributeValues()
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary(),
+ content: string.Empty);
+ var expectedAttribute = new KeyValuePair("class", "btn");
+ tagHelperOutput.Attributes.Add(expectedAttribute);
+
+ var tagBuilder = new TagBuilder("p");
+
+ // Act
+ tagHelperOutput.MergeAttributes(tagBuilder);
+
+ // Assert
+ var attribute = Assert.Single(tagHelperOutput.Attributes);
+ Assert.Equal(expectedAttribute, attribute);
+ }
+
+ [Fact]
+ public void MergeAttributes_Combines_TagHelperOutputAttributeValues()
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary(),
+ content: string.Empty);
+ var expectedOutputAttribute = new KeyValuePair("class", "btn");
+ tagHelperOutput.Attributes.Add(expectedOutputAttribute);
+
+ var tagBuilder = new TagBuilder("p");
+ var expectedBuilderAttribute = new KeyValuePair("for", "hello");
+ tagBuilder.Attributes.Add(expectedBuilderAttribute);
+
+ // Act
+ tagHelperOutput.MergeAttributes(tagBuilder);
+
+ // Assert
+ Assert.Equal(tagHelperOutput.Attributes.Count, 2);
+ var attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("class"));
+ Assert.Equal(expectedOutputAttribute.Value, attribute.Value);
+ attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("for"));
+ Assert.Equal(expectedBuilderAttribute.Value, attribute.Value);
+ }
+
+ [Fact]
+ public void Merge_CombinesAllTagHelperOutputAndTagBuilderProperties()
+ {
+ // Arrange
+ var tagHelperOutput = new TagHelperOutput(
+ "p",
+ attributes: new Dictionary(),
+ content: "Hello from tagHelperOutput");
+ var expectedOutputAttribute = new KeyValuePair("class", "btn");
+ tagHelperOutput.Attributes.Add(expectedOutputAttribute);
+
+ var tagBuilder = new TagBuilder("div");
+ var expectedBuilderAttribute = new KeyValuePair("for", "hello");
+ tagBuilder.Attributes.Add(expectedBuilderAttribute);
+ tagBuilder.InnerHtml = "Hello from tagBuilder.";
+
+ // Act
+ tagHelperOutput.Merge(tagBuilder);
+
+ // Assert
+ Assert.Equal("div", tagHelperOutput.TagName);
+ Assert.Equal("Hello from tagHelperOutputHello from tagBuilder.", tagHelperOutput.Content);
+ Assert.Equal(tagHelperOutput.Attributes.Count, 2);
+ var attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("class"));
+ Assert.Equal(expectedOutputAttribute.Value, attribute.Value);
+ attribute = Assert.Single(tagHelperOutput.Attributes, kvp => kvp.Key.Equals("for"));
+ Assert.Equal(expectedBuilderAttribute.Value, attribute.Value);
+ }
+ }
+}