【问题标题】:ASP.NET Core: programatically set the resource file for localized viewASP.NET Core:以编程方式为本地化视图设置资源文件
【发布时间】:2021-03-13 10:54:32
【问题描述】:

我有一个用 ASP.NET Core v2.1 编写的网络应用程序。该应用使用通过LocalizationOptions.ResourcesPath = "Resources" 配置的本地化视图,并且通过在cshtml 文件中注入IViewLocalizer 访问本地化字符串。

在某些情况下,我想使用与位于 Resources 文件夹中的默认资源文件不同的资源文件来渲染视图。另一个资源文件与默认的具有相同的键(无需更改视图),因此只会呈现不同的文本。

例如在控制器中考虑这样的操作方法,看看我想解决的问题:

public async Task<IActionResult> ShowSomething([FromQuery] bool useDifferentResource)
{
    if (useDifferentResource)
    {
        // how to render the view which will use different resource file
        // then the default found in Resources folder?
        // return View("MyView");
    }
    
    // this renders the view with the default resource file found in Resources folder
    return View("MyView");
}

【问题讨论】:

    标签: c# asp.net asp.net-mvc asp.net-core localization


    【解决方案1】:

    首先,我不太确定您要求的必要性。但是通过深入研究源代码,我发现这是可能的,并在此处提出了解决方案。

    实际上IViewLocalizer 是由注册为单例的IHtmlLocalizerFactory 实例化的。这取决于IStringLocalizerFactory,它也被注册为单例。这里我们使用资源文件本地化(由ResourceManager管理),所以实现类是ResourceManagerStringLocalizerFactory。 该工厂使用选项LocalizationOptions 来获取配置的ResourcesPath 用于创建IStringLocalizer 的实例,该实例由HtmlLocalizer 包装,最后包装在ViewLocalizer 中。这里的重点是结果由缓存键缓存,具体取决于视图/页面路径和程序集的名称(嵌入资源)。因此,在第一次创建 ViewLocalizer 的实例(可通过 DI 获得)之后,它将被缓存,您无法更改配置的 ResourcesPath 或拦截以某种方式更改它。

    这意味着我们需要一个自定义的ResourceManagerStringLocalizerFactory 来覆盖Create 方法(实际上它不是虚拟的,但我们可以重新实现它)。我们需要在缓存键中再包含一个因素(运行时资源路径),以便缓存能够正常工作。 ResourceManagerStringLocalizerFactory 中还有一种虚拟方法可以被覆盖以提供运行时资源路径:GetResourceLocationAttribute。为了最小化自定义ResourceManagerStringLocalizerFactory 的实现代码,我选择了覆盖该方法。通过阅读源代码可以看出,提供自己的运行时资源路径并不是唯一的拦截点,但似乎是最简单的。

    这是核心原则。然而,当涉及到完整解决方案的实施时,事情就没有那么简单了。这是完整的代码:

    /// <summary>
    /// A ViewLocalizer that can be aware of the request feature IActiveViewLocalizerFeature to use instead of 
    /// basing on the default implementation of ViewLocalizer
    /// </summary>
    public class ActiveLocalizerAwareViewLocalizer : ViewLocalizer
    {
        readonly IHttpContextAccessor _httpContextAccessor;
        public ActiveLocalizerAwareViewLocalizer(IHtmlLocalizerFactory localizerFactory, IHostingEnvironment hostingEnvironment,
            IHttpContextAccessor httpContextAccessor) : base(localizerFactory, hostingEnvironment)
        {
            _httpContextAccessor = httpContextAccessor;
        }
    
        public override LocalizedHtmlString this[string key, params object[] arguments]
        {
            get
            {
                var localizer = _getActiveLocalizer();
                return localizer == null ? base[key, arguments] : localizer[key, arguments];
            }
        }
    
        public override LocalizedHtmlString this[string key]
        {
            get
            {
                var localizer = _getActiveLocalizer();
                return localizer == null ? base[key] : localizer[key];
            }
        }
        IHtmlLocalizer _getActiveLocalizer()
        {
            return _httpContextAccessor.HttpContext.Features.Get<IActiveViewLocalizerFeature>()?.ViewLocalizer;
        }
    }
    
    public static class HtmlLocalizerFactoryWithRuntimeResourcesPathExtensions
    {
        public static T WithResourcesPath<T>(this T factory, string resourcesPath) where T : IHtmlLocalizerFactory
        {
            if (factory is IRuntimeResourcesPath overridableFactory)
            {
                overridableFactory.SetRuntimeResourcesPath(resourcesPath);
            }
            return factory;
        }
    }
    
    public interface IActiveViewLocalizerFeature
    {
        IHtmlLocalizer ViewLocalizer { get; }
    }
    
    public class ActiveViewLocalizerFeature : IActiveViewLocalizerFeature
    {
        public ActiveViewLocalizerFeature(IHtmlLocalizer viewLocalizer)
        {
            ViewLocalizer = viewLocalizer;
        }
        public IHtmlLocalizer ViewLocalizer { get; }
    }
    
    public interface IRuntimeResourcesPath
    {
        string ResourcesPath { get; }
        void SetRuntimeResourcesPath(string resourcesPath);
    }
    
    public class RuntimeResourcesPathHtmlLocalizerFactory : HtmlLocalizerFactory, IRuntimeResourcesPath
    {
        readonly IStringLocalizerFactory _stringLocalizerFactory;
        public RuntimeResourcesPathHtmlLocalizerFactory(IStringLocalizerFactory localizerFactory) : base(localizerFactory)
        {
            _stringLocalizerFactory = localizerFactory;
        }
        //NOTE: the factory is registered as a singleton, so we need this to manage different resource paths used on different tasks
        readonly AsyncLocal<string> _asyncResourcePath = new AsyncLocal<string>();
        public string ResourcesPath => _asyncResourcePath.Value;
    
        void IRuntimeResourcesPath.SetRuntimeResourcesPath(string resourcesPath)
        {
            _asyncResourcePath.Value = resourcesPath;
        }
        public override IHtmlLocalizer Create(string baseName, string location)
        {
            if (_stringLocalizerFactory is IRuntimeResourcesPath overridableFactory)
            {
                overridableFactory.SetRuntimeResourcesPath(ResourcesPath);
            }
            return base.Create(baseName, location);
        }
    }
    
    public static class RuntimeResourcesPathHtmlLocalizerFactoryExtensions
    {
        /// <summary>
        /// Creates an IHtmlLocalizer with a runtime resources path (instead of using the configured ResourcesPath)
        /// </summary>
        public static IHtmlLocalizer CreateWithResourcesPath(this IHtmlLocalizerFactory factory, string resourcesPath, string baseName, string location = null)
        {
            location = location ?? Assembly.GetEntryAssembly().GetName().Name;
            var result = factory.WithResourcesPath(resourcesPath).Create(baseName, location);
            factory.WithResourcesPath(null);
            return result;
        }
    }
    
    public static class RuntimeResourcesPathLocalizationExtensions
    {
        static IHtmlLocalizer _useLocalizer(ActionContext actionContext, string resourcesPath, string viewPath)
        {
            var factory = actionContext.HttpContext.RequestServices.GetRequiredService<IHtmlLocalizerFactory>();
    
            viewPath = viewPath.Substring(0, viewPath.Length - Path.GetExtension(viewPath).Length).TrimStart('/', '\\')
                       .Replace("/", ".").Replace("\\", ".");
    
            var location = Assembly.GetEntryAssembly().GetName().Name;
    
            var localizer = factory.CreateWithResourcesPath(resourcesPath, viewPath, location);
            actionContext.HttpContext.Features.Set<IActiveViewLocalizerFeature>(new ActiveViewLocalizerFeature(localizer));
            return localizer;
        }
        /// <summary>
        /// Can be used inside Controller
        /// </summary>
        public static IHtmlLocalizer UseLocalizer(this ActionContext actionContext, string resourcesPath, string viewOrPageName = null)
        {
            //find the view before getting the path
            var razorViewEngine = actionContext.HttpContext.RequestServices.GetRequiredService<IRazorViewEngine>();
            if (actionContext is ControllerContext cc)
            {
                viewOrPageName = viewOrPageName ?? cc.ActionDescriptor.ActionName;
                var viewResult = razorViewEngine.FindView(actionContext, viewOrPageName, false);
                return _useLocalizer(actionContext, resourcesPath, viewResult.View.Path);
            }
            var pageResult = razorViewEngine.FindPage(actionContext, viewOrPageName);
            //NOTE: here we have pageResult.Page is an IRazorPage but we don't use that to call UseLocalizer
            //because that IRazorPage instance has very less info (lacking ViewContext, PageContext ...)
            //The only precious info we have here is the Page.Path
            return _useLocalizer(actionContext, resourcesPath, pageResult.Page.Path);
        }
        /// <summary>
        /// Can be used inside Razor View or Razor Page
        /// </summary>
        public static IHtmlLocalizer UseLocalizer(this IRazorPage razorPage, string resourcesPath)
        {
            var path = razorPage.ViewContext.ExecutingFilePath;
            if (string.IsNullOrEmpty(path))
            {
                path = razorPage.ViewContext.View.Path;
            }
            if (path == null) return null;
            return _useLocalizer(razorPage.ViewContext, resourcesPath, path);
        }
        /// <summary>
        /// Can be used inside PageModel
        /// </summary>
        public static IHtmlLocalizer UseLocalizer(this PageModel pageModel, string resourcesPath)
        {
            return pageModel.PageContext.UseLocalizer(resourcesPath, pageModel.RouteData.Values["page"]?.ToString()?.TrimStart('/'));
        }
    }
    

    我在开头提到的自定义ResourceManagerStringLocalizerFactory

    public class RuntimeResourcesPathResourceManagerStringLocalizerFactory 
        : ResourceManagerStringLocalizerFactory, IRuntimeResourcesPath, IStringLocalizerFactory
    {
        readonly AsyncLocal<string> _asyncResourcePath = new AsyncLocal<string>();
        public string ResourcesPath => _asyncResourcePath.Value;
        private readonly ConcurrentDictionary<string, ResourceManagerStringLocalizer> _localizerCache =
            new ConcurrentDictionary<string, ResourceManagerStringLocalizer>();
    
        public RuntimeResourcesPathResourceManagerStringLocalizerFactory(IOptions<LocalizationOptions> localizationOptions, ILoggerFactory loggerFactory) : base(localizationOptions, loggerFactory)
        {
        }
        protected override ResourceLocationAttribute GetResourceLocationAttribute(Assembly assembly)
        {
            //we is where we override the configured ResourcesPath and use the runtime ResourcesPath.
            return ResourcesPath == null ? base.GetResourceLocationAttribute(assembly) : new ResourceLocationAttribute(ResourcesPath);
        }        
    
        public void SetRuntimeResourcesPath(string resourcesPath)
        {
            _asyncResourcePath.Value = resourcesPath;
        }
        /// <summary>
        /// Almost cloned from the source code of ResourceManagerStringLocalizerFactory
        /// We need to re-implement this because the framework code caches the result of Create using a cache key depending on only baseName & location.
        /// But here we introduce one more parameter of (runtime) ResourcesPath, so we need to include that in the cache key as well for 
        /// it to work properly (otherwise each time changing the runtime ResourcesPath, the same cached result will be returned, which is wrong).
        /// </summary>        
        IStringLocalizer IStringLocalizerFactory.Create(string baseName, string location)
        {
            if (baseName == null)
            {
                throw new ArgumentNullException(nameof(baseName));
            }
    
            if (location == null)
            {
                throw new ArgumentNullException(nameof(location));
            }
    
            return _localizerCache.GetOrAdd($"B={baseName},L={location},R={ResourcesPath}", _ =>
            {
                var assemblyName = new AssemblyName(location);
                var assembly = Assembly.Load(assemblyName);
                baseName = GetResourcePrefix(baseName, location);
    
                return CreateResourceManagerStringLocalizer(assembly, baseName);
            });
        }
    }
    

    另外一个扩展类来帮助方便地注册自定义服务:

    public static class RuntimeResourcesPathLocalizationServiceCollectionExtensions
    {
        public static IServiceCollection AddRuntimeResourcesPathForLocalization(this IServiceCollection services)
        {
            services.AddSingleton<IStringLocalizerFactory, RuntimeResourcesPathResourceManagerStringLocalizerFactory>();
            services.AddSingleton<IHtmlLocalizerFactory, RuntimeResourcesPathHtmlLocalizerFactory>();
            return services.AddSingleton<IViewLocalizer, ActiveLocalizerAwareViewLocalizer>();
        }
    }
    

    我们还实现了自定义IViewLocalizer,以便可以在您的代码中无缝使用它。它的工作只是检查是否有通过HttpContext 共享的IHtmlLocalizer 的任何活动实例(作为称为IActiveViewLocalizerFeature 的功能。每个不同的运行时资源路径将创建一个不同的IHtmlLocalizer,将作为活动共享localizer。通常在一个请求范围内(以及在视图上下文中),我们通常只需要使用一个运行时资源路径(在渲染视图之前一开始就指定)。

    注册自定义服务:

    services.AddRuntimeResourcesPathForLocalization();
    

    将本地化程序与运行时资源路径一起使用:

    public async Task<IActionResult> ShowSomething([FromQuery] bool useDifferentResource)
    {
      if (useDifferentResource)
      {
         this.UseLocalizer("resources path of your choice");
      }
        
      return View("MyView");
    }
    

    注意:控制器或 PageModel 范围内的 UseLocalizer 效率不高,因为需要额外的逻辑来查找视图/页面(使用 IRazorViewEngine,您可以在代码中看到) .因此,如果可能,您应该将UseLocalizer 移至RazorPageView。切换条件可以通过视图模型或任何其他方式(视图数据,视图包,...)传递。

    【讨论】:

    • 你能解释一下你的第一句话吗? “首先我不太确定您的要求的必要性”...对于我的情况,还有其他更简单的解决方案吗?只是我需要根据一些业务情况在视图中显示不同的本地化文本。
    • @PetrFelzmann 据我了解,本地化似乎只涉及一个论点:当前的文化或语言。因此,如果您有其他参数来控制应该显示哪些参数,特别是仅应用于视图的一部分(而不是整个视图),您可能需要这里的解决方案。否则,例如您的不同的本地化文本应用于整个视图,然后相应地切换文化。
    • 例如:通常情况下,你有lang的查询字符串来切换文化,但如果你的意思是你需要更复杂的业务来确定lang,我们有更简单的解决方案,当然整个视图将受到该文化的影响。在这里,我了解到您的视图的不同部分需要应用不同的本地化资源(来自不同的路径)。
    • 我的问题不在于文化。我想保留文化,但只需更改同一资源键的文本值。但是为什么你不确定我的要求的必要性?
    • @PetrFelzmann 因为我不太了解您的原始要求。我只是了解您直接想要什么:以编程方式设置资源路径。是的,我发现这很有趣并试图解决它。就是这样。
    猜你喜欢
    • 2015-12-21
    • 1970-01-01
    • 1970-01-01
    • 2012-06-10
    • 2014-04-26
    • 1970-01-01
    • 1970-01-01
    • 2022-01-22
    • 1970-01-01
    相关资源
    最近更新 更多