From 861ae52de51f174e14701d6cdbb88e6543638b03 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Sat, 24 Sep 2016 12:55:04 -0700 Subject: [PATCH] Added support for get culture information from request's route data information. [Fixes #122] Get segment from mapped route for CustomRequestCultureProvider --- Localization.sln | 14 ++ ...soft.AspNetCore.Localization.Routing.xproj | 19 ++ .../Properties/AssemblyInfo.cs | 11 + .../RouteDataRequestCultureProvider.cs | 74 +++++++ .../project.json | 34 +++ ...eptLanguageHeaderRequestCultureProvider.cs | 5 +- .../CookieRequestCultureProvider.cs | 3 +- .../QueryStringRequestCultureProvider.cs | 5 +- .../project.json | 6 +- ...spNetCore.Localization.Routing.Tests.xproj | 21 ++ .../RouteDataRequestCultureProviderTest.cs | 193 ++++++++++++++++++ .../project.json | 24 +++ 12 files changed, 403 insertions(+), 6 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Localization.Routing/Microsoft.AspNetCore.Localization.Routing.xproj create mode 100644 src/Microsoft.AspNetCore.Localization.Routing/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNetCore.Localization.Routing/RouteDataRequestCultureProvider.cs create mode 100644 src/Microsoft.AspNetCore.Localization.Routing/project.json create mode 100644 test/Microsoft.AspNetCore.Localization.Routing.Tests/Microsoft.AspNetCore.Localization.Routing.Tests.xproj create mode 100644 test/Microsoft.AspNetCore.Localization.Routing.Tests/RouteDataRequestCultureProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Localization.Routing.Tests/project.json diff --git a/Localization.sln b/Localization.sln index 3c4a667429..276c09e515 100644 --- a/Localization.sln +++ b/Localization.sln @@ -35,6 +35,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResourcesClassLibraryWithAt EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResourcesClassLibraryNoAttribute", "test\ResourcesClassLibraryNoAttribute\ResourcesClassLibraryNoAttribute.xproj", "{34740578-D5B5-4FB4-AFD4-5E87B5443E20}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Localization.Routing", "src\Microsoft.AspNetCore.Localization.Routing\Microsoft.AspNetCore.Localization.Routing.xproj", "{E1B8DA18-7885-40ED-94ED-881BB0804F23}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Localization.Routing.Tests", "test\Microsoft.AspNetCore.Localization.Routing.Tests\Microsoft.AspNetCore.Localization.Routing.Tests.xproj", "{375B000B-5DC0-4D16-AC0C-A5432D8E7581}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,6 +89,14 @@ Global {34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Debug|Any CPU.Build.0 = Debug|Any CPU {34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Release|Any CPU.ActiveCfg = Release|Any CPU {34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Release|Any CPU.Build.0 = Release|Any CPU + {E1B8DA18-7885-40ED-94ED-881BB0804F23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1B8DA18-7885-40ED-94ED-881BB0804F23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1B8DA18-7885-40ED-94ED-881BB0804F23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1B8DA18-7885-40ED-94ED-881BB0804F23}.Release|Any CPU.Build.0 = Release|Any CPU + {375B000B-5DC0-4D16-AC0C-A5432D8E7581}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {375B000B-5DC0-4D16-AC0C-A5432D8E7581}.Debug|Any CPU.Build.0 = Debug|Any CPU + {375B000B-5DC0-4D16-AC0C-A5432D8E7581}.Release|Any CPU.ActiveCfg = Release|Any CPU + {375B000B-5DC0-4D16-AC0C-A5432D8E7581}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -101,5 +113,7 @@ Global {B1B441BA-3AC8-49F8-850D-E5A178E77DE2} = {B723DB83-A670-4BCB-95FB-195361331AD2} {F27639B9-913E-43AF-9D64-BBD98D9A420A} = {B723DB83-A670-4BCB-95FB-195361331AD2} {34740578-D5B5-4FB4-AFD4-5E87B5443E20} = {B723DB83-A670-4BCB-95FB-195361331AD2} + {E1B8DA18-7885-40ED-94ED-881BB0804F23} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767} + {375B000B-5DC0-4D16-AC0C-A5432D8E7581} = {B723DB83-A670-4BCB-95FB-195361331AD2} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNetCore.Localization.Routing/Microsoft.AspNetCore.Localization.Routing.xproj b/src/Microsoft.AspNetCore.Localization.Routing/Microsoft.AspNetCore.Localization.Routing.xproj new file mode 100644 index 0000000000..d992283c12 --- /dev/null +++ b/src/Microsoft.AspNetCore.Localization.Routing/Microsoft.AspNetCore.Localization.Routing.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + e1b8da18-7885-40ed-94ed-881bb0804f23 + Microsoft.AspNetCore.Localization.Routing + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Localization.Routing/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Localization.Routing/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..76feceeff0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Localization.Routing/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// 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.Reflection; +using System.Resources; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] +[assembly: AssemblyCompany("Microsoft Corporation.")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyProduct("Microsoft ASP.NET Core")] diff --git a/src/Microsoft.AspNetCore.Localization.Routing/RouteDataRequestCultureProvider.cs b/src/Microsoft.AspNetCore.Localization.Routing/RouteDataRequestCultureProvider.cs new file mode 100644 index 0000000000..93921b1cb2 --- /dev/null +++ b/src/Microsoft.AspNetCore.Localization.Routing/RouteDataRequestCultureProvider.cs @@ -0,0 +1,74 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Localization.Routing +{ + /// + /// Determines the culture information for a request via values in the route data. + /// + public class RouteDataRequestCultureProvider : RequestCultureProvider + { + /// + /// The key that contains the culture name. + /// Defaults to "culture". + /// + public string RouteDataStringKey { get; set; } = "culture"; + + /// + /// The key that contains the UI culture name. If not specified or no value is found, + /// will be used. + /// Defaults to "ui-culture". + /// + public string UIRouteDataStringKey { get; set; } = "ui-culture"; + + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + string culture = null; + string uiCulture = null; + + if (!string.IsNullOrEmpty(RouteDataStringKey)) + { + culture = httpContext.GetRouteValue(RouteDataStringKey)?.ToString(); + } + + if (!string.IsNullOrEmpty(UIRouteDataStringKey)) + { + uiCulture = httpContext.GetRouteValue(UIRouteDataStringKey)?.ToString(); + } + + if (culture == null && uiCulture == null) + { + // No values specified for either so no match + return TaskCache.DefaultCompletedTask; + } + + if (culture != null && uiCulture == null) + { + // Value for culture but not for UI culture so default to culture value for both + uiCulture = culture; + } + + if (culture == null && uiCulture != null) + { + // Value for UI culture but not for culture so default to UI culture value for both + culture = uiCulture; + } + + var providerResultCulture = new ProviderCultureResult(culture, uiCulture); + + return Task.FromResult(providerResultCulture); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Localization.Routing/project.json b/src/Microsoft.AspNetCore.Localization.Routing/project.json new file mode 100644 index 0000000000..bc0d9694a9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Localization.Routing/project.json @@ -0,0 +1,34 @@ +{ + "version": "1.1.0-*", + "description": "Provides a request culture provider which gets culture and ui-culture from request's route data.", + "packOptions": { + "repository": { + "type": "git", + "url": "https://github.com/aspnet/localization" + }, + "tags": [ + "aspnetcore", + "localization" + ] + }, + "buildOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk", + "nowarn": [ + "CS1591" + ], + "xmlDoc": true + }, + "dependencies": { + "Microsoft.AspNetCore.Localization": "1.1.0-*", + "Microsoft.AspNetCore.Routing.Abstractions": "1.1.0-*", + "Microsoft.Extensions.TaskCache.Sources": { + "type": "build", + "version": "1.1.0-*" + } + }, + "frameworks": { + "net451": {}, + "netstandard1.3": {} + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Localization/AcceptLanguageHeaderRequestCultureProvider.cs b/src/Microsoft.AspNetCore.Localization/AcceptLanguageHeaderRequestCultureProvider.cs index 13d74313c1..7162ef52a9 100644 --- a/src/Microsoft.AspNetCore.Localization/AcceptLanguageHeaderRequestCultureProvider.cs +++ b/src/Microsoft.AspNetCore.Localization/AcceptLanguageHeaderRequestCultureProvider.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Localization @@ -33,7 +34,7 @@ namespace Microsoft.AspNetCore.Localization if (acceptLanguageHeader == null || acceptLanguageHeader.Count == 0) { - return Task.FromResult((ProviderCultureResult)null); + return TaskCache.DefaultCompletedTask; } var languages = acceptLanguageHeader.AsEnumerable(); @@ -53,7 +54,7 @@ namespace Microsoft.AspNetCore.Localization return Task.FromResult(new ProviderCultureResult(orderedLanguages)); } - return Task.FromResult((ProviderCultureResult)null); + return TaskCache.DefaultCompletedTask; } } } diff --git a/src/Microsoft.AspNetCore.Localization/CookieRequestCultureProvider.cs b/src/Microsoft.AspNetCore.Localization/CookieRequestCultureProvider.cs index 67ecbdf53c..b1c389225b 100644 --- a/src/Microsoft.AspNetCore.Localization/CookieRequestCultureProvider.cs +++ b/src/Microsoft.AspNetCore.Localization/CookieRequestCultureProvider.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Localization { @@ -39,7 +40,7 @@ namespace Microsoft.AspNetCore.Localization if (string.IsNullOrEmpty(cookie)) { - return Task.FromResult(null); + return TaskCache.DefaultCompletedTask; } var providerResultCulture = ParseCookieValue(cookie); diff --git a/src/Microsoft.AspNetCore.Localization/QueryStringRequestCultureProvider.cs b/src/Microsoft.AspNetCore.Localization/QueryStringRequestCultureProvider.cs index 2cd4c737e7..a39756c9b7 100644 --- a/src/Microsoft.AspNetCore.Localization/QueryStringRequestCultureProvider.cs +++ b/src/Microsoft.AspNetCore.Localization/QueryStringRequestCultureProvider.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Localization { @@ -36,7 +37,7 @@ namespace Microsoft.AspNetCore.Localization var request = httpContext.Request; if (!request.QueryString.HasValue) { - return Task.FromResult((ProviderCultureResult)null); + return TaskCache.DefaultCompletedTask; } string queryCulture = null; @@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.Localization if (queryCulture == null && queryUICulture == null) { // No values specified for either so no match - return Task.FromResult((ProviderCultureResult)null); + return TaskCache.DefaultCompletedTask; } if (queryCulture != null && queryUICulture == null) diff --git a/src/Microsoft.AspNetCore.Localization/project.json b/src/Microsoft.AspNetCore.Localization/project.json index a7abf870b7..69c612b7ea 100644 --- a/src/Microsoft.AspNetCore.Localization/project.json +++ b/src/Microsoft.AspNetCore.Localization/project.json @@ -23,7 +23,11 @@ "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*", "Microsoft.Extensions.Globalization.CultureInfoCache": "1.1.0-*", "Microsoft.Extensions.Localization.Abstractions": "1.1.0-*", - "Microsoft.Extensions.Options": "1.1.0-*" + "Microsoft.Extensions.Options": "1.1.0-*", + "Microsoft.Extensions.TaskCache.Sources": { + "type": "build", + "version": "1.1.0-*" + } }, "frameworks": { "net451": {}, diff --git a/test/Microsoft.AspNetCore.Localization.Routing.Tests/Microsoft.AspNetCore.Localization.Routing.Tests.xproj b/test/Microsoft.AspNetCore.Localization.Routing.Tests/Microsoft.AspNetCore.Localization.Routing.Tests.xproj new file mode 100644 index 0000000000..57ded6b43e --- /dev/null +++ b/test/Microsoft.AspNetCore.Localization.Routing.Tests/Microsoft.AspNetCore.Localization.Routing.Tests.xproj @@ -0,0 +1,21 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 375b000b-5dc0-4d16-ac0c-a5432d8e7581 + Microsoft.AspNetCore.Localization.Routing.Tests + .\obj + .\bin\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Localization.Routing.Tests/RouteDataRequestCultureProviderTest.cs b/test/Microsoft.AspNetCore.Localization.Routing.Tests/RouteDataRequestCultureProviderTest.cs new file mode 100644 index 0000000000..b5400a2cd6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Localization.Routing.Tests/RouteDataRequestCultureProviderTest.cs @@ -0,0 +1,193 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Localization.Routing +{ + public class RouteDataRequestCultureProviderTest + { + [Theory] + [InlineData("{culture}/{ui-culture}/hello", "ar-SA/ar-YE/hello", "ar-SA", "ar-YE")] + [InlineData("{CULTURE}/{UI-CULTURE}/hello", "ar-SA/ar-YE/hello", "ar-SA", "ar-YE")] + [InlineData("{culture}/{ui-culture}/hello", "unsupported/unsupported/hello", "en-US", "en-US")] + [InlineData("{culture}/hello", "ar-SA/hello", "ar-SA", "en-US")] + [InlineData("hello", "hello", "en-US", "en-US")] + [InlineData("{ui-culture}/hello", "ar-YE/hello", "en-US", "ar-YE")] + public async Task GetCultureInfo_FromRouteData( + string routeTemplate, + string requestUrl, + string expectedCulture, + string expectedUICulture) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRouter(routes => + { + routes.MapRoute(routeTemplate, (IApplicationBuilder fork) => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-YE") + } + }; + options.RequestCultureProviders = new[] + { + new RouteDataRequestCultureProvider() + { + Options = options + } + }; + fork.UseRequestLocalization(options); + + fork.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + return context.Response.WriteAsync( + $"{requestCulture.Culture.Name},{requestCulture.UICulture.Name}"); + }); + }); + }); + }) + .ConfigureServices(services => + { + services.AddRouting(); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync(requestUrl); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var data = await response.Content.ReadAsStringAsync(); + Assert.Equal($"{expectedCulture},{expectedUICulture}", data); + } + } + + [Fact] + public async Task GetDefaultCultureInfo_IfCultureKeysAreMissing() + { + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US") + }; + options.RequestCultureProviders = new[] + { + new RouteDataRequestCultureProvider() + { + Options = options + } + }; + app.UseRequestLocalization(options); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + return context.Response.WriteAsync( + $"{requestCulture.Culture.Name},{requestCulture.UICulture.Name}"); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var data = await response.Content.ReadAsStringAsync(); + Assert.Equal("en-US,en-US", data); + } + } + + [Theory] + [InlineData("{c}/{uic}/hello", "ar-SA/ar-YE/hello", "ar-SA", "ar-YE")] + [InlineData("{C}/{UIC}/hello", "ar-SA/ar-YE/hello", "ar-SA", "ar-YE")] + [InlineData("{c}/hello", "ar-SA/hello", "ar-SA", "en-US")] + [InlineData("hello", "hello", "en-US", "en-US")] + [InlineData("{uic}/hello", "ar-YE/hello", "en-US", "ar-YE")] + public async Task GetCultureInfo_FromRouteData_WithCustomKeys( + string routeTemplate, + string requestUrl, + string expectedCulture, + string expectedUICulture) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRouter(routes => + { + routes.MapRoute(routeTemplate, (IApplicationBuilder fork) => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-YE") + } + }; + options.RequestCultureProviders = new[] + { + new RouteDataRequestCultureProvider() + { + Options = options, + RouteDataStringKey = "c", + UIRouteDataStringKey = "uic" + } + }; + fork.UseRequestLocalization(options); + + fork.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + return context.Response.WriteAsync( + $"{requestCulture.Culture.Name},{requestCulture.UICulture.Name}"); + }); + }); + }); + }) + .ConfigureServices(services => + { + services.AddRouting(); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync(requestUrl); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var data = await response.Content.ReadAsStringAsync(); + Assert.Equal($"{expectedCulture},{expectedUICulture}", data); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Localization.Routing.Tests/project.json b/test/Microsoft.AspNetCore.Localization.Routing.Tests/project.json new file mode 100644 index 0000000000..a8d6a420c7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Localization.Routing.Tests/project.json @@ -0,0 +1,24 @@ +{ + "buildOptions": { + "warningsAsErrors": true + }, + "dependencies": { + "dotnet-test-xunit": "2.2.0-*", + "Microsoft.AspNetCore.Localization.Routing": "1.1.0-*", + "Microsoft.AspNetCore.Routing": "1.1.0-*", + "Microsoft.AspNetCore.TestHost": "1.1.0-*", + "xunit": "2.2.0-*" + }, + "testRunner": "xunit", + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0-*", + "type": "platform" + } + } + }, + "net451": {} + } +} \ No newline at end of file