Reduce allocations during HTML generation

- #3918
- precompute size of `StringBuilder` in `ExpressionHelper`
- reduce `string` allocations in `ViewDataEvaluator`
 - also get rid of `Enumeration` state machines
- reduce the size of a few objects; use more expression-valued properties
 - e.g. don't store `_modelType` in `ModelExplorer`
- add `EmptyArray<TElement>`; make empty arrays consistently `static`
- avoid `string.Split()` in HTML and tag helpers

nits:
- make `ExpressionHelperTest` tests more stringent
- correct `Message` for an `ArgumentNullException`
- remove excess `using`s in classes I touched (but often ended up leaving otherwise unchanged)
- improve doc comments
- remove `ToString()` call on a `string`
- avoid encoding `string.Empty`
- fix test file name
- remove useless variables
- correct spelling
- improve whitespace
This commit is contained in:
Doug Bunting 2016-08-02 14:54:17 -07:00
parent 92682b71f3
commit 4cca6b09f0
55 changed files with 357 additions and 278 deletions

View File

@ -169,7 +169,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
}
// Flag that indiciates if TooManyModelErrorException has already been added to this dictionary.
// Flag that indicates if TooManyModelErrorException has already been added to this dictionary.
private bool HasRecordedMaxModelError { get; set; }
/// <summary>
@ -625,7 +625,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
/// <summary>
/// Removes all keys and values from ths instance of <see cref="ModelStateDictionary"/>.
/// Removes all keys and values from this instance of <see cref="ModelStateDictionary"/>.
/// </summary>
public void Clear()
{

View File

@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public abstract class ModelStateEntry
{
private ModelErrorCollection _errors;
/// <summary>
/// Gets the raw value from the request associated with this entry.
/// </summary>

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Internal;
#if NETSTANDARD1_6
using System.Reflection;
#endif
@ -83,7 +84,7 @@ namespace Microsoft.AspNetCore.Mvc
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
return EmptyArray<string>.Instance;
}
var split = original.Split(',').Select(piece => piece.Trim()).Where(piece => !string.IsNullOrEmpty(piece));

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Initializes a new instance of <see cref="ChallengeResult"/>.
/// </summary>
public ChallengeResult()
: this(new string[] { })
: this(EmptyArray<string>.Instance)
{
}
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc
/// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
/// challenge.</param>
public ChallengeResult(AuthenticationProperties properties)
: this(new string[] { }, properties)
: this(EmptyArray<string>.Instance, properties)
{
}

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc
// The value used is a non default value so that it avoids getting mixed with other action constraints
// with default order.
/// <inheritdoc />
int IActionConstraint.Order { get; } = ConsumesActionConstraintOrder;
int IActionConstraint.Order => ConsumesActionConstraintOrder;
/// <summary>
/// Gets or sets the supported request content types. Used to select an action when there would otherwise be

View File

@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc
public int Order { get; set; }
/// <inheritdoc />
public bool IsReusable { get; } = true;
public bool IsReusable => true;
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Initializes a new instance of <see cref="ForbidResult"/>.
/// </summary>
public ForbidResult()
: this(new string[] { })
: this(EmptyArray<string>.Instance)
{
}
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc
/// <param name="properties"><see cref="AuthenticationProperties"/> used to perform the authentication
/// challenge.</param>
public ForbidResult(AuthenticationProperties properties)
: this(new string[] { }, properties)
: this(EmptyArray<string>.Instance, properties)
{
}

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Mvc.Formatters
@ -18,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Sets mapping for the format to specified media type.
/// Sets mapping for the format to specified media type.
/// If the format already exists, the media type will be overwritten with the new value.
/// </summary>
/// <param name="format">The format value.</param>
@ -39,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
/// <summary>
/// Sets mapping for the format to specified media type.
/// Sets mapping for the format to specified media type.
/// If the format already exists, the media type will be overwritten with the new value.
/// </summary>
/// <param name="format">The format value.</param>

View File

@ -11,8 +11,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ClientValidatorCache
{
private readonly IReadOnlyList<IClientModelValidator> EmptyArray = new IClientModelValidator[0];
private readonly ConcurrentDictionary<ModelMetadata, CacheEntry> _cacheEntries = new ConcurrentDictionary<ModelMetadata, CacheEntry>();
public IReadOnlyList<IClientModelValidator> GetValidators(ModelMetadata metadata, IClientModelValidatorProvider validatorProvider)
@ -106,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (count == 0)
{
return EmptyArray;
return EmptyArray<IClientModelValidator>.Instance;
}
var validators = new IClientModelValidator[count];

View File

@ -3,7 +3,6 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
@ -13,8 +12,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ControllerActionInvokerCache
{
private readonly IFilterMetadata[] EmptyFilterArray = new IFilterMetadata[0];
private readonly IActionDescriptorCollectionProvider _collectionProvider;
private readonly IFilterProvider[] _filterProviders;
@ -134,7 +131,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (count == 0)
{
return EmptyFilterArray;
return EmptyArray<IFilterMetadata>.Instance;
}
else
{

View File

@ -0,0 +1,19 @@
// 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.
#if !NET451
using System;
#endif
namespace Microsoft.AspNetCore.Mvc.Internal
{
public static class EmptyArray<TElement>
{
public static TElement[] Instance { get; } =
#if NET451
new TElement[0];
#else
Array.Empty<TElement>();
#endif
}
}

View File

@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private void SetContentType(ActionContext context, FileResult result)
{
var response = context.HttpContext.Response;
response.ContentType = result.ContentType.ToString();
response.ContentType = result.ContentType;
}
protected static ILogger CreateLogger<T>(ILoggerFactory factory)

View File

@ -14,7 +14,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
/// </summary>
public class PrefixContainer
{
private static readonly string[] EmptyArray = new string[0];
private static readonly char[] Delimiters = new char[] { '[', '.' };
private readonly ICollection<string> _originalValues;
@ -31,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (_originalValues.Count == 0)
{
_sortedValues = EmptyArray;
_sortedValues = EmptyArray<string>.Instance;
}
else
{

View File

@ -11,8 +11,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ValidatorCache
{
private readonly IReadOnlyList<IModelValidator> EmptyArray = new IModelValidator[0];
private readonly ConcurrentDictionary<ModelMetadata, CacheEntry> _cacheEntries = new ConcurrentDictionary<ModelMetadata, CacheEntry>();
public IReadOnlyList<IModelValidator> GetValidators(ModelMetadata metadata, IModelValidatorProvider validatorProvider)
@ -105,7 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (count == 0)
{
return EmptyArray;
return EmptyArray<IModelValidator>.Instance;
}
var validators = new IModelValidator[count];

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
Debug.Assert(targetType == typeof(TElement[]), "GenericModelBinder only creates this binder for arrays.");
return new TElement[0];
return EmptyArray<TElement>.Instance;
}
/// <inheritdoc />

View File

@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
@ -36,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
model = new EmptyFormCollection();
}
bindingContext.Result = ModelBindingResult.Success(model);
}

View File

@ -3,9 +3,9 @@
using System;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Internal;
namespace Microsoft.AspNetCore.Mvc.Localization
{
@ -14,11 +14,6 @@ namespace Microsoft.AspNetCore.Mvc.Localization
/// </summary>
public class LocalizedHtmlString : IHtmlContent
{
#if NETSTANDARD1_4
private static readonly object[] EmptyArguments = Array.Empty<object>();
#else
private static readonly object[] EmptyArguments = new object[0];
#endif
private readonly object[] _arguments;
/// <summary>
@ -27,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Localization
/// <param name="name">The name of the string resource.</param>
/// <param name="value">The string resource.</param>
public LocalizedHtmlString(string name, string value)
: this(name, value, isResourceNotFound: false, arguments: EmptyArguments)
: this(name, value, isResourceNotFound: false, arguments: EmptyArray<string>.Instance)
{
}
@ -38,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.Localization
/// <param name="value">The string resource.</param>
/// <param name="isResourceNotFound">A flag that indicates if the resource is not found.</param>
public LocalizedHtmlString(string name, string value, bool isResourceNotFound)
: this(name, value, isResourceNotFound, arguments: EmptyArguments)
: this(name, value, isResourceNotFound, arguments: EmptyArray<string>.Instance)
{
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.Extensions.Primitives;
@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// <param name="relativePath">Path of the view file relative to the application base.</param>
/// <param name="compilationResult">The <see cref="Compilation.CompilationResult"/>.</param>
public CompilerCacheResult(string relativePath, CompilationResult compilationResult)
: this(relativePath, compilationResult, new IChangeToken[0])
: this(relativePath, compilationResult, EmptyArray<IChangeToken>.Instance)
{
}
@ -86,6 +87,5 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// Gets a delegate that creates an instance of the <see cref="IRazorPage"/>.
/// </summary>
public Func<IRazorPage> PageFactory { get; }
}
}

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Caching.Memory;
@ -31,8 +32,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor
private const string ControllerKey = "controller";
private const string AreaKey = "area";
private static readonly ViewLocationCacheItem[] EmptyViewStartLocationCacheItems =
new ViewLocationCacheItem[0];
private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);
private readonly IRazorPageFactoryProvider _pageFactory;
@ -415,7 +414,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// Only need to lookup _ViewStarts for the main page.
var viewStartPages = isMainPage ?
GetViewStartPages(relativePath, expirationTokens) :
EmptyViewStartLocationCacheItems;
EmptyArray<ViewLocationCacheItem>.Instance;
return new ViewLocationCacheResult(
new ViewLocationCacheItem(factoryResult.RazorPageFactory, relativePath),

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileSystemGlobbing;
@ -18,13 +19,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers.Internal
/// </summary>
public class GlobbingUrlBuilder
{
private static readonly IReadOnlyList<string> EmptyList =
#if NET451
new string[0];
#else
Array.Empty<string>();
#endif
// Valid whitespace characters defined by the HTML5 spec.
private static readonly char[] ValidAttributeWhitespaceChars =
new[] { '\t', '\n', '\u000C', '\r', ' ' };
@ -116,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers.Internal
{
if (string.IsNullOrEmpty(include))
{
return EmptyList;
return EmptyArray<string>.Instance;
}
var cacheKey = new GlobbingUrlKey(include, exclude);
@ -130,7 +124,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers.Internal
var includeEnumerator = includeTokenizer.GetEnumerator();
if (!includeEnumerator.MoveNext())
{
return EmptyList;
return EmptyArray<string>.Instance;
}
var options = new MemoryCacheEntryOptions();

View File

@ -119,7 +119,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix = string.Empty;
var fieldNameBase = oldPrefix;
var result = new HtmlContentBuilder();
var viewEngine = serviceProvider.GetRequiredService<ICompositeViewEngine>();
var viewBufferScope = serviceProvider.GetRequiredService<IViewBufferScope>();
@ -138,7 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
container: htmlHelper.ViewData.ModelExplorer,
metadata: itemMetadata,
model: item);
var fieldName = string.Format(CultureInfo.InvariantCulture, "{0}[{1}]", fieldNameBase, index++);
var fieldName = string.Format(CultureInfo.InvariantCulture, "{0}[{1}]", oldPrefix, index++);
var templateBuilder = new TemplateBuilder(
viewEngine,

View File

@ -86,7 +86,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
viewData.TemplateInfo.HtmlFieldPrefix = string.Empty;
var fieldNameBase = oldPrefix;
var result = new HtmlContentBuilder();
var viewEngine = serviceProvider.GetRequiredService<ICompositeViewEngine>();
var viewBufferScope = serviceProvider.GetRequiredService<IViewBufferScope>();
@ -105,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
container: htmlHelper.ViewData.ModelExplorer,
metadata: itemMetadata,
model: item);
var fieldName = string.Format(CultureInfo.InvariantCulture, "{0}[{1}]", fieldNameBase, index++);
var fieldName = string.Format(CultureInfo.InvariantCulture, "{0}[{1}]", oldPrefix, index++);
var templateBuilder = new TemplateBuilder(
viewEngine,

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 System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
@ -37,86 +38,166 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
return expressionText;
}
// Determine size of string needed (length) and number of segments it contains (segmentCount). Put another
// way, segmentCount tracks the number of times the loop below should iterate. This avoids adding ".model"
// and / or an extra leading "." and then removing them after the loop. Other information collected in this
// first loop helps with length and segmentCount adjustments. containsIndexers is somewhat separate: If
// true, expression strings are not cached for the expression.
//
// After the corrections below the first loop, length is usually exactly the size of the returned string.
// However when containsIndexers is true, the calculation is approximate because either evaluating indexer
// expressions multiple times or saving indexer strings can get expensive. Optimizing for the common case
// of a collection (not a dictionary) with less than 100 elements. If that assumption proves to be
// incorrect, the StringBuilder will be enlarged but hopefully just once.
var containsIndexers = false;
var lastIsModel = false;
var length = 0;
var segmentCount = 0;
var trailingMemberExpressions = 0;
var part = expression.Body;
// Builder to concatenate the names for property/field accessors within an expression to create a string.
var builder = new StringBuilder();
while (part != null)
{
if (part.NodeType == ExpressionType.Call)
switch (part.NodeType)
{
containsIndexers = true;
var methodExpression = (MethodCallExpression)part;
if (!IsSingleArgumentIndexer(methodExpression))
{
case ExpressionType.Call:
var methodExpression = (MethodCallExpression)part;
if (IsSingleArgumentIndexer(methodExpression))
{
containsIndexers = true;
lastIsModel = false;
length += "[99]".Length;
part = methodExpression.Object;
segmentCount++;
trailingMemberExpressions = 0;
}
else
{
// Unsupported.
part = null;
}
break;
case ExpressionType.ArrayIndex:
var binaryExpression = (BinaryExpression)part;
containsIndexers = true;
lastIsModel = false;
length += "[99]".Length;
part = binaryExpression.Left;
segmentCount++;
trailingMemberExpressions = 0;
break;
case ExpressionType.MemberAccess:
var memberExpressionPart = (MemberExpression)part;
var name = memberExpressionPart.Member.Name;
// If identifier contains "__", it is "reserved for use by the implementation" and likely
// compiler- or Razor-generated e.g. the name of a field in a delegate's generated class.
if (name.Contains("__"))
{
// Exit loop.
part = null;
}
else
{
lastIsModel = string.Equals("model", name, StringComparison.OrdinalIgnoreCase);
length += name.Length + 1;
part = memberExpressionPart.Expression;
segmentCount++;
trailingMemberExpressions++;
}
break;
case ExpressionType.Parameter:
// Unsupported but indicates previous member access was not the view's Model.
lastIsModel = false;
part = null;
break;
default:
// Unsupported.
part = null;
break;
}
InsertIndexerInvocationText(
builder,
methodExpression.Arguments.Single(),
expression);
part = methodExpression.Object;
}
else if (part.NodeType == ExpressionType.ArrayIndex)
}
// If name would start with ".model", then strip that part away.
if (lastIsModel)
{
length -= ".model".Length;
segmentCount--;
trailingMemberExpressions--;
}
// Trim the leading "." if present. The loop below special-cases the last property to avoid this addition.
if (trailingMemberExpressions > 0)
{
length--;
}
Debug.Assert(segmentCount >= 0);
if (segmentCount == 0)
{
Debug.Assert(!containsIndexers);
if (expressionTextCache != null)
{
containsIndexers = true;
var binaryExpression = (BinaryExpression)part;
InsertIndexerInvocationText(
builder,
binaryExpression.Right,
expression);
part = binaryExpression.Left;
expressionTextCache.Entries.TryAdd(expression, string.Empty);
}
else if (part.NodeType == ExpressionType.MemberAccess)
{
var memberExpressionPart = (MemberExpression)part;
var name = memberExpressionPart.Member.Name;
// If identifier contains "__", it is "reserved for use by the implementation" and likely compiler-
// or Razor-generated e.g. the name of a field in a delegate's generated class.
if (name.Contains("__"))
{
// Exit loop. Should have the entire name because previous MemberAccess has same name as the
// leftmost expression node (a variable).
return string.Empty;
}
var builder = new StringBuilder(length);
part = expression.Body;
while (part != null && segmentCount > 0)
{
segmentCount--;
switch (part.NodeType)
{
case ExpressionType.Call:
Debug.Assert(containsIndexers);
var methodExpression = (MethodCallExpression)part;
InsertIndexerInvocationText(builder, methodExpression.Arguments.Single(), expression);
part = methodExpression.Object;
break;
}
builder.Insert(0, name);
builder.Insert(0, '.');
part = memberExpressionPart.Expression;
}
else
{
break;
case ExpressionType.ArrayIndex:
Debug.Assert(containsIndexers);
var binaryExpression = (BinaryExpression)part;
InsertIndexerInvocationText(builder, binaryExpression.Right, expression);
part = binaryExpression.Left;
break;
case ExpressionType.MemberAccess:
var memberExpression = (MemberExpression)part;
var name = memberExpression.Member.Name;
Debug.Assert(!name.Contains("__"));
builder.Insert(0, name);
if (segmentCount > 0)
{
// One or more parts to the left of this part are coming.
builder.Insert(0, '.');
}
part = memberExpression.Expression;
break;
default:
// Should be unreachable due to handling in above loop.
Debug.Assert(false);
break;
}
}
// If parts start with "model", then strip that part away.
if (part == null || part.NodeType != ExpressionType.Parameter)
{
var text = builder.ToString();
if (text.StartsWith(".model", StringComparison.OrdinalIgnoreCase))
{
// 6 is the length of the string ".model".
builder.Remove(0, 6);
}
}
if (builder.Length > 0)
{
// Trim the leading "." if present.
builder.Replace(".", string.Empty, 0, 1);
}
Debug.Assert(segmentCount == 0);
expressionText = builder.ToString();
if (expressionTextCache != null && !containsIndexers)
{
expressionTextCache.Entries.TryAdd(expression, expressionText);

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
public class ExpressionTextCache
{
/// <inheritdoc />
public ConcurrentDictionary<LambdaExpression, string> Entries { get; } =
public ConcurrentDictionary<LambdaExpression, string> Entries { get; } =
new ConcurrentDictionary<LambdaExpression, string>(LambdaExpressionComparer.Instance);
// This comparer is tightly coupled with the logic of ExpressionHelper.GetExpressionText.
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
return true;
}
// We will cache only pure member access expressions. Hence we compare two expressions
// We will cache only pure member access expressions. Hence we compare two expressions
// to be equal only if they are identical member access expressions.
var expression1 = lambdaExpression1.Body;
var expression2 = lambdaExpression2.Body;
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
return false;
}
}
else
else
{
return true;
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Logging;
@ -14,7 +15,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
internal static class MvcViewFeaturesLoggerExtensions
{
private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
private static readonly string[] EmptyArguments = new string[0];
private static readonly Action<ILogger, string, string[], Exception> _viewComponentExecuting;
private static readonly Action<ILogger, string, double, string, Exception> _viewComponentExecuted;
@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
if (arguments == null || arguments.Length == 0)
{
return EmptyArguments;
return EmptyArray<string>.Instance;
}
var formattedArguments = new string[arguments.Length];

View File

@ -3,14 +3,13 @@
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public static class ValidationHelpers
{
private static readonly ModelStateEntry[] EmptyModelStateEntries = new ModelStateEntry[0];
public static string GetModelErrorMessageOrDefault(ModelError modelError)
{
Debug.Assert(modelError != null);
@ -80,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
return entries;
}
return EmptyModelStateEntries;
return EmptyArray<ModelStateEntry>.Instance;
}
private static void Visit(

View File

@ -154,7 +154,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
// We check if expression is wrapped with conversion to object expression
// and unwrap it if necessary, because Expression<Func<TModel, object>>
// automatically creates a convert to object expression for expresions
// automatically creates a convert to object expression for expressions
// returning value types
var unaryExpression = expression.Body as UnaryExpression;

View File

@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
@ -26,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc
public class RemoteAttribute : ValidationAttribute, IClientModelValidator
{
private string _additionalFields = string.Empty;
private string[] _additionalFieldsSplit = new string[0];
private string[] _additionalFieldsSplit = EmptyArray<string>.Instance;
private bool _checkedForLocalizer;
private IStringLocalizer _stringLocalizer;
@ -275,7 +276,7 @@ namespace Microsoft.AspNetCore.Mvc
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
return EmptyArray<string>.Instance;
}
var split = original

View File

@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
}
/// <summary>
/// Return valid HTML 4.01 "id" attribute for an element with the given <paramref name="name"/>.
/// Returns a valid HTML 4.01 "id" attribute value for an element with the given <paramref name="name"/>.
/// </summary>
/// <param name="name">The original element name.</param>
/// <param name="invalidCharReplacement">
@ -99,9 +99,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// <paramref name="name"/>.
/// </param>
/// <returns>
/// Valid HTML 4.01 "id" attribute for an element with the given <paramref name="name"/>.
/// Valid HTML 4.01 "id" attribute value for an element with the given <paramref name="name"/>.
/// </returns>
/// <remarks>Valid "id" attributes are defined in http://www.w3.org/TR/html401/types.html#type-id</remarks>
/// <remarks>
/// Valid "id" attributes are defined in http://www.w3.org/TR/html401/types.html#type-id
/// </remarks>
public static string CreateSanitizedId(string name, string invalidCharReplacement)
{
if (invalidCharReplacement == null)
@ -205,7 +207,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
writer.Write(" ");
writer.Write(key);
writer.Write("=\"");
encoder.Encode(writer, attribute.Value ?? string.Empty);
if (attribute.Value != null)
{
encoder.Encode(writer, attribute.Value);
}
writer.Write("\"");
}
}

View File

@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
Writer = writer;
FormContext = new FormContext();
ClientValidationEnabled = htmlHelperOptions.ClientValidationEnabled;
Html5DateRenderingMode = htmlHelperOptions.Html5DateRenderingMode;
ValidationSummaryMessageElement = htmlHelperOptions.ValidationSummaryMessageElement;
@ -228,7 +228,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
public FormContext GetFormContextForClientValidation()
{
return (ClientValidationEnabled) ? FormContext : null;
return ClientValidationEnabled ? FormContext : null;
}
}
}

View File

@ -558,6 +558,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
_attributes = attributes;
}
public int Count => _attributes.Count;
public bool IsReadOnly => true;

View File

@ -404,10 +404,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var resolvedLabelText = labelText ??
modelExplorer.Metadata.DisplayName ??
modelExplorer.Metadata.PropertyName;
if (resolvedLabelText == null)
if (resolvedLabelText == null && expression != null)
{
resolvedLabelText =
string.IsNullOrEmpty(expression) ? string.Empty : expression.Split('.').Last();
var index = expression.LastIndexOf('.');
if (index == -1)
{
// Expression does not contain a dot separator.
resolvedLabelText = expression;
}
else
{
resolvedLabelText = expression.Substring(index + 1);
}
}
if (string.IsNullOrEmpty(resolvedLabelText))
@ -1289,8 +1297,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
object typeAttributeValue;
if (htmlAttributes != null && htmlAttributes.TryGetValue("type", out typeAttributeValue))
{
if (string.Equals(typeAttributeValue.ToString(), "file", StringComparison.OrdinalIgnoreCase) ||
string.Equals(typeAttributeValue.ToString(), "image", StringComparison.OrdinalIgnoreCase))
var typeAttributeString = typeAttributeValue.ToString();
if (string.Equals(typeAttributeString, "file", StringComparison.OrdinalIgnoreCase) ||
string.Equals(typeAttributeString, "image", StringComparison.OrdinalIgnoreCase))
{
// 'value' attribute is not needed for 'file' and 'image' input types.
addValue = false;

View File

@ -181,16 +181,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <summary>
/// Creates a dictionary of HTML attributes from the input object,
/// translating underscores to dashes in each public instance property.
///
/// If the object is already an <see cref="IDictionary{String, Object}"/> instance, then it is
/// returned as-is.
/// </summary>
/// <param name="htmlAttributes">Anonymous object describing HTML attributes.</param>
/// <returns>A dictionary that represents HTML attributes.</returns>
/// <remarks>
/// If the object is already an <see cref="IDictionary{String, Object}"/> instance, then a shallow copy is
/// returned.
/// <example>
/// <c>new { data_name="value" }</c> will translate to the entry <c>{ "data-name", "value" }</c>
/// in the resulting dictionary.
/// </example>
/// </summary>
/// <param name="htmlAttributes">Anonymous object describing HTML attributes.</param>
/// <returns>A dictionary that represents HTML attributes.</returns>
/// </remarks>
public static IDictionary<string, object> AnonymousObjectToHtmlAttributes(object htmlAttributes)
{
var dictionary = htmlAttributes as IDictionary<string, object>;
@ -799,13 +800,21 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
// we want to fall back to the field name rather than the ModelType.
// This is similar to how the GenerateLabel get the text of a label.
var resolvedDisplayName = modelExplorer.Metadata.DisplayName ?? modelExplorer.Metadata.PropertyName;
if (resolvedDisplayName == null)
if (resolvedDisplayName == null && expression != null)
{
resolvedDisplayName =
string.IsNullOrEmpty(expression) ? string.Empty : expression.Split('.').Last();
var index = expression.LastIndexOf('.');
if (index == -1)
{
// Expression does not contain a dot separator.
resolvedDisplayName = expression;
}
else
{
resolvedDisplayName = expression.Substring(index + 1);
}
}
return resolvedDisplayName;
return resolvedDisplayName ?? string.Empty;
}
protected virtual string GenerateDisplayText(ModelExplorer modelExplorer)
@ -978,14 +987,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
bool useViewData,
object htmlAttributes)
{
var tagBuilder =
_htmlGenerator.GenerateHidden(
ViewContext,
modelExplorer,
expression,
value,
useViewData,
htmlAttributes);
var tagBuilder = _htmlGenerator.GenerateHidden(
ViewContext,
modelExplorer,
expression,
value,
useViewData,
htmlAttributes);
if (tagBuilder == null)
{
return HtmlString.Empty;

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures

View File

@ -20,7 +20,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private object _model;
private Func<object, object> _modelAccessor;
private Type _modelType;
private List<ModelExplorer> _properties;
/// <summary>
@ -30,8 +29,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <param name="metadata">The <see cref="ModelMetadata"/>.</param>
/// <param name="model">The model object. May be <c>null</c>.</param>
public ModelExplorer(
IModelMetadataProvider metadataProvider,
ModelMetadata metadata,
IModelMetadataProvider metadataProvider,
ModelMetadata metadata,
object model)
{
if (metadataProvider == null)
@ -172,29 +171,23 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
get
{
if (_modelType == null)
if (Metadata.IsNullableValueType)
{
if (Model == null)
{
// If the model is null, then use the declared model type;
_modelType = Metadata.ModelType;
}
else if (Metadata.IsNullableValueType)
{
// We have a model, but if it's a nullable value type, then Model.GetType() will return
// the non-nullable type (int? -> int). Since it's a value type, there's no subclassing,
// just go with the declared type.
_modelType = Metadata.ModelType;
}
else
{
// We have a model, and it's not a nullable so use the runtime type to handle
// cases where the model is a subclass of the declared type and has extra data.
_modelType = Model.GetType();
}
// We have a model, but if it's a nullable value type, then Model.GetType() will return
// the non-nullable type (int? -> int). Since it's a value type, there's no subclassing,
// just go with the declared type.
return Metadata.ModelType;
}
return _modelType;
if (Model == null)
{
// If the model is null, then use the declared model type;
return Metadata.ModelType;
}
// We have a model, and it's not a nullable, so use the runtime type to handle
// cases where the model is a subclass of the declared type and has extra data.
return Model.GetType();
}
}
@ -203,7 +196,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// </summary>
/// <remarks>
/// Includes a <see cref="ModelExplorer"/> for each property of the <see cref="ModelMetadata"/>
/// for <see cref="ModelType"/>.
/// for <see cref="ModelType"/>.
/// </remarks>
public IEnumerable<ModelExplorer> Properties
{
@ -260,8 +253,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
return Properties.FirstOrDefault(p => string.Equals(
p.Metadata.PropertyName,
name,
p.Metadata.PropertyName,
name,
StringComparison.Ordinal));
}
@ -383,7 +376,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <returns>A <see cref="ModelExplorer"/>.</returns>
/// <remarks>
/// <para>
/// A <see cref="ModelExplorer"/> created by
/// A <see cref="ModelExplorer"/> created by
/// <see cref="GetExplorerForExpression(Type, Func{object, object})"/>
/// represents the result of executing an arbitrary expression against the model contained
/// in the current <see cref="ModelExplorer"/> instance.
@ -411,7 +404,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <returns>A <see cref="ModelExplorer"/>.</returns>
/// <remarks>
/// <para>
/// A <see cref="ModelExplorer"/> created by
/// A <see cref="ModelExplorer"/> created by
/// <see cref="GetExplorerForExpression(ModelMetadata, Func{object, object})"/>
/// represents the result of executing an arbitrary expression against the model contained
/// in the current <see cref="ModelExplorer"/> instance.
@ -444,7 +437,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
private ModelExplorer CreateExplorerForProperty(
ModelMetadata propertyMetadata,
ModelMetadata propertyMetadata,
PropertyHelper propertyHelper)
{
if (propertyHelper == null)

View File

@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
@ -156,7 +157,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
// Since we call Save() after the response has been sent, we need to initialize an empty session
// so that it is established before the headers are sent.
session.Set(TempDataSessionStateKey, new byte[] { });
session.Set(TempDataSessionStateKey, EmptyArray<byte>.Instance);
}
return tempDataDictionary ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

View File

@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
if (bufferScope == null)
{
throw new ArgumentNullException(nameof(_bufferScope));
throw new ArgumentNullException(nameof(bufferScope));
}
if (viewContext == null)

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
@ -98,57 +99,58 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private static ViewDataInfo InnerEvalComplexExpression(object indexableObject, string expression)
{
foreach (var expressionPair in GetRightToLeftExpressions(expression))
Debug.Assert(expression != null);
var leftExpression = expression;
do
{
var subExpression = expressionPair.Left;
var postExpression = expressionPair.Right;
var subTargetInfo = GetPropertyValue(indexableObject, subExpression);
if (subTargetInfo != null)
var targetInfo = GetPropertyValue(indexableObject, leftExpression);
if (targetInfo != null)
{
if (string.IsNullOrEmpty(postExpression))
if (leftExpression.Length == expression.Length)
{
return subTargetInfo;
// Nothing remaining in expression after leftExpression.
return targetInfo;
}
if (subTargetInfo.Value != null)
if (targetInfo.Value != null)
{
var potential = InnerEvalComplexExpression(subTargetInfo.Value, postExpression);
if (potential != null)
var rightExpression = expression.Substring(leftExpression.Length + 1);
targetInfo = InnerEvalComplexExpression(targetInfo.Value, rightExpression);
if (targetInfo != null)
{
return potential;
return targetInfo;
}
}
}
leftExpression = GetNextShorterExpression(leftExpression);
}
while (!string.IsNullOrEmpty(leftExpression));
return null;
}
// Produces an enumeration of combinations of property names given a complex expression in the following order:
// this["one.two.three.four"]
// this["one.two.three][four"]
// this["one.two][three.four"]
// this["one][two.three.four"]
// Given "one.two.three.four" initially, calls return
// "one.two.three"
// "one.two"
// "one"
// ""
// Recursion of InnerEvalComplexExpression() further sub-divides these cases to cover the full set of
// combinations shown in Eval(ViewDataDictionary, string) comments.
private static IEnumerable<ExpressionPair> GetRightToLeftExpressions(string expression)
private static string GetNextShorterExpression(string expression)
{
yield return new ExpressionPair(expression, string.Empty);
if (string.IsNullOrEmpty(expression))
{
return string.Empty;
}
var lastDot = expression.LastIndexOf('.');
var subExpression = expression;
var postExpression = string.Empty;
while (lastDot > -1)
if (lastDot == -1)
{
subExpression = expression.Substring(0, lastDot);
postExpression = expression.Substring(lastDot + 1);
yield return new ExpressionPair(subExpression, postExpression);
lastDot = subExpression.LastIndexOf('.');
return string.Empty;
}
return expression.Substring(startIndex: 0, length: lastDot);
}
private static ViewDataInfo GetIndexedPropertyValue(object indexableObject, string key)
@ -205,17 +207,5 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return new ViewDataInfo(container, propertyInfo);
}
private struct ExpressionPair
{
public readonly string Left;
public readonly string Right;
public ExpressionPair(string left, string right)
{
Left = left;
Right = right;
}
}
}
}

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
public class OverloadActionConstraint : IActionConstraint
{
/// <inheritdoc />
public int Order { get; } = int.MaxValue;
public int Order => int.MaxValue;
/// <inheritdoc />
public bool Accept(ActionConstraintContext context)

View File

@ -1,7 +1,6 @@
// 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.Linq;
using Microsoft.Extensions.Primitives;
using Xunit;

View File

@ -3,10 +3,7 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
using Xunit;

View File

@ -1,7 +1,6 @@
// 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 Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using Xunit;

View File

@ -134,7 +134,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
generator.Verify();
Assert.Equal("a", output.TagName);
Assert.Empty(output.Attributes);
Assert.True(output.Content.GetContent().Length == 0);
Assert.Empty(output.Content.GetContent());
}
[Fact]
@ -185,7 +185,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
generator.Verify();
Assert.Equal("a", output.TagName);
Assert.Empty(output.Attributes);
Assert.True(output.Content.GetContent().Length == 0);
Assert.Empty(output.Content.GetContent());
}
[Fact]
@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal("a", output.TagName);
Assert.Empty(output.Attributes);
Assert.True(output.Content.GetContent().Length == 0);
Assert.Empty(output.Content.GetContent());
}
[Fact]
@ -300,7 +300,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal("a", output.TagName);
Assert.Empty(output.Attributes);
Assert.True(output.Content.GetContent().Length == 0);
Assert.Empty(output.Content.GetContent());
}
[Fact]
@ -357,7 +357,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal("a", output.TagName);
Assert.Empty(output.Attributes);
Assert.True(output.Content.GetContent().Length == 0);
Assert.Empty(output.Content.GetContent());
}
[Theory]

