| File: ResourceManagerStringLocalizerFactory.cs | Web Access |
| Project: src\aspnetcore\src\Localization\Localization\src\Microsoft.Extensions.Localization.csproj (Microsoft.Extensions.Localization) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using System.IO; using System.Reflection; using System.Resources; using Microsoft.AspNetCore.Shared; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.Localization; /// <summary> /// An <see cref="IStringLocalizerFactory"/> that creates instances of <see cref="ResourceManagerStringLocalizer"/>. /// </summary> /// <remarks> /// <see cref="ResourceManagerStringLocalizerFactory"/> offers multiple ways to set the relative path of /// resources to be used. They are, in order of precedence: /// <see cref="ResourceLocationAttribute"/> -> <see cref="LocalizationOptions.ResourcesPath"/> -> the project root. /// </remarks> public class ResourceManagerStringLocalizerFactory : IStringLocalizerFactory { private readonly IResourceNamesCache _resourceNamesCache = new ResourceNamesCache(); private readonly ConcurrentDictionary<string, ResourceManagerStringLocalizer> _localizerCache = new ConcurrentDictionary<string, ResourceManagerStringLocalizer>(); private readonly string _resourcesRelativePath; private readonly ILoggerFactory _loggerFactory; /// <summary> /// Creates a new <see cref="ResourceManagerStringLocalizer"/>. /// </summary> /// <param name="localizationOptions">The <see cref="IOptions{LocalizationOptions}"/>.</param> /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> public ResourceManagerStringLocalizerFactory( IOptions<LocalizationOptions> localizationOptions, ILoggerFactory loggerFactory) { ArgumentNullThrowHelper.ThrowIfNull(localizationOptions); ArgumentNullThrowHelper.ThrowIfNull(loggerFactory); _resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty; _loggerFactory = loggerFactory; if (!string.IsNullOrEmpty(_resourcesRelativePath)) { _resourcesRelativePath = _resourcesRelativePath.Replace(Path.AltDirectorySeparatorChar, '.') .Replace(Path.DirectorySeparatorChar, '.') + "."; } } /// <summary> /// Gets the resource prefix used to look up the resource. /// </summary> /// <param name="typeInfo">The type of the resource to be looked up.</param> /// <returns>The prefix for resource lookup.</returns> protected virtual string GetResourcePrefix(TypeInfo typeInfo) { ArgumentNullThrowHelper.ThrowIfNull(typeInfo); return GetResourcePrefix(typeInfo, GetRootNamespace(typeInfo.Assembly), GetResourcePath(typeInfo.Assembly)); } /// <summary> /// Gets the resource prefix used to look up the resource. /// </summary> /// <param name="typeInfo">The type of the resource to be looked up.</param> /// <param name="baseNamespace">The base namespace of the application.</param> /// <param name="resourcesRelativePath">The folder containing all resources.</param> /// <returns>The prefix for resource lookup.</returns> /// <remarks> /// For the type "Sample.Controllers.Home" if there's a resourceRelativePath return /// "Sample.Resourcepath.Controllers.Home" if there isn't one then it would return "Sample.Controllers.Home". /// </remarks> protected virtual string GetResourcePrefix(TypeInfo typeInfo, string? baseNamespace, string? resourcesRelativePath) { ArgumentNullThrowHelper.ThrowIfNull(typeInfo); ArgumentThrowHelper.ThrowIfNullOrEmpty(baseNamespace); ArgumentThrowHelper.ThrowIfNullOrEmpty(typeInfo.FullName); if (string.IsNullOrEmpty(resourcesRelativePath)) { return typeInfo.FullName; } else { // This expectation is defined by dotnet's automatic resource storage. // We have to conform to "{RootNamespace}.{ResourceLocation}.{FullTypeName - RootNamespace}". return baseNamespace + "." + resourcesRelativePath + TrimPrefix(typeInfo.FullName, baseNamespace + "."); } } /// <summary> /// Gets the resource prefix used to look up the resource. /// </summary> /// <param name="baseResourceName">The name of the resource to be looked up</param> /// <param name="baseNamespace">The base namespace of the application.</param> /// <returns>The prefix for resource lookup.</returns> protected virtual string GetResourcePrefix(string baseResourceName, string baseNamespace) { ArgumentThrowHelper.ThrowIfNullOrEmpty(baseResourceName); ArgumentThrowHelper.ThrowIfNullOrEmpty(baseNamespace); var assemblyName = new AssemblyName(baseNamespace); var assembly = Assembly.Load(assemblyName); var rootNamespace = GetRootNamespace(assembly); var resourceLocation = GetResourcePath(assembly); var locationPath = rootNamespace + "." + resourceLocation; baseResourceName = locationPath + TrimPrefix(baseResourceName, baseNamespace + "."); return baseResourceName; } /// <summary> /// Creates a <see cref="ResourceManagerStringLocalizer"/> using the <see cref="Assembly"/> and /// <see cref="Type.FullName"/> of the specified <see cref="Type"/>. /// </summary> /// <param name="resourceSource">The <see cref="Type"/>.</param> /// <returns>The <see cref="ResourceManagerStringLocalizer"/>.</returns> public IStringLocalizer Create(Type resourceSource) { ArgumentNullThrowHelper.ThrowIfNull(resourceSource); // Get without Add to prevent unnecessary lambda allocation if (!_localizerCache.TryGetValue(resourceSource.AssemblyQualifiedName!, out var localizer)) { var typeInfo = resourceSource.GetTypeInfo(); var baseName = GetResourcePrefix(typeInfo); var assembly = typeInfo.Assembly; localizer = CreateResourceManagerStringLocalizer(assembly, baseName); _localizerCache[resourceSource.AssemblyQualifiedName!] = localizer; } return localizer; } /// <summary> /// Creates a <see cref="ResourceManagerStringLocalizer"/>. /// </summary> /// <param name="baseName">The base name of the resource to load strings from.</param> /// <param name="location">The location to load resources from.</param> /// <returns>The <see cref="ResourceManagerStringLocalizer"/>.</returns> public IStringLocalizer Create(string baseName, string location) { ArgumentNullThrowHelper.ThrowIfNull(baseName); ArgumentNullThrowHelper.ThrowIfNull(location); return _localizerCache.GetOrAdd($"B={baseName},L={location}", _ => { var assemblyName = new AssemblyName(location); var assembly = Assembly.Load(assemblyName); baseName = GetResourcePrefix(baseName, location); return CreateResourceManagerStringLocalizer(assembly, baseName); }); } /// <summary>Creates a <see cref="ResourceManagerStringLocalizer"/> for the given input.</summary> /// <param name="assembly">The assembly to create a <see cref="ResourceManagerStringLocalizer"/> for.</param> /// <param name="baseName">The base name of the resource to search for.</param> /// <returns>A <see cref="ResourceManagerStringLocalizer"/> for the given <paramref name="assembly"/> and <paramref name="baseName"/>.</returns> /// <remarks>This method is virtual for testing purposes only.</remarks> protected virtual ResourceManagerStringLocalizer CreateResourceManagerStringLocalizer( Assembly assembly, string baseName) { return new ResourceManagerStringLocalizer( new ResourceManager(baseName, assembly), assembly, baseName, _resourceNamesCache, _loggerFactory.CreateLogger<ResourceManagerStringLocalizer>()); } /// <summary> /// Gets the resource prefix used to look up the resource. /// </summary> /// <param name="location">The general location of the resource.</param> /// <param name="baseName">The base name of the resource.</param> /// <param name="resourceLocation">The location of the resource within <paramref name="location"/>.</param> /// <returns>The resource prefix used to look up the resource.</returns> protected virtual string GetResourcePrefix(string location, string baseName, string resourceLocation) { // Re-root the base name if a resources path is set return location + "." + resourceLocation + TrimPrefix(baseName, location + "."); } /// <summary>Gets a <see cref="ResourceLocationAttribute"/> from the provided <see cref="Assembly"/>.</summary> /// <param name="assembly">The assembly to get a <see cref="ResourceLocationAttribute"/> from.</param> /// <returns>The <see cref="ResourceLocationAttribute"/> associated with the given <see cref="Assembly"/>.</returns> /// <remarks>This method is protected and virtual for testing purposes only.</remarks> protected virtual ResourceLocationAttribute? GetResourceLocationAttribute(Assembly assembly) { return assembly.GetCustomAttribute<ResourceLocationAttribute>(); } /// <summary>Gets a <see cref="RootNamespaceAttribute"/> from the provided <see cref="Assembly"/>.</summary> /// <param name="assembly">The assembly to get a <see cref="RootNamespaceAttribute"/> from.</param> /// <returns>The <see cref="RootNamespaceAttribute"/> associated with the given <see cref="Assembly"/>.</returns> /// <remarks>This method is protected and virtual for testing purposes only.</remarks> protected virtual RootNamespaceAttribute? GetRootNamespaceAttribute(Assembly assembly) { return assembly.GetCustomAttribute<RootNamespaceAttribute>(); } private string? GetRootNamespace(Assembly assembly) { var rootNamespaceAttribute = GetRootNamespaceAttribute(assembly); if (rootNamespaceAttribute != null) { return rootNamespaceAttribute.RootNamespace; } return assembly.GetName().Name; } private string GetResourcePath(Assembly assembly) { var resourceLocationAttribute = GetResourceLocationAttribute(assembly); // If we don't have an attribute assume all assemblies use the same resource location. var resourceLocation = resourceLocationAttribute == null ? _resourcesRelativePath : resourceLocationAttribute.ResourceLocation + "."; resourceLocation = resourceLocation .Replace(Path.DirectorySeparatorChar, '.') .Replace(Path.AltDirectorySeparatorChar, '.'); return resourceLocation; } private static string? TrimPrefix(string name, string prefix) { if (name.StartsWith(prefix, StringComparison.Ordinal)) { return name.Substring(prefix.Length); } return name; } }