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

View File

@ -3,11 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers;
@ -101,7 +99,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <summary>
/// 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>
[HtmlAttributeName(VaryByUserAttributeName)]
public bool VaryByUser { get; set; }
@ -261,18 +259,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Append(keyName)
.Append("(");
var tokenFound = false;
foreach (var item in Tokenize(value))
{
tokenFound = true;
var values = Tokenize(value);
// Perf: Avoid allocating enumerator
for (var i = 0; i < values.Count; i++)
{
var item = values[i];
builder.Append(item)
.Append(CacheKeyTokenSeparator)
.Append(sourceCollection[item])
.Append(CacheKeyTokenSeparator);
}
if (tokenFound)
if (values.Count > 0)
{
// Remove the trailing separator
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,
string keyName,
string value,
T sourceCollection,
Func<T, string, StringValues> accessor)
TSourceCollection sourceCollection,
Func<TSourceCollection, string, StringValues> accessor)
{
if (!string.IsNullOrEmpty(value))
{
@ -296,10 +295,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Append(keyName)
.Append("(");
var tokenFound = false;
foreach (var item in Tokenize(value))
var values = Tokenize(value);
// Perf: Avoid allocating enumerator
for (var i = 0; i < values.Count; i++)
{
tokenFound = true;
var item = values[i];
builder.Append(item)
.Append(CacheKeyTokenSeparator)
@ -307,7 +308,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Append(CacheKeyTokenSeparator);
}
if (tokenFound)
if (values.Count > 0)
{
// Remove the trailing separator
builder.Length -= CacheKeyTokenSeparator.Length;
@ -327,8 +328,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
.Append(nameof(VaryByRoute))
.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;
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)
.Select(token => token.Trim())
.Where(token => token.Length > 0);
var values = value.Split(AttributeSeparator, StringSplitOptions.RemoveEmptyEntries);
if (values.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.
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Razor.TagHelpers;
@ -67,30 +67,41 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
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();
if (string.IsNullOrWhiteSpace(currentEnvironmentName))
if (string.IsNullOrEmpty(currentEnvironmentName))
{
// No current environment name, do nothing
return;
}
if (environments.Any(name =>
string.Equals(name.Trim(), currentEnvironmentName, StringComparison.OrdinalIgnoreCase)))
var values = Names.Split(NameSeparator, StringSplitOptions.RemoveEmptyEntries);
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;
}
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
output.SuppressOutput();
}

View File

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

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
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.
var htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
// Perf: Avoid allocating enumerator
// 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))
{
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.
private static IEnumerable<string> GetInputTypeHints(ModelExplorer modelExplorer)
{
var inputTypeHints = new string[]
if (!string.IsNullOrEmpty(modelExplorer.Metadata.TemplateHint))
{
modelExplorer.Metadata.TemplateHint,
modelExplorer.Metadata.DataTypeName,
};
yield return modelExplorer.Metadata.TemplateHint;
}
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

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
@ -23,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// <returns>The <see cref="ModeMatchResult{TMode}"/>.</returns>
public static ModeMatchResult<TMode> DetermineMode<TMode>(
TagHelperContext context,
IEnumerable<ModeAttributes<TMode>> modeInfos)
IReadOnlyList<ModeAttributes<TMode>> modeInfos)
{
if (context == null)
{
@ -39,27 +38,32 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var matchedAttributes = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
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);
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
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));
}
else
{
// Perf: Avoid allocating enumerator
// 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;
if (!matchedAttributes.TryGetValue(attribute, out attributeMatch))
{
@ -87,14 +91,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
private static PresentMissingAttributes GetPresentMissingAttributes(
TagHelperContext context,
IEnumerable<string> requiredAttributes)
string[] requiredAttributes)
{
// Check for all attribute values
var presentAttributes = 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) ||
context.AllAttributes[attribute] == null ||
(context.AllAttributes[attribute].Value is string &&
@ -114,9 +120,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
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="excludePattern">The file globbing exclude pattern.</param>
/// <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);
@ -112,9 +112,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
if (!Cache.TryGetValue(cacheKey, out files))
{
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);
}
@ -128,15 +129,24 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
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();
matcher.AddIncludePatterns(includePatterns.Select(pattern => TrimLeadingTildeSlash(pattern)));
var trimmedIncludePatterns = new List<string>();
for (var i = 0; i < includePatterns.Length; i++)
{
trimmedIncludePatterns.Add(TrimLeadingTildeSlash(includePatterns[i]));
}
matcher.AddIncludePatterns(trimmedIncludePatterns);
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);

