From c74a945dda2c96af30bb9dd1482a67edfe6fdd56 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Tue, 30 Oct 2018 20:09:17 -0700 Subject: [PATCH] Convert `RouteValueDictionary` values to `string` using `CultureInfo.InvariantCulture` (#8674) * Convert `RouteValueDictionary` values to `string` using `CultureInfo.InvariantCulture` - #8578 - user may override this choice in one case: - register a custom `IValueProviderFactory` to pass another `CultureInfo` into the `RouteValueProvider` - values are used as programmatic tokens outside `RouteValueProvider` nits: - take VS suggestions in changed classes - take VS suggestions in files I had open :) --- .../ActionSelectorBenchmark.cs | 7 +- .../Formatters/FormatFilter.cs | 6 +- .../Internal/ActionSelector.cs | 18 ++- .../Internal/NormalizedRouteValue.cs | 3 +- .../ModelBinding/RouteValueProvider.cs | 5 +- .../Routing/KnownRouteValueConstraint.cs | 6 +- .../Routing/PageLinkGeneratorExtensions.cs | 6 +- .../Routing/UrlHelperBase.cs | 10 +- .../ApplicationModels/PageRouteModel.cs | 3 +- .../DefaultPageHandlerMethodSelector.cs | 14 +- .../Internal/PageRouteModelFactory.cs | 2 +- .../Cache/CacheTagKey.cs | 9 +- .../LinkTagHelper.cs | 5 +- .../ViewFeatures/PartialViewResultExecutor.cs | 9 +- .../ViewFeatures/ViewResultExecutor.cs | 11 +- .../OverloadActionConstraint.cs | 5 +- .../Formatters/FormatFilterTest.cs | 32 +++- .../Internal/ActionSelectorTest.cs | 44 ++++++ .../Binders/BodyModelBinderTests.cs | 10 +- .../ModelBinding/RouteValueProviderTests.cs | 39 +++++ .../Routing/KnownRouteValueConstraintTests.cs | 30 ++++ .../PageLinkGeneratorExtensionsTest.cs | 2 +- .../Routing/UrlHelperExtensionsTest.cs | 50 ++++++ .../RazorViewEngineTest.cs | 24 +++ .../DefaultPageHandlerMethodSelectorTest.cs | 52 +++++++ .../CacheTagKeyTest.cs | 25 +++ .../LinkTagHelperTest.cs | 143 +++++++++++++++++- .../PartialViewResultExecutorTest.cs | 26 ++++ .../ViewFeatures/ViewResultExecutorTest.cs | 26 ++++ 29 files changed, 550 insertions(+), 72 deletions(-) diff --git a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ActionSelectorBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ActionSelectorBenchmark.cs index 5b4cebad21..d207316ddc 100644 --- a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ActionSelectorBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/ActionSelectorBenchmark.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -123,8 +124,8 @@ private static IReadOnlyList NaiveSelectCandidates(ActionDescr var isMatch = true; foreach (var kvp in action.RouteValues) { - var routeValue = Convert.ToString(routeValues[kvp.Key]) ?? string.Empty; - + var routeValue = Convert.ToString(routeValues[kvp.Key], CultureInfo.InvariantCulture) ?? + string.Empty; if (string.IsNullOrEmpty(kvp.Value) && string.IsNullOrEmpty(routeValue)) { // Match @@ -156,7 +157,7 @@ private static ActionDescriptor CreateActionDescriptor(object obj) var routeValues = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kvp in new RouteValueDictionary(obj)) { - routeValues.Add(kvp.Key, Convert.ToString(kvp.Value) ?? string.Empty); + routeValues.Add(kvp.Key, Convert.ToString(kvp.Value, CultureInfo.InvariantCulture) ?? string.Empty); } return new ActionDescriptor() diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/FormatFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/FormatFilter.cs index dd7b568fce..e70f0130c9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/FormatFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/FormatFilter.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.Globalization; using System.Linq; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Filters; @@ -59,7 +60,7 @@ public virtual string GetFormat(ActionContext context) if (context.RouteData.Values.TryGetValue("format", out var obj)) { // null and string.Empty are equivalent for route values. - var routeValue = obj?.ToString(); + var routeValue = Convert.ToString(obj, CultureInfo.InvariantCulture); return string.IsNullOrEmpty(routeValue) ? null : routeValue; } @@ -166,8 +167,7 @@ public void OnResultExecuting(ResultExecutingContext context) return; } - var objectResult = context.Result as ObjectResult; - if (objectResult == null) + if (!(context.Result is ObjectResult objectResult)) { return; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionSelector.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionSelector.cs index 210250ff69..5dba1a7654 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionSelector.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionSelector.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading; using Microsoft.AspNetCore.Mvc.Abstractions; @@ -81,11 +82,10 @@ public IReadOnlyList SelectCandidates(RouteContext context) var values = new string[keys.Length]; for (var i = 0; i < keys.Length; i++) { - context.RouteData.Values.TryGetValue(keys[i], out object value); - + context.RouteData.Values.TryGetValue(keys[i], out var value); if (value != null) { - values[i] = value as string ?? Convert.ToString(value); + values[i] = value as string ?? Convert.ToString(value, CultureInfo.InvariantCulture); } } @@ -220,9 +220,11 @@ protected virtual IReadOnlyList SelectBestActions(IReadOnlyLis var actionsWithConstraint = new List(); var actionsWithoutConstraint = new List(); - var constraintContext = new ActionConstraintContext(); - constraintContext.Candidates = candidates; - constraintContext.RouteContext = context; + var constraintContext = new ActionConstraintContext + { + Candidates = candidates, + RouteContext = context + }; // Perf: Avoid allocations for (var i = 0; i < candidates.Count; i++) @@ -294,7 +296,7 @@ protected virtual IReadOnlyList SelectBestActions(IReadOnlyLis // canonical entries. When you don't hit a case-sensitive match it will try the case-insensitive dictionary // so you still get correct behaviors. // - // The difference here is because while MVC is case-insensitive, doing a case-sensitive comparison is much + // The difference here is because while MVC is case-insensitive, doing a case-sensitive comparison is much // faster. We also expect that most of the URLs we process are canonically-cased because they were generated // by Url.Action or another routing api. // @@ -316,7 +318,7 @@ public Cache(ActionDescriptorCollection actions) OrdinalEntries = new Dictionary>(StringArrayComparer.Ordinal); OrdinalIgnoreCaseEntries = new Dictionary>(StringArrayComparer.OrdinalIgnoreCase); - // We need to first identify of the keys that action selection will look at (in route data). + // We need to first identify of the keys that action selection will look at (in route data). // We want to only consider conventionally routed actions here. var routeKeys = new HashSet(StringComparer.OrdinalIgnoreCase); for (var i = 0; i < actions.Items.Count; i++) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/NormalizedRouteValue.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/NormalizedRouteValue.cs index 2da629c22e..3013ee3171 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/NormalizedRouteValue.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/NormalizedRouteValue.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.Globalization; namespace Microsoft.AspNetCore.Mvc.Internal { @@ -45,7 +46,7 @@ public static string GetNormalizedRouteValue(ActionContext context, string key) normalizedValue = value; } - var stringRouteValue = routeValue?.ToString(); + var stringRouteValue = Convert.ToString(routeValue, CultureInfo.InvariantCulture); if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase)) { return normalizedValue; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/RouteValueProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/RouteValueProvider.cs index 6c9a04e281..615b4bd437 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/RouteValueProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/RouteValueProvider.cs @@ -88,10 +88,9 @@ public override ValueProviderResult GetValue(string key) throw new ArgumentNullException(nameof(key)); } - object value; - if (_values.TryGetValue(key, out value)) + if (_values.TryGetValue(key, out var value)) { - var stringValue = value as string ?? value?.ToString() ?? string.Empty; + var stringValue = value as string ?? Convert.ToString(value, Culture) ?? string.Empty; return new ValueProviderResult(stringValue, Culture); } else diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/KnownRouteValueConstraint.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/KnownRouteValueConstraint.cs index 6989c22695..d3a7f5a32a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/KnownRouteValueConstraint.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/KnownRouteValueConstraint.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; @@ -51,10 +52,9 @@ public KnownRouteValueConstraint(IActionDescriptorCollectionProvider actionDescr throw new ArgumentNullException(nameof(values)); } - object obj; - if (values.TryGetValue(routeKey, out obj)) + if (values.TryGetValue(routeKey, out var obj)) { - var value = obj as string; + var value = Convert.ToString(obj, CultureInfo.InvariantCulture); if (value != null) { var actionDescriptors = GetAndValidateActionDescriptors(httpContext); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/PageLinkGeneratorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/PageLinkGeneratorExtensions.cs index a7e9cde9c0..032831560b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/PageLinkGeneratorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/PageLinkGeneratorExtensions.cs @@ -1,10 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Routing; -using System; namespace Microsoft.AspNetCore.Routing { @@ -104,7 +104,7 @@ public static class PageLinkGeneratorExtensions } var address = CreateAddress(httpContext: null, page, handler, values); - return generator.GetPathByAddress(address, address.ExplicitValues, pathBase, fragment, options); + return generator.GetPathByAddress(address, address.ExplicitValues, pathBase, fragment, options); } /// @@ -230,4 +230,4 @@ private static RouteValueDictionary GetAmbientValues(HttpContext httpContext) return httpContext?.Features.Get()?.RouteValues; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs index c468e5280c..8e84b081b3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Text; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Routing; @@ -163,8 +163,7 @@ protected string GenerateUrl(string protocol, string host, string virtualPath, s // Perf: In most of the common cases, GenerateUrl is called with a null protocol, host and fragment. // In such cases, we might not need to build any URL as the url generated is mostly same as the virtual path available in pathData. // For such common cases, this FastGenerateUrl method saves a string allocation per GenerateUrl call. - string url; - if (TryFastGenerateUrl(protocol, host, virtualPath, fragment, out url)) + if (TryFastGenerateUrl(protocol, host, virtualPath, fragment, out var url)) { return url; } @@ -227,8 +226,7 @@ protected string GenerateUrl(string protocol, string host, string path) // Perf: In most of the common cases, GenerateUrl is called with a null protocol, host and fragment. // In such cases, we might not need to build any URL as the url generated is mostly same as the virtual path available in pathData. // For such common cases, this FastGenerateUrl method saves a string allocation per GenerateUrl call. - string url; - if (TryFastGenerateUrl(protocol, host, path, fragment: null, out url)) + if (TryFastGenerateUrl(protocol, host, path, fragment: null, out var url)) { return url; } @@ -351,7 +349,7 @@ private static object CalculatePageName(ActionContext context, RouteValueDiction } else if (ambientValues != null) { - currentPagePath = ambientValues["page"]?.ToString(); + currentPagePath = Convert.ToString(ambientValues["page"], CultureInfo.InvariantCulture); } else { diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModel.cs index 8c94ae18a7..a150977094 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageRouteModel.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.ApplicationModels @@ -96,7 +95,7 @@ public PageRouteModel(PageRouteModel other) public IList Selectors { get; } /// - /// Gets a collection of route values that must be present in the + /// Gets a collection of route values that must be present in the /// for the corresponding page to be selected. /// /// diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs index 2369705ed7..e2a619ca32 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageHandlerMethodSelector.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { @@ -57,8 +57,10 @@ public HandlerMethodDescriptor Select(PageContext context) if (ambiguousMatches == null) { - ambiguousMatches = new List(); - ambiguousMatches.Add(bestMatch); + ambiguousMatches = new List + { + bestMatch + }; } ambiguousMatches.Add(handler); @@ -165,13 +167,13 @@ private static int GetScore(HandlerMethodDescriptor descriptor) private static string GetHandlerName(PageContext context) { - var handlerName = Convert.ToString(context.RouteData.Values[Handler]); + var handlerName = Convert.ToString(context.RouteData.Values[Handler], CultureInfo.InvariantCulture); if (!string.IsNullOrEmpty(handlerName)) { return handlerName; } - if (context.HttpContext.Request.Query.TryGetValue(Handler, out StringValues queryValues)) + if (context.HttpContext.Request.Query.TryGetValue(Handler, out var queryValues)) { return queryValues[0]; } @@ -192,4 +194,4 @@ private static string GetFuzzyMatchHttpMethod(PageContext context) return null; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs index 63f06ded68..d6699015be 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs @@ -67,7 +67,7 @@ private static void PopulateRouteModel(PageRouteModel model, string pageRoute, s if (!AttributeRouteModel.IsOverridePattern(routeTemplate) && string.Equals(IndexFileName, fileName, StringComparison.OrdinalIgnoreCase)) { - // For pages without an override route, and ending in /Index.cshtml, we want to allow + // For pages without an override route, and ending in /Index.cshtml, we want to allow // incoming routing, but force outgoing routes to match to the path sans /Index. selectorModel.AttributeRouteModel.SuppressLinkGeneration = true; diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Cache/CacheTagKey.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Cache/CacheTagKey.cs index 6db23cdcee..bf327f30ca 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/Cache/CacheTagKey.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/Cache/CacheTagKey.cs @@ -7,7 +7,6 @@ using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Razor.Infrastructure; -using Microsoft.AspNetCore.Mvc.TagHelpers.Internal; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Internal; @@ -25,7 +24,8 @@ public class CacheTagKey : IEquatable private static readonly Func CookieAccessor = (c, key) => c[key]; private static readonly Func HeaderAccessor = (c, key) => c[key]; private static readonly Func QueryAccessor = (c, key) => c[key]; - private static readonly Func RouteValueAccessor = (c, key) => c[key]?.ToString(); + private static readonly Func RouteValueAccessor = (c, key) => + Convert.ToString(c[key], CultureInfo.InvariantCulture); private const string CacheKeyTokenSeparator = "||"; private const string VaryByName = "VaryBy"; @@ -91,7 +91,10 @@ private CacheTagKey(CacheTagHelperBase tagHelper) _cookies = ExtractCollection(tagHelper.VaryByCookie, request.Cookies, CookieAccessor); _headers = ExtractCollection(tagHelper.VaryByHeader, request.Headers, HeaderAccessor); _queries = ExtractCollection(tagHelper.VaryByQuery, request.Query, QueryAccessor); - _routeValues = ExtractCollection(tagHelper.VaryByRoute, tagHelper.ViewContext.RouteData.Values, RouteValueAccessor); + _routeValues = ExtractCollection( + tagHelper.VaryByRoute, + tagHelper.ViewContext.RouteData.Values, + RouteValueAccessor); _varyByUser = tagHelper.VaryByUser; _varyByCulture = tagHelper.VaryByCulture; diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs index d58aee8711..d082db1ef6 100644 --- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/LinkTagHelper.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Hosting; @@ -441,7 +442,7 @@ private bool HasStyleSheetLinkType(TagHelperAttributeList attributes) } else if (stringValue == null) { - stringValue = attributeValue.ToString(); + stringValue = Convert.ToString(attributeValue, CultureInfo.InvariantCulture); } var hasRelStylesheet = string.Equals("stylesheet", stringValue, StringComparison.Ordinal); @@ -570,4 +571,4 @@ private enum Mode Fallback = 2, } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs index 18ecbebe3d..2644558bc6 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -199,22 +200,20 @@ private static string GetActionName(ActionContext context) throw new ArgumentNullException(nameof(context)); } - object routeValue; - if (!context.RouteData.Values.TryGetValue(ActionNameKey, out routeValue)) + if (!context.RouteData.Values.TryGetValue(ActionNameKey, out var routeValue)) { return null; } var actionDescriptor = context.ActionDescriptor; string normalizedValue = null; - string value; - if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out value) && + if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out var value) && !string.IsNullOrEmpty(value)) { normalizedValue = value; } - var stringRouteValue = routeValue?.ToString(); + var stringRouteValue = Convert.ToString(routeValue, CultureInfo.InvariantCulture); if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase)) { return normalizedValue; diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs index af284f0908..4a2ca98b6d 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -115,10 +116,10 @@ public virtual ViewEngineResult FindView(ActionContext actionContext, ViewResult "Microsoft.AspNetCore.Mvc.ViewFound", new { - actionContext = actionContext, + actionContext, isMainPage = true, result = viewResult, - viewName = viewName, + viewName, view = result.View, }); } @@ -133,10 +134,10 @@ public virtual ViewEngineResult FindView(ActionContext actionContext, ViewResult "Microsoft.AspNetCore.Mvc.ViewNotFound", new { - actionContext = actionContext, + actionContext, isMainPage = true, result = viewResult, - viewName = viewName, + viewName, searchedLocations = result.SearchedLocations }); } @@ -199,7 +200,7 @@ private static string GetActionName(ActionContext context) normalizedValue = value; } - var stringRouteValue = routeValue?.ToString(); + var stringRouteValue = Convert.ToString(routeValue, CultureInfo.InvariantCulture); if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase)) { return normalizedValue; diff --git a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/OverloadActionConstraint.cs b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/OverloadActionConstraint.cs index 67e5df033c..2ae5b527d0 100644 --- a/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/OverloadActionConstraint.cs +++ b/src/Microsoft.AspNetCore.Mvc.WebApiCompatShim/OverloadActionConstraint.cs @@ -97,8 +97,7 @@ private List GetOverloadableParameters(ActionSelectorCandid } var parameters = new List(); - object optionalParametersObject; - candidate.Action.Properties.TryGetValue("OptionalParameters", out optionalParametersObject); + candidate.Action.Properties.TryGetValue("OptionalParameters", out var optionalParametersObject); var optionalParameters = (HashSet)optionalParametersObject; foreach (var parameter in candidate.Action.Parameters) { @@ -191,4 +190,4 @@ private class OverloadedParameter public string Prefix { get; set; } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/FormatFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/FormatFilterTest.cs index 84778f73b3..7d5760fca9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/FormatFilterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/FormatFilterTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Buffers; using System.Collections.Generic; using Microsoft.AspNetCore.Http; @@ -8,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -28,13 +30,10 @@ public enum FormatSource } [Theory] - [InlineData("json", FormatSource.RouteData, "application/json")] - [InlineData("json", FormatSource.QueryData, "application/json")] - [InlineData("json", FormatSource.RouteAndQueryData, "application/json")] - public void FormatFilter_ContextContainsFormat_DefaultFormat( - string format, - FormatSource place, - string contentType) + [InlineData("json", FormatSource.RouteData)] + [InlineData("json", FormatSource.QueryData)] + [InlineData("json", FormatSource.RouteAndQueryData)] + public void FormatFilter_ContextContainsFormat_DefaultFormat(string format, FormatSource place) { // Arrange var mediaType = new StringSegment("application/json"); @@ -305,6 +304,25 @@ public void FormatFilter_MoreSpecificThan_Produces() Assert.Equal(expected, filter.GetFormat(context)); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void FormatFilter_GetFormat_UsesInvariantCulture() + { + // Arrange + var mockObjects = new MockObjects(); + var context = mockObjects.CreateResultExecutingContext(); + context.RouteData.Values["format"] = new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)); + var expected = "10/31/2018 07:37:38 -07:00"; + var filterAttribute = new FormatFilterAttribute(); + var filter = new FormatFilter(mockObjects.OptionsManager, NullLoggerFactory.Instance); + + // Act + var format = filter.GetFormat(context); + + // Assert + Assert.Equal(expected, filter.GetFormat(context)); + } + [Fact] public void FormatFilter_ExplicitContentType_SetOnObjectResult_TakesPrecedence() { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionSelectorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionSelectorTest.cs index 5b3adffcbc..214c6eebb7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionSelectorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionSelectorTest.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -64,6 +65,49 @@ public void SelectCandidates_SingleMatch() Assert.Collection(candidates, (a) => Assert.Same(actions[0], a)); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void SelectCandidates_SingleMatch_UsesInvariantCulture() + { + var actions = new ActionDescriptor[] + { + new ActionDescriptor() + { + DisplayName = "A1", + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "controller", "Home" }, + { "action", "Index" }, + { "date", "10/31/2018 07:37:38 -07:00" }, + }, + }, + new ActionDescriptor() + { + DisplayName = "A2", + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "controller", "Home" }, + { "action", "About" } + }, + }, + }; + + var selector = CreateSelector(actions); + + var routeContext = CreateRouteContext("GET"); + routeContext.RouteData.Values.Add("controller", "Home"); + routeContext.RouteData.Values.Add("action", "Index"); + routeContext.RouteData.Values.Add( + "date", + new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7))); + + // Act + var candidates = selector.SelectCandidates(routeContext); + + // Assert + Assert.Collection(candidates, (a) => Assert.Same(actions[0], a)); + } + [Fact] public void SelectCandidates_MultipleMatches() { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs index 71318bc1c3..97ceca0963 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs @@ -829,7 +829,7 @@ public override Task ReadRequestBodyAsync(InputFormatterCo private class TestableXmlSerializerInputFormatter : XmlSerializerInputFormatter { - private bool _throwNonInputFormatterException; + private readonly bool _throwNonInputFormatterException; public TestableXmlSerializerInputFormatter(bool throwNonInputFormatterException) : base(new MvcOptions()) @@ -851,7 +851,7 @@ public override Task ReadRequestBodyAsync(InputFormatterCo private class TestableXmlDataContractSerializerInputFormatter : XmlDataContractSerializerInputFormatter { - private bool _throwNonInputFormatterException; + private readonly bool _throwNonInputFormatterException; public TestableXmlDataContractSerializerInputFormatter(bool throwNonInputFormatterException) : base(new MvcOptions()) @@ -899,7 +899,7 @@ public override Task ReadRequestBodyAsync(InputFormatterCo private class DerivedXmlSerializerInputFormatter : XmlSerializerInputFormatter { - private bool _throwNonInputFormatterException; + private readonly bool _throwNonInputFormatterException; public DerivedXmlSerializerInputFormatter(bool throwNonInputFormatterException) : base(new MvcOptions()) @@ -921,7 +921,7 @@ public override Task ReadRequestBodyAsync(InputFormatterCo private class DerivedXmlDataContractSerializerInputFormatter : XmlDataContractSerializerInputFormatter { - private bool _throwNonInputFormatterException; + private readonly bool _throwNonInputFormatterException; public DerivedXmlDataContractSerializerInputFormatter(bool throwNonInputFormatterException) : base(new MvcOptions()) @@ -952,4 +952,4 @@ public class Person public string Name { get; set; } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/RouteValueProviderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/RouteValueProviderTests.cs index 8c8c050ae2..a013595eac 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/RouteValueProviderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/RouteValueProviderTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Xunit; namespace Microsoft.AspNetCore.Mvc.ModelBinding @@ -45,6 +46,44 @@ public void GetValueProvider_ReturnsValue_IfKeyIsPresent() Assert.Equal("test-value", (string)result); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void GetValueProvider_ReturnsValue_UsesInvariantCulture() + { + // Arrange + var values = new RouteValueDictionary(new Dictionary + { + { "test-key", new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)) }, + }); + var provider = new RouteValueProvider(BindingSource.Query, values); + + // Act + var result = provider.GetValue("test-key"); + + // Assert + Assert.Equal("10/31/2018 07:37:38 -07:00", (string)result); + } + + [Fact] + public void GetValueProvider_ReturnsValue_UsesSpecifiedCulture() + { + // Arrange + var values = new RouteValueDictionary(new Dictionary + { + { "test-key", new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)) }, + }); + var provider = new RouteValueProvider(BindingSource.Query, values, new CultureInfo("de-CH")); + + // de-CH culture is slightly different on Windows versus other platforms. + var expected = TestPlatformHelper.IsWindows ? "31.10.2018 07:37:38 -07:00" : "31.10.18 07:37:38 -07:00"; + + // Act + var result = provider.GetValue("test-key"); + + // Assert + Assert.Equal(expected, (string)result); + } + [Fact] public void ContainsPrefix_ReturnsNullValue_IfKeyIsPresent() { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/KnownRouteValueConstraintTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/KnownRouteValueConstraintTests.cs index 61b51de28f..103b4ad14b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/KnownRouteValueConstraintTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/KnownRouteValueConstraintTests.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; @@ -233,6 +234,35 @@ public void ServiceInjected_RouteKey_Exists_MatchSucceeds(string keyName, RouteD Assert.True(match); } + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + [ReplaceCulture("de-CH", "de-CH")] + public void ServiceInjected_RouteKey_Exists_UsesInvariantCulture(RouteDirection direction) + { + // Arrange + var actionDescriptor = CreateActionDescriptor("testArea", "testController", "testAction"); + actionDescriptor.RouteValues.Add("randomKey", "10/31/2018 07:37:38 -07:00"); + + var provider = CreateActionDescriptorCollectionProvider(actionDescriptor); + + var constraint = new KnownRouteValueConstraint(provider); + + var values = new RouteValueDictionary() + { + { "area", "testArea" }, + { "controller", "testController" }, + { "action", "testAction" }, + { "randomKey", new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)) }, + }; + + // Act + var match = constraint.Match(httpContext: null, route: null, "randomKey", values, direction); + + // Assert + Assert.True(match); + } + private static HttpContext GetHttpContext(ActionDescriptor actionDescriptor, bool setupRequestServices = true) { var descriptorCollectionProvider = CreateActionDescriptorCollectionProvider(actionDescriptor); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/PageLinkGeneratorExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/PageLinkGeneratorExtensionsTest.cs index 3fc85649c6..c0b84528e0 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/PageLinkGeneratorExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/PageLinkGeneratorExtensionsTest.cs @@ -219,4 +219,4 @@ private HttpContext CreateHttpContext(object ambientValues = null) return httpContext; } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs index 24d9ce80f5..b63c66e0fe 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Moq; using Xunit; @@ -248,6 +249,55 @@ public void Page_UsesAmbientRouteValue_WhenPageIsNull() }); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void Page_UsesAmbientRouteValueAndInvariantCulture_WhenPageIsNotNull() + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData + { + Values = + { + { "page", new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)) }, + } + }; + var actionContext = new ActionContext + { + ActionDescriptor = new ActionDescriptor + { + RouteValues = new Dictionary + { + { "page", "10/31/2018 07:37:38 -07:00" }, + }, + }, + RouteData = routeData, + }; + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("New Page", new { id = 13 }); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("10/31/New Page", value.Value); + }); + } + [Fact] public void Page_SetsHandlerToNull_IfValueIsNotSpecifiedInRouteValues() { diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs index f522b54a49..99240071cf 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -1614,6 +1614,30 @@ public void GetNormalizedRouteValue_ReturnsValueFromRouteValues() Assert.Equal("Route-Value", result); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void GetNormalizedRouteValue_UsesInvariantCulture() + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor(); + actionDescriptor.RouteValues.Add(key, "Route-Value"); + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)); + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Equal("10/31/2018 07:37:38 -07:00", result); + } + [Fact] public void GetNormalizedRouteValue_ReturnsRouteValue_IfValueDoesNotMatch() { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs index f4fe43fdf2..34b081d84d 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/DefaultPageHandlerMethodSelectorTest.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Options; using Xunit; @@ -419,6 +420,57 @@ public void Select_ReturnsHandlerThatMatchesHandler() Assert.Same(descriptor1, actual); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void Select_ReturnsHandlerThatMatchesHandler_UsesInvariantCulture() + { + // Arrange + var descriptor1 = new HandlerMethodDescriptor + { + HttpMethod = "POST", + Name = "10/31/2018 07:37:38 -07:00", + }; + + var descriptor2 = new HandlerMethodDescriptor + { + HttpMethod = "POST", + Name = "Delete", + }; + + var pageContext = new PageContext + { + ActionDescriptor = new CompiledPageActionDescriptor + { + HandlerMethods = new List() + { + descriptor1, + descriptor2, + }, + }, + RouteData = new RouteData + { + Values = + { + { "handler", new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)) }, + } + }, + HttpContext = new DefaultHttpContext + { + Request = + { + Method = "Post" + }, + }, + }; + var selector = CreateSelector(); + + // Act + var actual = selector.Select(pageContext); + + // Assert + Assert.Same(descriptor1, actual); + } + [Fact] public void Select_HandlerFromQueryString() { diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/CacheTagKeyTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/CacheTagKeyTest.cs index 36ce9d6216..9f7190a6e0 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/CacheTagKeyTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/CacheTagKeyTest.cs @@ -304,6 +304,31 @@ public void GenerateKey_UsesVaryByRoute(string varyByRoute, string expected) Assert.Equal(expected, key); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void GenerateKey_UsesVaryByRoute_UsesInvariantCulture() + { + // Arrange + var tagHelperContext = GetTagHelperContext(); + var cacheTagHelper = new CacheTagHelper( + new CacheTagHelperMemoryCacheFactory(Mock.Of()), new HtmlTestEncoder()) + { + ViewContext = GetViewContext(), + VaryByRoute = "Category", + }; + cacheTagHelper.ViewContext.RouteData.Values["id"] = 4; + cacheTagHelper.ViewContext.RouteData.Values["category"] = + new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)); + var expected = "CacheTagHelper||testid||VaryByRoute(Category||10/31/2018 07:37:38 -07:00)"; + + // Act + var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext); + var key = cacheTagKey.GenerateKey(); + + // Assert + Assert.Equal(expected, key); + } + [Fact] public void GenerateKey_UsesVaryByUser_WhenUserIsNotAuthenticated() { diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/LinkTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/LinkTagHelperTest.cs index ddf6ed2834..09ebce26e6 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/LinkTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/LinkTagHelperTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -20,6 +21,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; @@ -161,8 +163,10 @@ public void HandlesMultipleAttributesSameNameCorrectly(TagHelperAttributeList ou helper.FallbackTestValue = "hidden"; helper.Href = "test.css"; - var expectedAttributes = new TagHelperAttributeList(output.Attributes); - expectedAttributes.Add(new TagHelperAttribute("href", "test.css")); + var expectedAttributes = new TagHelperAttributeList(output.Attributes) + { + new TagHelperAttribute("href", "test.css") + }; // Act helper.Process(context, output); @@ -605,6 +609,47 @@ public void RendersLinkTagsForGlobbedHrefResults() Assert.Equal(expectedContent, content); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void RendersLinkTagsForGlobbedHrefResults_UsesInvariantCulture() + { + // Arrange + var expectedContent = "" + + ""; + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "rel", new ConvertToStyleSheet() }, + { "href", "/css/site.css" }, + { "asp-href-include", "**/*.css" }, + }); + var output = MakeTagHelperOutput("link", attributes: new TagHelperAttributeList + { + { "rel", new HtmlString("stylesheet") }, + }); + var globbingUrlBuilder = new Mock( + new TestFileProvider(), + Mock.Of(), + PathString.Empty); + globbingUrlBuilder.Setup(g => g.BuildUrlList(null, "**/*.css", null)) + .Returns(new[] { "/base.css" }); + + var helper = GetHelper(); + + helper.GlobbingUrlBuilder = globbingUrlBuilder.Object; + helper.Href = "/css/site.css"; + helper.HrefInclude = "**/*.css"; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal("link", output.TagName); + Assert.Equal("/css/site.css", output.Attributes["href"].Value); + var content = HtmlContentUtilities.HtmlContentToString(output, new HtmlTestEncoder()); + Assert.Equal(expectedContent, content); + } + [Fact] public void RendersLinkTagsForGlobbedHrefResults_EncodesAsExpected() { @@ -991,5 +1036,99 @@ private static IUrlHelperFactory MakeUrlHelperFactory() return urlHelperFactory.Object; } + + private class ConvertToStyleSheet : IConvertible + { + public TypeCode GetTypeCode() + { + throw new NotImplementedException(); + } + + public bool ToBoolean(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public byte ToByte(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public char ToChar(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public DateTime ToDateTime(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public decimal ToDecimal(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public double ToDouble(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public short ToInt16(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public int ToInt32(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public long ToInt64(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public sbyte ToSByte(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public float ToSingle(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public string ToString(IFormatProvider provider) + { + Assert.Equal(CultureInfo.InvariantCulture, provider); + return "stylesheet"; + } + + public object ToType(Type conversionType, IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public ushort ToUInt16(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public uint ToUInt32(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public ulong ToUInt64(IFormatProvider provider) + { + throw new NotImplementedException(); + } + + public override string ToString() + { + return "something else"; + } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs index ff2abdd59e..3a34cc25d2 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -10,6 +11,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -75,6 +77,30 @@ public void FindView_UsesActionDescriptorName_IfViewNameIsNull() Assert.Equal(viewName, viewEngineResult.ViewName); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void FindView_UsesActionDescriptorName_IfViewNameIsNull_UsesInvariantCulture() + { + // Arrange + var viewName = "10/31/2018 07:37:38 -07:00"; + var context = GetActionContext(viewName); + context.RouteData.Values["action"] = new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)); + + var executor = GetViewExecutor(); + + var viewResult = new PartialViewResult + { + ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()), + TempData = Mock.Of(), + }; + + // Act + var viewEngineResult = executor.FindView(context, viewResult); + + // Assert + Assert.Equal(viewName, viewEngineResult.ViewName); + } + [Fact] public void FindView_ReturnsExpectedNotFoundResult_WithGetViewLocations() { diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs index 415a45700b..f8a8ae8745 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -74,6 +76,30 @@ public void FindView_UsesActionDescriptorName_IfViewNameIsNull() Assert.Equal(viewName, viewEngineResult.ViewName); } + [Fact] + [ReplaceCulture("de-CH", "de-CH")] + public void FindView_UsesActionDescriptorName_IfViewNameIsNull_UsesInvariantCulture() + { + // Arrange + var viewName = "10/31/2018 07:37:38 -07:00"; + var context = GetActionContext(viewName); + context.RouteData.Values["action"] = new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7)); + + var executor = GetViewExecutor(); + + var viewResult = new ViewResult + { + ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()), + TempData = Mock.Of(), + }; + + // Act + var viewEngineResult = executor.FindView(context, viewResult); + + // Assert + Assert.Equal(viewName, viewEngineResult.ViewName); + } + [Fact] public void FindView_ReturnsExpectedNotFoundResult_WithGetViewLocations() {