首页
/ ASP.NET Core 本地化(Localization)请求文化判定与资源机制深度解析

ASP.NET Core 本地化(Localization)请求文化判定与资源机制深度解析

2026-09-08 11:23:04作者:瞿蔚英Wynne

本仓库的 src/Middleware/Localizationsrc/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 提供 IStringLocalizerIStringLocalizer<T>IStringLocalizerFactoryLocalizedString 等公开契约;
    • Localization/src 提供基于 ResourceManager 的默认实现(ResourceManagerStringLocalizerFactory / ResourceManagerStringLocalizer),用于把 "Hello" 这样的 key 解析为对应文化下的资源文本。

仓库同时提供了可运行的完整样例与功能测试资产,供学习与回归验证:

原 README 的定位是一份「导航 + 生态索引」,除了本仓库内容,它还索引了社区在三个方向上的扩展(详见本文第七、八节)。

二、RequestLocalizationMiddleware:一条请求的文化是如何被决定的

2.1 中间件的完整执行流程

RequestLocalizationMiddleware.cs 中的 Invoke 是整条判定管线的核心,其算法可概括为:

  1. _options.DefaultRequestCulture 作为兜底值 requestCulture
  2. 按顺序遍历 _options.RequestCultureProviders 中的每一个 IRequestCultureProvider
  3. 调用 provider.DetermineProviderCultureResult(context)
    • 返回 null → 表示该 Provider 无法判定,继续尝试下一个;
    • 返回结果 → 分别拿出 CulturesUICultures 列表,用 GetCultureInfoSupportedCultures / SupportedUICultures 中做匹配;
    • 若能匹配出有效的 cultureInfouiCultureInfo(至少一个),则补全缺失的一侧为 DefaultRequestCulture 的对应值,生成 RequestCulture,记录该 provider 为 winningProviderbreak 跳出循环
  4. 把结果写入 context.Features.Set<IRequestCultureFeature>(...)
  5. 调用 SetCurrentThreadCulture,把 CultureInfo.CurrentCultureCultureInfo.CurrentUICulture 同时设置好;
  6. 若配置了 ApplyCurrentCultureToResponseHeaders,则回写响应头 Content-Language: <UICulture.Name>
  7. 调用 _next(context) 进入后续中间件。

因此,第一个能给出非空结果的 Provider 拥有最高优先级,这决定了文化判定是「先到先得」的短路模型。

2.2 父文化回退与匹配细节(源码级)

匹配不是简单的字符串相等。GetCultureInfo 在候选名无法精确命中时,会沿父文化链向上回溯,但受两层保护:

  • 最大回退深度常量 MaxCultureFallbackDepth = 5RequestLocalizationMiddleware.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:它携带有序的 CulturesUICultures 列表,中间件只认这个统一载体,从而实现"Provider 各显神通、中间件统一消化"的解耦。相关类型定义见 ProviderCultureResult.cs

四、默认 Provider 顺序与 RequestLocalizationOptions 全量配置

4.1 默认链:QueryString → Cookie → Accept-Language

RequestLocalizationOptions.cs 的构造函数给出了默认优先级:

  1. QueryStringRequestCultureProvider
  2. CookieRequestCultureProvider
  3. AcceptLanguageHeaderRequestCultureProvider

也就是说,一个普通请求在不带任何参数和 Cookie、只靠浏览器语言头的情况下,由最末位的 Accept-Language Provider 兜底。

4.2 Options 属性速查表

属性 默认值 作用
DefaultRequestCulture CultureInfo.CurrentCulture / CurrentUICulture 所有 Provider 都无法判定时的最终兜底文化
SupportedCultures 仅当前文化 允许被设置为请求 Culture 的清单
SupportedUICultures 仅当前 UI 文化 允许被设置为请求 UICulture 的清单
FallBackToParentCultures true 匹配失败时是否回退到父文化(如 fr-FRfr
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

AddInitialRequestCultureProviderRequestLocalizationOptionsExtensions.cs)则把自定义 Provider 插入到列表头部,确保它拥有最高优先级:

options.AddInitialRequestCultureProvider(new CustomRequestCultureProvider(async context =>
{
    // 例如:从数据库 / Session 中读取用户的语言偏好
    return new ProviderCultureResult("zh-CN");
}));

4.4 UseRequestLocalization 的多种注册姿势

ApplicationBuilderExtensions.cs 提供了四个重载,覆盖从"最简"到"完全自定义"的所有场景:

  1. UseRequestLocalization():使用 DI 中注册的 RequestLocalizationOptions(默认值);
  2. UseRequestLocalization(RequestLocalizationOptions):复用外部创建好的 Options 实例;
  3. UseRequestLocalization(Action<RequestLocalizationOptions>):内联配置,样例即此形态(注释提醒:此时会新建一个不来自服务容器的 Options);
  4. 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.csRequestLocalizationMiddlewareTest.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.csLocalizationOptions.cs)可指定全局资源目录。仓库样例即采用此模式并把资源放在自定义子目录中:

