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>
|
/// </summary>
|
||||||
public class TagHelperExecutionContext
|
public class TagHelperExecutionContext
|
||||||
{
|
{
|
||||||
private readonly string _tagName;
|
|
||||||
private readonly string _uniqueId;
|
|
||||||
private readonly TagMode _tagMode;
|
|
||||||
private readonly List<ITagHelper> _tagHelpers;
|
private readonly List<ITagHelper> _tagHelpers;
|
||||||
private readonly Func<Task> _executeChildContentAsync;
|
|
||||||
private readonly Action<HtmlEncoder> _startTagHelperWritingScope;
|
private readonly Action<HtmlEncoder> _startTagHelperWritingScope;
|
||||||
private readonly Func<TagHelperContent> _endTagHelperWritingScope;
|
private readonly Func<TagHelperContent> _endTagHelperWritingScope;
|
||||||
private TagHelperContent _childContent;
|
private TagHelperContent _childContent;
|
||||||
|
private string _tagName;
|
||||||
|
private string _uniqueId;
|
||||||
|
private TagMode _tagMode;
|
||||||
|
private Func<Task> _executeChildContentAsync;
|
||||||
private Dictionary<HtmlEncoder, TagHelperContent> _perEncoderChildContent;
|
private Dictionary<HtmlEncoder, TagHelperContent> _perEncoderChildContent;
|
||||||
private TagHelperAttributeList _htmlAttributes;
|
private TagHelperAttributeList _htmlAttributes;
|
||||||
private TagHelperAttributeList _allAttributes;
|
private TagHelperAttributeList _allAttributes;
|
||||||
|
|
@ -63,26 +63,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
Action<HtmlEncoder> startTagHelperWritingScope,
|
Action<HtmlEncoder> startTagHelperWritingScope,
|
||||||
Func<TagHelperContent> endTagHelperWritingScope)
|
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)
|
if (startTagHelperWritingScope == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(startTagHelperWritingScope));
|
throw new ArgumentNullException(nameof(startTagHelperWritingScope));
|
||||||
|
|
@ -94,14 +74,11 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
}
|
}
|
||||||
|
|
||||||
_tagHelpers = new List<ITagHelper>();
|
_tagHelpers = new List<ITagHelper>();
|
||||||
_executeChildContentAsync = executeChildContentAsync;
|
|
||||||
|
Reinitialize(tagName, tagMode, items, uniqueId, executeChildContentAsync);
|
||||||
|
|
||||||
_startTagHelperWritingScope = startTagHelperWritingScope;
|
_startTagHelperWritingScope = startTagHelperWritingScope;
|
||||||
_endTagHelperWritingScope = endTagHelperWritingScope;
|
_endTagHelperWritingScope = endTagHelperWritingScope;
|
||||||
|
|
||||||
_tagMode = tagMode;
|
|
||||||
_tagName = tagName;
|
|
||||||
Items = items;
|
|
||||||
_uniqueId = uniqueId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -118,7 +95,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection of items used to communicate with other <see cref="ITagHelper"/>s.
|
/// Gets the collection of items used to communicate with other <see cref="ITagHelper"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDictionary<object, object> Items { get; }
|
public IDictionary<object, object> Items { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <see cref="ITagHelper"/>s that should be run.
|
/// <see cref="ITagHelper"/>s that should be run.
|
||||||
|
|
@ -214,6 +191,53 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
_allAttributes.Add(name, value);
|
_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 for testing.
|
||||||
internal async Task<TagHelperContent> GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder)
|
internal async Task<TagHelperContent> GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TagHelperScopeManager
|
public class TagHelperScopeManager
|
||||||
{
|
{
|
||||||
private readonly Stack<TagHelperExecutionContext> _executionScopes;
|
private readonly ExecutionContextPool _executionContextPool;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Instantiates a new <see cref="TagHelperScopeManager"/>.
|
/// Instantiates a new <see cref="TagHelperScopeManager"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TagHelperScopeManager()
|
public TagHelperScopeManager()
|
||||||
{
|
{
|
||||||
_executionScopes = new Stack<TagHelperExecutionContext>();
|
_executionContextPool = new ExecutionContextPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -72,12 +72,13 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
}
|
}
|
||||||
|
|
||||||
IDictionary<object, object> items;
|
IDictionary<object, object> items;
|
||||||
|
var parentExecutionContext = _executionContextPool.Current;
|
||||||
|
|
||||||
// If we're not wrapped by another TagHelper, then there will not be a parentExecutionContext.
|
// 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>(
|
items = new CopyOnWriteDictionary<object, object>(
|
||||||
_executionScopes.Peek().Items,
|
parentExecutionContext.Items,
|
||||||
comparer: EqualityComparer<object>.Default);
|
comparer: EqualityComparer<object>.Default);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -85,7 +86,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
items = new Dictionary<object, object>();
|
items = new Dictionary<object, object>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var executionContext = new TagHelperExecutionContext(
|
var executionContext = _executionContextPool.Rent(
|
||||||
tagName,
|
tagName,
|
||||||
tagMode,
|
tagMode,
|
||||||
items,
|
items,
|
||||||
|
|
@ -94,8 +95,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
startTagHelperWritingScope,
|
startTagHelperWritingScope,
|
||||||
endTagHelperWritingScope);
|
endTagHelperWritingScope);
|
||||||
|
|
||||||
_executionScopes.Push(executionContext);
|
|
||||||
|
|
||||||
return executionContext;
|
return executionContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +105,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
/// <c>null</c> otherwise.</returns>
|
/// <c>null</c> otherwise.</returns>
|
||||||
public TagHelperExecutionContext End()
|
public TagHelperExecutionContext End()
|
||||||
{
|
{
|
||||||
if (_executionScopes.Count == 0)
|
if (_executionContextPool.Current == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
Resources.FormatScopeManager_EndCannotBeCalledWithoutACallToBegin(
|
Resources.FormatScopeManager_EndCannotBeCalledWithoutACallToBegin(
|
||||||
|
|
@ -115,14 +114,61 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
nameof(TagHelperScopeManager)));
|
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.
|
// 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.
|
// 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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
|
@ -13,6 +14,106 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
|
||||||
{
|
{
|
||||||
public class TagHelperExecutionContextTest
|
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]
|
[Theory]
|
||||||
[InlineData(TagMode.SelfClosing)]
|
[InlineData(TagMode.SelfClosing)]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue