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
This commit is contained in:
parent
b03d3aa56f
commit
78451efa7f
|
|
@ -14,14 +14,14 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
|||
/// </summary>
|
||||
public class TagHelperExecutionContext
|
||||
{
|
||||
private readonly string _tagName;
|
||||
private readonly string _uniqueId;
|
||||
private readonly TagMode _tagMode;
|
||||
private readonly List<ITagHelper> _tagHelpers;
|
||||
private readonly Func<Task> _executeChildContentAsync;
|
||||
private readonly Action<HtmlEncoder> _startTagHelperWritingScope;
|
||||
private readonly Func<TagHelperContent> _endTagHelperWritingScope;
|
||||
private TagHelperContent _childContent;
|
||||
private string _tagName;
|
||||
private string _uniqueId;
|
||||
private TagMode _tagMode;
|
||||
private Func<Task> _executeChildContentAsync;
|
||||
private Dictionary<HtmlEncoder, TagHelperContent> _perEncoderChildContent;
|
||||
private TagHelperAttributeList _htmlAttributes;
|
||||
private TagHelperAttributeList _allAttributes;
|
||||
|
|
@ -63,26 +63,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
|||
Action<HtmlEncoder> startTagHelperWritingScope,
|
||||
Func<TagHelperContent> 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<ITagHelper>();
|
||||
_executeChildContentAsync = executeChildContentAsync;
|
||||
|
||||
Reinitialize(tagName, tagMode, items, uniqueId, executeChildContentAsync);
|
||||
|
||||
_startTagHelperWritingScope = startTagHelperWritingScope;
|
||||
_endTagHelperWritingScope = endTagHelperWritingScope;
|
||||
|
||||
_tagMode = tagMode;
|
||||
_tagName = tagName;
|
||||
Items = items;
|
||||
_uniqueId = uniqueId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -118,7 +95,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
|||
/// <summary>
|
||||
/// Gets the collection of items used to communicate with other <see cref="ITagHelper"/>s.
|
||||
/// </summary>
|
||||
public IDictionary<object, object> Items { get; }
|
||||
public IDictionary<object, object> Items { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ITagHelper"/>s that should be run.
|
||||
|
|
@ -214,6 +191,53 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
|||
_allAttributes.Add(name, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the <see cref="TagHelperExecutionContext"/> and updates its state with the provided values.
|
||||
/// </summary>
|
||||
/// <param name="tagName">The tag name to use.</param>
|
||||
/// <param name="tagMode">The <see cref="TagMode"/> to use.</param>
|
||||
/// <param name="items">The <see cref="IDictionary{Object, Object}"/> to use.</param>
|
||||
/// <param name="uniqueId">The unique id to use.</param>
|
||||
/// <param name="executeChildContentAsync">The <see cref="Func{Task}"/> to use.</param>
|
||||
public void Reinitialize(
|
||||
string tagName,
|
||||
TagMode tagMode,
|
||||
IDictionary<object, object> items,
|
||||
string uniqueId,
|
||||
Func<Task> 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<TagHelperContent> GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
|||
/// </summary>
|
||||
public class TagHelperScopeManager
|
||||
{
|
||||
private readonly Stack<TagHelperExecutionContext> _executionScopes;
|
||||
private readonly ExecutionContextPool _executionContextPool;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="TagHelperScopeManager"/>.
|
||||
/// </summary>
|
||||
public TagHelperScopeManager()
|
||||
{
|
||||
_executionScopes = new Stack<TagHelperExecutionContext>();
|
||||
_executionContextPool = new ExecutionContextPool();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -72,12 +72,13 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
|||
}
|
||||
|
||||
IDictionary<object, object> 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<object, object>(
|
||||
_executionScopes.Peek().Items,
|
||||
parentExecutionContext.Items,
|
||||
comparer: EqualityComparer<object>.Default);
|
||||
}
|
||||
else
|
||||
|
|
@ -85,7 +86,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
|||
items = new Dictionary<object, object>();
|
||||
}
|
||||
|
||||
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
|
|||
/// <c>null</c> otherwise.</returns>
|
||||
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<TagHelperExecutionContext> _executionContexts;
|
||||
private int _nextIndex;
|
||||
|
||||
public ExecutionContextPool()
|
||||
{
|
||||
return _executionScopes.Peek();
|
||||
_executionContexts = new List<TagHelperExecutionContext>();
|
||||
}
|
||||
|
||||
return null;
|
||||
public TagHelperExecutionContext Current => _nextIndex > 0 ? _executionContexts[_nextIndex - 1] : null;
|
||||
|
||||
public TagHelperExecutionContext Rent(
|
||||
string tagName,
|
||||
TagMode tagMode,
|
||||
IDictionary<object, object> items,
|
||||
string uniqueId,
|
||||
Func<Task> executeChildContentAsync,
|
||||
Action<HtmlEncoder> startTagHelperWritingScope,
|
||||
Func<TagHelperContent> 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--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Task> executeChildContentAsync = () =>
|
||||
{
|
||||
callCount++;
|
||||
return Task.FromResult(true);
|
||||
};
|
||||
Action<HtmlEncoder> startTagHelperWritingScope = _ => { };
|
||||
Func<TagHelperContent> endTagHelperWritingScope = () => null;
|
||||
var executionContext = new TagHelperExecutionContext(
|
||||
tagName,
|
||||
tagMode,
|
||||
items: new Dictionary<object, object>(),
|
||||
uniqueId: string.Empty,
|
||||
executeChildContentAsync: executeChildContentAsync,
|
||||
startTagHelperWritingScope: startTagHelperWritingScope,
|
||||
endTagHelperWritingScope: endTagHelperWritingScope);
|
||||
var updatedTagName = "p";
|
||||
var updatedTagMode = TagMode.SelfClosing;
|
||||
var updatedCallCount = 0;
|
||||
Func<Task> updatedExecuteChildContentAsync = () =>
|
||||
{
|
||||
updatedCallCount++;
|
||||
return Task.FromResult(true);
|
||||
};
|
||||
executionContext.AddMinimizedHtmlAttribute("something");
|
||||
|
||||
// Act - 1
|
||||
executionContext.Reinitialize(
|
||||
updatedTagName,
|
||||
updatedTagMode,
|
||||
items: new Dictionary<object, object>(),
|
||||
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<object, object>();
|
||||
var uniqueId = "some unique id";
|
||||
var callCount = 0;
|
||||
Func<Task> executeChildContentAsync = () =>
|
||||
{
|
||||
callCount++;
|
||||
return Task.FromResult(true);
|
||||
};
|
||||
Action<HtmlEncoder> startWritingScope = _ => { };
|
||||
Func<TagHelperContent> endWritingScope = () => null;
|
||||
var executionContext = new TagHelperExecutionContext(
|
||||
tagName,
|
||||
tagMode,
|
||||
items,
|
||||
uniqueId,
|
||||
executeChildContentAsync,
|
||||
startWritingScope,
|
||||
endWritingScope);
|
||||
var updatedItems = new Dictionary<object, object>();
|
||||
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)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue