Remove the use of `Linq` and unneeded enumerators from Mvc `TagHelper`s.

- Prior to this change we were using `Linq` throughout our `TagHelper`s which resulted excess allocations.
- Also went through our data structures and changed some from `Enumerable<T>` to `IList<T>` where we were just not exposing the more accessible types (list).
- Changed several locations of `foreach` to `for` to remove the enumerator allocation.

#3599
This commit is contained in:
N. Taylor Mullen 2015-11-24 12:41:39 -08:00
parent a14fdd8637
commit 490fcf1ab4
18 changed files with 277 additions and 196 deletions

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
@ -32,6 +31,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private const string RouteValuesDictionaryName = "asp-all-route-data"; private const string RouteValuesDictionaryName = "asp-all-route-data";
private const string RouteValuesPrefix = "asp-route-"; private const string RouteValuesPrefix = "asp-route-";
private const string Href = "href"; private const string Href = "href";
private IDictionary<string, string> _routeValues;
/// <summary> /// <summary>
/// Creates a new <see cref="AnchorTagHelper"/>. /// Creates a new <see cref="AnchorTagHelper"/>.
@ -98,8 +98,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// Additional parameters for the route. /// Additional parameters for the route.
/// </summary> /// </summary>
[HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)] [HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)]
public IDictionary<string, string> RouteValues { get; set; } = public IDictionary<string, string> RouteValues
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); {
get
{
if (_routeValues == null)
{
_routeValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
return _routeValues;
}
set
{
_routeValues = value;
}
}
/// <inheritdoc /> /// <inheritdoc />
/// <remarks>Does nothing if user provides an <c>href</c> attribute.</remarks> /// <remarks>Does nothing if user provides an <c>href</c> attribute.</remarks>
@ -148,11 +162,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
} }
else else
{ {
// Convert from Dictionary<string, string> to Dictionary<string, object>. IDictionary<string, object> routeValues = null;
var routeValues = RouteValues.ToDictionary( if (_routeValues != null && _routeValues.Count > 0)
kvp => kvp.Key, {
kvp => (object)kvp.Value, // Convert from Dictionary<string, string> to Dictionary<string, object>.
StringComparer.OrdinalIgnoreCase); routeValues = new Dictionary<string, object>(_routeValues.Count, StringComparer.OrdinalIgnoreCase);
foreach (var routeValue in _routeValues)
{
routeValues.Add(routeValue.Key, routeValue.Value);
}
}
TagBuilder tagBuilder; TagBuilder tagBuilder;
if (Route == null) if (Route == null)
@ -180,13 +199,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
} }
else else
{ {
tagBuilder = Generator.GenerateRouteLink(linkText: string.Empty, tagBuilder = Generator.GenerateRouteLink(
routeName: Route, linkText: string.Empty,
protocol: Protocol, routeName: Route,
hostName: Host, protocol: Protocol,
fragment: Fragment, hostName: Host,
routeValues: routeValues, fragment: Fragment,
htmlAttributes: null); routeValues: routeValues,
htmlAttributes: null);
} }
if (tagBuilder != null) if (tagBuilder != null)

View File

