RuntimeResourceSet improvements (#44454)
authorMarek Safar <marek.safar@gmail.com>
Fri, 13 Nov 2020 19:03:25 +0000 (20:03 +0100)
committerGitHub <noreply@github.com>
Fri, 13 Nov 2020 19:03:25 +0000 (20:03 +0100)
* RuntimeResourceSet improvements

- remove unnecessary base class calls to trim more of base type
- remove code paths which were never executed
- replaced nested locking with simple locks
- fix caching for case insensitive mode
- add tests for more code paths

* Move local caseInsensitiveTable initialisation

* Clean up unused code

* Revert "Clean up unused code"

It's actually used via reflection in System.Resources.Extensions

src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs
src/libraries/System.Private.CoreLib/src/System/Resources/RuntimeResourceSet.cs
src/libraries/System.Resources.Extensions/tests/BinaryResourceWriterUnitTest.cs
src/libraries/System.Resources.ResourceManager/tests/ResourceManagerTests.cs

index 0ab52cc..54c921a 100644 (file)
@@ -27,26 +27,16 @@ namespace System.Resources
     // default file format.
     //
 
-    internal struct ResourceLocator
+    internal readonly struct ResourceLocator
     {
-        internal object? _value;  // Can be null.
-        internal int _dataPos;
-
         internal ResourceLocator(int dataPos, object? value)
         {
-            _dataPos = dataPos;
-            _value = value;
+            DataPosition = dataPos;
+            Value = value;
         }
 
-        internal int DataPosition => _dataPos;
-
-        // Allows adding in profiling data in a future version, or a special
-        // resource profiling build.  We could also use WeakReference.
-        internal object? Value
-        {
-            get => _value;
-            set => _value = value;
-        }
+        internal int DataPosition { get; }
+        internal object? Value { get; }
 
         internal static bool CanCache(ResourceTypeCode value)
         {
@@ -800,8 +790,12 @@ namespace System.Resources
             // Read RuntimeResourceSet header
             // Do file version check
             int version = _store.ReadInt32();
-            if (version != RuntimeResourceSet.Version && version != 1)
-                throw new ArgumentException(SR.Format(SR.Arg_ResourceFileUnsupportedVersion, RuntimeResourceSet.Version, version));
+
+            // File format version number
+            const int CurrentVersion = 2;
+
+            if (version != CurrentVersion && version != 1)
+                throw new ArgumentException(SR.Format(SR.Arg_ResourceFileUnsupportedVersion, CurrentVersion, version));
             _version = version;
 
             _numResources = _store.ReadInt32();
index f6de75e..90c55d9 100644 (file)
@@ -1,18 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-/*============================================================
-**
-**
-**
-**
-**
-** Purpose: CultureInfo-specific collection of resources.
-**
-**
-===========================================================*/
-
-#nullable enable
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
@@ -169,8 +157,6 @@ namespace System.Resources
 #endif
     sealed class RuntimeResourceSet : ResourceSet, IEnumerable
     {
-        internal const int Version = 2;            // File format version number
-
         // Cache for resources.  Key is the resource name, which can be cached
         // for arbitrarily long times, since the object is usually a string
         // literal that will live for the lifetime of the appdomain.  The
@@ -178,7 +164,7 @@ namespace System.Resources
         private Dictionary<string, ResourceLocator>? _resCache;
 
 
-        // For our special load-on-demand reader, cache the cast.  The
+        // For our special load-on-demand reader. The
         // RuntimeResourceSet's implementation knows how to treat this reader specially.
         private ResourceReader? _defaultReader;
 
@@ -189,32 +175,19 @@ namespace System.Resources
         // don't exist.
         private Dictionary<string, ResourceLocator>? _caseInsensitiveTable;
 
-        // If we're not using our custom reader, then enumerate through all
-        // the resources once, adding them into the table.
-        private bool _haveReadFromReader;
-
 #if !RESOURCES_EXTENSIONS
-        internal RuntimeResourceSet(string fileName) : base(false)
+        internal RuntimeResourceSet(string fileName) :
+            this(new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
         {
-            _resCache = new Dictionary<string, ResourceLocator>(FastResourceComparer.Default);
-            Stream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
-            _defaultReader = new ResourceReader(stream, _resCache, false);
-            Reader = _defaultReader;
         }
 
-        internal RuntimeResourceSet(Stream stream, bool permitDeserialization = false) : base(false)
+        internal RuntimeResourceSet(Stream stream, bool permitDeserialization = false) :
+            base(false)
         {
             _resCache = new Dictionary<string, ResourceLocator>(FastResourceComparer.Default);
             _defaultReader = new ResourceReader(stream, _resCache, permitDeserialization);
-            Reader = _defaultReader;
         }
 #else
-        private
-#if NETFRAMEWORK
-        new
-#endif
-        IResourceReader Reader => _defaultReader!;
-
         internal RuntimeResourceSet(IResourceReader reader) :
             // explicitly do not call IResourceReader constructor since it caches all resources
             // the purpose of RuntimeResourceSet is to lazily load and cache.
@@ -235,32 +208,18 @@ namespace System.Resources
 
         protected override void Dispose(bool disposing)
         {
-            if (Reader == null)
+            if (_defaultReader is null)
                 return;
 
             if (disposing)
             {
-                lock (Reader)
-                {
-                    _resCache = null;
-                    if (_defaultReader != null)
-                    {
-                        _defaultReader.Close();
-                        _defaultReader = null;
-                    }
-                    _caseInsensitiveTable = null;
-                    // Set Reader to null to avoid a race in GetObject.
-                    base.Dispose(disposing);
-                }
-            }
-            else
-            {
-                // Just to make sure we always clear these fields in the future...
-                _resCache = null;
-                _caseInsensitiveTable = null;
-                _defaultReader = null;
-                base.Dispose(disposing);
+                _defaultReader?.Close();
             }
+
+            _defaultReader = null;
+            _resCache = null;
+            _caseInsensitiveTable = null;
+            base.Dispose(disposing);
         }
 
         public override IDictionaryEnumerator GetEnumerator()
@@ -275,14 +234,13 @@ namespace System.Resources
 
         private IDictionaryEnumerator GetEnumeratorHelper()
         {
-            IResourceReader copyOfReader = Reader;
-            if (copyOfReader == null || _resCache == null)
+            ResourceReader? reader = _defaultReader;
+            if (reader is null)
                 throw new ObjectDisposedException(null, SR.ObjectDisposed_ResourceSet);
 
-            return copyOfReader.GetEnumerator();
+            return reader.GetEnumerator();
         }
 
-
         public override string? GetString(string key)
         {
             object? o = GetObject(key, false, true);
@@ -309,158 +267,104 @@ namespace System.Resources
         {
             if (key == null)
                 throw new ArgumentNullException(nameof(key));
-            if (Reader == null || _resCache == null)
+
+            ResourceReader? reader = _defaultReader;
+            Dictionary<string, ResourceLocator>? cache = _resCache;
+            if (reader is null || cache is null)
                 throw new ObjectDisposedException(null, SR.ObjectDisposed_ResourceSet);
 
-            object? value = null;
-            ResourceLocator resLocation;
+            object? value;
+            ResourceLocator resEntry;
 
-            lock (Reader)
+            lock (cache)
             {
-                if (Reader == null)
-                    throw new ObjectDisposedException(null, SR.ObjectDisposed_ResourceSet);
-
-                if (_defaultReader != null)
+                // Find the offset within the data section
+                int dataPos;
+                if (cache.TryGetValue(key, out resEntry))
                 {
-                    // Find the offset within the data section
-                    int dataPos = -1;
-                    if (_resCache.TryGetValue(key, out resLocation))
-                    {
-                        value = resLocation.Value;
-                        dataPos = resLocation.DataPosition;
-                    }
+                    value = resEntry.Value;
+                    if (value != null)
+                        return value;
 
-                    if (dataPos == -1 && value == null)
-                    {
-                        dataPos = _defaultReader.FindPosForResource(key);
-                    }
+                    // When data type cannot be cached
+                    dataPos = resEntry.DataPosition;
+                    return isString ? reader.LoadString(dataPos) : reader.LoadObject(dataPos, out _);
+                }
 
-                    if (dataPos != -1 && value == null)
-                    {
-                        Debug.Assert(dataPos >= 0, "data section offset cannot be negative!");
-                        // Normally calling LoadString or LoadObject requires
-                        // taking a lock.  Note that in this case, we took a
-                        // lock on the entire RuntimeResourceSet, which is
-                        // sufficient since we never pass this ResourceReader
-                        // to anyone else.
-                        ResourceTypeCode typeCode;
-                        if (isString)
-                        {
-                            value = _defaultReader.LoadString(dataPos);
-                            typeCode = ResourceTypeCode.String;
-                        }
-                        else
-                        {
-                            value = _defaultReader.LoadObject(dataPos, out typeCode);
-                        }
-
-                        resLocation = new ResourceLocator(dataPos, (ResourceLocator.CanCache(typeCode)) ? value : null);
-                        lock (_resCache)
-                        {
-                            _resCache[key] = resLocation;
-                        }
-                    }
+                dataPos = reader.FindPosForResource(key);
+                if (dataPos >= 0)
+                {
+                    value = ReadValue(reader, dataPos, isString, out resEntry);
+                    cache[key] = resEntry;
+                    return value;
+                }
+            }
 
-                    if (value != null || !ignoreCase)
-                    {
-                        return value;  // may be null
-                    }
-                }  // if (_defaultReader != null)
+            if (!ignoreCase)
+            {
+                return null;
+            }
 
-                // At this point, we either don't have our default resource reader
-                // or we haven't found the particular resource we're looking for
-                // and may have to search for it in a case-insensitive way.
-                if (!_haveReadFromReader)
-                {
-                    // If necessary, init our case insensitive hash table.
-                    if (ignoreCase)
-                    {
-                        _caseInsensitiveTable ??= new Dictionary<string, ResourceLocator>(StringComparer.OrdinalIgnoreCase);
-                    }
+            // We haven't found the particular resource we're looking for
+            // and may have to search for it in a case-insensitive way.
+            bool initialize = false;
+            Dictionary<string, ResourceLocator>? caseInsensitiveTable = _caseInsensitiveTable;
+            if (caseInsensitiveTable == null)
+            {
+                caseInsensitiveTable = new Dictionary<string, ResourceLocator>(StringComparer.OrdinalIgnoreCase);
+                initialize = true;
+            }
 
-                    if (_defaultReader == null)
-                    {
-                        IDictionaryEnumerator en = Reader.GetEnumerator();
-                        while (en.MoveNext())
-                        {
-                            DictionaryEntry entry = en.Entry;
-                            string readKey = (string)entry.Key;
-                            ResourceLocator resLoc = new ResourceLocator(-1, entry.Value);
-                            _resCache.Add(readKey, resLoc);
-                            if (ignoreCase)
-                            {
-                                Debug.Assert(_caseInsensitiveTable != null);
-                                _caseInsensitiveTable.Add(readKey, resLoc);
-                            }
-                        }
-                        // Only close the reader if it is NOT our default one,
-                        // since we need it around to resolve ResourceLocators.
-                        if (!ignoreCase)
-                            Reader.Close();
-                    }
-                    else
-                    {
-                        Debug.Assert(ignoreCase, "This should only happen for case-insensitive lookups");
-                        Debug.Assert(_caseInsensitiveTable != null);
-                        ResourceReader.ResourceEnumerator en = _defaultReader.GetEnumeratorInternal();
-                        while (en.MoveNext())
-                        {
-                            // Note: Always ask for the resource key before the data position.
-                            string currentKey = (string)en.Key;
-                            int dataPos = en.DataPosition;
-                            ResourceLocator resLoc = new ResourceLocator(dataPos, null);
-                            _caseInsensitiveTable.Add(currentKey, resLoc);
-                        }
-                    }
-                    _haveReadFromReader = true;
-                }
-                object? obj = null;
-                bool found = false;
-                bool keyInWrongCase = false;
-                if (_defaultReader != null)
-                {
-                    if (_resCache.TryGetValue(key, out resLocation))
-                    {
-                        found = true;
-                        obj = ResolveResourceLocator(resLocation, key, _resCache, keyInWrongCase);
-                    }
-                }
-                if (!found && ignoreCase)
+            lock (caseInsensitiveTable)
+            {
+                if (initialize)
                 {
-                    Debug.Assert(_caseInsensitiveTable != null);
-                    if (_caseInsensitiveTable.TryGetValue(key, out resLocation))
+                    ResourceReader.ResourceEnumerator en = reader.GetEnumeratorInternal();
+                    while (en.MoveNext())
                     {
-                        found = true;
-                        keyInWrongCase = true;
-                        obj = ResolveResourceLocator(resLocation, key, _resCache, keyInWrongCase);
+                        // The resource key must be read before the data position.
+                        string currentKey = (string)en.Key;
+                        ResourceLocator resLoc = new ResourceLocator(en.DataPosition, null);
+                        caseInsensitiveTable.Add(currentKey, resLoc);
                     }
+
+                    _caseInsensitiveTable = caseInsensitiveTable;
                 }
-                return obj;
-            } // lock(Reader)
+
+                if (!caseInsensitiveTable.TryGetValue(key, out resEntry))
+                    return null;
+
+                if (resEntry.Value != null)
+                    return resEntry.Value;
+
+                value = ReadValue(reader, resEntry.DataPosition, isString, out resEntry);
+
+                if (resEntry.Value != null)
+                    caseInsensitiveTable[key] = resEntry;
+            }
+
+            return value;
         }
 
-        // The last parameter indicates whether the lookup required a
-        // case-insensitive lookup to succeed, indicating we shouldn't add
-        // the ResourceLocation to our case-sensitive cache.
-        private object? ResolveResourceLocator(ResourceLocator resLocation, string key, Dictionary<string, ResourceLocator> copyOfCache, bool keyInWrongCase)
+        private static object? ReadValue (ResourceReader reader, int dataPos, bool isString, out ResourceLocator locator)
         {
-            // We need to explicitly resolve loosely linked manifest
-            // resources, and we need to resolve ResourceLocators with null objects.
-            object? value = resLocation.Value;
-            if (value == null)
+            object? value;
+            ResourceTypeCode typeCode;
+
+            // Normally calling LoadString or LoadObject requires
+            // taking a lock.  Note that in this case, we took a
+            // lock before calling this method.
+            if (isString)
             {
-                ResourceTypeCode typeCode;
-                lock (Reader)
-                {
-                    Debug.Assert(_defaultReader != null);
-                    value = _defaultReader.LoadObject(resLocation.DataPosition, out typeCode);
-                }
-                if (!keyInWrongCase && ResourceLocator.CanCache(typeCode))
-                {
-                    resLocation.Value = value;
-                    copyOfCache[key] = resLocation;
-                }
+                value = reader.LoadString(dataPos);
+                typeCode = ResourceTypeCode.String;
             }
+            else
+            {
+                value = reader.LoadObject(dataPos, out typeCode);
+            }
+
+            locator = new ResourceLocator(dataPos, ResourceLocator.CanCache(typeCode) ? value : null);
             return value;
         }
     }
index c6061a8..34c3faa 100644 (file)
@@ -470,7 +470,7 @@ namespace System.Resources.Extensions.Tests
         {
             ResourceManager resourceManager = new ResourceManager(typeof(TestData));
             ResourceSet resSet = resourceManager.GetResourceSet(CultureInfo.InvariantCulture, true, true);
-            IResourceReader reader = (IResourceReader)resSet.GetType().GetProperty("Reader", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(resSet);
+            IResourceReader reader = (IResourceReader)resSet.GetType().GetField("_defaultReader", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(resSet);
             Assert.IsType<DeserializingResourceReader>(reader);
         }
 
index 9c038b5..2bedd24 100644 (file)
@@ -54,6 +54,8 @@ namespace System.Resources.Tests
             ResourceManager resourceManager = new ResourceManager("System.Resources.Tests.Resources.TestResx", typeof(ResourceManagerTests).GetTypeInfo().Assembly);
             string actual = resourceManager.GetString(key);
             Assert.Equal(expectedValue, actual);
+            Assert.Same(actual, resourceManager.GetString(key));
+            Assert.Equal(expectedValue, resourceManager.GetObject(key));
         }
 
         [Theory]
@@ -297,6 +299,8 @@ namespace System.Resources.Tests
             var culture = new CultureInfo("en-US");
             ResourceSet set = manager.GetResourceSet(culture, true, true);
             Assert.Equal(expectedValue, set.GetString(key));
+            Assert.Equal(expectedValue, set.GetObject(key));
+            Assert.Equal(expectedValue, set.GetString(key));
         }
 
         [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
@@ -308,6 +312,19 @@ namespace System.Resources.Tests
             var culture = new CultureInfo("en-US");
             ResourceSet set = manager.GetResourceSet(culture, true, true);
             Assert.Equal(expectedValue, set.GetObject(key));
+            Assert.Equal(expectedValue, set.GetObject(key));
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsBinaryFormatterSupported))]
+        [MemberData(nameof(EnglishNonStringResourceData))]
+        public static void GetResourceSet_NonStringsIgnoreCase(string key, object expectedValue, bool requiresBinaryFormatter)
+        {
+            _ = requiresBinaryFormatter;
+            var manager = new ResourceManager("System.Resources.Tests.Resources.TestResx.netstandard17", typeof(ResourceManagerTests).GetTypeInfo().Assembly);
+            var culture = new CultureInfo("en-US");
+            ResourceSet set = manager.GetResourceSet(culture, true, true);
+            Assert.Equal(expectedValue, set.GetObject(key.ToLower(), true));
+            Assert.Equal(expectedValue, set.GetObject(key.ToLower(), true));
         }
 
         [ConditionalTheory(Helpers.IsDrawingSupported)]
@@ -385,6 +402,18 @@ namespace System.Resources.Tests
             Assert.Throws<ArgumentException>(() => new ResourceManager("name", assembly, null));
         }
 
+        [Fact]
+        public static void GetStringAfterDispose()
+        {
+            var manager = new ResourceManager("System.Resources.Tests.Resources.TestResx", typeof(ResourceManagerTests).GetTypeInfo().Assembly);
+            var culture = new CultureInfo("en-US");
+            ResourceSet set = manager.GetResourceSet(culture, true, true);
+
+            set.GetString("Any");
+            ((IDisposable)set).Dispose();
+            Assert.Throws<ObjectDisposedException> (() => set.GetString("Any"));
+        }
+
         private class MockAssembly : Assembly
         {
         }