View File

@ -149,7 +149,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[InlineData("\r\n \r\n", "\r\n Something Else \r\n", "\r\n Something Else \r\n")]
[InlineData("\r\n \r\n", "Some Content", "Some Content")]
public async Task ProcessAsync_DoesNotOverrideOutputContent(
string childContent, string outputContent, string expectedOutputContent)
string childContent,
string outputContent,
string expectedOutputContent)
{
// Arrange
var tagBuilder = new TagBuilder("span2");
@ -209,7 +211,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[InlineData("Content of validation message", "Content of validation message")]
[InlineData("\r\n \r\n", "New HTML")]
public async Task ProcessAsync_MergesTagBuilderFromGenerateValidationMessage(
string childContent, string expectedOutputContent)
string childContent,
string expectedOutputContent)
{
// Arrange
var tagBuilder = new TagBuilder("span2");

View File

@ -1,4 +1,6 @@
using System;
// 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 Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit.Sdk;

View File

@ -157,8 +157,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
(Expression<Func<TestModel, string>>)(m => value)
},
{
// These two expressions are not actually equivalent. However ExpressionHelper returns
// string.Empty for these two expressions and hence they are considered as equivalent by the
// These two expressions are not actually equivalent. However ExpressionHelper returns
// string.Empty for these two expressions and hence they are considered as equivalent by the
// cache.
(Expression<Func<TestModel, string>>)(m => Model),
(Expression<Func<TestModel, TestModel>>)(m => m)
@ -206,10 +206,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
(Expression<Func<IDictionary<string, TestModel>, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory),
(Expression<Func<TestModel, Category>>)(model => model.SelectedCategory)
},
{
(Expression<Func<IList<TestModel>, Category>>)(model => model[2].SelectedCategory),
(Expression<Func<IList<TestModel>, Category>>)(model => model[2].SelectedCategory)
},
{
(Expression<Func<TestModel, string>>)(m => Model),
(Expression<Func<TestModel, string>>)(m => m.Model)
@ -226,10 +222,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
(Expression<Func<TestModel, string>>)(m => key),
(Expression<Func<TestModel, string>>)(m => value)
},
{
(Expression<Func<IDictionary<string, TestModel>, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory),
(Expression<Func<IDictionary<string, TestModel>, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory)
},
};
}
}
@ -256,7 +248,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
var text2 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache);
// Assert
Assert.Same(text1, text2); // Cached
Assert.Same(text1, text2); // cached
}
[Theory]
@ -270,6 +262,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
var text2 = ExpressionHelper.GetExpressionText(expression, _expressionTextCache);
// Assert
Assert.Equal(text1, text2, StringComparer.Ordinal);
Assert.NotSame(text1, text2); // not cached
}
@ -284,7 +277,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
var text2 = ExpressionHelper.GetExpressionText(expression2, _expressionTextCache);
// Assert
Assert.Same(text1, text2);
Assert.Same(text1, text2); // cached
}
[Theory]
@ -298,6 +291,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
var text2 = ExpressionHelper.GetExpressionText(expression2, _expressionTextCache);
// Assert
Assert.NotEqual(text1, text2, StringComparer.Ordinal);
Assert.NotSame(text1, text2);
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