services.AddLocalization(options => options.ResourcesPath = "My/Resources");

配合 sample 工程下的 Startup.es-ES.resxStartup.fr-FR.resxStartup.ja-JP.resxStartup.zh-CN.resxStartup.zh.resx,可以看到命名语言后缀的 resx 并列放置这一约定:资源文件名 Startup.<culture>.resx 中的 <culture> 段决定其所属文化。

7.2 ResourceLocationAttribute / RootNamespaceAttribute:类库级资源定位

当本地化资源内嵌在独立类库时,就需要用到两个程序集级特性:

  • ResourceLocationAttribute:显式覆盖资源所在相对文件夹(优先级最高);
  • RootNamespaceAttribute:当程序集名与代码根命名空间不一致时,校正资源根命名空间。

仓库测试资产提供了两组对比例子:

7.3 与请求文化打通:局部化 MVC/视图 场景

仓库资源侧的测试资产 testassets/LocalizationWebsite/ 覆盖了更完整的形态:为模型 Models/Customer.fr-FR.resx 提供数据注解本地化字符串,为 Startup 类型提供位于根目录或子目录的资源(StartupResourcesAtRootFolder.fr-FR.resxResources/StartupResourcesInFolder.fr-FR.resx),以及从独立类库取资源与 GetAllStrings 的验证站点(StartupGetAllStrings.cs)。它们与本文第二节的 RequestLocalizationMiddleware 正好构成完整链路:Provider 决定线程文化 → IStringLocalizer 依线程文化命中对应 resx

八、原 README 的社区资源扩展方向与本仓库的对应物

原 README 在 "Localization Resources" 一节列出了社区对 _IStringLocalizer_ 的替代实现,例如从 JSON 文件、PO 文件取资源。这些方向在本仓库抽象层的对应扩展点如下:

  1. 自定义 IStringLocalizerFactory / IStringLocalizer:只需实现 IStringLocalizerFactory.csIStringLocalizer.cs 两个接口,并在 services.AddLocalization() 之外追加注册你的工厂替换默认 ResourceManagerStringLocalizerFactory 即可。README 中"custom ResourceManagerStringLocalizer"的语义即:保留 key 查找逻辑、替换底层资源读取源。
  2. PO/JSON 等文本化资源源:抽象层不关心资源物理载体,GetString 只需返回 LocalizedString;将资源文件编译为附属程序集的机制(.resx → satellite assembly)仅是默认实现的策略,JSON/PO 实现可绕过它直接从文件读取。
  3. 数据注解/模型验证本地化IStringLocalizer<T> 的强类型入口天然适配数据注解错误消息的翻译,testassets 中 Customer.fr-FR.resx 即演示了模型级资源如何随文化切换(MVC 数据注解集成可参考仓库 src/Localization 之外的 Mvc.DataAnnotations 模块用法,但其文化来源同样依赖本文的请求判定层)。

需要说明的是:原 README 中指向外部仓库(如 Entropy 示例、OrchardCore、hishamco 的 JSON/Session 扩展)的超链接属于仓库外部生态,本文不展开其外部链接内容;理解其设计意图后,用本仓库提供的四个内置 Provider、CustomRequestCultureProviderIStringLocalizerFactory 扩展点即可实现等价能力。

九、端到端可运行样例与验证路径

若要在本地直观体验整套机制,可直接运行仓库样例:

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-NOTREALpp-NOTREAL 这类假文化项,用于演示"未声明文化被 Provider 判定后会被中间件过滤、最终回退到默认文化"的行为。

单元与功能测试是理解边界行为的最佳教材:

十、实践要点小结

  1. 优先级本质是顺序RequestLocalizationOptions 默认链 QueryString → Cookie → Accept-Language,任何自定义 Provider 通过 AddInitialRequestCultureProvider 可升至首位;返回 null 即主动让贤。
  2. 安全与文化归一:中间件只与 SupportedCultures 精确匹配、至多向上回退 5 层父文化;对请求输入绝不盲目构造 CultureInfo,这是防御性设计的源码级体现。
  3. 判定与取值分层:请求文化判定(Microsoft.AspNetCore.Localization)与字符串资源(Microsoft.Extensions.Localization)是解耦的两层,前者产出线程文化,后者按文化消费资源——扩展时不必同时改动两边。
  4. 资源路径三条规则ResourceLocationAttribute > LocalizationOptions.ResourcesPath > 项目根目录;类库场景务必核对程序集名与 RootNamespace 是否一致。
  5. 验证优先:仓库中每个 Provider 都有独立单元测试,动手做自定义扩展前先跑一遍 src/Middleware/Localization/test 下的测试,可快速校准对短路与回退语义的理解。

延伸阅读:结合仓库内 Localization.slnfMiddleware.slnf 可了解工程间的引用边界;文化判定在 MVC/数据注解/视图本地化中的消费方式可对照 Mvc.DataAnnotationsLocalization 相关实现继续深入。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391