File: src\libraries\System.Private.CoreLib\src\System\Resources\ResourceManager.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Reflection;
 
namespace System.Resources
{
    // Resource Manager exposes an assembly's resources to an application for
    // the correct CultureInfo.  An example would be localizing text for a
    // user-visible message.  Create a set of resource files listing a name
    // for a message and its value, compile them using ResGen, put them in
    // an appropriate place (your assembly manifest(?)), then create a Resource
    // Manager and query for the name of the message you want.  The Resource
    // Manager will use CultureInfo.GetCurrentUICulture() to look
    // up a resource for your user's locale settings.
    //
    // Users should ideally create a resource file for every culture, or
    // at least a meaningful subset.  The filenames will follow the naming
    // scheme:
    //
    // basename.culture name.resources
    //
    // The base name can be the name of your application, or depending on
    // the granularity desired, possibly the name of each class.  The culture
    // name is determined from CultureInfo's Name property.
    // An example file name may be MyApp.en-US.resources for
    // MyApp's US English resources.
    //
    // -----------------
    // Refactoring Notes
    // -----------------
    // In Feb 08, began first step of refactoring ResourceManager to improve
    // maintainability (sd changelist 3012100). This resulted in breaking
    // apart the InternalGetResourceSet "big loop" so that the file-based
    // and manifest-based lookup was located in separate methods.
    // In Apr 08, continued refactoring so that file-based and manifest-based
    // concerns are encapsulated by separate classes. At construction, the
    // ResourceManager creates one of these classes based on whether the
    // RM will need to use file-based or manifest-based resources, and
    // afterwards refers to this through the interface IResourceGroveler.
    //
    // Serialization Compat: Ideally, we could have refactored further but
    // this would have broken serialization compat. For example, the
    // ResourceManager member UseManifest and UseSatelliteAssem are no
    // longer relevant on ResourceManager. Similarly, other members could
    // ideally be moved to the file-based or manifest-based classes
    // because they are only relevant for those types of lookup.
    //
    // Solution now / in the future:
    // For now, we simply use a mediator class so that we can keep these
    // members on ResourceManager but allow the file-based and manifest-
    // based classes to access/set these members in a uniform way. See
    // ResourceManagerMediator.
    // We encapsulate fallback logic in a fallback iterator class, so that
    // this logic isn't duplicated in several methods.
    //
    // In the future, we can also look into further factoring and better
    // design of IResourceGroveler interface to accommodate unused parameters
    // that don't make sense for either file-based or manifest-based lookup paths.
    //
    // Benefits of this refactoring:
    // - Makes it possible to understand what the ResourceManager does,
    // which is key for maintainability.
    // - Makes the ResourceManager more extensible by identifying and
    // encapsulating what varies
    // - Unearthed a bug that's been lurking a while in file-based
    // lookup paths for InternalGetResourceSet if createIfNotExists is
    // false.
    // - Reuses logic, e.g. by breaking apart the culture fallback into
    // the fallback iterator class, we don't have to repeat the
    // sometimes confusing fallback logic across multiple methods
    // - Fxcop violations reduced to 1/5th of original count. Most
    // importantly, code complexity violations disappeared.
    // - Finally, it got rid of dead code paths. Because the big loop was
    // so confusing, it masked unused chunks of code. Also, dividing
    // between file-based and manifest-based allowed functionaliy
    // unused in silverlight to fall out.
    //
    // Note: this type is integral to the construction of exception objects,
    // and sometimes this has to be done in low memory situtations (OOM) or
    // to create TypeInitializationExceptions due to failure of a static class
    // constructor. This type needs to be extremely careful and assume that
    // any type it references may have previously failed to construct, so statics
    // belonging to that type may not be initialized. FrameworkEventSource.Log
    // is one such example.
    //
 
    public partial class ResourceManager
    {
        internal sealed class CultureNameResourceSetPair
        {
            public string? lastCultureName;
            public ResourceSet? lastResourceSet;
        }
 
        protected string BaseNameField; // The field is protected for .NET Framework compatibility
        protected Assembly? MainAssembly;    // Need the assembly manifest sometimes.
 
