Skip to content

Commit 9384848

Browse files
committed
Cache walk of the culture tree when building resource name list:
- #15
1 parent f6119d4 commit 9384848

File tree

7 files changed

+227
-45
lines changed

7 files changed

+227
-45
lines changed

Localization.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CultureInfoGenerator", "src
2424
EndProject
2525
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Framework.Globalization.CultureInfoCache", "src\Microsoft.Framework.Globalization.CultureInfoCache\Microsoft.Framework.Globalization.CultureInfoCache.xproj", "{F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A}"
2626
EndProject
27+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B723DB83-A670-4BCB-95FB-195361331AD2}"
28+
EndProject
29+
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Framework.Localization.Test", "test\Microsoft.Framework.Localization.Test\Microsoft.Framework.Localization.Test.xproj", "{287AD58D-DF34-4F16-8616-FD78FA1CADF9}"
30+
EndProject
2731
Global
2832
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2933
Debug|Any CPU = Debug|Any CPU
@@ -54,6 +58,10 @@ Global
5458
{F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A}.Debug|Any CPU.Build.0 = Debug|Any CPU
5559
{F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A}.Release|Any CPU.ActiveCfg = Release|Any CPU
5660
{F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A}.Release|Any CPU.Build.0 = Release|Any CPU
61+
{287AD58D-DF34-4F16-8616-FD78FA1CADF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
62+
{287AD58D-DF34-4F16-8616-FD78FA1CADF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
63+
{287AD58D-DF34-4F16-8616-FD78FA1CADF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
64+
{287AD58D-DF34-4F16-8616-FD78FA1CADF9}.Release|Any CPU.Build.0 = Release|Any CPU
5765
EndGlobalSection
5866
GlobalSection(SolutionProperties) = preSolution
5967
HideSolutionNode = FALSE
@@ -65,5 +73,6 @@ Global
6573
{55D9501F-15B9-4339-A0AB-6082850E5FCE} = {79878809-8D1C-4BD4-BA99-F1F13FF96FD8}
6674
{BD22AE1C-6631-4DA6-874D-0DC0F803CEAB} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767}
6775
{F3988D3A-A4C8-4FD7-BAFE-13E0D0A1659A} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767}
76+
{287AD58D-DF34-4F16-8616-FD78FA1CADF9} = {B723DB83-A670-4BCB-95FB-195361331AD2}
6877
EndGlobalSection
6978
EndGlobal

NuGet.Config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<configuration>
33
<packageSources>
44
<add key="AspNetVNext" value="https://www.myget.org/F/aspnetvnext/api/v2" />
5+
<add key="xunit" value="https://www.myget.org/F/xunit/api/v2" />
56
<add key="NuGet" value="https://nuget.org/api/v2/" />
67
</packageSources>
78
</configuration>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Runtime.CompilerServices;
5+
6+
[assembly: InternalsVisibleTo("Microsoft.Framework.Localization.Test")]

src/Microsoft.Framework.Localization/ResourceManagerStringLocalizer.cs

Lines changed: 35 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ namespace Microsoft.Framework.Localization
1818
/// </summary>
1919
public class ResourceManagerStringLocalizer : IStringLocalizer
2020
{
21-
private readonly ConcurrentDictionary<MissingManifestCacheKey, object> _missingManifestCache =
22-
new ConcurrentDictionary<MissingManifestCacheKey, object>();
21+
private readonly ConcurrentDictionary<string, object> _missingManifestCache =
22+
new ConcurrentDictionary<string, object>();
2323

24+
private static readonly ConcurrentDictionary<string, IList<string>> _resourceNamesCache =
25+
new ConcurrentDictionary<string, IList<string>>();
26+
2427
/// <summary>
2528
/// Creates a new <see cref="ResourceManagerStringLocalizer"/>.
2629
/// </summary>
@@ -96,9 +99,10 @@ public IStringLocalizer WithCulture(CultureInfo culture)
9699
/// <param name="name">The name of the string resource.</param>
97100
/// <param name="culture">The <see cref="CultureInfo"/> to get the string for.</param>
98101
/// <returns>The resource string, or <c>null</c> if none was found.</returns>
99-
protected string GetStringSafely([NotNull] string name, [NotNull] CultureInfo culture)
102+
protected string GetStringSafely([NotNull] string name, CultureInfo culture)
100103
{
101-
var cacheKey = new MissingManifestCacheKey(name, culture ?? CultureInfo.CurrentUICulture);
104+
var cacheKey = $"name={name}&culture={(culture ?? CultureInfo.CurrentUICulture).Name}";
105+
102106
if (_missingManifestCache.ContainsKey(cacheKey))
103107
{
104108
return null;
@@ -136,7 +140,6 @@ IEnumerator IEnumerable.GetEnumerator()
136140
/// <returns>The <see cref="IEnumerator{LocalizedString}"/>.</returns>
137141
protected IEnumerator<LocalizedString> GetEnumerator([NotNull] CultureInfo culture)
138142
{
139-
// TODO: I'm sure something here should be cached, probably the whole result
140143
var resourceNames = GetResourceNamesFromCultureHierarchy(culture);
141144

142145
foreach (var name in resourceNames)
@@ -146,6 +149,12 @@ protected IEnumerator<LocalizedString> GetEnumerator([NotNull] CultureInfo cultu
146149
}
147150
}
148151

152+
// Internal to allow testing
153+
internal static void ClearResourceNamesCache()
154+
{
155+
_resourceNamesCache.Clear();
156+
}
157+
149158
private IEnumerable<string> GetResourceNamesFromCultureHierarchy(CultureInfo startingCulture)
150159
{
151160
var currentCulture = startingCulture;
@@ -155,20 +164,10 @@ private IEnumerable<string> GetResourceNamesFromCultureHierarchy(CultureInfo sta
155164
{
156165
try
157166
{
158-
var resourceStreamName = ResourceBaseName;
159-
if (!string.IsNullOrEmpty(currentCulture.Name))
167+
var cultureResourceNames = GetResourceNamesForCulture(currentCulture);
168+
foreach (var resourceName in cultureResourceNames)
160169
{
161-
resourceStreamName += "." + currentCulture.Name;
162-
}
163-
resourceStreamName += ".resources";
164-
using (var cultureResourceStream = ResourceAssembly.GetManifestResourceStream(resourceStreamName))
165-
using (var resources = new ResourceReader(cultureResourceStream))
166-
{
167-
foreach (DictionaryEntry entry in resources)
168-
{
169-
var resourceName = (string)entry.Key;
170-
resourceNames.Add(resourceName);
171-
}
170+
resourceNames.Add(resourceName);
172171
}
173172
}
174173
catch (MissingManifestResourceException) { }
@@ -185,43 +184,34 @@ private IEnumerable<string> GetResourceNamesFromCultureHierarchy(CultureInfo sta
185184
return resourceNames;
186185
}
187186

188-
private class MissingManifestCacheKey : IEquatable<MissingManifestCacheKey>
187+
private IList<string> GetResourceNamesForCulture(CultureInfo culture)
189188
{
190-
private readonly int _hashCode;
191-
192-
public MissingManifestCacheKey(string name, CultureInfo culture)
189+
var resourceStreamName = ResourceBaseName;
190+
if (!string.IsNullOrEmpty(culture.Name))
193191
{
194-
Name = name;
195-
CultureInfo = culture;
196-
_hashCode = new { Name, CultureInfo }.GetHashCode();
192+
resourceStreamName += "." + culture.Name;
197193
}
194+
resourceStreamName += ".resources";
198195

199-
public string Name { get; }
196+
var cacheKey = $"assembly={ResourceAssembly.FullName};resourceStreamName={resourceStreamName}";
200197

201-
public CultureInfo CultureInfo { get; }
202-
203-
public bool Equals(MissingManifestCacheKey other)
198+
var cultureResourceNames = _resourceNamesCache.GetOrAdd(cacheKey, key =>
204199
{
205-
return string.Equals(Name, other.Name, StringComparison.Ordinal)
206-
&& CultureInfo == other.CultureInfo;
207-
}
208-
209-
public override bool Equals(object obj)
210-
{
211-
var other = obj as MissingManifestCacheKey;
212-
213-
if (other != null)
200+
var names = new List<string>();
201+
using (var cultureResourceStream = ResourceAssembly.GetManifestResourceStream(key))
202+
using (var resources = new ResourceReader(cultureResourceStream))
214203
{
215-
return Equals(other);
204+
foreach (DictionaryEntry entry in resources)
205+
{
206+
var resourceName = (string)entry.Key;
207+
names.Add(resourceName);
208+
}
216209
}
217210

218-
return base.Equals(obj);
219-
}
211+
return names;
212+
});
220213

221-
public override int GetHashCode()
222-
{
223-
return _hashCode;
224-
}
214+
return cultureResourceNames;
225215
}
226216
}
227217
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<PropertyGroup>
4+
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
5+
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
6+
</PropertyGroup>
7+
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
8+
<PropertyGroup Label="Globals">
9+
<ProjectGuid>287ad58d-df34-4f16-8616-fd78fa1cadf9</ProjectGuid>
10+
<RootNamespace>Microsoft.Framework.Localization.Test</RootNamespace>
11+
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
12+
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
13+
</PropertyGroup>
14+
<PropertyGroup>
15+
<SchemaVersion>2.0</SchemaVersion>
16+
</PropertyGroup>
17+
<ItemGroup>
18+
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
19+
</ItemGroup>
20+
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
21+
</Project>
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Globalization;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Reflection;
8+
using System.Resources;
9+
using Moq;
10+
using Xunit;
11+
12+
namespace Microsoft.Framework.Localization.Test
13+
{
14+
public class ResourceManagerStringLocalizerTest
15+
{
16+
[Fact]
17+
public void EnumeratorCachesCultureWalkForSameAssembly()
18+
{
19+
// Arrange
20+
ResourceManagerStringLocalizer.ClearResourceNamesCache();
21+
var resourceManager = new Mock<ResourceManager>();
22+
var resourceAssembly = new Mock<TestAssembly1>();
23+
resourceAssembly.Setup(rm => rm.GetManifestResourceStream(It.IsAny<string>()))
24+
.Returns(() => MakeResourceStream());
25+
var baseName = "test";
26+
var localizer1 = new ResourceManagerStringLocalizer(
27+
resourceManager.Object,
28+
resourceAssembly.Object,
29+
baseName);
30+
var localizer2 = new ResourceManagerStringLocalizer(
31+
resourceManager.Object,
32+
resourceAssembly.Object,
33+
baseName);
34+
35+
// Act
36+
for (int i = 0; i < 5; i++)
37+
{
38+
localizer1.ToList();
39+
localizer2.ToList();
40+
}
41+
42+
// Assert
43+
var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture);
44+
resourceAssembly.Verify(
45+
rm => rm.GetManifestResourceStream(It.IsAny<string>()),
46+
Times.Exactly(expectedCallCount));
47+
}
48+
49+
[Fact]
50+
public void EnumeratorCacheIsScopedByAssembly()
51+
{
52+
// Arrange
53+
ResourceManagerStringLocalizer.ClearResourceNamesCache();
54+
var resourceManager = new Mock<ResourceManager>();
55+
var resourceAssembly1 = new Mock<TestAssembly1>();
56+
resourceAssembly1.CallBase = true;
57+
var resourceAssembly2 = new Mock<TestAssembly2>();
58+
resourceAssembly2.CallBase = true;
59+
resourceAssembly1.Setup(rm => rm.GetManifestResourceStream(It.IsAny<string>()))
60+
.Returns(() => MakeResourceStream());
61+
resourceAssembly2.Setup(rm => rm.GetManifestResourceStream(It.IsAny<string>()))
62+
.Returns(() => MakeResourceStream());
63+
var baseName = "test";
64+
var localizer1 = new ResourceManagerStringLocalizer(
65+
resourceManager.Object,
66+
resourceAssembly1.Object,
67+
baseName);
68+
var localizer2 = new ResourceManagerStringLocalizer(
69+
resourceManager.Object,
70+
resourceAssembly2.Object,
71+
baseName);
72+
73+
// Act
74+
localizer1.ToList();
75+
localizer2.ToList();
76+
77+
// Assert
78+
var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture);
79+
resourceAssembly1.Verify(
80+
rm => rm.GetManifestResourceStream(It.IsAny<string>()),
81+
Times.Exactly(expectedCallCount));
82+
resourceAssembly2.Verify(
83+
rm => rm.GetManifestResourceStream(It.IsAny<string>()),
84+
Times.Exactly(expectedCallCount));
85+
}
86+
87+
private static Stream MakeResourceStream()
88+
{
89+
var stream = new MemoryStream();
90+
var resourceWriter = new ResourceWriter(stream);
91+
resourceWriter.AddResource("TestName", "value");
92+
resourceWriter.Generate();
93+
stream.Position = 0;
94+
return stream;
95+
}
96+
97+
private static int GetCultureInfoDepth(CultureInfo culture)
98+
{
99+
var result = 0;
100+
var currentCulture = culture;
101+
102+
while (true)
103+
{
104+
result++;
105+
106+
if (currentCulture == currentCulture.Parent)
107+
{
108+
break;
109+
}
110+
111+
currentCulture = currentCulture.Parent;
112+
}
113+
114+
return result;
115+
}
116+
117+
public class TestAssembly1 : Assembly
118+
{
119+
public override string FullName
120+
{
121+
get
122+
{
123+
return nameof(TestAssembly1);
124+
}
125+
}
126+
}
127+
128+
public class TestAssembly2 : Assembly
129+
{
130+
public override string FullName
131+
{
132+
get
133+
{
134+
return nameof(TestAssembly2);
135+
}
136+
}
137+
}
138+
}
139+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"dependencies": {
3+
"Moq": "4.2.1502.911",
4+
"xunit": "2.1.0-*",
5+
"xunit.runner.dnx": "2.1.0-*",
6+
"Microsoft.Framework.Localization": "1.0.0-*"
7+
},
8+
9+
"commands": {
10+
"test": "xunit.runner.dnx"
11+
},
12+
13+
"frameworks": {
14+
"dnx451": { }
15+
}
16+
}

0 commit comments

Comments
 (0)