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 :)
This commit is contained in:
Doug Bunting 2018-10-30 20:09:17 -07:00 committed by GitHub
parent 734b919b02
commit c74a945dda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 550 additions and 72 deletions

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.Performance
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 @@ namespace Microsoft.AspNetCore.Mvc.Performance
var routeValues = new Dictionary<string, string>(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()

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Filters;
@ -59,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
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 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
return;
}
var objectResult = context.Result as ObjectResult;
if (objectResult == null)
if (!(context.Result is ObjectResult objectResult))
{
return;
}

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var actionsWithConstraint = new List<ActionSelectorCandidate>();
var actionsWithoutConstraint = new List<ActionSelectorCandidate>();
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 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// 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 @@ namespace Microsoft.AspNetCore.Mvc.Internal
OrdinalEntries = new Dictionary<string[], List<ActionDescriptor>>(StringArrayComparer.Ordinal);
OrdinalIgnoreCaseEntries = new Dictionary<string[], List<ActionDescriptor>>(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<string>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < actions.Items.Count; i++)

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
namespace Microsoft.AspNetCore.Mvc.Internal
{
@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
normalizedValue = value;
}
var stringRouteValue = routeValue?.ToString();
var stringRouteValue = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase))
{
return normalizedValue;

View File

@ -88,10 +88,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
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

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.Routing
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);

View File

@ -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 @@ namespace Microsoft.AspNetCore.Routing
}
var address = CreateAddress(httpContext: null, page, handler, values);
return generator.GetPathByAddress<RouteValuesAddress>(address, address.ExplicitValues, pathBase, fragment, options);
return generator.GetPathByAddress(address, address.ExplicitValues, pathBase, fragment, options);
}
/// <summary>
@ -230,4 +230,4 @@ namespace Microsoft.AspNetCore.Routing
return httpContext?.Features.Get<IRouteValuesFeature>()?.RouteValues;
}
}
}
}

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// 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 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// 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 @@ namespace Microsoft.AspNetCore.Mvc.Routing
}
else if (ambientValues != null)
{
currentPagePath = ambientValues["page"]?.ToString();
currentPagePath = Convert.ToString(ambientValues["page"], CultureInfo.InvariantCulture);
}
else
{

View File

@ -5,7 +5,6 @@ using System;
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 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
public IList<SelectorModel> Selectors { get; }
/// <summary>
/// Gets a collection of route values that must be present in the <see cref="RouteData.Values"/>
/// Gets a collection of route values that must be present in the <see cref="RouteData.Values"/>
/// for the corresponding page to be selected.
/// </summary>
/// <remarks>

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
if (ambiguousMatches == null)
{
ambiguousMatches = new List<HandlerMethodDescriptor>();
ambiguousMatches.Add(bestMatch);
ambiguousMatches = new List<HandlerMethodDescriptor>
{
bestMatch
};
}
ambiguousMatches.Add(handler);
@ -165,13 +167,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
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 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
return null;
}
}
}
}

View File

@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
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;

View File

@ -7,7 +7,6 @@ using System.Globalization;
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 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers.Cache
private static readonly Func<IRequestCookieCollection, string, string> CookieAccessor = (c, key) => c[key];
private static readonly Func<IHeaderDictionary, string, string> HeaderAccessor = (c, key) => c[key];
private static readonly Func<IQueryCollection, string, string> QueryAccessor = (c, key) => c[key];
private static readonly Func<RouteValueDictionary, string, string> RouteValueAccessor = (c, key) => c[key]?.ToString();
private static readonly Func<RouteValueDictionary, string, string> RouteValueAccessor = (c, key) =>
Convert.ToString(c[key], CultureInfo.InvariantCulture);
private const string CacheKeyTokenSeparator = "||";
private const string VaryByName = "VaryBy";
@ -91,7 +91,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers.Cache
_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;

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
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 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Fallback = 2,
}
}
}
}

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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;

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
"Microsoft.AspNetCore.Mvc.ViewFound",
new
{
actionContext = actionContext,
actionContext,
isMainPage = true,
result = viewResult,
viewName = viewName,
viewName,
view = result.View,
});
}
@ -133,10 +134,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
"Microsoft.AspNetCore.Mvc.ViewNotFound",
new
{
actionContext = actionContext,
actionContext,
isMainPage = true,
result = viewResult,
viewName = viewName,
viewName,
searchedLocations = result.SearchedLocations
});
}
@ -199,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
normalizedValue = value;
}
var stringRouteValue = routeValue?.ToString();
var stringRouteValue = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase))
{
return normalizedValue;

View File

@ -97,8 +97,7 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
}
var parameters = new List<OverloadedParameter>();
object optionalParametersObject;
candidate.Action.Properties.TryGetValue("OptionalParameters", out optionalParametersObject);
candidate.Action.Properties.TryGetValue("OptionalParameters", out var optionalParametersObject);
var optionalParameters = (HashSet<string>)optionalParametersObject;
foreach (var parameter in candidate.Action.Parameters)
{
@ -191,4 +190,4 @@ namespace Microsoft.AspNetCore.Mvc.WebApiCompatShim
public string Prefix { get; set; }
}
}
}
}

View File

@ -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.Abstractions;
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 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
}
[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 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
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()
{

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.Internal;
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 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
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<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "controller", "Home" },
{ "action", "Index" },
{ "date", "10/31/2018 07:37:38 -07:00" },
},
},
new ActionDescriptor()
{
DisplayName = "A2",
RouteValues = new Dictionary<string, string>(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()
{

View File

@ -829,7 +829,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private class TestableXmlSerializerInputFormatter : XmlSerializerInputFormatter
{
private bool _throwNonInputFormatterException;
private readonly bool _throwNonInputFormatterException;
public TestableXmlSerializerInputFormatter(bool throwNonInputFormatterException)
: base(new MvcOptions())
@ -851,7 +851,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private class TestableXmlDataContractSerializerInputFormatter : XmlDataContractSerializerInputFormatter
{
private bool _throwNonInputFormatterException;
private readonly bool _throwNonInputFormatterException;
public TestableXmlDataContractSerializerInputFormatter(bool throwNonInputFormatterException)
: base(new MvcOptions())
@ -899,7 +899,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private class DerivedXmlSerializerInputFormatter : XmlSerializerInputFormatter
{
private bool _throwNonInputFormatterException;
private readonly bool _throwNonInputFormatterException;
public DerivedXmlSerializerInputFormatter(bool throwNonInputFormatterException)
: base(new MvcOptions())
@ -921,7 +921,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private class DerivedXmlDataContractSerializerInputFormatter : XmlDataContractSerializerInputFormatter
{
private bool _throwNonInputFormatterException;
private readonly bool _throwNonInputFormatterException;
public DerivedXmlDataContractSerializerInputFormatter(bool throwNonInputFormatterException)
: base(new MvcOptions())
@ -952,4 +952,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public string Name { get; set; }
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
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 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal("test-value", (string)result);
}
[Fact]
[ReplaceCulture("de-CH", "de-CH")]
public void GetValueProvider_ReturnsValue_UsesInvariantCulture()
{
// Arrange
var values = new RouteValueDictionary(new Dictionary<string, object>
{
{ "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<string, object>
{
{ "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()
{

View File

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Controllers;
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 @@ namespace Microsoft.AspNetCore.Mvc.Routing
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);

View File

@ -219,4 +219,4 @@ namespace Microsoft.AspNetCore.Routing
return httpContext;
}
}
}
}

View File

@ -7,6 +7,7 @@ using System.Linq;
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 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Routing
});
}
[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<string, string>
{
{ "page", "10/31/2018 07:37:38 -07:00" },
},
},
RouteData = routeData,
};
var urlHelper = CreateMockUrlHelper(actionContext);
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
.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<RouteValueDictionary>(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()
{

View File

@ -1614,6 +1614,30 @@ namespace Microsoft.AspNetCore.Mvc.Razor
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()
{

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
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 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
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<HandlerMethodDescriptor>()
{
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()
{

View File

@ -304,6 +304,31 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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<IMemoryCache>()), 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()
{

View File

@ -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.ViewEngines;
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 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Equal(expectedContent, content);
}
[Fact]
[ReplaceCulture("de-CH", "de-CH")]
public void RendersLinkTagsForGlobbedHrefResults_UsesInvariantCulture()
{
// Arrange
var expectedContent = "<link rel=\"stylesheet\" href=\"HtmlEncode[[/css/site.css]]\" />" +
"<link rel=\"stylesheet\" href=\"HtmlEncode[[/base.css]]\" />";
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<GlobbingUrlBuilder>(
new TestFileProvider(),
Mock.Of<IMemoryCache>(),
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 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
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";
}
}
}
}

View File

@ -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.Formatters;
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 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.Equal(viewName, viewEngineResult.ViewName);
}
[Fact]
public void FindView_ReturnsExpectedNotFoundResult_WithGetViewLocations()
{

View File

@ -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.Controllers;
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 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
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<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.Equal(viewName, viewEngineResult.ViewName);
}
[Fact]
public void FindView_ReturnsExpectedNotFoundResult_WithGetViewLocations()
{