預設 .resx 檔案式資源管理在多伺服器、多語系、頻繁更新文字時維護困難。將資源集中存放於 資料庫,並實作 ResourceProviderFactory、IResourceProvider、IResourceReader 三大介面,即可實現:
<configuration>
<system.web>
<globalization
resourceProviderFactoryType="Westwind.Globalization.DbSimpleResourceProviderFactory, Westwind.Globalization"
/>
</system.web>
</configuration>
[DesignTimeResourceProviderFactoryAttribute(typeof(DbDesignTimeResourceProviderFactory))]
public class DbSimpleResourceProviderFactory : ResourceProviderFactory
{
public override IResourceProvider CreateGlobalResourceProvider(string className)
{
return new DbSimpleResourceProvider(null, className);
}
public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
{
// 將虛擬路徑轉為資源集合名稱,如 "~/Pages/About.aspx" → "pages.about"
var resourceSet = DbResourceConfiguration.Current.StripVirtualPath(virtualPath).ToLower();
return new DbSimpleResourceProvider(null, resourceSet);
}
}
public class DbSimpleResourceProvider : IResourceProvider, IImplicitResourceProvider
{
private readonly string _resourceSetName;
private readonly ListDictionary _resourceCache = new ListDictionary(); // culture → IDictionary
public DbSimpleResourceProvider(string virtualPath, string className)
{
_resourceSetName = className;
}
// 取得指定文化的資源字典(含快取)
private IDictionary GetResourceCache(string cultureName)
{
cultureName ??= "";
if (_resourceCache[cultureName] is IDictionary dict) return dict;
// 從資料庫載入
var mgr = new DbResourceDataManager();
dict = mgr.GetResourceSet(cultureName, _resourceSetName);
_resourceCache[cultureName] = dict;
return dict;
}
public void ClearResourceCache() => _resourceCache.Clear();
// 核心:取得單一資源值(含回退邏輯)
object IResourceProvider.GetObject(string resourceKey, CultureInfo culture)
{
var cultureName = culture?.Name ?? CultureInfo.CurrentUICulture.Name;
return GetObjectInternal(resourceKey, cultureName);
}
private object GetObjectInternal(string key, string cultureName)
{
var dict = GetResourceCache(cultureName);
object val = dict?[key];
// 1. 具體文化無值 → 回退到中性文化
if (val == null && cultureName.Length > 3)
return GetObjectInternal(key, cultureName.Substring(0, 2));
// 2. 仍無值 → 回退到不變文化
if (val == null)
{
dict = GetResourceCache("");
val = dict?[key];
}
// 3. 仍無值 → 可選:自動在資料庫建立空白鍵值,避免下次再查
if (val == null && string.IsNullOrEmpty(cultureName) && DbResourceConfiguration.Current.AddMissingResources)
{
new DbResourceDataManager().AddResource(key, "", "", _resourceSetName);
val = "";
}
return val ?? "";
}
// 隱性資源(如 meta:resourcekey)讀取
public IDictionary GetImplicitResourceDictionary(CultureInfo culture)
{
return GetResourceCache(culture?.Name ?? "");
}
// 提供設計階段列舉用
public IResourceReader ResourceReader => new DbSimpleResourceReader(GetResourceCache(""));
}
public class DbSimpleResourceReader : IResourceReader
{
private readonly IDictionary _resources;
public DbSimpleResourceReader(IDictionary resources) => _resources = resources;
public IDictionaryEnumerator GetEnumerator() => _resources.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _resources.GetEnumerator();
public void Close() { }
public void Dispose() { }
}
CREATE TABLE LocalizationResources (
ResourceSet NVARCHAR(256) NOT NULL, -- 對應 className / virtualPath
CultureCode NVARCHAR(10) NOT NULL, -- 'zh-TW', 'zh', 'en-US', ''(invariant)
ResourceKey NVARCHAR(256) NOT NULL,
ResourceValue NVARCHAR(MAX) NULL,
CONSTRAINT PK_LocalizationResources PRIMARY KEY (ResourceSet, CultureCode, ResourceKey)
);
public class DbResourceDataManager
{
// 取得某資源集合在指定文化下的所有鍵值
public IDictionary GetResourceSet(string culture, string resourceSet) { /* ... */ }
// 新增/更新單一資源
public void AddResource(string key, string value, string culture, string resourceSet) { /* ... */ }
}
實作 DbDesignTimeResourceProviderFactory 繼承 DesignTimeResourceProviderFactoryAttribute,即可在 Visual Studio 編輯 .aspx/.ascx 時自動從資料庫讀取資源鍵值,提供 IntelliSense。
provider.ClearResourceCache() 或重啟應用程式集區。SqlCacheDependency 或外部訊息佇列(Redis Pub/Sub)通知各節點清除快取。zh-TW → zh → invariant 符合 .NET 標準文化回退規則。將本地化資源搬進資料庫,讓「改字」不再需要開發人員介入,是國際化專案邁向 DevOps 的關鍵一步。