Blazor Server如何动态加载本地化资源无需重建应用?
Blazor Server运行时动态加载修改后的.resx资源文件
可以实现运行时修改.resx文件后无需重建应用,直接动态加载新的本地化数据,但默认的Blazor本地化机制依赖编译时生成的资源绑定,无法自动感知文件变化,需要自定义资源加载逻辑来实现动态刷新。
核心实现步骤
1. 自定义动态资源提供器
默认的ResourceManager基于编译后的资源程序集工作,无法读取运行时修改的.resx文件。需要实现IStringLocalizerProvider接口,自定义资源读取与缓存刷新逻辑:
public class DynamicResxStringLocalizerProvider : IStringLocalizerProvider { private readonly string _resourcesPath; private readonly Dictionary<string, Dictionary<string, string>> _resourceCache = new(); private readonly object _cacheLock = new(); public DynamicResxStringLocalizerProvider(IWebHostEnvironment env) { _resourcesPath = Path.Combine(env.ContentRootPath, "Resources"); // 监控.resx文件变化,自动清空对应缓存 var watcher = new FileSystemWatcher(_resourcesPath, "*.resx"); watcher.Changed += (s, e) => ClearCacheForResource(e.Name); watcher.Created += (s, e) => ClearCacheForResource(e.Name); watcher.Deleted += (s, e) => ClearCacheForResource(e.Name); watcher.EnableRaisingEvents = true; } public IStringLocalizer Create(Type resourceSource) => new DynamicResxStringLocalizer(this, resourceSource.Name); public IStringLocalizer Create(string baseName, string location) => new DynamicResxStringLocalizer(this, baseName); internal Dictionary<string, string> GetResourceStrings(string resourceName, CultureInfo culture) { var cacheKey = $"{resourceName}.{culture.Name}"; lock (_cacheLock) { if (_resourceCache.TryGetValue(cacheKey, out var strings)) return strings; // 优先读取对应文化的.resx,不存在则回退到默认文化文件 var resxPath = Path.Combine(_resourcesPath, $"{resourceName}.{culture.Name}.resx"); if (!File.Exists(resxPath)) { resxPath = Path.Combine(_resourcesPath, $"{resourceName}.resx"); if (!File.Exists(resxPath)) return new Dictionary<string, string>(); } // 解析.resx文件为键值对 var reader = new ResXResourceReader(resxPath); strings = reader.Cast<DictionaryEntry>() .ToDictionary(entry => entry.Key.ToString(), entry => entry.Value.ToString()); _resourceCache[cacheKey] = strings; return strings; } } private void ClearCacheForResource(string fileName) { var resourceBaseName = Path.GetFileNameWithoutExtension(fileName); lock (_cacheLock) { var keysToRemove = _resourceCache.Keys.Where(k => k.StartsWith(resourceBaseName)).ToList(); foreach (var key in keysToRemove) _resourceCache.Remove(key); } } } // 配套的自定义StringLocalizer实现 public class DynamicResxStringLocalizer : IStringLocalizer { private readonly DynamicResxStringLocalizerProvider _provider; private readonly string _resourceName; public DynamicResxStringLocalizer(DynamicResxStringLocalizerProvider provider, string resourceName) { _provider = provider; _resourceName = resourceName; } public LocalizedString this[string name] { get { var value = GetString(name); return new LocalizedString(name, value ?? name, resourceNotFound: value == null); } } public LocalizedString this[string name, params object[] arguments] { get { var format = GetString(name); var value = string.Format(format ?? name, arguments); return new LocalizedString(name, value, resourceNotFound: format == null); } } public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) { var culture = CultureInfo.CurrentUICulture; var strings = _provider.GetResourceStrings(_resourceName, culture); return strings.Select(kv => new LocalizedString(kv.Key, kv.Value, false)); } public IStringLocalizer WithCulture(CultureInfo culture) => this; private string GetString(string name) { var culture = CultureInfo.CurrentUICulture; var strings = _provider.GetResourceStrings(_resourceName, culture); return strings.TryGetValue(name, out var value) ? value : null; } }
2. 替换本地化服务配置
在Program.cs中替换默认的资源提供器,启用自定义逻辑:
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); // 替换默认的StringLocalizerProvider为自定义实现 builder.Services.AddSingleton<IStringLocalizerProvider, DynamicResxStringLocalizerProvider>();
3. 实现UI修改.resx的功能
创建Blazor组件,让用户通过界面输入键值对并写入.resx文件:
@page "/manage-localization" @inject IWebHostEnvironment Env <h3>添加/修改本地化键值对</h3> <div class="form-group mb-3"> <label>资源文件名(不含后缀):</label> <input @bind="@ResourceName" class="form-control" placeholder="如Shared" /> </div> <div class="form-group mb-3"> <label>语言代码(如zh-CN、en-US):</label> <input @bind="@CultureName" class="form-control" placeholder="如zh-CN" /> </div> <div class="form-group mb-3"> <label>键:</label> <input @bind="@Key" class="form-control" /> </div> <div class="form-group mb-3"> <label>值:</label> <input @bind="@Value" class="form-control" /> </div> <button @onclick="SaveLocalizationEntry" class="btn btn-primary">保存</button> @code { private string ResourceName { get; set; } = "Shared"; private string CultureName { get; set; } = "zh-CN"; private string Key { get; set; } private string Value { get; set; } private void SaveLocalizationEntry() { var resourcesPath = Path.Combine(Env.ContentRootPath, "Resources"); var resxPath = Path.Combine(resourcesPath, $"{ResourceName}.{CultureName}.resx"); // 不存在则创建空文件 if (!File.Exists(resxPath)) { using var writer = new ResXResourceWriter(resxPath); writer.Generate(); } // 读取现有条目,避免覆盖 var entries = new List<ResXDataNode>(); using (var reader = new ResXResourceReader(resxPath)) { entries.AddRange(reader.Cast<DictionaryEntry>().Select(entry => new ResXDataNode(entry.Key.ToString(), entry.Value))); } // 替换或添加新条目 var existingEntry = entries.FirstOrDefault(e => e.Name == Key); if (existingEntry != null) entries.Remove(existingEntry); entries.Add(new ResXDataNode(Key, Value)); // 写入文件 using var writer = new ResXResourceWriter(resxPath); foreach (var entry in entries) writer.AddResource(entry); writer.Generate(); } }
4. 确保刷新后生效
用户修改完成后,刷新浏览器即可触发自定义提供器重新读取.resx文件(文件变化时缓存已被自动清空),新的本地化值会立即生效。
注意事项
- 文件权限:确保应用进程对
Resources目录有读写权限,否则无法修改或读取文件。 - 并发处理:多用户同时修改同一文件时,需添加文件锁避免冲突,比如在写入逻辑中使用
lock。 - 缓存策略:示例中文件变化时立即清空缓存,可根据业务需求调整缓存过期时间,平衡性能与实时性。
- 格式验证:写入.resx时要确保XML格式合法,避免因非法字符导致解析失败。
内容的提问来源于stack exchange,提问作者Kasun Jalitha
相关产品推荐
相关产品推荐