        private Dictionary<string, ResourceSet>? _resourceSets;
        private readonly string? _moduleDir;          // For assembly-ignorant directory location
 
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
        private readonly Type? _userResourceSet;      // Which ResourceSet instance to create
 
        private CultureInfo? _neutralResourcesCulture;  // For perf optimizations.
 
        private CultureNameResourceSetPair? _lastUsedResourceCache;
 
        private bool _ignoreCase;   // Whether case matters in GetString & GetObject
 
        private bool _useManifest;  // Use Assembly manifest, or grovel disk.
 
        // Whether to fall back to the main assembly or a particular
        // satellite for the neutral resources.
        private UltimateResourceFallbackLocation _fallbackLoc;
        // Version number of satellite assemblies to look for.  May be null.
        private Version? _satelliteContractVersion;
        private bool _lookedForSatelliteContractVersion;
 
        private IResourceGroveler _resourceGroveler;
 
        public static readonly int MagicNumber = unchecked((int)0xBEEFCACE);  // If only hex had a K...
 
        // Version number so ResMgr can get the ideal set of classes for you.
        // ResMgr header is:
        // 1) MagicNumber (little endian Int32)
        // 2) HeaderVersionNumber (little endian Int32)
        // 3) Num Bytes to skip past ResMgr header (little endian Int32)
        // 4) IResourceReader type name for this file (bytelength-prefixed UTF-8 String)
        // 5) ResourceSet type name for this file (bytelength-prefixed UTF8 String)
        public static readonly int HeaderVersionNumber = 1;
 
        // It would be better if we could use a _neutralCulture instead of calling
        // CultureInfo.InvariantCulture everywhere, but we run into problems with the .cctor.  CultureInfo
        // initializes assembly, which initializes ResourceManager, which tries to get a CultureInfo which isn't
        // there yet because CultureInfo's class initializer hasn't finished.  If we move SystemResMgr off of
        // Assembly (or at least make it an internal property) we should be able to circumvent this problem.
 
        // These Strings are used to avoid using Reflection in CreateResourceSet.
        internal const string ResReaderTypeName = "System.Resources.ResourceReader";
        internal const string ResSetTypeName = "System.Resources.RuntimeResourceSet";
        internal const string ResFileExtension = ".resources";
        internal const int ResFileExtensionLength = 10;
 
        protected ResourceManager()
        {
            _lastUsedResourceCache = new CultureNameResourceSetPair();
            ResourceManagerMediator mediator = new ResourceManagerMediator(this);
            _resourceGroveler = new ManifestBasedResourceGroveler(mediator);
            BaseNameField = string.Empty;
        }
 
        // Constructs a Resource Manager for files beginning with
        // baseName in the directory specified by resourceDir
        // or in the current directory.  This Assembly-ignorant constructor is
        // mostly useful for testing your own ResourceSet implementation.
        //
        // A good example of a baseName might be "Strings".  BaseName
        // should not end in ".resources".
        //
        // Note: System.Windows.Forms uses this method at design time.
        //
        private ResourceManager(string baseName, string resourceDir,
            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
            Type? userResourceSet)
        {
            ArgumentNullException.ThrowIfNull(baseName);
            ArgumentNullException.ThrowIfNull(resourceDir);
 
            BaseNameField = baseName;
 
            _moduleDir = resourceDir;
            _userResourceSet = userResourceSet;
            _resourceSets = new Dictionary<string, ResourceSet>();
            _lastUsedResourceCache = new CultureNameResourceSetPair();
 
            ResourceManagerMediator mediator = new ResourceManagerMediator(this);
            _resourceGroveler = new FileBasedResourceGroveler(mediator);
        }
 
        public ResourceManager(string baseName, Assembly assembly)
        {
            ArgumentNullException.ThrowIfNull(baseName);
            ArgumentNullException.ThrowIfNull(assembly);
 
            if (assembly is not RuntimeAssembly)
                throw new ArgumentException(SR.Argument_MustBeRuntimeAssembly);
 
            MainAssembly = assembly;
            BaseNameField = baseName;
 
            CommonAssemblyInit();
        }
 
        public ResourceManager(string baseName, Assembly assembly,
            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
        Type? usingResourceSet)
        {
            ArgumentNullException.ThrowIfNull(baseName);
            ArgumentNullException.ThrowIfNull(assembly);
 
            if (assembly is not RuntimeAssembly)
                throw new ArgumentException(SR.Argument_MustBeRuntimeAssembly);
 
            MainAssembly = assembly;
            BaseNameField = baseName;
 
            if (usingResourceSet != null && (usingResourceSet != typeof(ResourceSet)) && !usingResourceSet.IsSubclassOf(typeof(ResourceSet)))
                throw new ArgumentException(SR.Arg_ResMgrNotResSet, nameof(usingResourceSet));
            _userResourceSet = usingResourceSet;
 
            CommonAssemblyInit();
        }
 
        public ResourceManager(Type resourceSource)
        {
            ArgumentNullException.ThrowIfNull(resourceSource);
 
            if (resourceSource is not RuntimeType)
                throw new ArgumentException(SR.Argument_MustBeRuntimeType);
 
            MainAssembly = resourceSource.Assembly;
 
            string? nameSpace = resourceSource.Namespace;
            char c = Type.Delimiter;
            BaseNameField = nameSpace is null
                ? resourceSource.Name
                : string.Concat(nameSpace, new ReadOnlySpan<char>(in c), resourceSource.Name);
 
            CommonAssemblyInit();
        }
 
        // Trying to unify code as much as possible, even though having to do a
        // security check in each constructor prevents it.
        [MemberNotNull(nameof(_resourceGroveler))]
        private void CommonAssemblyInit()
        {
            // Now we can use the managed resources even when using PRI's to support the APIs GetObject, GetStream...etc.
            _useManifest = true;
 
            _resourceSets = new Dictionary<string, ResourceSet>();
            _lastUsedResourceCache = new CultureNameResourceSetPair();
 
            ResourceManagerMediator mediator = new ResourceManagerMediator(this);
            _resourceGroveler = new ManifestBasedResourceGroveler(mediator);
 
            Debug.Assert(MainAssembly != null);
            _neutralResourcesCulture = ManifestBasedResourceGroveler.GetNeutralResourcesLanguage(MainAssembly, out _fallbackLoc);
        }
 
        // Gets the base name for the ResourceManager.
        public virtual string BaseName => BaseNameField;
 
        // Whether we should ignore the capitalization of resources when calling
        // GetString or GetObject.
        public virtual bool IgnoreCase
        {
            get => _ignoreCase;
            set => _ignoreCase = value;
        }
 
        // Returns the Type of the ResourceSet the ResourceManager uses
        // to construct ResourceSets.
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
        public virtual Type ResourceSetType => _userResourceSet ?? typeof(RuntimeResourceSet);
 
        protected UltimateResourceFallbackLocation FallbackLocation
        {
            get => _fallbackLoc;
            set => _fallbackLoc = value;
        }
 
        // Tells the ResourceManager to call Close on all ResourceSets and
        // release all resources.  This will shrink your working set by
        // potentially a substantial amount in a running application.  Any
        // future resource lookups on this ResourceManager will be as
        // expensive as the very first lookup, since it will need to search
        // for files and load resources again.
        //
        // This may be useful in some complex threading scenarios, where
        // creating a new ResourceManager isn't quite the correct behavior.
        public virtual void ReleaseAllResources()
        {
            Debug.Assert(_resourceSets != null);
            Dictionary<string, ResourceSet> localResourceSets = _resourceSets;
 
            // If any calls to Close throw, at least leave ourselves in a
            // consistent state.
            _resourceSets = new Dictionary<string, ResourceSet>();
            _lastUsedResourceCache = new CultureNameResourceSetPair();
 
            lock (localResourceSets)
            {
                foreach ((_, ResourceSet resourceSet) in localResourceSets)
                {
                    resourceSet.Close();
                }
            }
        }
 
        public static ResourceManager CreateFileBasedResourceManager(string baseName, string resourceDir,
            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
            Type? usingResourceSet)
        {
            return new ResourceManager(baseName, resourceDir, usingResourceSet);
        }
 
        // Given a CultureInfo, GetResourceFileName generates the name for
        // the binary file for the given CultureInfo.  This method uses
        // CultureInfo's Name property as part of the file name for all cultures
        // other than the invariant culture.  This method does not touch the disk,
        // and is used only to construct what a resource file name (suitable for
        // passing to the ResourceReader constructor) or a manifest resource file
        // name should look like.
        //
        // This method can be overridden to look for a different extension,
        // such as ".ResX", or a completely different format for naming files.
        protected virtual string GetResourceFileName(CultureInfo culture)
        {
            // If this is the neutral culture, don't include the culture name.
            if (culture.HasInvariantCultureName)
            {
                return BaseNameField + ResFileExtension;
            }
            else
            {
                CultureInfo.VerifyCultureName(culture.Name, throwException: true);
                return BaseNameField + "." + culture.Name + ResFileExtension;
            }
        }
 
        // WARNING: This function must be kept in sync with ResourceFallbackManager.GetEnumerator()
        // Return the first ResourceSet, based on the first culture ResourceFallbackManager would return
        internal ResourceSet? GetFirstResourceSet(CultureInfo culture)
        {
            // Logic from ResourceFallbackManager.GetEnumerator()
            if (_neutralResourcesCulture != null && culture.Name == _neutralResourcesCulture.Name)
            {
                culture = CultureInfo.InvariantCulture;
            }
 
            if (_lastUsedResourceCache != null)
            {
                lock (_lastUsedResourceCache)
                {
                    if (culture.Name == _lastUsedResourceCache.lastCultureName)
                        return _lastUsedResourceCache.lastResourceSet;
                }
            }
 
            // Look in the ResourceSet table
            Dictionary<string, ResourceSet>? localResourceSets = _resourceSets;
            ResourceSet? rs = null;
            if (localResourceSets != null)
            {
                lock (localResourceSets)
                {
                    localResourceSets.TryGetValue(culture.Name, out rs);
                }
            }
 
            if (rs != null)
            {
                // update the cache with the most recent ResourceSet
                if (_lastUsedResourceCache != null)
                {
                    lock (_lastUsedResourceCache)
                    {
                        _lastUsedResourceCache.lastCultureName = culture.Name;
                        _lastUsedResourceCache.lastResourceSet = rs;
                    }
                }
                return rs;
            }
 
            return null;
        }
 
        // Looks up a set of resources for a particular CultureInfo.  This is
        // not useful for most users of the ResourceManager - call
        // GetString() or GetObject() instead.
        //
        // The parameters let you control whether the ResourceSet is created
        // if it hasn't yet been loaded and if parent CultureInfos should be
        // loaded as well for resource inheritance.
        //
        public virtual ResourceSet? GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
        {
            ArgumentNullException.ThrowIfNull(culture);
 
            Dictionary<string, ResourceSet>? localResourceSets = _resourceSets;
            ResourceSet? rs;
            if (localResourceSets != null)
            {
                lock (localResourceSets)
                {
                    if (localResourceSets.TryGetValue(culture.Name, out rs))
                        return rs;
                }
            }
 
            if (_useManifest && culture.HasInvariantCultureName)
            {
                string fileName = GetResourceFileName(culture);
                Debug.Assert(MainAssembly != null);
                Stream? stream = MainAssembly.GetManifestResourceStream(fileName);
                if (createIfNotExists && stream != null)
                {
                    rs = ((ManifestBasedResourceGroveler)_resourceGroveler).CreateResourceSet(stream, MainAssembly);
                    Debug.Assert(localResourceSets != null);
                    AddResourceSet(localResourceSets, culture.Name, ref rs);
                    return rs;
                }
            }
 
            return InternalGetResourceSet(culture, createIfNotExists, tryParents);
        }
 
