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:
N. Taylor Mullen 2016-02-17 15:21:56 -08:00
parent b03d3aa56f
commit 78451efa7f
3 changed files with 214 additions and 43 deletions

View File

@ -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)
{

View File

@ -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--;
}
}
}

View File

@ -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)]