ASP.NET Core 本地化(Localization)请求文化判定与资源机制深度解析
本仓库的
src/Middleware/Localization与src/Localization两个源码目录共同构成了 ASP.NET Core 的本地化能力:前者负责「为每个 HTTP 请求自动推导应使用的 Culture / UICulture」并把结果固化到当前线程与请求特征中,后者负责「根据推导出的文化从资源(.resx / 自定义源)中取出本地化字符串」。本文以仓库根目录下 src/Middleware/Localization/README.md 的目录说明为骨架,结合模块内中间件、Provider、Options、工厂类及样例与测试源码逐层展开。读完你既能熟练配置UseRequestLocalization完成多语言站点,也能仿照社区方向自行扩展文化来源(QueryString、Cookie、请求头、会话、JSON、数据库等)与字符串资源后端。
一、Localization 目录总览:抽象、实现与样例的分工
在仓库中,本地化能力拆成两个独立工程组,分工非常清晰:
- src/Middleware/Localization/ —— 请求级文化(culture)判定的实现,输出程序集
Microsoft.AspNetCore.Localization,核心交付物是RequestLocalizationMiddleware以及一组可插拔的IRequestCultureProvider。这一层解决的核心问题是:请求进来后,系统应该用哪种语言处理并响应。 - src/Localization/ —— 字符串本地化(localized strings)的抽象与实现:
Abstractions/src提供IStringLocalizer、IStringLocalizer<T>、IStringLocalizerFactory、LocalizedString等公开契约;Localization/src提供基于ResourceManager的默认实现(ResourceManagerStringLocalizerFactory/ResourceManagerStringLocalizer),用于把"Hello"这样的 key 解析为对应文化下的资源文本。
仓库同时提供了可运行的完整样例与功能测试资产,供学习与回归验证:
- src/Middleware/Localization/sample/:演示请求文化判定的最小可运行站点(支持 QueryString / Cookie / 下拉框切换语言并实时展示"获胜 Provider")。
- src/Middleware/Localization/testassets/LocalizationWebsite/:覆盖多种布局(根目录资源、子目录资源、自定义文化保留、类库资源、
Content-Language响应头等)的测试站点。 - src/Middleware/Localization/test/:针对每个 Provider 与中间件的单元/功能测试。
原 README 的定位是一份「导航 + 生态索引」,除了本仓库内容,它还索引了社区在三个方向上的扩展(详见本文第七、八节)。
二、RequestLocalizationMiddleware:一条请求的文化是如何被决定的
2.1 中间件的完整执行流程
RequestLocalizationMiddleware.cs 中的 Invoke 是整条判定管线的核心,其算法可概括为:
- 以
_options.DefaultRequestCulture作为兜底值requestCulture; - 按顺序遍历
_options.RequestCultureProviders中的每一个IRequestCultureProvider; - 调用
provider.DetermineProviderCultureResult(context):- 返回
null→ 表示该 Provider 无法判定,继续尝试下一个; - 返回结果 → 分别拿出
Cultures与UICultures列表,用GetCultureInfo在SupportedCultures/SupportedUICultures中做匹配; - 若能匹配出有效的
cultureInfo或uiCultureInfo(至少一个),则补全缺失的一侧为DefaultRequestCulture的对应值,生成RequestCulture,记录该 provider 为winningProvider,break 跳出循环。
- 返回
- 把结果写入
context.Features.Set<IRequestCultureFeature>(...); - 调用
SetCurrentThreadCulture,把CultureInfo.CurrentCulture与CultureInfo.CurrentUICulture同时设置好; - 若配置了
ApplyCurrentCultureToResponseHeaders,则回写响应头Content-Language: <UICulture.Name>; - 调用
_next(context)进入后续中间件。
因此,第一个能给出非空结果的 Provider 拥有最高优先级,这决定了文化判定是「先到先得」的短路模型。
2.2 父文化回退与匹配细节(源码级)
匹配不是简单的字符串相等。GetCultureInfo 在候选名无法精确命中时,会沿父文化链向上回溯,但受两层保护:
- 最大回退深度常量
MaxCultureFallbackDepth = 5(RequestLocalizationMiddleware.cs),防止恶意/异常文化名引发无限递归; - 只有当
FallBackToParentCultures/FallBackToParentUICultures(默认均为true)开启时才允许父文化回退,且整个匹配只用文化名做比较,不加载完整文化信息对象,避免为不可信输入(HTTP 请求头)构造CultureInfo的开销与抛异常风险。
典型场景:应用只声明支持 fr,而请求头声明 fr-FR,此时系统会把请求文化归一为 fr——这正是原 README 所述"文化归一化"在中间件内的落地方式。
2.3 判定结果如何暴露给业务代码
RequestCultureFeature 通过 context.Features.Get<IRequestCultureFeature>() 暴露,业务代码可同时拿到结果与获胜 Provider:
var feature = context.Features.Get<IRequestCultureFeature>();
var requestCulture = feature.RequestCulture; // Culture 与 UICulture
var winningProvider = feature.Provider; // 例如 QueryStringRequestCultureProvider
在 src/Middleware/Localization/sample/Startup.cs 中,样例页面正是这样把 Winning provider、当前请求文化、当前线程文化、日期与货币格式并排展示出来,方便肉眼验证不同来源的优先级。
三、内置的四种 RequestCultureProvider
仓库默认实现了三个具体 Provider(另加一个可编程的自定义 Provider),全部继承自抽象基类 RequestCultureProvider.cs,该基类维护了对 RequestLocalizationOptions 的引用,并提供静态 NullProviderCultureResult(表示"本 Provider 无法判定,请试下一个")。
3.1 QueryStringRequestCultureProvider —— 从查询串取文化
QueryStringRequestCultureProvider.cs 从 ?culture=xx&ui-culture=yy 中读取文化,两个可配置项:
| 配置属性 | 默认值 | 含义 |
|---|---|---|
QueryStringKey |
culture |
指定 Culture 的查询参数名 |
UIQueryStringKey |
ui-culture |
指定 UICulture 的查询参数名;缺省时复用 Culture 值 |
实现细节值得注意:当只给了 culture 而没给 ui-culture(或相反)时,代码会把单一值同时复制给两侧(见 QueryStringRequestCultureProvider.cs),保证 Culture 与 UICulture 不会出现一边为空的情况。
3.2 CookieRequestCultureProvider —— 从 Cookie 记住用户偏好
CookieRequestCultureProvider.cs 让用户偏好可以跨请求持久化:
- 默认 Cookie 名:
static readonly string DefaultCookieName = ".AspNetCore.Culture"; - Cookie 值格式:
c=<Culture>|uic=<UICulture>(内部以|分隔、c=与uic=为前缀); - 配套提供了两个静态工具方法:
MakeCookieValue(RequestCulture)把文化对编码成 Cookie 值;ParseCookieValue(string)负责反向解析,解析失败返回null。
样例页面的 useCookie() JS(见 src/Middleware/Localization/sample/Startup.cs)展示了服务端如何让浏览器回写该 Cookie:
var cookieValue = '.AspNetCore.Culture=c=' + culture + '|uic=' + uiCulture;
document.cookie = cookieValue;
即"用户在页面上选一次语言 → 写 Cookie → 之后每次请求都由 CookieProvider 命中,无需再带 QueryString"。
3.3 AcceptLanguageHeaderRequestCultureProvider —— 尊重浏览器的语言偏好
AcceptLanguageHeaderRequestCultureProvider.cs 解析请求头 Accept-Language:
- 关键可配置属性
MaximumAcceptLanguageHeaderValuesToTry(默认3):只取头中最前面 3 个语言值参与解析,避免在脏数据上浪费 CPU(注释里明确写道"mitigate potentially spinning CPU"); - 代码使用
StringWithQualityHeaderValueComparer.QualityComparer按 q 值降序排序后,把语言名列表整体放入ProviderCultureResult返回; - 注意它返回的是一个候选列表而非单个文化,中间件随后会遍历候选,逐个尝试与
SupportedCultures精确匹配或父文化回退——所以即便请求头写了多个语言,只要排在前面的是受支持的文化,就能被选中。
3.4 判定结果类型 ProviderCultureResult
三种具体 Provider 返回的都是 ProviderCultureResult:它携带有序的 Cultures 与 UICultures 列表,中间件只认这个统一载体,从而实现"Provider 各显神通、中间件统一消化"的解耦。相关类型定义见 ProviderCultureResult.cs。
四、默认 Provider 顺序与 RequestLocalizationOptions 全量配置
4.1 默认链:QueryString → Cookie → Accept-Language
RequestLocalizationOptions.cs 的构造函数给出了默认优先级:
QueryStringRequestCultureProviderCookieRequestCultureProviderAcceptLanguageHeaderRequestCultureProvider
也就是说,一个普通请求在不带任何参数和 Cookie、只靠浏览器语言头的情况下,由最末位的 Accept-Language Provider 兜底。
4.2 Options 属性速查表
| 属性 | 默认值 | 作用 |
|---|---|---|
DefaultRequestCulture |
CultureInfo.CurrentCulture / CurrentUICulture |
所有 Provider 都无法判定时的最终兜底文化 |
SupportedCultures |
仅当前文化 | 允许被设置为请求 Culture 的清单 |
SupportedUICultures |
仅当前 UI 文化 | 允许被设置为请求 UICulture 的清单 |
FallBackToParentCultures |
true |
匹配失败时是否回退到父文化(如 fr-FR → fr) |
FallBackToParentUICultures |
true |
同上,作用于 UICulture |
CultureInfoUseUserOverride |
true |
构建 CultureInfo 时是否使用用户系统覆盖设置 |
ApplyCurrentCultureToResponseHeaders |
false |
为 true 时把 UICulture 写入响应的 Content-Language 头 |
RequestCultureProviders |
见 4.1 | 有序的 Provider 列表 |
4.3 流式 API:AddSupportedCultures / SetDefaultCulture
仓库为 Options 提供了链式配置方法(RequestLocalizationOptions.cs),样例的典型用法如下:
var supportedCultures = new[] { "en-US", "en-AU", "en-GB", "es-ES", "ja-JP", "fr-FR", "zh", "zh-CN" };
app.UseRequestLocalization(options =>
options
.AddSupportedCultures(supportedCultures) // 一次性替换 SupportedCultures
.AddSupportedUICultures(supportedCultures) // 一次性替换 SupportedUICultures
.SetDefaultCulture(supportedCultures[0])); // 设置默认文化为 en-US
AddInitialRequestCultureProvider(RequestLocalizationOptionsExtensions.cs)则把自定义 Provider 插入到列表头部,确保它拥有最高优先级:
options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context =>
{
// 例如:从数据库 / Session 中读取用户的语言偏好
return new ProviderCultureResult("zh-CN");
}));
4.4 UseRequestLocalization 的多种注册姿势
ApplicationBuilderExtensions.cs 提供了四个重载,覆盖从"最简"到"完全自定义"的所有场景:
UseRequestLocalization():使用 DI 中注册的RequestLocalizationOptions(默认值);UseRequestLocalization(RequestLocalizationOptions):复用外部创建好的 Options 实例;UseRequestLocalization(Action<RequestLocalizationOptions>):内联配置,样例即此形态(注释提醒:此时会新建一个不来自服务容器的 Options);UseRequestLocalization(params string[] cultures):只给一串文化名,第一个即默认文化,其余自动同时填入 SupportedCultures/UICultures。
五、自定义 Provider:理解 README 中"社区 Provider"的扩展点
原 README 明确指出社区围绕 _RequestCultureProvider_ 做适配,例如把 JSON 文件或 Session 值作为请求文化来源。这类扩展在本仓库代码中的落地方式有两种:
方式 A:继承 RequestCultureProvider 并实现抽象方法
public sealed class SessionRequestCultureProvider : RequestCultureProvider
{
public const string SessionKey = ".Culture";
public override Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext)
{
var culture = httpContext.Session?.GetString(SessionKey);
if (string.IsNullOrEmpty(culture))
{
return NullProviderCultureResult; // 交给下一个 Provider
}
return Task.FromResult<ProviderCultureResult?>(new ProviderCultureResult(culture));
}
}
方式 B:直接使用 CustomRequestCultureProvider + 委托
CustomRequestCultureProvider.cs 接收 Func<HttpContext, Task<ProviderCultureResult?>>,无需新建类即可把"查 Session / 查用户表 / 读 JSON"的逻辑内联进配置(样例 Startup 中已留好此注释占位,见 src/Middleware/Localization/sample/Startup.cs)。
无论哪种方式,只要返回 null 就不会抢占后续 Provider——这正是"可回退的自定义来源"设计要点,社区 JSON / Session Provider 的思路与仓库内置 Provider 完全同构。相关行为均有单元测试覆盖,例如 CustomRequestCultureProviderTest.cs 与 RequestLocalizationMiddlewareTest.cs 验证了短路、回退与优先级语义。
六、CustomRequestCultureProvider 判定的先后:AddInitial 的真实语义
需要特别强调:Provider 判定的"先到先得"只取决于列表顺序。上面 4.3 中 AddInitialRequestCultureProvider 之所以语义为"最高优先级",正因为它的实现是 RequestCultureProviders.Insert(0, provider)。若你想调整内置三者的相对顺序,直接重排 options.RequestCultureProviders 即可,无需改动中间件。
综合 StartupBuilderAPIs.cs 与功能测试 LocalizationSampleTest.cs,仓库还验证了组合使用场景:先 AddRequestLocalization 注册 Options,再在 Configure 中通过配置回调补齐 Provider——这也说明文化判定层的扩展点是高度正交、可组合的。
七、资源侧抽象:IStringLocalizer 契约与默认 ResourceManager 实现
文化判定只是第一步,真正"取出对应语言的字符串"由资源层负责。src/Localization 内的抽象定义于 src/Localization/Abstractions/src/IStringLocalizer.cs:
IStringLocalizer:按 key 取文本,支持this[string name]、this[string name, params object[] arguments](格式化)、GetAllStrings(bool includeParentCultures);IStringLocalizer<T>与其实现StringLocalizer<T>:类型安全的强类型入口,资源名自动绑定到类型T的全名;LocalizedString:带ResourceNotFound标志的取值结果,用于区分"存在但为空"与"key 不存在"。
默认实现则位于 src/Localization/Localization/src/ResourceManagerStringLocalizerFactory.cs。该工厂是理解资源路径规则的关键,其类注释给出了三条相对路径规则的优先级顺序:
ResourceLocationAttribute > LocalizationOptions.ResourcesPath > 项目根目录
工厂构造函数把 LocalizationOptions.ResourcesPath 中的目录分隔符统一替换为 . 并追加句点(见 ResourceManagerStringLocalizerFactory.cs),从而把"文件夹路径"换算成"资源基名",例如 My/Resources 会成为前缀 My.Resources.。
7.1 ResourcesPath:统一资源子目录
AddLocalization 的服务注册(见 LocalizationServiceCollectionExtensions.cs 与 LocalizationOptions.cs)可指定全局资源目录。仓库样例即采用此模式并把资源放在自定义子目录中:
services.AddLocalization(options => options.ResourcesPath = "My/Resources");
配合 sample 工程下的 Startup.es-ES.resx、Startup.fr-FR.resx、Startup.ja-JP.resx、Startup.zh-CN.resx、Startup.zh.resx,可以看到命名语言后缀的 resx 并列放置这一约定:资源文件名 Startup.<culture>.resx 中的 <culture> 段决定其所属文化。
7.2 ResourceLocationAttribute / RootNamespaceAttribute:类库级资源定位
当本地化资源内嵌在独立类库时,就需要用到两个程序集级特性:
ResourceLocationAttribute:显式覆盖资源所在相对文件夹(优先级最高);RootNamespaceAttribute:当程序集名与代码根命名空间不一致时,校正资源根命名空间。
仓库测试资产提供了两组对比例子:
- testassets/ResourcesClassLibraryNoAttribute/:无特性,依赖默认的根命名空间推导(
Resources/Model.resx对应Model类型); - testassets/ResourcesClassLibraryWithAttribute/:带
ResourceLocationAttribute(ResourceFolder)与RootNamespaceAttribute,把资源定位到非默认子目录。
7.3 与请求文化打通:局部化 MVC/视图 场景
仓库资源侧的测试资产 testassets/LocalizationWebsite/ 覆盖了更完整的形态:为模型 Models/Customer.fr-FR.resx 提供数据注解本地化字符串,为 Startup 类型提供位于根目录或子目录的资源(StartupResourcesAtRootFolder.fr-FR.resx、Resources/StartupResourcesInFolder.fr-FR.resx),以及从独立类库取资源与 GetAllStrings 的验证站点(StartupGetAllStrings.cs)。它们与本文第二节的 RequestLocalizationMiddleware 正好构成完整链路:Provider 决定线程文化 → IStringLocalizer 依线程文化命中对应 resx。
八、原 README 的社区资源扩展方向与本仓库的对应物
原 README 在 "Localization Resources" 一节列出了社区对 _IStringLocalizer_ 的替代实现,例如从 JSON 文件、PO 文件取资源。这些方向在本仓库抽象层的对应扩展点如下:
- 自定义
IStringLocalizerFactory/IStringLocalizer:只需实现 IStringLocalizerFactory.cs 与 IStringLocalizer.cs 两个接口,并在services.AddLocalization()之外追加注册你的工厂替换默认ResourceManagerStringLocalizerFactory即可。README 中"customResourceManagerStringLocalizer"的语义即:保留 key 查找逻辑、替换底层资源读取源。 - PO/JSON 等文本化资源源:抽象层不关心资源物理载体,
GetString只需返回LocalizedString;将资源文件编译为附属程序集的机制(.resx → satellite assembly)仅是默认实现的策略,JSON/PO 实现可绕过它直接从文件读取。 - 数据注解/模型验证本地化:
IStringLocalizer<T>的强类型入口天然适配数据注解错误消息的翻译,testassets 中Customer.fr-FR.resx即演示了模型级资源如何随文化切换(MVC 数据注解集成可参考仓库 src/Localization 之外的 Mvc.DataAnnotations 模块用法,但其文化来源同样依赖本文的请求判定层)。
需要说明的是:原 README 中指向外部仓库(如 Entropy 示例、OrchardCore、hishamco 的 JSON/Session 扩展)的超链接属于仓库外部生态,本文不展开其外部链接内容;理解其设计意图后,用本仓库提供的四个内置 Provider、CustomRequestCultureProvider 与 IStringLocalizerFactory 扩展点即可实现等价能力。
九、端到端可运行样例与验证路径
若要在本地直观体验整套机制,可直接运行仓库样例:
cd src/Middleware/Localization/sample
dotnet run
样例站点启动后:
- 通过 URL
/?culture=es-ES&ui-culture=es-ES观察 QueryStringProvider 生效; - 点击页面 "go cookie" 按钮观察 CookieProvider 生效并跨请求保持;
- 页面会展示 Winning provider 名称、当前请求 Culture/UICulture、当前线程 Culture/UICulture,以及同一日期/货币在 invariant 与当前文化下的不同格式化结果;
- 下拉框还故意提供了
en-NOTREAL、pp-NOTREAL这类假文化项,用于演示"未声明文化被 Provider 判定后会被中间件过滤、最终回退到默认文化"的行为。
单元与功能测试是理解边界行为的最佳教材:
- test/UnitTests/AcceptLanguageHeaderRequestCultureProviderTest.cs:q 值排序与"最多尝试 3 个"的限制;
- test/UnitTests/CookieRequestCultureProviderTest.cs:Cookie 值编解码与非法值返回 null;
- test/UnitTests/QueryStringRequestCultureProviderTest.cs:单值复制双 culture 的语义;
- test/FunctionalTests/LocalizationTest.cs:testassets 多个 Startup 变体的端到端行为;
- src/Localization/Localization/test/Microsoft.Extensions.Localization.Tests/:ResourceManager 工厂与
StringLocalizer<T>的资源命名推导测试。
十、实践要点小结
- 优先级本质是顺序:
RequestLocalizationOptions默认链 QueryString → Cookie → Accept-Language,任何自定义 Provider 通过AddInitialRequestCultureProvider可升至首位;返回null即主动让贤。 - 安全与文化归一:中间件只与
SupportedCultures精确匹配、至多向上回退 5 层父文化;对请求输入绝不盲目构造CultureInfo,这是防御性设计的源码级体现。 - 判定与取值分层:请求文化判定(
Microsoft.AspNetCore.Localization)与字符串资源(Microsoft.Extensions.Localization)是解耦的两层,前者产出线程文化,后者按文化消费资源——扩展时不必同时改动两边。 - 资源路径三条规则:
ResourceLocationAttribute>LocalizationOptions.ResourcesPath> 项目根目录;类库场景务必核对程序集名与RootNamespace是否一致。 - 验证优先:仓库中每个 Provider 都有独立单元测试,动手做自定义扩展前先跑一遍
src/Middleware/Localization/test下的测试,可快速校准对短路与回退语义的理解。
延伸阅读:结合仓库内 Localization.slnf、Middleware.slnf 可了解工程间的引用边界;文化判定在 MVC/数据注解/视图本地化中的消费方式可对照 Mvc.DataAnnotations 与 Localization 相关实现继续深入。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00