View File

@ -20,12 +20,12 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
get
{
var expected1 = @"<input baz=""HtmlEncode[[BazValue]]"" id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" " +
@"value=""HtmlEncode[[ModelStateValue]]"" />";
@"value=""HtmlEncode[[ModelStateValue]]"" />";
yield return new object[] { new Dictionary<string, object> { { "baz", "BazValue" } }, expected1 };
yield return new object[] { new { baz = "BazValue" }, expected1 };
var expected2 = @"<input foo-baz=""HtmlEncode[[BazValue]]"" id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" " +
@"value=""HtmlEncode[[ModelStateValue]]"" />";
@"value=""HtmlEncode[[ModelStateValue]]"" />";
yield return new object[] { new Dictionary<string, object> { { "foo-baz", "BazValue" } }, expected2 };
yield return new object[] { new { foo_baz = "BazValue" }, expected2 };
}
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{
// Arrange
var expected = @"<input id=""HtmlEncode[[Property1]]"" key=""HtmlEncode[[value]]"" name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" " +
@"value=""HtmlEncode[[test]]"" />";
@"value=""HtmlEncode[[test]]"" />";
var attributes = new { key = "value" };
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithNullModelAndNonNullViewData());
@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{
// Arrange
var expected = @"<input data-key=""HtmlEncode[[value]]"" id=""HtmlEncode[[Property1]]"" name=""HtmlEncode[[Property1]]"" type=""HtmlEncode[[hidden]]"" " +
@"value=""HtmlEncode[[test]]"" />";
@"value=""HtmlEncode[[test]]"" />";
var attributes = new Dictionary<string, object> { { "data-key", "value" } };
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithNullModelAndNonNullViewData());
@ -292,7 +292,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{
// Arrange
var expected = @"<input baz=""HtmlEncode[[BazValue]]"" id=""HtmlEncode[[keyNotFound]]"" name=""HtmlEncode[[keyNotFound]]"" type=""HtmlEncode[[hidden]]"" " +
@"value="""" />";
@"value="""" />";
var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues());
var attributes = new Dictionary<string, object> { { "baz", "BazValue" } };