View File

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

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// <summary>
/// Creates an <see cref="ModeAttributes{TMode}"/>/
/// </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>
{

View File

@ -1,25 +1,22 @@
// 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.Collections.Generic;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{
/// <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>
/// <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>
{
/// <summary>
/// The <see cref="ITagHelper"/>'s mode.
/// The <see cref="AspNet.Razor.TagHelpers.ITagHelper"/>'s mode.
/// </summary>
public TMode Mode { get; set; }
/// <summary>
/// The names of attributes required for this mode.
/// </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>
public static ModeMatchAttributes<TMode> Create<TMode>(
TMode mode,
IEnumerable<string> presentAttributes)
IList<string> presentAttributes)
{
return Create(mode, presentAttributes, missingAttributes: null);
}
@ -25,8 +25,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// </summary>
public static ModeMatchAttributes<TMode> Create<TMode>(
TMode mode,
IEnumerable<string> presentAttributes,
IEnumerable<string> missingAttributes)
IList<string> presentAttributes,
IList<string> missingAttributes)
{
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.
using System.Collections.Generic;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
{
/// <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>
/// <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>
{
/// <summary>
/// The <see cref="ITagHelper"/>'s mode.
/// The <see cref="AspNet.Razor.TagHelpers.ITagHelper"/>'s mode.
/// </summary>
public TMode Mode { get; set; }
/// <summary>
/// The names of attributes that were present in this match.
/// </summary>
public IEnumerable<string> PresentAttributes { get; set; }
public IList<string> PresentAttributes { get; set; }
/// <summary>
/// The names of attributes that were missing in this match.
/// </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.
// 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 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
{
/// <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>
/// <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>
{
/// <summary>
@ -32,47 +26,5 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
/// <see cref="FullMatches"/>.
/// </summary>
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.Rendering;
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
using Microsoft.AspNet.Mvc.TagHelpers.Logging;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory;
@ -241,10 +242,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Href = output.Attributes[HrefAttributeName]?.Value as string;
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.Any())
if (modeResult.FullMatches.Count == 0)
{
// No attributes matched so we have nothing to do
return;
@ -266,7 +266,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var builder = new DefaultTagHelperContent();
// 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))
{
@ -384,8 +392,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
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;
if (AppendVersion == true &&
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}'.");
}
public static void TagHelperPartialMatches<TMode>(
this ILogger logger,
string uniqueId,
string viewPath,
IEnumerable<ModeMatchAttributes<TMode>> partialMatches)
public static void TagHelperModeMatchResult<TMode>(
this ILogger logger,
ModeMatchResult<TMode> modeMatchResult,
string uniqueId,
string viewPath,
ITagHelper tagHelper)
{
var logValues = new PartialModeMatchLogValues<TMode>(
uniqueId,
viewPath,
partialMatches);
if (logger.IsEnabled(LogLevel.Warning) && modeMatchResult.PartiallyMatchedAttributes.Count > 0)
{
// Build the list of partial matches that contain attributes not appearing in at least one full match
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(
this ILogger logger,
ITagHelper tagHelper,
string uniqueId)
{
_skippingProcessing(
logger,
tagHelper,
uniqueId,
null);
var logValues = new PartialModeMatchLogValues<TMode>(
uniqueId,
viewPath,
partialOnlyMatches);
logger.LogWarning(logValues);
}
if (logger.IsEnabled(LogLevel.Verbose) && modeMatchResult.FullMatches.Count == 0)
{
_skippingProcessing(logger, tagHelper, uniqueId, null);
}
}
/// <summary>

View File

@ -57,9 +57,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var formContext = ViewContext.FormContext;
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.Diagnostics;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Mvc.Razor.TagHelpers;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
using Microsoft.AspNet.Mvc.TagHelpers.Logging;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.Caching.Memory;
@ -209,10 +209,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Src = output.Attributes[SrcAttributeName]?.Value as string;
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.Any())
if (modeResult.FullMatches.Count == 0)
{
// No attributes matched so we have nothing to do
return;
@ -234,7 +233,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
var builder = new DefaultTagHelperContent();
// 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))
{
@ -290,7 +297,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
EnsureGlobbingUrlBuilder();
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.
builder.AppendHtml(Environment.NewLine)
@ -312,8 +319,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
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))
{
var encodedKey = JavaScriptEncoder.Encode(attribute.Name);
@ -373,8 +382,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
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;
if (AppendVersion == true &&
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.
using System;
using Microsoft.AspNet.Mvc.TagHelpers.Logging;
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.Logging;
using Moq;
@ -22,7 +23,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var viewPath = "Views/Home/Index.cshtml";
// Act
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId, viewPath);
logger.TagHelperModeMatchResult(modeMatchResult, uniqueId, viewPath, tagHelper.Object);
// Assert
Mock.Get(logger).Verify(l => l.Log(
@ -48,7 +49,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var viewPath = "Views/Home/Index.cshtml";
// Act
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId, viewPath);
logger.TagHelperModeMatchResult(modeMatchResult, uniqueId, viewPath, tagHelper.Object);
// Assert
Mock.Get(logger).Verify(l => l.Log(
@ -79,7 +80,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var viewPath = "Views/Home/Index.cshtml";
// Act
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId, viewPath);
logger.TagHelperModeMatchResult(modeMatchResult, uniqueId, viewPath, tagHelper.Object);
// Assert
Mock.Get(logger).Verify(l => l.Log(
@ -110,7 +111,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
var viewPath = "Views/Home/Index.cshtml";
// Act
modeMatchResult.LogDetails(logger, tagHelper.Object, uniqueId, viewPath);
logger.TagHelperModeMatchResult(modeMatchResult, uniqueId, viewPath, tagHelper.Object);
// Assert
Mock.Get(logger).Verify(l => l.Log(