From b7b3273fa49d31f004a4b739be142db8d068e357 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Fri, 29 Jan 2016 15:48:14 -0800 Subject: [PATCH] Lazily initialize `TagHelperAttributeList`s. - Not all `TagHelper`s have unbound HTML attributes or any attributes at all. A great example of this is MVC's input `TagHelper` which usually takes the format of ``. By lazily initializing we don't build extra attribute lists where not needed. - Moved `TagHelperContext` and `TagHelperOutput` creation to CreateX methods on `TagHelperExecutionContext`. #604 --- .../TagHelpers/TagHelperExecutionContext.cs | 98 +++++++++---------- .../Runtime/TagHelpers/TagHelperRunner.cs | 13 +-- .../ReadOnlyTagHelperAttributeList.cs | 4 +- .../TagHelpers/TagHelperAttributeList.cs | 14 +++ .../TagHelpers/TagHelperContext.cs | 25 +---- .../TagHelpers/TagHelperOutput.cs | 27 +++-- .../TagHelperExecutionContextTest.cs | 66 +++---------- .../TagHelpers/TagHelperScopeManagerTest.cs | 16 +-- .../ReadOnlyTagHelperAttributeListTest.cs | 2 +- 9 files changed, 112 insertions(+), 153 deletions(-) diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs index 9126524c52..4c76ca5ecf 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs @@ -14,12 +14,17 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers /// public class TagHelperExecutionContext { + private readonly string _tagName; + private readonly string _uniqueId; + private readonly TagMode _tagMode; private readonly List _tagHelpers; private readonly Func _executeChildContentAsync; private readonly Action _startTagHelperWritingScope; private readonly Func _endTagHelperWritingScope; private TagHelperContent _childContent; private Dictionary _perEncoderChildContent; + private TagHelperAttributeList _htmlAttributes; + private TagHelperAttributeList _allAttributes; /// /// Internal for testing purposes only. @@ -93,19 +98,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers _startTagHelperWritingScope = startTagHelperWritingScope; _endTagHelperWritingScope = endTagHelperWritingScope; - TagMode = tagMode; - HtmlAttributes = new TagHelperAttributeList(); - AllAttributes = new TagHelperAttributeList(); - TagName = tagName; + _tagMode = tagMode; + _tagName = tagName; Items = items; - UniqueId = uniqueId; + _uniqueId = uniqueId; } - /// - /// Gets the HTML syntax of the element in the Razor source. - /// - public TagMode TagMode { get; } - /// /// Indicates if has been called. /// @@ -122,21 +120,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers /// public IDictionary Items { get; } - /// - /// HTML attributes. - /// - public TagHelperAttributeList HtmlAttributes { get; } - - /// - /// bound attributes and HTML attributes. - /// - public TagHelperAttributeList AllAttributes { get; } - - /// - /// An identifier unique to the HTML element this context is for. - /// - public string UniqueId { get; } - /// /// s that should be run. /// @@ -148,16 +131,19 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers } } - /// - /// The HTML tag name in the Razor source. - /// - public string TagName { get; } - /// /// The s' output. /// public TagHelperOutput Output { get; set; } + public TagHelperContext CreateTagHelperContext() => + new TagHelperContext(_allAttributes, Items, _uniqueId); + public TagHelperOutput CreateTagHelperOutput() => + new TagHelperOutput(_tagName, _htmlAttributes, GetChildContentAsync) + { + TagMode = _tagMode + }; + /// /// Tracks the given . /// @@ -183,9 +169,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers throw new ArgumentNullException(nameof(name)); } + EnsureHtmlAttributes(); + EnsureAllAttributes(); + var attribute = new TagHelperAttribute(name); - HtmlAttributes.Add(attribute); - AllAttributes.Add(attribute); + _htmlAttributes.Add(attribute); + _allAttributes.Add(attribute); } /// @@ -200,8 +189,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers throw new ArgumentNullException(nameof(name)); } - HtmlAttributes.Add(name, value); - AllAttributes.Add(name, value); + EnsureHtmlAttributes(); + EnsureAllAttributes(); + + var attribute = new TagHelperAttribute(name, value); + _htmlAttributes.Add(attribute); + _allAttributes.Add(attribute); } /// @@ -216,24 +209,13 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers throw new ArgumentNullException(nameof(name)); } - AllAttributes.Add(name, value); + EnsureAllAttributes(); + + _allAttributes.Add(name, value); } - /// - /// Executes children asynchronously with the given in scope and returns their - /// rendered content. - /// - /// - /// If true, multiple calls with the same will not cause children to - /// re-execute; returns cached content. - /// - /// - /// The to use when the page handles - /// non- C# expressions. If null, executes children with - /// the page's current . - /// - /// A that on completion returns the rendered child content. - public async Task GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder) + // Internal for testing. + internal async Task GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder) { // Get cached content for this encoder. TagHelperContent childContent; @@ -272,5 +254,21 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers return new DefaultTagHelperContent().SetContent(childContent); } + + private void EnsureHtmlAttributes() + { + if (_htmlAttributes == null) + { + _htmlAttributes = new TagHelperAttributeList(); + } + } + + private void EnsureAllAttributes() + { + if (_allAttributes == null) + { + _allAttributes = new TagHelperAttributeList(); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperRunner.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperRunner.cs index 7e11d150d2..f945eaebb4 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperRunner.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperRunner.cs @@ -27,10 +27,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers throw new ArgumentNullException(nameof(executionContext)); } - var tagHelperContext = new TagHelperContext( - executionContext.AllAttributes, - executionContext.Items, - executionContext.UniqueId); + var tagHelperContext = executionContext.CreateTagHelperContext(); OrderTagHelpers(executionContext.TagHelpers); @@ -39,13 +36,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers executionContext.TagHelpers[i].Init(tagHelperContext); } - var tagHelperOutput = new TagHelperOutput( - executionContext.TagName, - executionContext.HtmlAttributes, - executionContext.GetChildContentAsync) - { - TagMode = executionContext.TagMode, - }; + var tagHelperOutput = executionContext.CreateTagHelperOutput(); for (var i = 0; i < executionContext.TagHelpers.Count; i++) { diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/ReadOnlyTagHelperAttributeList.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/ReadOnlyTagHelperAttributeList.cs index 441c1e8451..a546883fa9 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/ReadOnlyTagHelperAttributeList.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/ReadOnlyTagHelperAttributeList.cs @@ -31,8 +31,8 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// . /// /// The collection to wrap. - public ReadOnlyTagHelperAttributeList(IEnumerable attributes) - : base(new List(attributes)) + public ReadOnlyTagHelperAttributeList(IList attributes) + : base(attributes) { } diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperAttributeList.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperAttributeList.cs index 73dde2f667..308292294c 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperAttributeList.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperAttributeList.cs @@ -25,6 +25,20 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// /// The collection to wrap. public TagHelperAttributeList(IEnumerable attributes) + : base (new List(attributes)) + { + if (attributes == null) + { + throw new ArgumentNullException(nameof(attributes)); + } + } + + /// + /// Instantiates a new instance of with the specified + /// . + /// + /// The collection to wrap. + public TagHelperAttributeList(List attributes) : base(attributes) { if (attributes == null) diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperContext.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperContext.cs index 6cc4d1c437..39865e692f 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperContext.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperContext.cs @@ -11,8 +11,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// public class TagHelperContext { - private ReadOnlyTagHelperAttributeList _allAttributes; - private IEnumerable _allAttributesData; + private static ReadOnlyTagHelperAttributeList EmptyAttributes = new TagHelperAttributeList(); /// /// Instantiates a new . @@ -22,15 +21,10 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// The unique identifier for the source element this /// applies to. public TagHelperContext( - IEnumerable allAttributes, + ReadOnlyTagHelperAttributeList allAttributes, IDictionary items, string uniqueId) { - if (allAttributes == null) - { - throw new ArgumentNullException(nameof(allAttributes)); - } - if (items == null) { throw new ArgumentNullException(nameof(items)); @@ -41,7 +35,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers throw new ArgumentNullException(nameof(uniqueId)); } - _allAttributesData = allAttributes; + AllAttributes = allAttributes ?? EmptyAttributes; Items = items; UniqueId = uniqueId; } @@ -49,18 +43,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// /// Every attribute associated with the current HTML element. /// - public ReadOnlyTagHelperAttributeList AllAttributes - { - get - { - if (_allAttributes == null) - { - _allAttributes = new TagHelperAttributeList(_allAttributesData); - } - - return _allAttributes; - } - } + public ReadOnlyTagHelperAttributeList AllAttributes { get; } /// /// Gets the collection of items used to communicate with other s. diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs index e1e5bea4ac..029ce1a9c0 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/TagHelpers/TagHelperOutput.cs @@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers public class TagHelperOutput : IHtmlContent { private readonly Func> _getChildContentAsync; + private TagHelperAttributeList _attributes; private TagHelperContent _preElement; private TagHelperContent _preContent; private TagHelperContent _content; @@ -26,7 +27,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers internal TagHelperOutput(string tagName) : this( tagName, - new TagHelperAttributeList(), + null, (useCachedResult, encoder) => Task.FromResult(new DefaultTagHelperContent())) { } @@ -45,19 +46,14 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers TagHelperAttributeList attributes, Func> getChildContentAsync) { - if (attributes == null) - { - throw new ArgumentNullException(nameof(attributes)); - } - if (getChildContentAsync == null) { throw new ArgumentNullException(nameof(getChildContentAsync)); } TagName = tagName; - Attributes = attributes; _getChildContentAsync = getChildContentAsync; + _attributes = attributes; } /// @@ -187,7 +183,18 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers /// a Microsoft.AspNetCore.Mvc.Rendering.HtmlString instance. MVC converts most other types to a /// , then HTML encodes the result. /// - public TagHelperAttributeList Attributes { get; } + public TagHelperAttributeList Attributes + { + get + { + if (_attributes == null) + { + _attributes = new TagHelperAttributeList(); + } + + return _attributes; + } + } /// /// Changes to generate nothing. @@ -287,9 +294,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers writer.Write(TagName); // Perf: Avoid allocating enumerator - for (var i = 0; i < Attributes.Count; i++) + for (var i = 0; i < (_attributes?.Count ?? 0); i++) { - var attribute = Attributes[i]; + var attribute = _attributes[i]; writer.Write(" "); writer.Write(attribute.Name); diff --git a/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs b/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs index 08cef8556e..905b714815 100644 --- a/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs @@ -18,13 +18,16 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers [InlineData(TagMode.SelfClosing)] [InlineData(TagMode.StartTagAndEndTag)] [InlineData(TagMode.StartTagOnly)] - public void TagMode_ReturnsExpectedValue(TagMode tagMode) + public void ExecutionContext_CreateTagHelperOutput_ReturnsExpectedTagMode(TagMode tagMode) { - // Arrange & Act + // Arrange var executionContext = new TagHelperExecutionContext("p", tagMode); + // Act + var output = executionContext.CreateTagHelperOutput(); + // Assert - Assert.Equal(tagMode, executionContext.TagMode); + Assert.Equal(tagMode, output.TagMode); } [Fact] @@ -270,51 +273,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers Assert.NotSame(content1, content2); } - public static TheoryData DictionaryCaseTestingData - { - get - { - return new TheoryData - { - { "class", "CLaSS" }, - { "Class", "class" }, - { "Class", "claSS" } - }; - } - } - - [Theory] - [MemberData(nameof(DictionaryCaseTestingData))] - public void HtmlAttributes_IgnoresCase(string originalName, string updatedName) - { - // Arrange - var executionContext = new TagHelperExecutionContext("p", TagMode.StartTagAndEndTag); - executionContext.HtmlAttributes.SetAttribute(originalName, "hello"); - - // Act - executionContext.HtmlAttributes.SetAttribute(updatedName, "something else"); - - // Assert - var attribute = Assert.Single(executionContext.HtmlAttributes); - Assert.Equal(new TagHelperAttribute(originalName, "something else"), attribute); - } - - [Theory] - [MemberData(nameof(DictionaryCaseTestingData))] - public void AllAttributes_IgnoresCase(string originalName, string updatedName) - { - // Arrange - var executionContext = new TagHelperExecutionContext("p", tagMode: TagMode.StartTagAndEndTag); - executionContext.AllAttributes.SetAttribute(originalName, value: false); - - // Act - executionContext.AllAttributes.SetAttribute(updatedName, true); - - // Assert - var attribute = Assert.Single(executionContext.AllAttributes); - Assert.Equal(new TagHelperAttribute(originalName, true), attribute); - } - [Fact] public void AddHtmlAttribute_MaintainsHtmlAttributes() { @@ -329,11 +287,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers // Act executionContext.AddHtmlAttribute("class", "btn"); executionContext.AddHtmlAttribute("foo", "bar"); + var output = executionContext.CreateTagHelperOutput(); // Assert Assert.Equal( expectedAttributes, - executionContext.HtmlAttributes, + output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default); } @@ -351,11 +310,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers // Act executionContext.AddMinimizedHtmlAttribute("checked"); executionContext.AddMinimizedHtmlAttribute("visible"); + var output = executionContext.CreateTagHelperOutput(); // Assert Assert.Equal( expectedAttributes, - executionContext.HtmlAttributes, + output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default); } @@ -377,11 +337,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers executionContext.AddHtmlAttribute("foo", "bar"); executionContext.AddMinimizedHtmlAttribute("checked"); executionContext.AddMinimizedHtmlAttribute("visible"); + var output = executionContext.CreateTagHelperOutput(); // Assert Assert.Equal( expectedAttributes, - executionContext.HtmlAttributes, + output.Attributes, CaseSensitiveTagHelperAttributeComparer.Default); } @@ -401,11 +362,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers executionContext.AddHtmlAttribute("class", "btn"); executionContext.AddTagHelperAttribute("something", true); executionContext.AddHtmlAttribute("foo", "bar"); + var context = executionContext.CreateTagHelperContext(); // Assert Assert.Equal( expectedAttributes, - executionContext.AllAttributes, + context.AllAttributes, CaseSensitiveTagHelperAttributeComparer.Default); } diff --git a/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperScopeManagerTest.cs b/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperScopeManagerTest.cs index 408020784d..98d6a433ca 100644 --- a/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperScopeManagerTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperScopeManagerTest.cs @@ -125,16 +125,17 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers } [Fact] - public void Begin_CreatesContextWithAppropriateTagName() + public void Begin_CreatesContexts_TagHelperOutput_WithAppropriateTagName() { // Arrange var scopeManager = new TagHelperScopeManager(); // Act var executionContext = BeginDefaultScope(scopeManager, tagName: "p"); + var output = executionContext.CreateTagHelperOutput(); // Assert - Assert.Equal("p", executionContext.TagName); + Assert.Equal("p", output.TagName); } [Fact] @@ -146,25 +147,27 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers // Act var executionContext = BeginDefaultScope(scopeManager, tagName: "p"); executionContext = BeginDefaultScope(scopeManager, tagName: "div"); + var output = executionContext.CreateTagHelperOutput(); // Assert - Assert.Equal("div", executionContext.TagName); + Assert.Equal("div", output.TagName); } [Theory] [InlineData(TagMode.SelfClosing)] [InlineData(TagMode.StartTagAndEndTag)] [InlineData(TagMode.StartTagOnly)] - public void Begin_SetsExecutionContextTagMode(TagMode tagMode) + public void Begin_SetsExecutionContexts_TagHelperOutputTagMode(TagMode tagMode) { // Arrange var scopeManager = new TagHelperScopeManager(); // Act var executionContext = BeginDefaultScope(scopeManager, "p", tagMode); + var output = executionContext.CreateTagHelperOutput(); // Assert - Assert.Equal(tagMode, executionContext.TagMode); + Assert.Equal(tagMode, output.TagMode); } [Fact] @@ -177,9 +180,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers var executionContext = BeginDefaultScope(scopeManager, tagName: "p"); executionContext = BeginDefaultScope(scopeManager, tagName: "div"); executionContext = scopeManager.End(); + var output = executionContext.CreateTagHelperOutput(); // Assert - Assert.Equal("p", executionContext.TagName); + Assert.Equal("p", output.TagName); } [Fact] diff --git a/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/ReadOnlyTagHelperAttributeListTest.cs b/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/ReadOnlyTagHelperAttributeListTest.cs index 99e35be6de..f8aaa4fe54 100644 --- a/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/ReadOnlyTagHelperAttributeListTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Runtime.Test/TagHelpers/ReadOnlyTagHelperAttributeListTest.cs @@ -730,7 +730,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers private class TestableReadOnlyTagHelperAttributes : ReadOnlyTagHelperAttributeList { public TestableReadOnlyTagHelperAttributes(IEnumerable attributes) - : base(attributes) + : base(new List(attributes)) { }