View File

@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
public void LabelHelpers_DisplayPropertyName()
{
// Arrange
var expectedLabel = "<label for=\"HtmlEncode[[Property1]]\">HtmlEncode[[Property1]]</label>";
var helper = DefaultTemplatesUtilities.GetHtmlHelper();
// Act
@ -45,14 +46,15 @@ namespace Microsoft.AspNetCore.Mvc.Core
var labelForResult = helper.LabelFor(m => m.Property1);
// Assert
Assert.Equal("<label for=\"HtmlEncode[[Property1]]\">HtmlEncode[[Property1]]</label>", HtmlContentUtilities.HtmlContentToString(labelResult));
Assert.Equal("<label for=\"HtmlEncode[[Property1]]\">HtmlEncode[[Property1]]</label>", HtmlContentUtilities.HtmlContentToString(labelForResult));
Assert.Equal(expectedLabel, HtmlContentUtilities.HtmlContentToString(labelResult));
Assert.Equal(expectedLabel, HtmlContentUtilities.HtmlContentToString(labelForResult));
}
[Fact]
public void LabelHelpers_DisplayPropertyName_ForNestedProperty()
{
// Arrange
var expectedLabel = "<label for=\"HtmlEncode[[Inner_Id]]\">HtmlEncode[[Id]]</label>";
var helper = DefaultTemplatesUtilities.GetHtmlHelper<OuterClass>(model: null);
// Act
@ -60,8 +62,8 @@ namespace Microsoft.AspNetCore.Mvc.Core
var labelForResult = helper.LabelFor(m => m.Inner.Id);
// Assert
Assert.Equal("<label for=\"HtmlEncode[[Inner_Id]]\">HtmlEncode[[Id]]</label>", HtmlContentUtilities.HtmlContentToString(labelResult));
Assert.Equal("<label for=\"HtmlEncode[[Inner_Id]]\">HtmlEncode[[Id]]</label>", HtmlContentUtilities.HtmlContentToString(labelForResult));
Assert.Equal(expectedLabel, HtmlContentUtilities.HtmlContentToString(labelResult));
Assert.Equal(expectedLabel, HtmlContentUtilities.HtmlContentToString(labelForResult));
}
[Fact]

