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 `<input asp-for="..." />`. By lazily initializing we don't build extra attribute lists where not needed.
- Moved `TagHelperContext` and `TagHelperOutput` creation to CreateX methods on `TagHelperExecutionContext`.

#604
This commit is contained in:
N. Taylor Mullen 2016-01-29 15:48:14 -08:00
parent 84ac19571c
commit b7b3273fa4
9 changed files with 112 additions and 153 deletions

View File

@ -14,12 +14,17 @@ 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 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 Dictionary<HtmlEncoder, TagHelperContent> _perEncoderChildContent; private Dictionary<HtmlEncoder, TagHelperContent> _perEncoderChildContent;
private TagHelperAttributeList _htmlAttributes;
private TagHelperAttributeList _allAttributes;
/// <summary> /// <summary>
/// Internal for testing purposes only. /// Internal for testing purposes only.
@ -93,19 +98,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
_startTagHelperWritingScope = startTagHelperWritingScope; _startTagHelperWritingScope = startTagHelperWritingScope;
_endTagHelperWritingScope = endTagHelperWritingScope; _endTagHelperWritingScope = endTagHelperWritingScope;
TagMode = tagMode; _tagMode = tagMode;
HtmlAttributes = new TagHelperAttributeList(); _tagName = tagName;
AllAttributes = new TagHelperAttributeList();
TagName = tagName;
Items = items; Items = items;
UniqueId = uniqueId; _uniqueId = uniqueId;
} }
/// <summary>
/// Gets the HTML syntax of the element in the Razor source.
/// </summary>
public TagMode TagMode { get; }
/// <summary> /// <summary>
/// Indicates if <see cref="GetChildContentAsync"/> has been called. /// Indicates if <see cref="GetChildContentAsync"/> has been called.
/// </summary> /// </summary>
@ -122,21 +120,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
/// </summary> /// </summary>
public IDictionary<object, object> Items { get; } public IDictionary<object, object> Items { get; }
/// <summary>
/// HTML attributes.
/// </summary>
public TagHelperAttributeList HtmlAttributes { get; }
/// <summary>
/// <see cref="ITagHelper"/> bound attributes and HTML attributes.
/// </summary>
public TagHelperAttributeList AllAttributes { get; }
/// <summary>
/// An identifier unique to the HTML element this context is for.
/// </summary>
public string UniqueId { get; }
/// <summary> /// <summary>
/// <see cref="ITagHelper"/>s that should be run. /// <see cref="ITagHelper"/>s that should be run.
/// </summary> /// </summary>
@ -148,16 +131,19 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
} }
} }
/// <summary>
/// The HTML tag name in the Razor source.
/// </summary>
public string TagName { get; }
/// <summary> /// <summary>
/// The <see cref="ITagHelper"/>s' output. /// The <see cref="ITagHelper"/>s' output.
/// </summary> /// </summary>
public TagHelperOutput Output { get; set; } public TagHelperOutput Output { get; set; }
public TagHelperContext CreateTagHelperContext() =>
new TagHelperContext(_allAttributes, Items, _uniqueId);
public TagHelperOutput CreateTagHelperOutput() =>
new TagHelperOutput(_tagName, _htmlAttributes, GetChildContentAsync)
{
TagMode = _tagMode
};
/// <summary> /// <summary>
/// Tracks the given <paramref name="tagHelper"/>. /// Tracks the given <paramref name="tagHelper"/>.
/// </summary> /// </summary>
@ -183,9 +169,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
throw new ArgumentNullException(nameof(name)); throw new ArgumentNullException(nameof(name));
} }
EnsureHtmlAttributes();
EnsureAllAttributes();
var attribute = new TagHelperAttribute(name); var attribute = new TagHelperAttribute(name);
HtmlAttributes.Add(attribute); _htmlAttributes.Add(attribute);
AllAttributes.Add(attribute); _allAttributes.Add(attribute);
} }
/// <summary> /// <summary>
@ -200,8 +189,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
throw new ArgumentNullException(nameof(name)); throw new ArgumentNullException(nameof(name));
} }
HtmlAttributes.Add(name, value); EnsureHtmlAttributes();
AllAttributes.Add(name, value); EnsureAllAttributes();
var attribute = new TagHelperAttribute(name, value);
_htmlAttributes.Add(attribute);
_allAttributes.Add(attribute);
} }
/// <summary> /// <summary>
@ -216,24 +209,13 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
throw new ArgumentNullException(nameof(name)); throw new ArgumentNullException(nameof(name));
} }
AllAttributes.Add(name, value); EnsureAllAttributes();
_allAttributes.Add(name, value);
} }
/// <summary> // Internal for testing.
/// Executes children asynchronously with the given <paramref name="encoder"/> in scope and returns their internal async Task<TagHelperContent> GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder)
/// rendered content.
/// </summary>
/// <param name="useCachedResult">
/// If <c>true</c>, multiple calls with the same <see cref="HtmlEncoder"/> will not cause children to
/// re-execute; returns cached content.
/// </param>
/// <param name="encoder">
/// The <see cref="HtmlEncoder"/> to use when the page handles
/// non-<see cref="Microsoft.AspNetCore.Html.IHtmlContent"/> C# expressions. If <c>null</c>, executes children with
/// the page's current <see cref="HtmlEncoder"/>.
/// </param>
/// <returns>A <see cref="Task"/> that on completion returns the rendered child content.</returns>
public async Task<TagHelperContent> GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder)
{ {
// Get cached content for this encoder. // Get cached content for this encoder.
TagHelperContent childContent; TagHelperContent childContent;
@ -272,5 +254,21 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
return new DefaultTagHelperContent().SetContent(childContent); return new DefaultTagHelperContent().SetContent(childContent);
} }
private void EnsureHtmlAttributes()
{
if (_htmlAttributes == null)
{
_htmlAttributes = new TagHelperAttributeList();
}
}
private void EnsureAllAttributes()
{
if (_allAttributes == null)
{
_allAttributes = new TagHelperAttributeList();
}
}
} }
} }

View File

@ -27,10 +27,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
throw new ArgumentNullException(nameof(executionContext)); throw new ArgumentNullException(nameof(executionContext));
} }
var tagHelperContext = new TagHelperContext( var tagHelperContext = executionContext.CreateTagHelperContext();
executionContext.AllAttributes,
executionContext.Items,
executionContext.UniqueId);
OrderTagHelpers(executionContext.TagHelpers); OrderTagHelpers(executionContext.TagHelpers);
@ -39,13 +36,7 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
executionContext.TagHelpers[i].Init(tagHelperContext); executionContext.TagHelpers[i].Init(tagHelperContext);
} }
var tagHelperOutput = new TagHelperOutput( var tagHelperOutput = executionContext.CreateTagHelperOutput();
executionContext.TagName,
executionContext.HtmlAttributes,
executionContext.GetChildContentAsync)
{
TagMode = executionContext.TagMode,
};
for (var i = 0; i < executionContext.TagHelpers.Count; i++) for (var i = 0; i < executionContext.TagHelpers.Count; i++)
{ {

View File

@ -31,8 +31,8 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// <paramref name="attributes"/>. /// <paramref name="attributes"/>.
/// </summary> /// </summary>
/// <param name="attributes">The collection to wrap.</param> /// <param name="attributes">The collection to wrap.</param>
public ReadOnlyTagHelperAttributeList(IEnumerable<TagHelperAttribute> attributes) public ReadOnlyTagHelperAttributeList(IList<TagHelperAttribute> attributes)
: base(new List<TagHelperAttribute>(attributes)) : base(attributes)
{ {
} }

View File

@ -25,6 +25,20 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// </summary> /// </summary>
/// <param name="attributes">The collection to wrap.</param> /// <param name="attributes">The collection to wrap.</param>
public TagHelperAttributeList(IEnumerable<TagHelperAttribute> attributes) public TagHelperAttributeList(IEnumerable<TagHelperAttribute> attributes)
: base (new List<TagHelperAttribute>(attributes))
{
if (attributes == null)
{
throw new ArgumentNullException(nameof(attributes));
}
}
/// <summary>
/// Instantiates a new instance of <see cref="TagHelperAttributeList"/> with the specified
/// <paramref name="attributes"/>.
/// </summary>
/// <param name="attributes">The collection to wrap.</param>
public TagHelperAttributeList(List<TagHelperAttribute> attributes)
: base(attributes) : base(attributes)
{ {
if (attributes == null) if (attributes == null)

View File

@ -11,8 +11,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// </summary> /// </summary>
public class TagHelperContext public class TagHelperContext
{ {
private ReadOnlyTagHelperAttributeList _allAttributes; private static ReadOnlyTagHelperAttributeList EmptyAttributes = new TagHelperAttributeList();
private IEnumerable<TagHelperAttribute> _allAttributesData;
/// <summary> /// <summary>
/// Instantiates a new <see cref="TagHelperContext"/>. /// Instantiates a new <see cref="TagHelperContext"/>.
@ -22,15 +21,10 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// <param name="uniqueId">The unique identifier for the source element this <see cref="TagHelperContext" /> /// <param name="uniqueId">The unique identifier for the source element this <see cref="TagHelperContext" />
/// applies to.</param> /// applies to.</param>
public TagHelperContext( public TagHelperContext(
IEnumerable<TagHelperAttribute> allAttributes, ReadOnlyTagHelperAttributeList allAttributes,
IDictionary<object, object> items, IDictionary<object, object> items,
string uniqueId) string uniqueId)
{ {
if (allAttributes == null)
{
throw new ArgumentNullException(nameof(allAttributes));
}
if (items == null) if (items == null)
{ {
throw new ArgumentNullException(nameof(items)); throw new ArgumentNullException(nameof(items));
@ -41,7 +35,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
throw new ArgumentNullException(nameof(uniqueId)); throw new ArgumentNullException(nameof(uniqueId));
} }
_allAttributesData = allAttributes; AllAttributes = allAttributes ?? EmptyAttributes;
Items = items; Items = items;
UniqueId = uniqueId; UniqueId = uniqueId;
} }
@ -49,18 +43,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// <summary> /// <summary>
/// Every attribute associated with the current HTML element. /// Every attribute associated with the current HTML element.
/// </summary> /// </summary>
public ReadOnlyTagHelperAttributeList AllAttributes public ReadOnlyTagHelperAttributeList AllAttributes { get; }
{
get
{
if (_allAttributes == null)
{
_allAttributes = new TagHelperAttributeList(_allAttributesData);
}
return _allAttributes;
}
}
/// <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.

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
public class TagHelperOutput : IHtmlContent public class TagHelperOutput : IHtmlContent
{ {
private readonly Func<bool, HtmlEncoder, Task<TagHelperContent>> _getChildContentAsync; private readonly Func<bool, HtmlEncoder, Task<TagHelperContent>> _getChildContentAsync;
private TagHelperAttributeList _attributes;
private TagHelperContent _preElement; private TagHelperContent _preElement;
private TagHelperContent _preContent; private TagHelperContent _preContent;
private TagHelperContent _content; private TagHelperContent _content;
@ -26,7 +27,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
internal TagHelperOutput(string tagName) internal TagHelperOutput(string tagName)
: this( : this(
tagName, tagName,
new TagHelperAttributeList(), null,
(useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent())) (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()))
{ {
} }
@ -45,19 +46,14 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
TagHelperAttributeList attributes, TagHelperAttributeList attributes,
Func<bool, HtmlEncoder, Task<TagHelperContent>> getChildContentAsync) Func<bool, HtmlEncoder, Task<TagHelperContent>> getChildContentAsync)
{ {
if (attributes == null)
{
throw new ArgumentNullException(nameof(attributes));
}
if (getChildContentAsync == null) if (getChildContentAsync == null)
{ {
throw new ArgumentNullException(nameof(getChildContentAsync)); throw new ArgumentNullException(nameof(getChildContentAsync));
} }
TagName = tagName; TagName = tagName;
Attributes = attributes;
_getChildContentAsync = getChildContentAsync; _getChildContentAsync = getChildContentAsync;
_attributes = attributes;
} }
/// <summary> /// <summary>
@ -187,7 +183,18 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
/// a <c>Microsoft.AspNetCore.Mvc.Rendering.HtmlString</c> instance. MVC converts most other types to a /// a <c>Microsoft.AspNetCore.Mvc.Rendering.HtmlString</c> instance. MVC converts most other types to a
/// <see cref="string"/>, then HTML encodes the result. /// <see cref="string"/>, then HTML encodes the result.
/// </remarks> /// </remarks>
public TagHelperAttributeList Attributes { get; } public TagHelperAttributeList Attributes
{
get
{
if (_attributes == null)
{
_attributes = new TagHelperAttributeList();
}
return _attributes;
}
}
/// <summary> /// <summary>
/// Changes <see cref="TagHelperOutput"/> to generate nothing. /// Changes <see cref="TagHelperOutput"/> to generate nothing.
@ -287,9 +294,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
writer.Write(TagName); writer.Write(TagName);
// Perf: Avoid allocating enumerator // 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(" ");
writer.Write(attribute.Name); writer.Write(attribute.Name);

View File

@ -18,13 +18,16 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
[InlineData(TagMode.SelfClosing)] [InlineData(TagMode.SelfClosing)]
[InlineData(TagMode.StartTagAndEndTag)] [InlineData(TagMode.StartTagAndEndTag)]
[InlineData(TagMode.StartTagOnly)] [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); var executionContext = new TagHelperExecutionContext("p", tagMode);
// Act
var output = executionContext.CreateTagHelperOutput();
// Assert // Assert
Assert.Equal(tagMode, executionContext.TagMode); Assert.Equal(tagMode, output.TagMode);
} }
[Fact] [Fact]
@ -270,51 +273,6 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
Assert.NotSame(content1, content2); Assert.NotSame(content1, content2);
} }
public static TheoryData<string, string> DictionaryCaseTestingData
{
get
{
return new TheoryData<string, string>
{
{ "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] [Fact]
public void AddHtmlAttribute_MaintainsHtmlAttributes() public void AddHtmlAttribute_MaintainsHtmlAttributes()
{ {
@ -329,11 +287,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
// Act // Act
executionContext.AddHtmlAttribute("class", "btn"); executionContext.AddHtmlAttribute("class", "btn");
executionContext.AddHtmlAttribute("foo", "bar"); executionContext.AddHtmlAttribute("foo", "bar");
var output = executionContext.CreateTagHelperOutput();
// Assert // Assert
Assert.Equal( Assert.Equal(
expectedAttributes, expectedAttributes,
executionContext.HtmlAttributes, output.Attributes,
CaseSensitiveTagHelperAttributeComparer.Default); CaseSensitiveTagHelperAttributeComparer.Default);
} }
@ -351,11 +310,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
// Act // Act
executionContext.AddMinimizedHtmlAttribute("checked"); executionContext.AddMinimizedHtmlAttribute("checked");
executionContext.AddMinimizedHtmlAttribute("visible"); executionContext.AddMinimizedHtmlAttribute("visible");
var output = executionContext.CreateTagHelperOutput();
// Assert // Assert
Assert.Equal( Assert.Equal(
expectedAttributes, expectedAttributes,
executionContext.HtmlAttributes, output.Attributes,
CaseSensitiveTagHelperAttributeComparer.Default); CaseSensitiveTagHelperAttributeComparer.Default);
} }
@ -377,11 +337,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
executionContext.AddHtmlAttribute("foo", "bar"); executionContext.AddHtmlAttribute("foo", "bar");
executionContext.AddMinimizedHtmlAttribute("checked"); executionContext.AddMinimizedHtmlAttribute("checked");
executionContext.AddMinimizedHtmlAttribute("visible"); executionContext.AddMinimizedHtmlAttribute("visible");
var output = executionContext.CreateTagHelperOutput();
// Assert // Assert
Assert.Equal( Assert.Equal(
expectedAttributes, expectedAttributes,
executionContext.HtmlAttributes, output.Attributes,
CaseSensitiveTagHelperAttributeComparer.Default); CaseSensitiveTagHelperAttributeComparer.Default);
} }
@ -401,11 +362,12 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
executionContext.AddHtmlAttribute("class", "btn"); executionContext.AddHtmlAttribute("class", "btn");
executionContext.AddTagHelperAttribute("something", true); executionContext.AddTagHelperAttribute("something", true);
executionContext.AddHtmlAttribute("foo", "bar"); executionContext.AddHtmlAttribute("foo", "bar");
var context = executionContext.CreateTagHelperContext();
// Assert // Assert
Assert.Equal( Assert.Equal(
expectedAttributes, expectedAttributes,
executionContext.AllAttributes, context.AllAttributes,
CaseSensitiveTagHelperAttributeComparer.Default); CaseSensitiveTagHelperAttributeComparer.Default);
} }

View File

@ -125,16 +125,17 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
} }
[Fact] [Fact]
public void Begin_CreatesContextWithAppropriateTagName() public void Begin_CreatesContexts_TagHelperOutput_WithAppropriateTagName()
{ {
// Arrange // Arrange
var scopeManager = new TagHelperScopeManager(); var scopeManager = new TagHelperScopeManager();
// Act // Act
var executionContext = BeginDefaultScope(scopeManager, tagName: "p"); var executionContext = BeginDefaultScope(scopeManager, tagName: "p");
var output = executionContext.CreateTagHelperOutput();
// Assert // Assert
Assert.Equal("p", executionContext.TagName); Assert.Equal("p", output.TagName);
} }
[Fact] [Fact]
@ -146,25 +147,27 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
// Act // Act
var executionContext = BeginDefaultScope(scopeManager, tagName: "p"); var executionContext = BeginDefaultScope(scopeManager, tagName: "p");
executionContext = BeginDefaultScope(scopeManager, tagName: "div"); executionContext = BeginDefaultScope(scopeManager, tagName: "div");
var output = executionContext.CreateTagHelperOutput();
// Assert // Assert
Assert.Equal("div", executionContext.TagName); Assert.Equal("div", output.TagName);
} }
[Theory] [Theory]
[InlineData(TagMode.SelfClosing)] [InlineData(TagMode.SelfClosing)]
[InlineData(TagMode.StartTagAndEndTag)] [InlineData(TagMode.StartTagAndEndTag)]
[InlineData(TagMode.StartTagOnly)] [InlineData(TagMode.StartTagOnly)]
public void Begin_SetsExecutionContextTagMode(TagMode tagMode) public void Begin_SetsExecutionContexts_TagHelperOutputTagMode(TagMode tagMode)
{ {
// Arrange // Arrange
var scopeManager = new TagHelperScopeManager(); var scopeManager = new TagHelperScopeManager();
// Act // Act
var executionContext = BeginDefaultScope(scopeManager, "p", tagMode); var executionContext = BeginDefaultScope(scopeManager, "p", tagMode);
var output = executionContext.CreateTagHelperOutput();
// Assert // Assert
Assert.Equal(tagMode, executionContext.TagMode); Assert.Equal(tagMode, output.TagMode);
} }
[Fact] [Fact]
@ -177,9 +180,10 @@ namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers
var executionContext = BeginDefaultScope(scopeManager, tagName: "p"); var executionContext = BeginDefaultScope(scopeManager, tagName: "p");
executionContext = BeginDefaultScope(scopeManager, tagName: "div"); executionContext = BeginDefaultScope(scopeManager, tagName: "div");
executionContext = scopeManager.End(); executionContext = scopeManager.End();
var output = executionContext.CreateTagHelperOutput();
// Assert // Assert
Assert.Equal("p", executionContext.TagName); Assert.Equal("p", output.TagName);
} }
[Fact] [Fact]

View File

@ -730,7 +730,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
private class TestableReadOnlyTagHelperAttributes : ReadOnlyTagHelperAttributeList private class TestableReadOnlyTagHelperAttributes : ReadOnlyTagHelperAttributeList
{ {
public TestableReadOnlyTagHelperAttributes(IEnumerable<TagHelperAttribute> attributes) public TestableReadOnlyTagHelperAttributes(IEnumerable<TagHelperAttribute> attributes)
: base(attributes) : base(new List<TagHelperAttribute>(attributes))
{ {
} }