        // InternalGetResourceSet is a non-threadsafe method where all the logic
        // for getting a resource set lives.  Access to it is controlled by
        // threadsafe methods such as GetResourceSet, GetString, & GetObject.
        // This will take a minimal number of locks.
        protected virtual ResourceSet? InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
        {
            Debug.Assert(culture != null, "culture != null");
            Debug.Assert(_resourceSets != null);
 
            Dictionary<string, ResourceSet> localResourceSets = _resourceSets;
            ResourceSet? rs = null;
            CultureInfo? foundCulture = null;
            lock (localResourceSets)
            {
                if (localResourceSets.TryGetValue(culture.Name, out rs))
                {
                    return rs;
                }
            }
 
            ResourceFallbackManager mgr = new ResourceFallbackManager(culture, _neutralResourcesCulture, tryParents);
 
            foreach (CultureInfo currentCultureInfo in mgr)
            {
                lock (localResourceSets)
                {
                    if (localResourceSets.TryGetValue(currentCultureInfo.Name, out rs))
                    {
                        // we need to update the cache if we fellback
                        if (culture != currentCultureInfo) foundCulture = currentCultureInfo;
                        break;
                    }
                }
 
                // InternalGetResourceSet will never be threadsafe.  However, it must
                // be protected against reentrancy from the SAME THREAD.  (ie, calling
                // GetSatelliteAssembly may send some window messages or trigger the
                // Assembly load event, which could fail then call back into the
                // ResourceManager).  It's happened.
 
                rs = _resourceGroveler.GrovelForResourceSet(currentCultureInfo, localResourceSets,
                                                           tryParents, createIfNotExists);
 
                // found a ResourceSet; we're done
                if (rs != null)
                {
                    foundCulture = currentCultureInfo;
                    break;
                }
            }
 
            if (rs != null && foundCulture != null)
            {
                // add entries to the cache for the cultures we have gone through
 
                // currentCultureInfo now refers to the culture that had resources.
                // update cultures starting from requested culture up to the culture
                // that had resources.
                foreach (CultureInfo updateCultureInfo in mgr)
                {
                    AddResourceSet(localResourceSets, updateCultureInfo.Name, ref rs);
 
                    // stop when we've added current or reached invariant (top of chain)
                    if (updateCultureInfo == foundCulture)
                    {
                        break;
                    }
                }
            }
 
            return rs;
        }
 
        // Simple helper to ease maintenance and improve readability.
        private static void AddResourceSet(Dictionary<string, ResourceSet> localResourceSets, string cultureName, ref ResourceSet rs)
        {
            // InternalGetResourceSet is both recursive and reentrant -
            // assembly load callbacks in particular are a way we can call
            // back into the ResourceManager in unexpectedly on the same thread.
            lock (localResourceSets)
            {
                // If another thread added this culture, return that.
                if (localResourceSets.TryGetValue(cultureName, out ResourceSet? lostRace))
                {
                    if (!ReferenceEquals(lostRace, rs))
                    {
                        // Note: In certain cases, we can be trying to add a ResourceSet for multiple
                        // cultures on one thread, while a second thread added another ResourceSet for one
                        // of those cultures.  If there is a race condition we must make sure our ResourceSet
                        // isn't in our dictionary before closing it.
                        if (!localResourceSets.ContainsValue(rs))
                            rs.Dispose();
                        rs = lostRace;
                    }
                }
                else
                {
                    localResourceSets.Add(cultureName, rs);
                }
            }
        }
 
        protected static Version? GetSatelliteContractVersion(Assembly a)
        {
            ArgumentNullException.ThrowIfNull(a);
 
            string? v = a.GetCustomAttribute<SatelliteContractVersionAttribute>()?.Version;
            if (v == null)
            {
                // Return null. The calling code will use the assembly version instead to avoid potential type
                // and library loads caused by CA lookup.
                return null;
            }
 
            if (!Version.TryParse(v, out Version? version))
            {
                throw new ArgumentException(SR.Format(SR.Arg_InvalidSatelliteContract_Asm_Ver, a, v));
            }
 
            return version;
        }
 
