我正在构建一个 .NET 8.0 Razor Page 站点,其中使用如下 URL 进行本地化/国际化:
example.org/us // United States
example.org/fr // France
example.org/pt // Portugal
example.org/se // Sweden
...
example.org/us/contact // contact page for USA (text is displayed in English)
example.org/fr/contact // contact page for France (text is displayed in French)
即URL 包含国家/地区的 ISO 3166-1 alpha-2 代码(例如 us、fr、pt、se 等)。文本来自标准资源文件 (resx)。
我的大部分实现都有效,但我有一个小问题,即不存在的页面返回
StatusCode 200
而不是404
。我用 Google 搜索过,查看了其他示例,但它们要么已过时/不再工作,要么仅适用于 MVC。这个问题是针对使用 @page "/..."
指令的 Razor Pages。
我需要有关实施的帮助和意见。
我所有的 Razor 页面的页面路由中都有
{lang}
。联系页面路径将为 /{lang}/contact
。 {lang}
指 ISO 3166-1 alpha-2 代码。
以下代码将
{lang}
参数添加到 Web 应用程序中的所有页面:
public class CultureTemplatePageRouteModelConvention: IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
foreach (var selector in model.Selectors)
{
var template = selector.AttributeRouteModel.Template;
if (template.StartsWith("MicrosoftIdentity")) continue; // Skip MicrosoftIdentity pages
// Prepend {lang}/ to the page routes allow for route-based localization
selector.AttributeRouteModel.Order = -1;
selector.AttributeRouteModel.Template = AttributeRouteModel.CombineTemplates("{lang}", template);
}
}
}
...在
program.cs
中实例化为:
builder.Services.AddRazorPages(options =>
{
// decorate all page routes with {lang} e.g. @page "/{lang}..."
options.Conventions.Add(new CultureTemplatePageRouteModelConvention());
});
为了设置所请求页面的实际文化,我实现了自定义
RequestCultureProvider
,其中包含以下内容:
public class CustomRouteDataRequestCultureProvider : RequestCultureProvider
{
public SupportedAppLanguages SupportedAppLanguages;
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
var lang = (string)httpContext.GetRouteValue("lang");
var urlCulture = httpContext.Request.Path.Value.Split('/')[1];
string[] container = [lang, urlCulture];
var culture = SupportedAppLanguages.Dict.Values.SingleOrDefault(langInApp => container.Contains(langInApp.Icc) );
if (culture != null)
{
return Task.FromResult(new ProviderCultureResult(culture.Culture));
}
// if no match, return 404
httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
return Task.FromResult(new ProviderCultureResult(Options.DefaultRequestCulture.Culture.TwoLetterISOLanguageName));
}
}
...通过
program.cs
实例化:
// get all languages supported by app via `appsettings.json`:
var supportedAppLanguages = builder.Configuration.GetSection("SupportedAppLanguages").Get<SupportedAppLanguages>();
var supportedCultures = supportedAppLanguages.Dict.Values.Select(langInApp => new CultureInfo(langInApp.Culture)).ToList();
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture = new RequestCulture(culture: "en-us", uiCulture: "en-us");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.FallBackToParentCultures = true;
options.RequestCultureProviders.Clear();
options.RequestCultureProviders.Insert(0, new CustomRouteDataRequestCultureProvider() { Options = options, SupportedAppLanguages = supportedAppLanguages });
});
为了防止用户输入奇怪的区域设置/国家/地区代码(并混淆搜索引擎/防止糟糕的 SEO),我创建了一个 MiddlewareFilter 来处理此类情况:
public class RouteConstraintMiddleware(RequestDelegate next, SupportedAppLanguages supportedAppLanguages)
{
public async Task Invoke(HttpContext context)
{
// The context.Response.StatusCode for pages like example.org/us/non-existing is 200 and not the correct 404.
// How can I implement a proper check?
if (context.Response.StatusCode == StatusCodes.Status404NotFound) return;
if (string.IsNullOrEmpty(context?.GetRouteValue("lang")?.ToString())) return;
// check for a match
var lang = context.GetRouteValue("lang").ToString();
var supported = supportedAppLanguages.Dict.Values.Any(langInApp => lang == langInApp.Icc);
if (!supported)
{
context.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
await next(context);
}
}
应用程序的当前状态:
example.org/us // 200. (correct)
example.org/fr // 200. (correct)
example.org/nonsense/contact // 404. (correct)
example.org/nonsense // 404. (correct)
example.org/us/non-existing // 200. (wrong, must return 404)
我该如何解决这个问题?这也是一个很好的实现吗(对我来说似乎有点笨拙)? 能否通过更好的方式解决?此处提供了简化的 Github 重现:https://github.com/shapeh/TestLocalization
希望大家指点。
问题是由您的
RouteConstraintMiddleware
引起的
这条线会缩短你的中间件pineline
if (string.IsNullOrEmpty(context?.GetRouteValue("lang")?.ToString())) return;
如果没有
await next(context);
,它不会进入下一个中间件,在该中间件中你会得到 404 响应
在我看来,你可以尝试在你的
{lang}
部分添加一个RouteConstraint,默认的Route中间件会为你处理它,这样你就不需要添加另一个中间件
一个最小的例子:
public class CultureConstraint : IRouteConstraint
{
public bool Match(
HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var routeValue))
{
return false;
}
var supportedAppLanguages = httpContext.RequestServices.GetService<IConfiguration>().GetSection("SupportedAppLanguages").Get<SupportedAppLanguages>();
var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
return supportedAppLanguages.Dict.Values.Select(x=>x.Icc).Contains(routeValueString);
}
}
注册约束:
builder.Services.AddRouting(options =>
{
options.LowercaseUrls = true;
options.AppendTrailingSlash = false;
options.ConstraintMap.Add("cultureconstraint", typeof(CultureConstraint));
});
修改
CultureTemplatePageRouteModelConvention
:
public class CultureTemplatePageRouteModelConvention: IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
foreach (var selector in model.Selectors)
{
var template = selector.AttributeRouteModel.Template;
if (template.StartsWith("MicrosoftIdentity")) continue; // Skip MicrosoftIdentity pages
// Prepend {lang}/ to the page routes allow for route-based localization
selector.AttributeRouteModel.Order = -1;
selector.AttributeRouteModel.Template = AttributeRouteModel.CombineTemplates("{lang:cultureconstraint}", template);
}
}
}
结果: