diff --git a/src/Microsoft.Extensions.Localization/Internal/AssemblyResourceStringProvider.cs b/src/Microsoft.Extensions.Localization/Internal/AssemblyResourceStringProvider.cs deleted file mode 100644 index f70a1c93f4..0000000000 --- a/src/Microsoft.Extensions.Localization/Internal/AssemblyResourceStringProvider.cs +++ /dev/null @@ -1,115 +0,0 @@ -// 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; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Resources; - -namespace Microsoft.Extensions.Localization.Internal -{ - public class AssemblyResourceStringProvider : IResourceStringProvider - { - private readonly AssemblyWrapper _assembly; - private readonly string _resourceBaseName; - private readonly IResourceNamesCache _resourceNamesCache; - - public AssemblyResourceStringProvider( - IResourceNamesCache resourceCache, - AssemblyWrapper resourceAssembly, - string resourceBaseName) - { - _resourceNamesCache = resourceCache; - _assembly = resourceAssembly; - _resourceBaseName = resourceBaseName; - } - - private string GetResourceCacheKey(CultureInfo culture) - { - var assemblyName = new AssemblyName(_assembly.FullName) - { - CultureName = culture.Name - }; - - return $"Assembly={assemblyName.FullName};resourceName={_resourceBaseName}"; - } - - private string GetResourceName(CultureInfo culture) - { - var resourceStreamName = _resourceBaseName; - if (!string.IsNullOrEmpty(culture.Name)) - { - resourceStreamName += "." + culture.Name; - } - resourceStreamName += ".resources"; - - return resourceStreamName; - } - - private IList ThrowOrNull(CultureInfo culture, bool throwOnMissing) - { - if (throwOnMissing) - { - throw new MissingManifestResourceException( - Resources.FormatLocalization_MissingManifest(GetResourceName(culture))); - } - - return null; - } - - public IList GetAllResourceStrings(CultureInfo culture, bool throwOnMissing) - { - var cacheKey = GetResourceCacheKey(culture); - - return _resourceNamesCache.GetOrAdd(cacheKey, _ => - { - var assembly = GetAssembly(culture); - if (assembly == null) - { - return ThrowOrNull(culture, throwOnMissing); - } - - var resourceStreamName = GetResourceName(culture); - using (var resourceStream = assembly.GetManifestResourceStream(resourceStreamName)) - { - if (resourceStream == null) - { - return ThrowOrNull(culture, throwOnMissing); - } - - using (var resources = new ResourceReader(resourceStream)) - { - var names = new List(); - foreach (DictionaryEntry entry in resources) - { - var resourceName = (string)entry.Key; - names.Add(resourceName); - } - return names; - } - } - }); - } - - protected virtual AssemblyWrapper GetAssembly(CultureInfo culture) - { - Assembly assembly; - var assemblyName = new AssemblyName(_assembly.FullName) - { - CultureName = culture.Name - }; - try - { - assembly = Assembly.Load(assemblyName); - } - catch (FileNotFoundException) - { - return null; - } - - return new AssemblyWrapper(assembly); - } - } -} diff --git a/src/Microsoft.Extensions.Localization/Internal/ResourceManagerStringProvider.cs b/src/Microsoft.Extensions.Localization/Internal/ResourceManagerStringProvider.cs new file mode 100644 index 0000000000..9eef8c84a8 --- /dev/null +++ b/src/Microsoft.Extensions.Localization/Internal/ResourceManagerStringProvider.cs @@ -0,0 +1,80 @@ +// 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; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Resources; + +namespace Microsoft.Extensions.Localization.Internal +{ + public class ResourceManagerStringProvider : IResourceStringProvider + { + private readonly IResourceNamesCache _resourceNamesCache; + private readonly ResourceManager _resourceManager; + private readonly Assembly _assembly; + private readonly string _resourceBaseName; + + public ResourceManagerStringProvider( + IResourceNamesCache resourceCache, + ResourceManager resourceManager, + Assembly assembly, + string baseName) + { + _resourceManager = resourceManager; + _resourceNamesCache = resourceCache; + _assembly = assembly; + _resourceBaseName = baseName; + } + + private string GetResourceCacheKey(CultureInfo culture) + { + var resourceName = _resourceManager.BaseName; + + return $"Culture={culture.Name};resourceName={resourceName};Assembly={_assembly.FullName}"; + } + + private string GetResourceName(CultureInfo culture) + { + var resourceStreamName = _resourceBaseName; + if (!string.IsNullOrEmpty(culture.Name)) + { + resourceStreamName += "." + culture.Name; + } + resourceStreamName += ".resources"; + + return resourceStreamName; + } + + public IList GetAllResourceStrings(CultureInfo culture, bool throwOnMissing) + { + var cacheKey = GetResourceCacheKey(culture); + + return _resourceNamesCache.GetOrAdd(cacheKey, _ => + { + // We purposly don't dispose the ResourceSet because it causes an ObjectDisposedException when you try to read the values later. + var resourceSet = _resourceManager.GetResourceSet(culture, createIfNotExists: true, tryParents: false); + if (resourceSet == null) + { + if (throwOnMissing) + { + throw new MissingManifestResourceException(Resources.FormatLocalization_MissingManifest(GetResourceName(culture))); + } + else + { + return null; + } + } + + var names = new List(); + foreach (DictionaryEntry entry in resourceSet) + { + names.Add((string)entry.Key); + } + + return names; + }); + } + } +} diff --git a/src/Microsoft.Extensions.Localization/ResourceManagerStringLocalizer.cs b/src/Microsoft.Extensions.Localization/ResourceManagerStringLocalizer.cs index 3d9a295818..e2e1a3f234 100644 --- a/src/Microsoft.Extensions.Localization/ResourceManagerStringLocalizer.cs +++ b/src/Microsoft.Extensions.Localization/ResourceManagerStringLocalizer.cs @@ -60,7 +60,11 @@ namespace Microsoft.Extensions.Localization ILogger logger) : this( resourceManager, - new AssemblyResourceStringProvider(resourceNamesCache, resourceAssemblyWrapper, baseName), + new ResourceManagerStringProvider( + resourceNamesCache, + resourceManager, + resourceAssemblyWrapper.Assembly, + baseName), baseName, resourceNamesCache, logger) diff --git a/test/LocalizationWebsite/StartupGetAllStrings.cs b/test/LocalizationWebsite/StartupGetAllStrings.cs new file mode 100644 index 0000000000..730a745d07 --- /dev/null +++ b/test/LocalizationWebsite/StartupGetAllStrings.cs @@ -0,0 +1,52 @@ +// 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.Linq; +using LocalizationWebsite.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +namespace LocalizationWebsite +{ + public class StartupGetAllStrings + { + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(options => options.ResourcesPath = "Resources"); + } + + public void Configure( + IApplicationBuilder app, + ILoggerFactory loggerFactory, + IStringLocalizer customerStringLocalizer) + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List() + { + new CultureInfo("fr-FR") + }, + SupportedUICultures = new List() + { + new CultureInfo("fr-FR") + } + }); + + app.Run(async (context) => + { + var strings = customerStringLocalizer.GetAllStrings(); + + await context.Response.WriteAsync(strings.Count().ToString()); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(string.Join(" ", strings)); + }); + } + } +} diff --git a/test/Microsoft.AspNetCore.Localization.FunctionalTests/LocalizationTest.cs b/test/Microsoft.AspNetCore.Localization.FunctionalTests/LocalizationTest.cs index 5181e87f42..f0a0a94228 100644 --- a/test/Microsoft.AspNetCore.Localization.FunctionalTests/LocalizationTest.cs +++ b/test/Microsoft.AspNetCore.Localization.FunctionalTests/LocalizationTest.cs @@ -23,6 +23,15 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests "kr10.00"); } + [Fact] + public Task Localization_GetAllStrings() + { + return RunTest( + typeof(StartupGetAllStrings), + "fr-FR", + "1 Bonjour from Customer in resources folder"); + } + [Fact] public Task Localization_ResourcesInClassLibrary_ReturnLocalizedValue() { @@ -81,7 +90,7 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Contains(expected, await response.Content.ReadAsStringAsync()); + Assert.Equal(expected, await response.Content.ReadAsStringAsync()); } } } diff --git a/test/Microsoft.Extensions.Localization.Tests/ResourceManagerStringLocalizerTest.cs b/test/Microsoft.Extensions.Localization.Tests/ResourceManagerStringLocalizerTest.cs index 58fe3561ca..ff7bfa9933 100644 --- a/test/Microsoft.Extensions.Localization.Tests/ResourceManagerStringLocalizerTest.cs +++ b/test/Microsoft.Extensions.Localization.Tests/ResourceManagerStringLocalizerTest.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.Globalization; using System.IO; using System.Linq; @@ -23,8 +24,12 @@ namespace Microsoft.Extensions.Localization var resourceNamesCache = new ResourceNamesCache(); var baseName = "test"; var resourceAssembly = new TestAssemblyWrapper(); - var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly); - var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceAssembly, baseName); + var resourceManager = new TestResourceManager(baseName, resourceAssembly); + var resourceStreamManager = new TestResourceStringProvider( + resourceNamesCache, + resourceManager, + resourceAssembly.Assembly, + baseName); var logger = Logger; var localizer1 = new ResourceManagerStringLocalizer(resourceManager, resourceStreamManager, @@ -46,7 +51,7 @@ namespace Microsoft.Extensions.Localization // Assert var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture); - Assert.Equal(expectedCallCount, resourceAssembly.GetManifestResourceStreamCallCount); + Assert.Equal(expectedCallCount, resourceAssembly.ManifestResourceStreamCallCount); } [Fact] @@ -55,12 +60,12 @@ namespace Microsoft.Extensions.Localization // Arrange var resourceNamesCache = new ResourceNamesCache(); var baseName = "test"; - var resourceAssembly1 = new TestAssemblyWrapper("Assembly1"); - var resourceAssembly2 = new TestAssemblyWrapper("Assembly2"); - var resourceManager1 = new TestResourceManager(baseName, resourceAssembly1.Assembly); - var resourceManager2 = new TestResourceManager(baseName, resourceAssembly2.Assembly); - var resourceStreamManager1 = new TestResourceStringProvider(resourceNamesCache, resourceAssembly1, baseName); - var resourceStreamManager2 = new TestResourceStringProvider(resourceNamesCache, resourceAssembly2, baseName); + var resourceAssembly1 = new TestAssemblyWrapper(typeof(ResourceManagerStringLocalizerTest)); + var resourceAssembly2 = new TestAssemblyWrapper(typeof(ResourceManagerStringLocalizer)); + var resourceManager1 = new TestResourceManager(baseName, resourceAssembly1); + var resourceManager2 = new TestResourceManager(baseName, resourceAssembly2); + var resourceStreamManager1 = new TestResourceStringProvider(resourceNamesCache, resourceManager1, resourceAssembly1.Assembly, baseName); + var resourceStreamManager2 = new TestResourceStringProvider(resourceNamesCache, resourceManager2, resourceAssembly2.Assembly, baseName); var logger = Logger; var localizer1 = new ResourceManagerStringLocalizer( resourceManager1, @@ -81,8 +86,8 @@ namespace Microsoft.Extensions.Localization // Assert var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture); - Assert.Equal(expectedCallCount, resourceAssembly1.GetManifestResourceStreamCallCount); - Assert.Equal(expectedCallCount, resourceAssembly2.GetManifestResourceStreamCallCount); + Assert.Equal(expectedCallCount, resourceAssembly1.ManifestResourceStreamCallCount); + Assert.Equal(expectedCallCount, resourceAssembly2.ManifestResourceStreamCallCount); } [Fact] @@ -92,8 +97,8 @@ namespace Microsoft.Extensions.Localization var baseName = "Resources.TestResource"; var resourceNamesCache = new ResourceNamesCache(); var resourceAssembly = new TestAssemblyWrapper(); - var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly); - var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceAssembly, baseName); + var resourceManager = new TestResourceManager(baseName, resourceAssembly); + var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceManager, resourceAssembly.Assembly, baseName); var logger = Logger; var localizer = new ResourceManagerStringLocalizer( resourceManager, @@ -117,8 +122,8 @@ namespace Microsoft.Extensions.Localization var baseName = "Resources.TestResource"; var resourceNamesCache = new ResourceNamesCache(); var resourceAssembly = new TestAssemblyWrapper(); - var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly); - var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceAssembly, baseName); + var resourceManager = new TestResourceManager(baseName, resourceAssembly); + var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceManager, resourceAssembly.Assembly, baseName); var logger = Logger; var localizer = new ResourceManagerStringLocalizer( @@ -145,8 +150,8 @@ namespace Microsoft.Extensions.Localization var baseName = "test"; var resourceNamesCache = new ResourceNamesCache(); var resourceAssembly = new TestAssemblyWrapper(); - var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly); - var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceAssembly, baseName); + var resourceManager = new TestResourceManager(baseName, resourceAssembly); + var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceManager, resourceAssembly.Assembly, baseName); var logger = Logger; var localizer = new ResourceManagerStringLocalizer( resourceManager, @@ -172,8 +177,9 @@ namespace Microsoft.Extensions.Localization // Arrange var resourceNamesCache = new ResourceNamesCache(); var baseName = "testington"; - var resourceAssembly = new TestAssemblyWrapper("Assembly1"); - var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly); + var resourceAssembly = new TestAssemblyWrapper(); + resourceAssembly.HasResources = false; + var resourceManager = new TestResourceManager(baseName, resourceAssembly); var logger = Logger; var localizer = new ResourceManagerWithCultureStringLocalizer( @@ -190,10 +196,13 @@ namespace Microsoft.Extensions.Localization // We have to access the result so it evaluates. localizer.GetAllStrings(includeParentCultures).ToArray(); }); + + var expectedTries = includeParentCultures ? 3 : 1; var expected = includeParentCultures ? "No manifests exist for the current culture." : $"The manifest 'testington.{CultureInfo.CurrentCulture}.resources' was not found."; Assert.Equal(expected, exception.Message); + Assert.Equal(expectedTries, resourceAssembly.ManifestResourceStreamCallCount); } private static Stream MakeResourceStream() @@ -233,49 +242,57 @@ namespace Microsoft.Extensions.Localization public class TestResourceManager : ResourceManager { - public TestResourceManager(string baseName, Assembly assembly) - : base(baseName, assembly) - { - } + private AssemblyWrapper _assemblyWrapper; - public override string GetString(string name, CultureInfo culture) => null; - } - - public class TestResourceStringProvider : AssemblyResourceStringProvider - { - private TestAssemblyWrapper _assemblyWrapper; - - public TestResourceStringProvider( - IResourceNamesCache resourceCache, - TestAssemblyWrapper assemblyWrapper, - string resourceBaseName) - : base(resourceCache, assemblyWrapper, resourceBaseName) + public TestResourceManager(string baseName, AssemblyWrapper assemblyWrapper) + : base(baseName, assemblyWrapper.Assembly) { _assemblyWrapper = assemblyWrapper; } - protected override AssemblyWrapper GetAssembly(CultureInfo culture) + public override string GetString(string name, CultureInfo culture) => null; + + public override ResourceSet GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) + { + var resourceStream = _assemblyWrapper.GetManifestResourceStream(BaseName); + + return resourceStream != null ? new ResourceSet(resourceStream) : null; + } + } + + public class TestResourceStringProvider : ResourceManagerStringProvider + { + public TestResourceStringProvider( + IResourceNamesCache resourceCache, + TestResourceManager resourceManager, + Assembly assembly, + string resourceBaseName) + : base(resourceCache, resourceManager, assembly, resourceBaseName) { - return _assemblyWrapper; } } public class TestAssemblyWrapper : AssemblyWrapper { - public TestAssemblyWrapper(string name = nameof(TestAssemblyWrapper)) - : base(typeof(TestAssemblyWrapper).GetTypeInfo().Assembly) + public TestAssemblyWrapper() + : this(typeof(TestAssemblyWrapper)) { - FullName = name; } - public int GetManifestResourceStreamCallCount { get; private set; } + public TestAssemblyWrapper(Type type) + : base(type.GetTypeInfo().Assembly) + { + } - public override string FullName { get; } + public bool HasResources { get; set; } = true; + + public int ManifestResourceStreamCallCount { get; private set; } public override Stream GetManifestResourceStream(string name) { - GetManifestResourceStreamCallCount++; - return MakeResourceStream(); + ManifestResourceStreamCallCount++; + + return HasResources ? MakeResourceStream() : null; } } }