        protected static CultureInfo GetNeutralResourcesLanguage(Assembly a)
        {
            // This method should be obsolete - replace it with the one below.
            // Unfortunately, we made it protected.
            return ManifestBasedResourceGroveler.GetNeutralResourcesLanguage(a, out _);
        }
 
        internal static bool IsDefaultType(string asmTypeName,
                                           string defaultTypeName)
        {
            Debug.Assert(asmTypeName != null, "asmTypeName was unexpectedly null");
 
            // First, compare type names
            int firstComma = asmTypeName.IndexOf(',');
            int typeNameLength = (firstComma != -1) ? firstComma : asmTypeName.Length;
 
            // Type names are case sensitive
            if (!asmTypeName.AsSpan(0, typeNameLength).Equals(defaultTypeName, StringComparison.Ordinal))
                return false;
 
            // No assembly name specified means system assembly.
            if (firstComma == -1)
                return true;
 
            // Now, compare assembly simple names, ignore the rest (version, public key token, etc.)
            int secondComma = asmTypeName.IndexOf(',', firstComma + 1);
            int simpleAsmNameLength = ((secondComma != -1) ? secondComma : asmTypeName.Length) - (firstComma + 1);
 
            // We have kept mscorlib as the simple assembly name for the default resource format. The type name of the default resource
            // format is de-facto a magic string that we check for and it is not actually used to load any types. There has not been
            // a good reason to change the magic string to have the current assembly name.
 
            // Assembly names are case insensitive
            return asmTypeName.AsSpan(firstComma + 1, simpleAsmNameLength).Trim().Equals("mscorlib", StringComparison.OrdinalIgnoreCase);
        }
 
        // Looks up a resource value for a particular name.  Looks in the
        // current thread's CultureInfo, and if not found, all parent CultureInfos.
        // Returns null if the resource wasn't found.
        //
        public virtual string? GetString(string name)
        {
            return GetString(name, null);
        }
 
        // Looks up a resource value for a particular name.  Looks in the
        // specified CultureInfo, and if not found, all parent CultureInfos.
        // Returns null if the resource wasn't found.
        //
        public virtual string? GetString(string name, CultureInfo? culture)
        {
            ArgumentNullException.ThrowIfNull(name);
 
            culture ??= CultureInfo.CurrentUICulture;
 
            ResourceSet? last = GetFirstResourceSet(culture);
 
            if (last != null)
            {
                string? value = last.GetString(name, _ignoreCase);
                if (value != null)
                    return value;
            }
 
            // This is the CultureInfo hierarchy traversal code for resource
            // lookups, similar but necessarily orthogonal to the ResourceSet
            // lookup logic.
            ResourceFallbackManager mgr = new ResourceFallbackManager(culture, _neutralResourcesCulture, true);
            foreach (CultureInfo currentCultureInfo in mgr)
            {
                ResourceSet? rs = InternalGetResourceSet(currentCultureInfo, true, true);
                if (rs == null)
                    break;
 
                if (rs != last)
                {
                    string? value = rs.GetString(name, _ignoreCase);
                    if (value != null)
                    {
                        // update last used ResourceSet
                        if (_lastUsedResourceCache != null)
                        {
                            lock (_lastUsedResourceCache)
                            {
                                _lastUsedResourceCache.lastCultureName = currentCultureInfo.Name;
                                _lastUsedResourceCache.lastResourceSet = rs;
                            }
                        }
                        return value;
                    }
 
                    last = rs;
                }
            }
 
            return null;
        }
 
        // Looks up a resource value for a particular name.  Looks in the
        // current thread's CultureInfo, and if not found, all parent CultureInfos.
        // Returns null if the resource wasn't found.
        //
        public virtual object? GetObject(string name)
        {
            return GetObject(name, null, true);
        }
 
        // Looks up a resource value for a particular name.  Looks in the
        // specified CultureInfo, and if not found, all parent CultureInfos.
        // Returns null if the resource wasn't found.
        public virtual object? GetObject(string name, CultureInfo? culture)
        {
            return GetObject(name, culture, true);
        }
 
        private object? GetObject(string name, CultureInfo? culture, bool wrapUnmanagedMemStream)
        {
            ArgumentNullException.ThrowIfNull(name);
 
            if (null == culture)
            {
                culture = CultureInfo.CurrentUICulture;
            }
 
            ResourceSet? last = GetFirstResourceSet(culture);
            if (last != null)
            {
                object? value = last.GetObject(name, _ignoreCase);
 
                if (value != null)
                {
                    if (value is UnmanagedMemoryStream stream && wrapUnmanagedMemStream)
                        return new UnmanagedMemoryStreamWrapper(stream);
                    else
                        return value;
                }
            }
 
            // This is the CultureInfo hierarchy traversal code for resource
            // lookups, similar but necessarily orthogonal to the ResourceSet
            // lookup logic.
            ResourceFallbackManager mgr = new ResourceFallbackManager(culture, _neutralResourcesCulture, true);
 
            foreach (CultureInfo currentCultureInfo in mgr)
            {
                ResourceSet? rs = InternalGetResourceSet(currentCultureInfo, true, true);
                if (rs == null)
                    break;
 
                if (rs != last)
                {
                    object? value = rs.GetObject(name, _ignoreCase);
                    if (value != null)
                    {
                        // update the last used ResourceSet
                        if (_lastUsedResourceCache != null)
                        {
                            lock (_lastUsedResourceCache)
                            {
                                _lastUsedResourceCache.lastCultureName = currentCultureInfo.Name;
                                _lastUsedResourceCache.lastResourceSet = rs;
                            }
                        }
 
                        if (value is UnmanagedMemoryStream stream && wrapUnmanagedMemStream)
                            return new UnmanagedMemoryStreamWrapper(stream);
                        else
                            return value;
                    }
 
                    last = rs;
                }
            }
 
            return null;
        }
 
        public UnmanagedMemoryStream? GetStream(string name)
        {
            return GetStream(name, null);
        }
 
        public UnmanagedMemoryStream? GetStream(string name, CultureInfo? culture)
        {
            object? obj = GetObject(name, culture, false);
            UnmanagedMemoryStream? ums = obj as UnmanagedMemoryStream;
            if (ums == null && obj != null)
                throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResourceNotStream_Name, name));
            return ums;
        }
 
        internal sealed class ResourceManagerMediator
        {
            private readonly ResourceManager _rm;
 
            internal ResourceManagerMediator(ResourceManager rm)
            {
                ArgumentNullException.ThrowIfNull(rm);
 
                _rm = rm;
            }
 
            // NEEDED ONLY BY FILE-BASED
            internal string? ModuleDir => _rm._moduleDir;
 
            [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
            internal Type? UserResourceSet => _rm._userResourceSet;
 
            internal string? BaseNameField => _rm.BaseNameField;
 
            internal CultureInfo? NeutralResourcesCulture
            {
                get => _rm._neutralResourcesCulture;
                set => _rm._neutralResourcesCulture = value;
            }
 
            internal string GetResourceFileName(CultureInfo culture) =>
                _rm.GetResourceFileName(culture);
 
            // NEEDED ONLY BY ASSEMBLY-BASED
            internal bool LookedForSatelliteContractVersion
            {
                get => _rm._lookedForSatelliteContractVersion;
                set => _rm._lookedForSatelliteContractVersion = value;
            }
 
            internal Version? SatelliteContractVersion
            {
                get => _rm._satelliteContractVersion;
                set => _rm._satelliteContractVersion = value;
            }
 
            internal static Version? ObtainSatelliteContractVersion(Assembly a) =>
                GetSatelliteContractVersion(a);
 
            internal UltimateResourceFallbackLocation FallbackLoc
            {
                get => _rm.FallbackLocation;
                set => _rm._fallbackLoc = value;
            }
 
            internal Assembly? MainAssembly => _rm.MainAssembly;
 
            // this is weird because we have BaseNameField accessor above, but we're sticking
            // with it for compat.
            internal string BaseName => _rm.BaseName;
        }
    }
}