You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 14:25:47