@ -3,11 +3,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
@ -101,7 +99,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary> /// <summary>
/// Gets or sets a value that determines if the cached result is to be varied by the Identity for the logged in /// Gets or sets a value that determines if the cached result is to be varied by the Identity for the logged in
/// <see cref="HttpContext.User"/>. /// <see cref="Http.HttpContext.User"/>.
/// </summary> /// </summary>
[HtmlAttributeName(VaryByUserAttributeName)] [HtmlAttributeName(VaryByUserAttributeName)]
public bool VaryByUser { get; set; } public bool VaryByUser { get; set; }
@ -261,18 +259,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Append(keyName) .Append(keyName)
.Append("("); .Append("(");
var tokenFound = false; var values = Tokenize(value);
foreach (var item in Tokenize(value))
{
tokenFound = true;
// Perf: Avoid allocating enumerator
for (var i = 0; i < values.Count; i++)
{
var item = values[i];
builder.Append(item) builder.Append(item)
.Append(CacheKeyTokenSeparator) .Append(CacheKeyTokenSeparator)
.Append(sourceCollection[item]) .Append(sourceCollection[item])
.Append(CacheKeyTokenSeparator); .Append(CacheKeyTokenSeparator);
} }
if (tokenFound) if (values.Count > 0)
{ {
// Remove the trailing separator // Remove the trailing separator
builder.Length -= CacheKeyTokenSeparator.Length; builder.Length -= CacheKeyTokenSeparator.Length;
@ -282,12 +281,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
} }
} }
private static void AddStringCollectionKey<T>( private static void AddStringCollectionKey<TSourceCollection>(
StringBuilder builder, StringBuilder builder,
string keyName, string keyName,
string value, string value,
T sourceCollection, TSourceCollection sourceCollection,
Func<T, string, StringValues> accessor) Func<TSourceCollection, string, StringValues> accessor)
{ {
if (!string.IsNullOrEmpty(value)) if (!string.IsNullOrEmpty(value))
{ {
@ -296,10 +295,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Append(keyName) .Append(keyName)
.Append("("); .Append("(");
var tokenFound = false; var values = Tokenize(value);
foreach (var item in Tokenize(value))
// Perf: Avoid allocating enumerator
for (var i = 0; i < values.Count; i++)
{ {
tokenFound = true; var item = values[i];
builder.Append(item) builder.Append(item)
.Append(CacheKeyTokenSeparator) .Append(CacheKeyTokenSeparator)
@ -307,7 +308,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Append(CacheKeyTokenSeparator); .Append(CacheKeyTokenSeparator);
} }
if (tokenFound) if (values.Count > 0)
{ {
// Remove the trailing separator // Remove the trailing separator
builder.Length -= CacheKeyTokenSeparator.Length; builder.Length -= CacheKeyTokenSeparator.Length;
@ -327,8 +328,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Append(nameof(VaryByRoute)) .Append(nameof(VaryByRoute))
.Append("("); .Append("(");
foreach (var route in Tokenize(VaryByRoute)) var varyByRoutes = Tokenize(VaryByRoute);
for (var i = 0; i < varyByRoutes.Count; i++)
{ {
var route = varyByRoutes[i];
tokenFound = true; tokenFound = true;
builder.Append(route) builder.Append(route)
@ -346,11 +349,27 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
} }
} }
private static IEnumerable<string> Tokenize(string value) private static IList<string> Tokenize(string value)
{ {
return value.Split(AttributeSeparator, StringSplitOptions.RemoveEmptyEntries) var values = value.Split(AttributeSeparator, StringSplitOptions.RemoveEmptyEntries);
.Select(token => token.Trim()) if (values.Length == 0)
.Where(token => token.Length > 0); {
return values;
}
var trimmedValues = new List<string>();
for (var i = 0; i < values.Length; i++)
{
var trimmedValue = values[i].Trim();
if (trimmedValue.Length > 0)
{
trimmedValues.Add(trimmedValue);
}
}
return trimmedValues;
} }
} }
} }

View File

@ -2,7 +2,7 @@
// 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;
using System.Linq; using System.Collections.Generic;
using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
@ -67,30 +67,41 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
return; return;
} }
var environments = Names.Split(NameSeparator, StringSplitOptions.RemoveEmptyEntries)
.Where(name => !string.IsNullOrWhiteSpace(name));
if (!environments.Any())
{
// Names contains only commas or empty entries, do nothing
return;
}
var currentEnvironmentName = HostingEnvironment.EnvironmentName?.Trim(); var currentEnvironmentName = HostingEnvironment.EnvironmentName?.Trim();
if (string.IsNullOrEmpty(currentEnvironmentName))
if (string.IsNullOrWhiteSpace(currentEnvironmentName))
{ {
// No current environment name, do nothing // No current environment name, do nothing
return; return;
} }
if (environments.Any(name => var values = Names.Split(NameSeparator, StringSplitOptions.RemoveEmptyEntries);
string.Equals(name.Trim(), currentEnvironmentName, StringComparison.OrdinalIgnoreCase))) var environments = new List<string>();
for (var i = 0; i < values.Length; i++)
{ {
// Matching environment name found, do nothing var trimmedValue = values[i].Trim();
if (trimmedValue.Length > 0)
{
environments.Add(trimmedValue);
}
}
if (environments.Count == 0)
{
// Names contains only commas or empty entries, do nothing
return; return;
} }
for (var i = 0; i < environments.Count; i++)
{
if (string.Equals(environments[i], currentEnvironmentName, StringComparison.OrdinalIgnoreCase))
{
// Matching environment name found, do nothing
return;
}
}
// No matching environment name found, suppress all output // No matching environment name found, suppress all output
output.SuppressOutput(); output.SuppressOutput();
} }

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
@ -28,6 +27,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
private const string RouteValuesDictionaryName = "asp-all-route-data"; private const string RouteValuesDictionaryName = "asp-all-route-data";
private const string RouteValuesPrefix = "asp-route-"; private const string RouteValuesPrefix = "asp-route-";
private const string HtmlActionAttributeName = "action"; private const string HtmlActionAttributeName = "action";
private IDictionary<string, string> _routeValues;
/// <summary> /// <summary>
/// Creates a new <see cref="FormTagHelper"/>. /// Creates a new <see cref="FormTagHelper"/>.
@ -85,8 +85,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// Additional parameters for the route. /// Additional parameters for the route.
/// </summary> /// </summary>
[HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)] [HtmlAttributeName(RouteValuesDictionaryName, DictionaryAttributePrefix = RouteValuesPrefix)]
public IDictionary<string, string> RouteValues { get; set; } = public IDictionary<string, string> RouteValues
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); {
get
{
if (_routeValues == null)
{
_routeValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
return _routeValues;
}
set
{
_routeValues = value;
}
}
/// <inheritdoc /> /// <inheritdoc />
/// <remarks> /// <remarks>
@ -133,11 +147,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
} }
else else
{ {
// Convert from Dictionary<string, string> to Dictionary<string, object>. IDictionary<string, object> routeValues = null;
var routeValues = RouteValues.ToDictionary( if (_routeValues != null && _routeValues.Count > 0)
kvp => kvp.Key, {
kvp => (object)kvp.Value, // Convert from Dictionary<string, string> to Dictionary<string, object>.
StringComparer.OrdinalIgnoreCase); routeValues = new Dictionary<string, object>(_routeValues.Count, StringComparer.OrdinalIgnoreCase);
foreach (var routeValue in _routeValues)
{
routeValues.Add(routeValue.Key, routeValue.Value);
}
}
TagBuilder tagBuilder; TagBuilder tagBuilder;
if (Route == null) if (Route == null)

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
@ -247,9 +246,11 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Prepare to move attributes from current element to <input type="checkbox"/> generated just below. // Prepare to move attributes from current element to <input type="checkbox"/> generated just below.
var htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); var htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
// Perf: Avoid allocating enumerator
// Construct attributes correctly (first attribute wins). // Construct attributes correctly (first attribute wins).
foreach (var attribute in output.Attributes) for (var i = 0; i < output.Attributes.Count; i++)
{ {
var attribute = output.Attributes[i];
if (!htmlAttributes.ContainsKey(attribute.Name)) if (!htmlAttributes.ContainsKey(attribute.Name))
{ {
htmlAttributes.Add(attribute.Name, attribute.Value); htmlAttributes.Add(attribute.Name, attribute.Value);
@ -389,15 +390,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// A variant of TemplateRenderer.GetViewNames(). Main change relates to bool? handling. // A variant of TemplateRenderer.GetViewNames(). Main change relates to bool? handling.
private static IEnumerable<string> GetInputTypeHints(ModelExplorer modelExplorer) private static IEnumerable<string> GetInputTypeHints(ModelExplorer modelExplorer)
{ {
var inputTypeHints = new string[] if (!string.IsNullOrEmpty(modelExplorer.Metadata.TemplateHint))
{ {
modelExplorer.Metadata.TemplateHint, yield return modelExplorer.Metadata.TemplateHint;
modelExplorer.Metadata.DataTypeName, }
};
foreach (string inputTypeHint in inputTypeHints.Where(s => !string.IsNullOrEmpty(s))) if (!string.IsNullOrEmpty(modelExplorer.Metadata.DataTypeName))
{ {
yield return inputTypeHint; yield return modelExplorer.Metadata.DataTypeName;
} }
// In most cases, we don't want to search for Nullable<T>. We want to search for T, which should handle // In most cases, we don't want to search for Nullable<T>. We want to search for T, which should handle

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
@ -23,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// <returns>The <see cref="ModeMatchResult{TMode}"/>.</returns> /// <returns>The <see cref="ModeMatchResult{TMode}"/>.</returns>
public static ModeMatchResult<TMode> DetermineMode<TMode>( public static ModeMatchResult<TMode> DetermineMode<TMode>(
TagHelperContext context, TagHelperContext context,
IEnumerable<ModeAttributes<TMode>> modeInfos) IReadOnlyList<ModeAttributes<TMode>> modeInfos)
{ {
if (context == null) if (context == null)
{ {
@ -39,27 +38,32 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var matchedAttributes = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase); var matchedAttributes = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
var result = new ModeMatchResult<TMode>(); var result = new ModeMatchResult<TMode>();
foreach (var modeInfo in modeInfos) // Perf: Avoid allocating enumerator
for (var i = 0; i < modeInfos.Count; i++)
{ {
var modeInfo = modeInfos[i];
var modeAttributes = GetPresentMissingAttributes(context, modeInfo.Attributes); var modeAttributes = GetPresentMissingAttributes(context, modeInfo.Attributes);
if (modeAttributes.Present.Any()) if (modeAttributes.Present.Count > 0)
{ {
if (!modeAttributes.Missing.Any()) if (modeAttributes.Missing.Count == 0)
{ {
// Perf: Avoid allocating enumerator
// A complete match, mark the attribute as fully matched // A complete match, mark the attribute as fully matched
foreach (var attribute in modeAttributes.Present) for (var j = 0; j < modeAttributes.Present.Count; j++)
{ {
matchedAttributes[attribute] = true; matchedAttributes[modeAttributes.Present[j]] = true;
} }
result.FullMatches.Add(ModeMatchAttributes.Create(modeInfo.Mode, modeInfo.Attributes)); result.FullMatches.Add(ModeMatchAttributes.Create(modeInfo.Mode, modeInfo.Attributes));
} }
else else
{ {
// Perf: Avoid allocating enumerator
// A partial match, mark the attribute as partially matched if not already fully matched // A partial match, mark the attribute as partially matched if not already fully matched
foreach (var attribute in modeAttributes.Present) for (var j = 0; j < modeAttributes.Present.Count; j++)
{ {
var attribute = modeAttributes.Present[j];
bool attributeMatch; bool attributeMatch;
if (!matchedAttributes.TryGetValue(attribute, out attributeMatch)) if (!matchedAttributes.TryGetValue(attribute, out attributeMatch))
{ {
@ -87,14 +91,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
private static PresentMissingAttributes GetPresentMissingAttributes( private static PresentMissingAttributes GetPresentMissingAttributes(
TagHelperContext context, TagHelperContext context,
IEnumerable<string> requiredAttributes) string[] requiredAttributes)
{ {
// Check for all attribute values // Check for all attribute values
var presentAttributes = new List<string>(); var presentAttributes = new List<string>();
var missingAttributes = new List<string>(); var missingAttributes = new List<string>();
foreach (var attribute in requiredAttributes) // Perf: Avoid allocating enumerator
for (var i = 0; i < requiredAttributes.Length; i++)
{ {
var attribute = requiredAttributes[i];
if (!context.AllAttributes.ContainsName(attribute) || if (!context.AllAttributes.ContainsName(attribute) ||
context.AllAttributes[attribute] == null || context.AllAttributes[attribute] == null ||
(context.AllAttributes[attribute].Value is string && (context.AllAttributes[attribute].Value is string &&
@ -114,9 +120,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
private class PresentMissingAttributes private class PresentMissingAttributes
{ {
public IEnumerable<string> Present { get; set; } public List<string> Present { get; set; }
public IEnumerable<string> Missing { get; set; } public List<string> Missing { get; set; }
} }
} }
} }

View File

@ -73,7 +73,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// <param name="includePattern">The file globbing include pattern.</param> /// <param name="includePattern">The file globbing include pattern.</param>
/// <param name="excludePattern">The file globbing exclude pattern.</param> /// <param name="excludePattern">The file globbing exclude pattern.</param>
/// <returns>The list of URLs</returns> /// <returns>The list of URLs</returns>
public virtual IEnumerable<string> BuildUrlList(string staticUrl, string includePattern, string excludePattern) public virtual ICollection<string> BuildUrlList(string staticUrl, string includePattern, string excludePattern)
{ {
var urls = new HashSet<string>(StringComparer.Ordinal); var urls = new HashSet<string>(StringComparer.Ordinal);
@ -112,9 +112,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
if (!Cache.TryGetValue(cacheKey, out files)) if (!Cache.TryGetValue(cacheKey, out files))
{ {
var options = new MemoryCacheEntryOptions(); var options = new MemoryCacheEntryOptions();
foreach (var pattern in includePatterns)
for (var i = 0; i < includePatterns.Length; i++)
{ {
var changeToken = FileProvider.Watch(pattern); var changeToken = FileProvider.Watch(includePatterns[i]);
options.AddExpirationToken(changeToken); options.AddExpirationToken(changeToken);
} }
@ -128,15 +129,24 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
return FindFiles(includePatterns, excludePatterns); return FindFiles(includePatterns, excludePatterns);
} }
private IEnumerable<string> FindFiles(IEnumerable<string> includePatterns, IEnumerable<string> excludePatterns) private IEnumerable<string> FindFiles(string[] includePatterns, string[] excludePatterns)
{ {
var matcher = MatcherBuilder != null ? MatcherBuilder() : new Matcher(); var matcher = MatcherBuilder != null ? MatcherBuilder() : new Matcher();
var trimmedIncludePatterns = new List<string>();
matcher.AddIncludePatterns(includePatterns.Select(pattern => TrimLeadingTildeSlash(pattern))); for (var i = 0; i < includePatterns.Length; i++)
{
trimmedIncludePatterns.Add(TrimLeadingTildeSlash(includePatterns[i]));
}
matcher.AddIncludePatterns(trimmedIncludePatterns);
if (excludePatterns != null) if (excludePatterns != null)
{ {
matcher.AddExcludePatterns(excludePatterns.Select(pattern => TrimLeadingTildeSlash(pattern))); var trimmedExcludePatterns = new List<string>();
for (var i = 0; i < excludePatterns.Length; i++)
{
trimmedExcludePatterns.Add(TrimLeadingTildeSlash(excludePatterns[i]));
}
matcher.AddExcludePatterns(trimmedExcludePatterns);
} }
var matches = matcher.Execute(_baseGlobbingDirectory); var matches = matcher.Execute(_baseGlobbingDirectory);

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// <summary> /// <summary>
/// Encodes a .NET string array for safe use as a JavaScript array literal, including inline in an HTML file. /// Encodes a .NET string array for safe use as a JavaScript array literal, including inline in an HTML file.
/// </summary> /// </summary>
public static string Encode(JavaScriptEncoder encoder, IEnumerable<string> values) public static string Encode(JavaScriptEncoder encoder, string[] values)
{ {
var writer = new StringWriter(); var writer = new StringWriter();
@ -23,14 +23,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
writer.Write('['); writer.Write('[');
foreach (var value in values) // Perf: Avoid allocating enumerator
for (var i = 0; i < values.Length; i++)
{ {
if (firstAdded) if (firstAdded)
{ {
writer.Write(','); writer.Write(',');
} }
writer.Write('"'); writer.Write('"');
encoder.Encode(writer, value); encoder.Encode(writer, values[i]);
writer.Write('"'); writer.Write('"');
firstAdded = true; firstAdded = true;
} }

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// <summary> /// <summary>
/// Creates an <see cref="ModeAttributes{TMode}"/>/ /// Creates an <see cref="ModeAttributes{TMode}"/>/
/// </summary> /// </summary>
public static ModeAttributes<TMode> Create<TMode>(TMode mode, IEnumerable<string> attributes) public static ModeAttributes<TMode> Create<TMode>(TMode mode, string[] attributes)
{ {
return new ModeAttributes<TMode> return new ModeAttributes<TMode>
{ {

View File

@ -1,25 +1,22 @@
// 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.Collections.Generic;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{ {
/// <summary> /// <summary>
/// A mapping of a <see cref="ITagHelper"/> mode to its required attributes. /// A mapping of a <see cref="AspNet.Razor.TagHelpers.ITagHelper"/> mode to its required attributes.
/// </summary> /// </summary>
/// <typeparam name="TMode">The type representing the <see cref="ITagHelper"/>'s mode.</typeparam> /// <typeparam name="TMode">The type representing the <see cref="AspNet.Razor.TagHelpers.ITagHelper"/>'s mode.</typeparam>
public class ModeAttributes<TMode> public class ModeAttributes<TMode>
{ {
/// <summary> /// <summary>
/// The <see cref="ITagHelper"/>'s mode. /// The <see cref="AspNet.Razor.TagHelpers.ITagHelper"/>'s mode.
/// </summary> /// </summary>
public TMode Mode { get; set; } public TMode Mode { get; set; }
/// <summary> /// <summary>
/// The names of attributes required for this mode. /// The names of attributes required for this mode.
/// </summary> /// </summary>
public IEnumerable<string> Attributes { get; set; } public string[] Attributes { get; set; }
} }
} }

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// </summary> /// </summary>
public static ModeMatchAttributes<TMode> Create<TMode>( public static ModeMatchAttributes<TMode> Create<TMode>(
TMode mode, TMode mode,
IEnumerable<string> presentAttributes) IList<string> presentAttributes)
{ {
return Create(mode, presentAttributes, missingAttributes: null); return Create(mode, presentAttributes, missingAttributes: null);
} }
@ -25,8 +25,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// </summary> /// </summary>
public static ModeMatchAttributes<TMode> Create<TMode>( public static ModeMatchAttributes<TMode> Create<TMode>(
TMode mode, TMode mode,
IEnumerable<string> presentAttributes, IList<string> presentAttributes,
IEnumerable<string> missingAttributes) IList<string> missingAttributes)
{ {
return new ModeMatchAttributes<TMode> return new ModeMatchAttributes<TMode>
{ {

View File

@ -2,29 +2,28 @@
// 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.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{ {
/// <summary> /// <summary>
/// A mapping of a <see cref="ITagHelper"/> mode to its missing and present attributes. /// A mapping of a <see cref="AspNet.Razor.TagHelpers.ITagHelper"/> mode to its missing and present attributes.
/// </summary> /// </summary>
/// <typeparam name="TMode">The type representing the <see cref="ITagHelper"/>'s mode.</typeparam> /// <typeparam name="TMode">The type representing the <see cref="AspNet.Razor.TagHelpers.ITagHelper"/>'s mode.</typeparam>
public class ModeMatchAttributes<TMode> public class ModeMatchAttributes<TMode>
{ {
/// <summary> /// <summary>
/// The <see cref="ITagHelper"/>'s mode. /// The <see cref="AspNet.Razor.TagHelpers.ITagHelper"/>'s mode.
/// </summary> /// </summary>
public TMode Mode { get; set; } public TMode Mode { get; set; }
/// <summary> /// <summary>
/// The names of attributes that were present in this match. /// The names of attributes that were present in this match.
/// </summary> /// </summary>
public IEnumerable<string> PresentAttributes { get; set; } public IList<string> PresentAttributes { get; set; }
/// <summary> /// <summary>
/// The names of attributes that were missing in this match. /// The names of attributes that were missing in this match.
/// </summary> /// </summary>
public IEnumerable<string> MissingAttributes { get; set; } public IList<string> MissingAttributes { get; set; }
} }
} }

View File

@ -1,20 +1,14 @@
// 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 Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Mvc.TagHelpers.Logging;
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{ {
/// <summary> /// <summary>
/// Result of determining the mode an <see cref="ITagHelper"/> will run in. /// Result of determining the mode an <see cref="AspNet.Razor.TagHelpers.ITagHelper"/> will run in.
/// </summary> /// </summary>
/// <typeparam name="TMode">The type representing the <see cref="ITagHelper"/>'s mode.</typeparam> /// <typeparam name="TMode">The type representing the <see cref="AspNet.Razor.TagHelpers.ITagHelper"/>'s mode.</typeparam>
public class ModeMatchResult<TMode> public class ModeMatchResult<TMode>
{ {
/// <summary> /// <summary>
@ -32,47 +26,5 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// <see cref="FullMatches"/>. /// <see cref="FullMatches"/>.
/// </summary> /// </summary>
public IList<string> PartiallyMatchedAttributes { get; } = new List<string>(); public IList<string> PartiallyMatchedAttributes { get; } = new List<string>();
/// <summary>
/// Logs the details of the <see cref="ModeMatchResult{TMode}"/>.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="tagHelper">The <see cref="ITagHelper"/>.</param>
/// <param name="uniqueId">The value of <see cref="TagHelperContext.UniqueId"/>.</param>
/// <param name="viewPath">The path to the view the <see cref="ITagHelper"/> is on.</param>
public void LogDetails<TTagHelper>(
ILogger logger,
TTagHelper tagHelper,
string uniqueId,
string viewPath)
where TTagHelper : ITagHelper
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
if (logger.IsEnabled(LogLevel.Warning) && PartiallyMatchedAttributes.Any())
{
// Build the list of partial matches that contain attributes not appearing in at least one full match
var partialOnlyMatches = PartialMatches.Where(
match => match.PresentAttributes.Any(
attribute => PartiallyMatchedAttributes.Contains(
attribute, StringComparer.OrdinalIgnoreCase)));
logger.TagHelperPartialMatches<TMode>(uniqueId, viewPath, partialOnlyMatches);
}
if (logger.IsEnabled(LogLevel.Verbose) && !FullMatches.Any())
{
logger.TagHelperSkippingProcessing(
tagHelper,
uniqueId);
}
}
} }
} }

View File

@ -10,6 +10,7 @@ using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Mvc.Razor.TagHelpers; using Microsoft.AspNet.Mvc.Razor.TagHelpers;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.TagHelpers.Internal; using Microsoft.AspNet.Mvc.TagHelpers.Internal;
using Microsoft.AspNet.Mvc.TagHelpers.Logging;
using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
@ -241,10 +242,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Href = output.Attributes[HrefAttributeName]?.Value as string; Href = output.Attributes[HrefAttributeName]?.Value as string;
var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails); var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails);
Logger.TagHelperModeMatchResult(modeResult, context.UniqueId, ViewContext.View.Path, this);
modeResult.LogDetails(Logger, this, context.UniqueId, ViewContext.View.Path); if (modeResult.FullMatches.Count == 0)
if (!modeResult.FullMatches.Any())
{ {
// No attributes matched so we have nothing to do // No attributes matched so we have nothing to do
return; return;
@ -266,7 +266,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var builder = new DefaultTagHelperContent(); var builder = new DefaultTagHelperContent();
// Get the highest matched mode // Get the highest matched mode
var mode = modeResult.FullMatches.Select(match => match.Mode).Max(); var mode = modeResult.FullMatches[0].Mode;
for (var i = 1; i < modeResult.FullMatches.Count; i++)
{
var currentMode = modeResult.FullMatches[i].Mode;
if (mode < currentMode)
{
mode = currentMode;
}
}
if (mode == Mode.GlobbedHref || mode == Mode.Fallback && !string.IsNullOrEmpty(HrefInclude)) if (mode == Mode.GlobbedHref || mode == Mode.Fallback && !string.IsNullOrEmpty(HrefInclude))
{ {
@ -384,8 +392,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
builder.AppendHtml("<link "); builder.AppendHtml("<link ");
foreach (var attribute in attributes) // Perf: Avoid allocating enumerator
for (var i = 0; i < attributes.Count; i++)
{ {
var attribute = attributes[i];
var attributeValue = attribute.Value; var attributeValue = attribute.Value;
if (AppendVersion == true && if (AppendVersion == true &&
string.Equals(attribute.Name, HrefAttributeName, StringComparison.OrdinalIgnoreCase)) string.Equals(attribute.Name, HrefAttributeName, StringComparison.OrdinalIgnoreCase))

View File

@ -19,30 +19,54 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Logging
"Skipping processing for tag helper '{TagHelper}' with id '{TagHelperId}'."); "Skipping processing for tag helper '{TagHelper}' with id '{TagHelperId}'.");
} }
public static void TagHelperPartialMatches<TMode>( public static void TagHelperModeMatchResult<TMode>(
this ILogger logger, this ILogger logger,
string uniqueId, ModeMatchResult<TMode> modeMatchResult,
string viewPath, string uniqueId,
IEnumerable<ModeMatchAttributes<TMode>> partialMatches) string viewPath,
ITagHelper tagHelper)
{ {
var logValues = new PartialModeMatchLogValues<TMode>( if (logger.IsEnabled(LogLevel.Warning) && modeMatchResult.PartiallyMatchedAttributes.Count > 0)
uniqueId, {
viewPath, // Build the list of partial matches that contain attributes not appearing in at least one full match
partialMatches); var partialOnlyMatches = new List<ModeMatchAttributes<TMode>>();
for (var i = 0; i < modeMatchResult.PartialMatches.Count; i++)
{
var presentAttributes = modeMatchResult.PartialMatches[i].PresentAttributes;
for (var j = 0; j < presentAttributes.Count; j++)
{
var present = presentAttributes[j];
var presentIsPartialOnlyMatch = false;
for (var k = 0; k < modeMatchResult.PartiallyMatchedAttributes.Count; k++)
{
var partiallyMatched = modeMatchResult.PartiallyMatchedAttributes[k];
if (string.Equals(partiallyMatched, present, StringComparison.OrdinalIgnoreCase))
{
presentIsPartialOnlyMatch = true;
break;
}
}
logger.LogWarning(logValues); if (presentIsPartialOnlyMatch)
} {
partialOnlyMatches.Add(modeMatchResult.PartialMatches[i]);
break;
}
}
}
public static void TagHelperSkippingProcessing( var logValues = new PartialModeMatchLogValues<TMode>(
this ILogger logger, uniqueId,
ITagHelper tagHelper, viewPath,
string uniqueId) partialOnlyMatches);
{
_skippingProcessing( logger.LogWarning(logValues);
logger, }
tagHelper,
uniqueId, if (logger.IsEnabled(LogLevel.Verbose) && modeMatchResult.FullMatches.Count == 0)
null); {
_skippingProcessing(logger, tagHelper, uniqueId, null);
}
} }
/// <summary> /// <summary>

View File

@ -57,9 +57,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var formContext = ViewContext.FormContext; var formContext = ViewContext.FormContext;
if (formContext.HasEndOfFormContent) if (formContext.HasEndOfFormContent)
{ {
foreach (var content in formContext.EndOfFormContent) // Perf: Avoid allocating enumerator
for (var i = 0; i < formContext.EndOfFormContent.Count; i++)
{ {
output.PostContent.Append(content); output.PostContent.Append(formContext.EndOfFormContent[i]);
} }
} }

View File

@ -3,12 +3,12 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Mvc.Razor.TagHelpers; using Microsoft.AspNet.Mvc.Razor.TagHelpers;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.TagHelpers.Internal; using Microsoft.AspNet.Mvc.TagHelpers.Internal;
using Microsoft.AspNet.Mvc.TagHelpers.Logging;
using Microsoft.AspNet.Mvc.ViewFeatures; using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
@ -209,10 +209,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Src = output.Attributes[SrcAttributeName]?.Value as string; Src = output.Attributes[SrcAttributeName]?.Value as string;
var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails); var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails);
Logger.TagHelperModeMatchResult(modeResult, context.UniqueId, ViewContext.View.Path, this);
modeResult.LogDetails(Logger, this, context.UniqueId, ViewContext.View.Path); if (modeResult.FullMatches.Count == 0)
if (!modeResult.FullMatches.Any())
{ {
// No attributes matched so we have nothing to do // No attributes matched so we have nothing to do
return; return;
@ -234,7 +233,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var builder = new DefaultTagHelperContent(); var builder = new DefaultTagHelperContent();
// Get the highest matched mode // Get the highest matched mode
var mode = modeResult.FullMatches.Select(match => match.Mode).Max(); var mode = modeResult.FullMatches[0].Mode;
for (var i = 1; i < modeResult.FullMatches.Count; i++)
{
var currentMode = modeResult.FullMatches[i].Mode;
if (mode < currentMode)
{
mode = currentMode;
}
}
if (mode == Mode.GlobbedSrc || mode == Mode.Fallback && !string.IsNullOrEmpty(SrcInclude)) if (mode == Mode.GlobbedSrc || mode == Mode.Fallback && !string.IsNullOrEmpty(SrcInclude))
{ {
@ -290,7 +297,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
EnsureGlobbingUrlBuilder(); EnsureGlobbingUrlBuilder();
var fallbackSrcs = GlobbingUrlBuilder.BuildUrlList(FallbackSrc, FallbackSrcInclude, FallbackSrcExclude); var fallbackSrcs = GlobbingUrlBuilder.BuildUrlList(FallbackSrc, FallbackSrcInclude, FallbackSrcExclude);
if (fallbackSrcs.Any()) if (fallbackSrcs.Count > 0)
{ {
// Build the <script> tag that checks the test method and if it fails, renders the extra script. // Build the <script> tag that checks the test method and if it fails, renders the extra script.
builder.AppendHtml(Environment.NewLine) builder.AppendHtml(Environment.NewLine)
@ -312,8 +319,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
builder.AppendHtml("<script"); builder.AppendHtml("<script");
foreach (var attribute in attributes) // Perf: Avoid allocating enumerator
for (var i = 0; i < attributes.Count; i++)
{ {
var attribute = attributes[i];
if (!attribute.Name.Equals(SrcAttributeName, StringComparison.OrdinalIgnoreCase)) if (!attribute.Name.Equals(SrcAttributeName, StringComparison.OrdinalIgnoreCase))
{ {
var encodedKey = JavaScriptEncoder.Encode(attribute.Name); var encodedKey = JavaScriptEncoder.Encode(attribute.Name);
@ -373,8 +382,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
builder.AppendHtml("<script"); builder.AppendHtml("<script");
foreach (var attribute in attributes) // Perf: Avoid allocating enumerator
for (var i = 0; i < attributes.Count; i++)
{ {
var attribute = attributes[i];
var attributeValue = attribute.Value; var attributeValue = attribute.Value;
if (AppendVersion == true && if (AppendVersion == true &&
string.Equals(attribute.Name, SrcAttributeName, StringComparison.OrdinalIgnoreCase)) string.Equals(attribute.Name, SrcAttributeName, StringComparison.OrdinalIgnoreCase))

View File

@ -2,6 +2,7 @@
// 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;
using Microsoft.AspNet.Mvc.TagHelpers.Logging;
using Microsoft.AspNet.Razor.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
@ -22,7 +23,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var viewPath = "Views/Home/Index.cshtml"; var viewPath = "Views/Home/Index.cshtml";
// Act // Act
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId, viewPath); logger.TagHelperModeMatchResult(modeMatchResult, uniqueId, viewPath, tagHelper.Object);
// Assert // Assert
Mock.Get(logger).Verify(l => l.Log( Mock.Get(logger).Verify(l => l.Log(
@ -48,7 +49,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var viewPath = "Views/Home/Index.cshtml"; var viewPath = "Views/Home/Index.cshtml";
// Act // Act
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId, viewPath); logger.TagHelperModeMatchResult(modeMatchResult, uniqueId, viewPath, tagHelper.Object);
// Assert // Assert
Mock.Get(logger).Verify(l => l.Log( Mock.Get(logger).Verify(l => l.Log(
@ -79,7 +80,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var viewPath = "Views/Home/Index.cshtml"; var viewPath = "Views/Home/Index.cshtml";
// Act // Act
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId, viewPath); logger.TagHelperModeMatchResult(modeMatchResult, uniqueId, viewPath, tagHelper.Object);
// Assert // Assert
Mock.Get(logger).Verify(l => l.Log( Mock.Get(logger).Verify(l => l.Log(
@ -110,7 +111,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var viewPath = "Views/Home/Index.cshtml"; var viewPath = "Views/Home/Index.cshtml";
// Act // Act
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId, viewPath); logger.TagHelperModeMatchResult(modeMatchResult, uniqueId, viewPath, tagHelper.Object);
// Assert // Assert
Mock.Get(logger).Verify(l => l.Log( Mock.Get(logger).Verify(l => l.Log(