View File

@ -469,7 +469,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
Expression<Func<PasswordModel, string>> expression,
string expected)
{
// Arrange
// Arrange
var model = new PasswordModel();
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
helper.ViewData.TemplateInfo.HtmlFieldPrefix = "pre";
@ -489,7 +489,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
// Act
var result = helper.PasswordFor(expression);
// Assert
// Assert
Assert.Equal(expected, HtmlContentUtilities.HtmlContentToString(result));
}

View File

@ -15,14 +15,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
[Fact]
public void TextAreaFor_GeneratesPlaceholderAttribute_WhenDisplayAttributePromptIsSetAndTypeIsValid()
{
// Arrange
// Arrange
var model = new TextAreaModelWithAPlaceholder();
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
// Act
var textArea = helper.TextAreaFor(m => m.Property1);
// Assert
// Assert
var result = HtmlContentUtilities.HtmlContentToString(textArea);
Assert.Contains(@"placeholder=""HtmlEncode[[placeholder]]""", result, StringComparison.Ordinal);
}
@ -30,14 +30,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
[Fact]
public void TextAreaFor_DoesNotGeneratePlaceholderAttribute_WhenNoPlaceholderPresentInModel()
{
// Arrange
// Arrange
var model = new TextAreaModelWithoutAPlaceholder();
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
// Act
var textArea = helper.TextAreaFor(m => m.Property1);
// Assert
// Assert
var result = HtmlContentUtilities.HtmlContentToString(textArea);
Assert.DoesNotContain(@"placeholder=""HtmlEncode[[placeholder]]""", result, StringComparison.Ordinal);
}

View File

@ -21,14 +21,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
[InlineData("number")]
public void TextBoxFor_GeneratesPlaceholderAttribute_WhenDisplayAttributePromptIsSetAndTypeIsValid(string type)
{
// Arrange
// Arrange
var model = new TextBoxModel();
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
// Act
var textBox = helper.TextBoxFor(m => m.Property1, new { type });
// Assert
// Assert
var result = HtmlContentUtilities.HtmlContentToString(textBox);
Assert.Contains(@"placeholder=""HtmlEncode[[placeholder]]""", result, StringComparison.Ordinal);
}
@ -48,14 +48,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
[InlineData("file")]
public void TextBoxFor_DoesNotGeneratePlaceholderAttribute_WhenDisplayAttributePromptIsSetAndTypeIsInvalid(string type)
{
// Arrange
// Arrange
var model = new TextBoxModel();
var helper = DefaultTemplatesUtilities.GetHtmlHelper(model);
// Act
var textBox = helper.TextBoxFor(m => m.Property1, new { type });
// Assert
// Assert
var result = HtmlContentUtilities.HtmlContentToString(textBox);
Assert.DoesNotContain(@"placeholder=""HtmlEncode[[placeholder]]""", result, StringComparison.Ordinal);
}