From 4cca6b09f0044660d64469e3278b584a17c356e9 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Tue, 2 Aug 2016 14:54:17 -0700 Subject: [PATCH] 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`; 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 --- .../ModelBinding/ModelStateDictionary.cs | 4 +- .../ModelBinding/ModelStateEntry.cs | 1 + .../BindAttribute.cs | 3 +- .../ChallengeResult.cs | 4 +- .../ConsumesAttribute.cs | 2 +- .../Filters/MiddlewareFilterAttribute.cs | 2 +- .../ForbidResult.cs | 4 +- .../Formatters/FormatterMappings.cs | 5 +- .../Internal/ClientValidatorCache.cs | 4 +- .../Internal/ControllerActionInvokerCache.cs | 5 +- .../Internal/EmptyArray.cs | 19 ++ .../Internal/FileResultExecutorBase.cs | 2 +- .../Internal/PrefixContainer.cs | 3 +- .../Internal/ValidatorCache.cs | 4 +- .../ModelBinding/Binders/ArrayModelBinder.cs | 3 +- .../Binders/FormCollectionModelBinder.cs | 3 +- .../LocalizedHtmlString.cs | 11 +- .../Internal/CompilerCacheResult.cs | 4 +- .../RazorViewEngine.cs | 5 +- .../Internal/GlobbingUrlBuilder.cs | 12 +- .../Internal/DefaultDisplayTemplates.cs | 3 +- .../Internal/DefaultEditorTemplates.cs | 3 +- .../Internal/ExpressionHelper.cs | 207 ++++++++++++------ .../Internal/ExpressionTextCache.cs | 6 +- .../MvcViewFeaturesLoggerExtensions.cs | 4 +- .../Internal/ValidationHelpers.cs | 5 +- .../ModelStateDictionaryExtensions.cs | 2 +- .../RemoteAttribute.cs | 5 +- .../Rendering/TagBuilder.cs | 14 +- .../Rendering/ViewContext.cs | 4 +- .../ViewFeatures/AttributeDictionary.cs | 1 + .../ViewFeatures/DefaultHtmlGenerator.cs | 19 +- .../ViewFeatures/HtmlHelper.cs | 44 ++-- .../ViewFeatures/IHtmlGenerator.cs | 1 - .../ViewFeatures/ModelExplorer.cs | 51 ++--- .../SessionStateTempDataProvider.cs | 3 +- .../ViewFeatures/TemplateBuilder.cs | 2 +- .../ViewFeatures/ViewDataEvaluator.cs | 72 +++--- .../OverloadActionConstraint.cs | 2 +- .../Formatters/MediaTypeTest.cs | 1 - .../Formatters/StreamOutputFormatterTest.cs | 3 - .../MediaTypeCollectionTest.cs | 1 - .../CompositeValueProviderTest.cs | 1 - .../EnumerableValueProviderTest.cs | 1 - ...ewEngineTest.cs => RazorViewEngineTest.cs} | 0 .../AnchorTagHelperTest.cs | 10 +- .../ValidationMessageTagHelperTest.cs | 7 +- .../MediaTypeAssert.cs | 4 +- .../Internal/ExpressionHelperTest.cs | 18 +- .../Rendering/HtmlHelperFormTest.cs | 1 - .../Rendering/HtmlHelperHiddenTest.cs | 10 +- .../HtmlHelperLabelExtensionsTest.cs | 10 +- .../Rendering/HtmlHelperPasswordTest.cs | 4 +- .../Rendering/HtmlHelperTextAreaTest.cs | 8 +- .../Rendering/HtmlHelperTextBoxTest.cs | 8 +- 55 files changed, 357 insertions(+), 278 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/EmptyArray.cs rename test/Microsoft.AspNetCore.Mvc.Razor.Test/{RazoreViewEngineTest.cs => RazorViewEngineTest.cs} (100%) diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs index 911a643d03..56b4b1aa97 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs @@ -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; } /// @@ -625,7 +625,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding } /// - /// Removes all keys and values from ths instance of . + /// Removes all keys and values from this instance of . /// public void Clear() { diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateEntry.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateEntry.cs index a4f6391bbc..5ece146fbe 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateEntry.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateEntry.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public abstract class ModelStateEntry { private ModelErrorCollection _errors; + /// /// Gets the raw value from the request associated with this entry. /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/BindAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/BindAttribute.cs index 08e80d744e..9a69dc91d9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/BindAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/BindAttribute.cs @@ -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.Instance; } var split = original.Split(',').Select(piece => piece.Trim()).Where(piece => !string.IsNullOrEmpty(piece)); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ChallengeResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/ChallengeResult.cs index d7d73eed6e..9881c332d6 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ChallengeResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ChallengeResult.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc /// Initializes a new instance of . /// public ChallengeResult() - : this(new string[] { }) + : this(EmptyArray.Instance) { } @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc /// used to perform the authentication /// challenge. public ChallengeResult(AuthenticationProperties properties) - : this(new string[] { }, properties) + : this(EmptyArray.Instance, properties) { } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs index 54b4d1bda0..c3c240e63e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs @@ -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. /// - int IActionConstraint.Order { get; } = ConsumesActionConstraintOrder; + int IActionConstraint.Order => ConsumesActionConstraintOrder; /// /// Gets or sets the supported request content types. Used to select an action when there would otherwise be diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Filters/MiddlewareFilterAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/Filters/MiddlewareFilterAttribute.cs index f88fd1a4c0..019f1551f3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Filters/MiddlewareFilterAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Filters/MiddlewareFilterAttribute.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc public int Order { get; set; } /// - public bool IsReusable { get; } = true; + public bool IsReusable => true; /// public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ForbidResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/ForbidResult.cs index e831976b18..9fbcf18137 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ForbidResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ForbidResult.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc /// Initializes a new instance of . /// public ForbidResult() - : this(new string[] { }) + : this(EmptyArray.Instance) { } @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc /// used to perform the authentication /// challenge. public ForbidResult(AuthenticationProperties properties) - : this(new string[] { }, properties) + : this(EmptyArray.Instance, properties) { } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/FormatterMappings.cs b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/FormatterMappings.cs index 3d66ecf5ca..b9075b0acc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/FormatterMappings.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/FormatterMappings.cs @@ -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(StringComparer.OrdinalIgnoreCase); /// - /// 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. /// /// The format value. @@ -39,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } /// - /// 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. /// /// The format value. diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ClientValidatorCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ClientValidatorCache.cs index 5dcec980bc..b77470c23b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ClientValidatorCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ClientValidatorCache.cs @@ -11,8 +11,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal { public class ClientValidatorCache { - private readonly IReadOnlyList EmptyArray = new IClientModelValidator[0]; - private readonly ConcurrentDictionary _cacheEntries = new ConcurrentDictionary(); public IReadOnlyList GetValidators(ModelMetadata metadata, IClientModelValidatorProvider validatorProvider) @@ -106,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal if (count == 0) { - return EmptyArray; + return EmptyArray.Instance; } var validators = new IClientModelValidator[count]; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs index a232dd09ff..70595cd7a5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionInvokerCache.cs @@ -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.Instance; } else { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/EmptyArray.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/EmptyArray.cs new file mode 100644 index 0000000000..f8e0972989 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/EmptyArray.cs @@ -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 + { + public static TElement[] Instance { get; } = +#if NET451 + new TElement[0]; +#else + Array.Empty(); +#endif + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs index 887dba2a1f..dd046cf234 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs @@ -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(ILoggerFactory factory) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/PrefixContainer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/PrefixContainer.cs index e5c8bfe12a..e984f8830b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/PrefixContainer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/PrefixContainer.cs @@ -14,7 +14,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// public class PrefixContainer { - private static readonly string[] EmptyArray = new string[0]; private static readonly char[] Delimiters = new char[] { '[', '.' }; private readonly ICollection _originalValues; @@ -31,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal if (_originalValues.Count == 0) { - _sortedValues = EmptyArray; + _sortedValues = EmptyArray.Instance; } else { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs index f1ee38da92..c41d9d3293 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs @@ -11,8 +11,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal { public class ValidatorCache { - private readonly IReadOnlyList EmptyArray = new IModelValidator[0]; - private readonly ConcurrentDictionary _cacheEntries = new ConcurrentDictionary(); public IReadOnlyList GetValidators(ModelMetadata metadata, IModelValidatorProvider validatorProvider) @@ -105,7 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal if (count == 0) { - return EmptyArray; + return EmptyArray.Instance; } var validators = new IModelValidator[count]; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinder.cs index 584cc32b0c..670013656e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ArrayModelBinder.cs @@ -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.Instance; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinder.cs index ce123c9276..a4a68458a2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/FormCollectionModelBinder.cs @@ -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); } diff --git a/src/Microsoft.AspNetCore.Mvc.Localization/LocalizedHtmlString.cs b/src/Microsoft.AspNetCore.Mvc.Localization/LocalizedHtmlString.cs index f9c19bfb22..4ecc996ded 100644 --- a/src/Microsoft.AspNetCore.Mvc.Localization/LocalizedHtmlString.cs +++ b/src/Microsoft.AspNetCore.Mvc.Localization/LocalizedHtmlString.cs @@ -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 /// public class LocalizedHtmlString : IHtmlContent { -#if NETSTANDARD1_4 - private static readonly object[] EmptyArguments = Array.Empty(); -#else - private static readonly object[] EmptyArguments = new object[0]; -#endif private readonly object[] _arguments; /// @@ -27,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Localization /// The name of the string resource. /// The string resource. public LocalizedHtmlString(string name, string value) - : this(name, value, isResourceNotFound: false, arguments: EmptyArguments) + : this(name, value, isResourceNotFound: false, arguments: EmptyArray.Instance) { } @@ -38,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.Localization /// The string resource. /// A flag that indicates if the resource is not found. public LocalizedHtmlString(string name, string value, bool isResourceNotFound) - : this(name, value, isResourceNotFound, arguments: EmptyArguments) + : this(name, value, isResourceNotFound, arguments: EmptyArray.Instance) { } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs index bc6e44b2ee..c889b23406 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCacheResult.cs @@ -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 /// Path of the view file relative to the application base. /// The . public CompilerCacheResult(string relativePath, CompilationResult compilationResult) - : this(relativePath, compilationResult, new IChangeToken[0]) + : this(relativePath, compilationResult, EmptyArray.Instance) { } @@ -86,6 +87,5 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal /// Gets a delegate that creates an instance of the . /// public Func PageFactory { get; } - } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs index b44647f7c6..b3dfab2373 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs @@ -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.Instance; return new ViewLocationCacheResult( new ViewLocationCacheItem(factoryResult.RazorPageFactory, relativePath), diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Internal/GlobbingUrlBuilder.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Internal/GlobbingUrlBuilder.cs index 4671f8bccc..1092a92c10 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Internal/GlobbingUrlBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Internal/GlobbingUrlBuilder.cs @@ -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 /// public class GlobbingUrlBuilder { - private static readonly IReadOnlyList EmptyList = -#if NET451 - new string[0]; -#else - Array.Empty(); -#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.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.Instance; } var options = new MemoryCacheEntryOptions(); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultDisplayTemplates.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultDisplayTemplates.cs index c4536b1328..96112c6390 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultDisplayTemplates.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultDisplayTemplates.cs @@ -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(); var viewBufferScope = serviceProvider.GetRequiredService(); @@ -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, diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs index a1d3b66e9f..328975701f 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/DefaultEditorTemplates.cs @@ -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(); var viewBufferScope = serviceProvider.GetRequiredService(); @@ -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, diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionHelper.cs index 414ff0038c..335166e3ea 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionHelper.cs @@ -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); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionTextCache.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionTextCache.cs index 1ebd526880..f6ff6d2bd4 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionTextCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ExpressionTextCache.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal public class ExpressionTextCache { /// - public ConcurrentDictionary Entries { get; } = + public ConcurrentDictionary Entries { get; } = new ConcurrentDictionary(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; } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/MvcViewFeaturesLoggerExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/MvcViewFeaturesLoggerExtensions.cs index a2d381de6a..39acf011ae 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/MvcViewFeaturesLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/MvcViewFeaturesLoggerExtensions.cs @@ -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 _viewComponentExecuting; private static readonly Action _viewComponentExecuted; @@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal { if (arguments == null || arguments.Length == 0) { - return EmptyArguments; + return EmptyArray.Instance; } var formattedArguments = new string[arguments.Length]; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidationHelpers.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidationHelpers.cs index 25e683468f..cf475fc652 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidationHelpers.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ValidationHelpers.cs @@ -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.Instance; } private static void Visit( diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs index 006e47771c..cecb6c30fe 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ModelStateDictionaryExtensions.cs @@ -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> - // 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; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs index 9ab924468b..0453572ab4 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs @@ -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.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.Instance; } var split = original diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs index 80eb96b782..723f5a1cb1 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/TagBuilder.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering } /// - /// Return valid HTML 4.01 "id" attribute for an element with the given . + /// Returns a valid HTML 4.01 "id" attribute value for an element with the given . /// /// The original element name. /// @@ -99,9 +99,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// . /// /// - /// Valid HTML 4.01 "id" attribute for an element with the given . + /// Valid HTML 4.01 "id" attribute value for an element with the given . /// - /// Valid "id" attributes are defined in http://www.w3.org/TR/html401/types.html#type-id + /// + /// Valid "id" attributes are defined in http://www.w3.org/TR/html401/types.html#type-id + /// 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("\""); } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/ViewContext.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/ViewContext.cs index 790829ceb8..957260bf94 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/ViewContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/ViewContext.cs @@ -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; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/AttributeDictionary.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/AttributeDictionary.cs index a3a3b38b6f..1951b1561c 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/AttributeDictionary.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/AttributeDictionary.cs @@ -558,6 +558,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures { _attributes = attributes; } + public int Count => _attributes.Count; public bool IsReadOnly => true; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs index e96a0c9d16..b7efa33a20 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs @@ -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; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs index 85405bbc2b..f1e9eaf475 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs @@ -181,16 +181,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// /// 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 instance, then it is - /// returned as-is. + /// + /// Anonymous object describing HTML attributes. + /// A dictionary that represents HTML attributes. + /// + /// If the object is already an instance, then a shallow copy is + /// returned. /// /// new { data_name="value" } will translate to the entry { "data-name", "value" } /// in the resulting dictionary. /// - /// - /// Anonymous object describing HTML attributes. - /// A dictionary that represents HTML attributes. + /// public static IDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes) { var dictionary = htmlAttributes as IDictionary; @@ -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; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs index 4a46799e74..6d7510dafc 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs @@ -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 diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ModelExplorer.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ModelExplorer.cs index 0130bc30a0..14d9295e95 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ModelExplorer.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ModelExplorer.cs @@ -20,7 +20,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures private object _model; private Func _modelAccessor; - private Type _modelType; private List _properties; /// @@ -30,8 +29,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// The . /// The model object. May be null. 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 /// /// /// Includes a for each property of the - /// for . + /// for . /// public IEnumerable 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 /// A . /// /// - /// A created by + /// A created by /// /// represents the result of executing an arbitrary expression against the model contained /// in the current instance. @@ -411,7 +404,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures /// A . /// /// - /// A created by + /// A created by /// /// represents the result of executing an arbitrary expression against the model contained /// in the current instance. @@ -444,7 +437,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } private ModelExplorer CreateExplorerForProperty( - ModelMetadata propertyMetadata, + ModelMetadata propertyMetadata, PropertyHelper propertyHelper) { if (propertyHelper == null) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SessionStateTempDataProvider.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SessionStateTempDataProvider.cs index 3146779fea..bfa32a70fd 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SessionStateTempDataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/SessionStateTempDataProvider.cs @@ -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.Instance); } return tempDataDictionary ?? new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs index d1d9421a75..2040495e9d 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/TemplateBuilder.cs @@ -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) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataEvaluator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataEvaluator.cs index e52b336458..e0846cc5ff 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataEvaluator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewDataEvaluator.cs @@ -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 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; - } - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/OverloadActionConstraint.cs b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/OverloadActionConstraint.cs index 041090d37a..67e5df033c 100644 --- a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/OverloadActionConstraint.cs +++ b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/OverloadActionConstraint.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim public class OverloadActionConstraint : IActionConstraint { /// - public int Order { get; } = int.MaxValue; + public int Order => int.MaxValue; /// public bool Accept(ActionConstraintContext context) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/MediaTypeTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/MediaTypeTest.cs index f5b989efbc..e3306bed1a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/MediaTypeTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/MediaTypeTest.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs index 5f29c2c59e..76fef9e1a2 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/StreamOutputFormatterTest.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/MediaTypeCollectionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/MediaTypeCollectionTest.cs index 548ac39177..5745821a79 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/MediaTypeCollectionTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/MediaTypeCollectionTest.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs index 6944ee9bdd..86e6be22d9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/CompositeValueProviderTest.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs index 1d1747ddf0..3b3067ae2e 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/EnumerableValueProviderTest.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Primitives; using Xunit; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazoreViewEngineTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs similarity index 100% rename from test/Microsoft.AspNetCore.Mvc.Razor.Test/RazoreViewEngineTest.cs rename to test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/AnchorTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/AnchorTagHelperTest.cs index 77da844599..bb5c5e6921 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/AnchorTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/AnchorTagHelperTest.cs @@ -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] diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs index 108df293e5..d704154584 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs @@ -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"); diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/MediaTypeAssert.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/MediaTypeAssert.cs index 64c12987b5..bfd7576836 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/MediaTypeAssert.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/MediaTypeAssert.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionHelperTest.cs index 36ce863aff..eb06df6954 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ExpressionHelperTest.cs @@ -157,8 +157,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal (Expression>)(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>)(m => Model), (Expression>)(m => m) @@ -206,10 +206,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory), (Expression>)(model => model.SelectedCategory) }, - { - (Expression, Category>>)(model => model[2].SelectedCategory), - (Expression, Category>>)(model => model[2].SelectedCategory) - }, { (Expression>)(m => Model), (Expression>)(m => m.Model) @@ -226,10 +222,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal (Expression>)(m => key), (Expression>)(m => value) }, - { - (Expression, string>>)(model => model[key].SelectedCategory.CategoryName.MainCategory), - (Expression, 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); } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperFormTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperFormTest.cs index 073c9ac518..974001f48f 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperFormTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperFormTest.cs @@ -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; diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperHiddenTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperHiddenTest.cs index 43566e6ffb..6b40ea2b91 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperHiddenTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperHiddenTest.cs @@ -20,12 +20,12 @@ namespace Microsoft.AspNetCore.Mvc.Rendering get { var expected1 = @""; + @"value=""HtmlEncode[[ModelStateValue]]"" />"; yield return new object[] { new Dictionary { { "baz", "BazValue" } }, expected1 }; yield return new object[] { new { baz = "BazValue" }, expected1 }; var expected2 = @""; + @"value=""HtmlEncode[[ModelStateValue]]"" />"; yield return new object[] { new Dictionary { { "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 = @""; + @"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 = @""; + @"value=""HtmlEncode[[test]]"" />"; var attributes = new Dictionary { { "data-key", "value" } }; var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithNullModelAndNonNullViewData()); @@ -292,7 +292,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { // Arrange var expected = @""; + @"value="""" />"; var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetViewDataWithModelStateAndModelAndViewDataValues()); var attributes = new Dictionary { { "baz", "BazValue" } }; diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperLabelExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperLabelExtensionsTest.cs index f4177af1d7..b4ae4b752f 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperLabelExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperLabelExtensionsTest.cs @@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.Core public void LabelHelpers_DisplayPropertyName() { // Arrange + var expectedLabel = ""; var helper = DefaultTemplatesUtilities.GetHtmlHelper(); // Act @@ -45,14 +46,15 @@ namespace Microsoft.AspNetCore.Mvc.Core var labelForResult = helper.LabelFor(m => m.Property1); // Assert - Assert.Equal("", HtmlContentUtilities.HtmlContentToString(labelResult)); - Assert.Equal("", HtmlContentUtilities.HtmlContentToString(labelForResult)); + Assert.Equal(expectedLabel, HtmlContentUtilities.HtmlContentToString(labelResult)); + Assert.Equal(expectedLabel, HtmlContentUtilities.HtmlContentToString(labelForResult)); } [Fact] public void LabelHelpers_DisplayPropertyName_ForNestedProperty() { // Arrange + var expectedLabel = ""; var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: null); // Act @@ -60,8 +62,8 @@ namespace Microsoft.AspNetCore.Mvc.Core var labelForResult = helper.LabelFor(m => m.Inner.Id); // Assert - Assert.Equal("", HtmlContentUtilities.HtmlContentToString(labelResult)); - Assert.Equal("", HtmlContentUtilities.HtmlContentToString(labelForResult)); + Assert.Equal(expectedLabel, HtmlContentUtilities.HtmlContentToString(labelResult)); + Assert.Equal(expectedLabel, HtmlContentUtilities.HtmlContentToString(labelForResult)); } [Fact] diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs index 192b50e282..4b502ecf87 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperPasswordTest.cs @@ -469,7 +469,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering Expression> 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)); } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextAreaTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextAreaTest.cs index a00633e372..1e5d51fc1c 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextAreaTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextAreaTest.cs @@ -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); } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextBoxTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextBoxTest.cs index e7dac3c03a..82b5710fde 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextBoxTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/HtmlHelperTextBoxTest.cs @@ -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); }