From 78451efa7fb5490a7e92ef31576c6bd0e57b30cf Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Wed, 17 Feb 2016 15:21:56 -0800 Subject: [PATCH] Pool `TagHelperExecutionContext`s within `TagHelperScopeManager`. - Currently the `TagHelperScopeManager` creates a new `TagHelperExecutionContext` per `TagHelper` on a given page. With this change the max number of `TagHelperExecutionContext`s per page is the number of nested levels that exist. - Added two tests validating that specific pieces of `TagHelperExecutionContext` are updated as expected. #674 --- .../TagHelpers/TagHelperExecutionContext.cs | 86 +++++++++------ .../TagHelpers/TagHelperScopeManager.cs | 70 +++++++++--- .../TagHelperExecutionContextTest.cs | 101 ++++++++++++++++++ 3 files changed, 214 insertions(+), 43 deletions(-) diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs index fc06c6d430..b249b4eccc 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperExecutionContext.cs @@ -14,14 +14,14 @@ 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 string _tagName; + private string _uniqueId; + private TagMode _tagMode; + private Func _executeChildContentAsync; private Dictionary _perEncoderChildContent; private TagHelperAttributeList _htmlAttributes; private TagHelperAttributeList _allAttributes; @@ -63,26 +63,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers Action startTagHelperWritingScope, Func endTagHelperWritingScope) { - if (tagName == null) - { - throw new ArgumentNullException(nameof(tagName)); - } - - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } - - if (uniqueId == null) - { - throw new ArgumentNullException(nameof(uniqueId)); - } - - if (executeChildContentAsync == null) - { - throw new ArgumentNullException(nameof(executeChildContentAsync)); - } - if (startTagHelperWritingScope == null) { throw new ArgumentNullException(nameof(startTagHelperWritingScope)); @@ -94,14 +74,11 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers } _tagHelpers = new List(); - _executeChildContentAsync = executeChildContentAsync; + + Reinitialize(tagName, tagMode, items, uniqueId, executeChildContentAsync); + _startTagHelperWritingScope = startTagHelperWritingScope; _endTagHelperWritingScope = endTagHelperWritingScope; - - _tagMode = tagMode; - _tagName = tagName; - Items = items; - _uniqueId = uniqueId; } /// @@ -118,7 +95,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers /// /// Gets the collection of items used to communicate with other s. /// - public IDictionary Items { get; } + public IDictionary Items { get; private set; } /// /// s that should be run. @@ -214,6 +191,53 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers _allAttributes.Add(name, value); } + /// + /// Clears the and updates its state with the provided values. + /// + /// The tag name to use. + /// The to use. + /// The to use. + /// The unique id to use. + /// The to use. + public void Reinitialize( + string tagName, + TagMode tagMode, + IDictionary items, + string uniqueId, + Func executeChildContentAsync) + { + if (tagName == null) + { + throw new ArgumentNullException(nameof(tagName)); + } + + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + if (uniqueId == null) + { + throw new ArgumentNullException(nameof(uniqueId)); + } + + if (executeChildContentAsync == null) + { + throw new ArgumentNullException(nameof(executeChildContentAsync)); + } + + _tagName = tagName; + _tagMode = tagMode; + Items = items; + _uniqueId = uniqueId; + _executeChildContentAsync = executeChildContentAsync; + _tagHelpers.Clear(); + _perEncoderChildContent?.Clear(); + _htmlAttributes = null; + _allAttributes = null; + _childContent = null; + } + // Internal for testing. internal async Task GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder) { diff --git a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperScopeManager.cs b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperScopeManager.cs index 80fa37cdbc..a48e95ee28 100644 --- a/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperScopeManager.cs +++ b/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperScopeManager.cs @@ -15,14 +15,14 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers /// public class TagHelperScopeManager { - private readonly Stack _executionScopes; + private readonly ExecutionContextPool _executionContextPool; /// /// Instantiates a new . /// public TagHelperScopeManager() { - _executionScopes = new Stack(); + _executionContextPool = new ExecutionContextPool(); } /// @@ -72,12 +72,13 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers } IDictionary items; + var parentExecutionContext = _executionContextPool.Current; // If we're not wrapped by another TagHelper, then there will not be a parentExecutionContext. - if (_executionScopes.Count > 0) + if (parentExecutionContext != null) { items = new CopyOnWriteDictionary( - _executionScopes.Peek().Items, + parentExecutionContext.Items, comparer: EqualityComparer.Default); } else @@ -85,7 +86,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers items = new Dictionary(); } - var executionContext = new TagHelperExecutionContext( + var executionContext = _executionContextPool.Rent( tagName, tagMode, items, @@ -94,8 +95,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers startTagHelperWritingScope, endTagHelperWritingScope); - _executionScopes.Push(executionContext); - return executionContext; } @@ -106,7 +105,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers /// null otherwise. public TagHelperExecutionContext End() { - if (_executionScopes.Count == 0) + if (_executionContextPool.Current == null) { throw new InvalidOperationException( Resources.FormatScopeManager_EndCannotBeCalledWithoutACallToBegin( @@ -115,14 +114,61 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers nameof(TagHelperScopeManager))); } - _executionScopes.Pop(); + _executionContextPool.ReturnCurrent(); - if (_executionScopes.Count != 0) + var parentExecutionContext = _executionContextPool.Current; + + return parentExecutionContext; + } + + private class ExecutionContextPool + { + private readonly List _executionContexts; + private int _nextIndex; + + public ExecutionContextPool() { - return _executionScopes.Peek(); + _executionContexts = new List(); } - return null; + public TagHelperExecutionContext Current => _nextIndex > 0 ? _executionContexts[_nextIndex - 1] : null; + + public TagHelperExecutionContext Rent( + string tagName, + TagMode tagMode, + IDictionary items, + string uniqueId, + Func executeChildContentAsync, + Action startTagHelperWritingScope, + Func endTagHelperWritingScope) + { + TagHelperExecutionContext tagHelperExecutionContext; + + if (_nextIndex == _executionContexts.Count) + { + tagHelperExecutionContext = new TagHelperExecutionContext( + tagName, + tagMode, + items, + uniqueId, + executeChildContentAsync, + startTagHelperWritingScope, + endTagHelperWritingScope); + + _executionContexts.Add(tagHelperExecutionContext); + } + else + { + tagHelperExecutionContext = _executionContexts[_nextIndex]; + tagHelperExecutionContext.Reinitialize(tagName, tagMode, items, uniqueId, executeChildContentAsync); + } + + _nextIndex++; + + return tagHelperExecutionContext; + } + + public void ReturnCurrent() => _nextIndex--; } } } \ No newline at end of file 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 905b714815..88278b4abc 100644 --- a/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Runtime.Test/Runtime/TagHelpers/TagHelperExecutionContextTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. 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 System.Text.Encodings.Web; @@ -13,6 +14,106 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers { public class TagHelperExecutionContextTest { + [Fact] + public async Task ExecutionContext_Reinitialize_UpdatesTagHelperOutputAsExpected() + { + // Arrange + var tagName = "div"; + var tagMode = TagMode.StartTagOnly; + var callCount = 0; + Func executeChildContentAsync = () => + { + callCount++; + return Task.FromResult(true); + }; + Action startTagHelperWritingScope = _ => { }; + Func endTagHelperWritingScope = () => null; + var executionContext = new TagHelperExecutionContext( + tagName, + tagMode, + items: new Dictionary(), + uniqueId: string.Empty, + executeChildContentAsync: executeChildContentAsync, + startTagHelperWritingScope: startTagHelperWritingScope, + endTagHelperWritingScope: endTagHelperWritingScope); + var updatedTagName = "p"; + var updatedTagMode = TagMode.SelfClosing; + var updatedCallCount = 0; + Func updatedExecuteChildContentAsync = () => + { + updatedCallCount++; + return Task.FromResult(true); + }; + executionContext.AddMinimizedHtmlAttribute("something"); + + // Act - 1 + executionContext.Reinitialize( + updatedTagName, + updatedTagMode, + items: new Dictionary(), + uniqueId: string.Empty, + executeChildContentAsync: updatedExecuteChildContentAsync); + executionContext.AddMinimizedHtmlAttribute("Another attribute"); + + // Assert - 1 + var output = executionContext.CreateTagHelperOutput(); + Assert.Equal(updatedTagName, output.TagName); + Assert.Equal(updatedTagMode, output.TagMode); + var attribute = Assert.Single(output.Attributes); + Assert.Equal("Another attribute", attribute.Name); + + // Act - 2 + await output.GetChildContentAsync(); + + // Assert - 2 + Assert.Equal(callCount, 0); + Assert.Equal(updatedCallCount, 1); + } + + [Fact] + public void ExecutionContext_Reinitialize_UpdatesTagHelperContextAsExpected() + { + // Arrange + var tagName = "div"; + var tagMode = TagMode.StartTagOnly; + var items = new Dictionary(); + var uniqueId = "some unique id"; + var callCount = 0; + Func executeChildContentAsync = () => + { + callCount++; + return Task.FromResult(true); + }; + Action startWritingScope = _ => { }; + Func endWritingScope = () => null; + var executionContext = new TagHelperExecutionContext( + tagName, + tagMode, + items, + uniqueId, + executeChildContentAsync, + startWritingScope, + endWritingScope); + var updatedItems = new Dictionary(); + var updatedUniqueId = "another unique id"; + executionContext.AddMinimizedHtmlAttribute("something"); + + // Act + executionContext.Reinitialize( + tagName, + tagMode, + updatedItems, + updatedUniqueId, + executeChildContentAsync); + executionContext.AddMinimizedHtmlAttribute("Another attribute"); + + // Assert + var context = executionContext.CreateTagHelperContext(); + var attribute = Assert.Single(context.AllAttributes); + Assert.Equal(attribute.Name, "Another attribute"); + Assert.Equal(updatedUniqueId, context.UniqueId); + Assert.Same(updatedItems, context.Items); + } [Theory] [InlineData(TagMode.SelfClosing)]