在 ASP.NET 核心 1.0 应用程序中可以选择通过 url/route 覆盖请求文化

Optionally override request culture via url/route in an ASP.NET Core 1.0 Application

我正在尝试覆盖当前请求的区域性。我使用自定义 ActionFilterAttribute.

使其部分工作
public sealed class LanguageActionFilter : ActionFilterAttribute
{
    private readonly ILogger logger;
    private readonly IOptions<RequestLocalizationOptions> localizationOptions;

    public LanguageActionFilter(ILoggerFactory loggerFactory, IOptions<RequestLocalizationOptions> options)
    {
        if (loggerFactory == null)
            throw new ArgumentNullException(nameof(loggerFactory));

        if (options == null)
            throw new ArgumentNullException(nameof(options));

        logger = loggerFactory.CreateLogger(nameof(LanguageActionFilter));
        localizationOptions = options;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        string culture = context.RouteData.Values["culture"]?.ToString();

        if (!string.IsNullOrWhiteSpace(culture))
        {
            logger.LogInformation($"Setting the culture from the URL: {culture}");

#if DNX46
            System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
            System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
#else
            CultureInfo.CurrentCulture = new CultureInfo(culture);
            CultureInfo.CurrentUICulture = new CultureInfo(culture);
#endif
        }

        base.OnActionExecuting(context);
    }
}

在控制器上我使用 LanguageActionFilter

[ServiceFilter(typeof(LanguageActionFilter))]
[Route("api/{culture}/[controller]")]
public class ProductsController : Controller
{
    ...
}

到目前为止这有效,但我有两个问题:

  1. 我不喜欢在每个控制器上都声明 {culture},因为我将在每条路线上都需要它。
  2. 我有一个默认文化不适用于这种方法,即使我出于显而易见的原因将其声明为 [Route("api/{culture=en-US}/[controller]")]

设置默认路由结果也不起作用。

app.UseMvc( routes =>
{
    routes.MapRoute(
        name: "DefaultRoute",
        template: "api/{culture=en-US}/{controller}"
    );
});

我还研究了自定义 IRequestCultureProvider 实现并将其添加到 UseRequestLocalization 方法中,例如

app.UseRequestLocalization(new RequestLocalizationOptions
{
    RequestCultureProviders = new List<IRequestCultureProvider>
    {
        new UrlCultureProvider()
    },
    SupportedCultures = new List<CultureInfo>
    {
        new CultureInfo("de-de"),
        new CultureInfo("en-us"),
        new CultureInfo("en-gb")
    },
    SupportedUICultures = new List<CultureInfo>
    {
        new CultureInfo("de-de"),
        new CultureInfo("en-us"),
        new CultureInfo("en-gb")
    }
}, new RequestCulture("en-US"));

但是我无法访问那里的路由(我假设是因为路由稍后在管道中完成)。当然我也可以尝试解析请求的url。而且我什至不知道我是否可以在这个地方改变路线,使其与上述路线与其中的文化相匹配。

通过查询参数传递区域性或更改路由内参数的顺序不是一个选项。

url 和 api/en-us/productsapi/products 都应该路由到同一个控制器,前者不会改变文化。

确定文化的顺序应该是

  1. 如果在url中有定义,取
  2. 如果未在 url 中定义,请检查查询字符串并使用它
  3. 如果查询中没有定义,检查cookies
  4. 如果cookie中没有定义,使用Accept-Language header。

2-4 通过 UseRequestLocalization 完成并且有效。此外,我不喜欢当前的方法必须向每个控制器添加两个属性({culture} 在路由和 [ServiceFilter(typeof(LanguageActionFilter))])。

编辑: 我还想将有效语言环境的数量限制为传递给 UseRequestLocalizationSupportedCultures 属性 中设置的 RequestLocalizationOptions

上面 LanguageActionFilter 中的

IOptions<RequestLocalizationOptions> localizationOptions 不起作用,因为它 returns RequestLocalizationOptions 的新实例,其中 SupportedCultures 总是 null 而不是传递给的那个。

FWIW 这是一个 RESTful WebApi 项目。

更新ASP.Net核心1.1

新的 RouteDataRequestCultureProvider 将作为 1.1 release 的一部分出现,希望这意味着您将不必再创建自己的请求提供程序。您可能仍然会发现此处的信息很有用(例如路由位),或者您可能有兴趣创建自己的请求文化提供者。


您可以创建 2 条路由,让您可以在 url 中使用和不使用文化段访问您的端点。 /api/en-EN/home/api/home 都将被路由到家庭控制器。 (因此 /api/blah/home 不会将路由与文化相匹配,并且会得到 404,因为 blah 控制器不存在)

对于这些起作用的路由,包含 culture 参数的路由具有更高的优先级并且 culture 参数包含一个正则表达式:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "apiCulture",
        template: "api/{culture:regex(^[a-z]{{2}}-[A-Z]{{2}}$)}/{controller}/{action=Index}/{id?}");

    routes.MapRoute(
        name: "defaultApi",
        template: "api/{controller}/{action=Index}/{id?}");                

});

以上路由适用于 MVC 风格的控制器,但如果您使用 wb api 风格的控制器构建休息界面,属性路由是 MVC 6 中的首选方式。

  • 一个选项是使用属性路由,但是如果您可以设置 url 的基段,则对所有 api 控制器使用基 class :

    [Route("api/{language:regex(^[[a-z]]{{2}}-[[A-Z]]{{2}}$)}/[controller]")]
    [Route("api/[controller]")]
    public class BaseApiController: Controller
    {
    }
    
    public class ProductController : BaseApiController
    {
        //This will bind to /api/product/1 and /api/en-EN/product/1
        [HttpGet("{id}")]
        public IActionResult GetById(string id)
        {
            return new ObjectResult(new { foo = "bar" });
        }
    } 
    
  • 一种不需要太多自定义代码就可以避免基数 class 的快速方法是通过 web api compatibility shim:

    • 添加包"Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-rc1-final"
    • 添加 shim 约定:

      services.AddMvc().AddWebApiConventions();
      
    • 确保您的控制器继承自 ApiController,这是由 shim 包添加的
    • 使用 te MapApiRoute 重载定义包含 culture 参数的路由:

      routes.MapWebApiRoute("apiLanguage", 
       "api/{language:regex(^[a-z]{{2}}-[A-Z]{{2}}$)}/{controller}/{id?}");
      
      routes.MapWebApiRoute("DefaultApi", 
       "api/{controller}/{id?}");
      
  • 更清晰和更好的选择是创建和应用您自己的 IApplicationModelConvention,它负责将文化前缀添加到您的属性路由。这超出了这个问题的范围,但我已经实现了这个 localization article

  • 的想法

然后您需要创建一个新的 IRequestCultureProvider 来查看请求 url 并从那里提取文化(如果提供)。

Once you upgrade to ASP .Net Core 1.1 you might avoid manually parsing the request url and extract the culture segment.

I have checked the implementation of RouteDataRequestCultureProvider in ASP.Net Core 1.1, and they use an HttpContext extension method GetRouteValue(string) for getting url segments inside the request provider:

culture = httpContext.GetRouteValue(RouteDataStringKey)?.ToString();

However I suspect (I haven't had a chance to try it yet) that this would only work when adding middleware as MVC filters. That way your middleware runs after the Routing middleware, which is the one adding the IRoutingFeature into the HttpContext. As a quick test, adding the following middleware before UseMvc will get you no route data:

app.Use(async (context, next) =>
{
    //always null
    var routeData = context.GetRouteData();
    await next();
});

为了实施新的 IRequestCultureProvider 您只需要:

  • 在请求 url 路径中搜索文化参数。
  • 如果没有找到参数,return null。 (如果所有提供者return null,将使用默认文化)
  • 如果找到文化参数,return 具有该文化的新 ProviderCultureResult。
  • 如果 localization middleware 不是受支持的文化之一,它将回退到默认文化。

实施将如下所示:

public class UrlCultureProvider : IRequestCultureProvider
{
    public Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
    {
        var url = httpContext.Request.Path;

        //Quick and dirty parsing of language from url path, which looks like "/api/de-DE/home"
        //This could be skipped after 1.1 if using the middleware as an MVC filter
        //since you will be able to just call the method GetRouteValue("culture") 
        //on the HttpContext and retrieve the route value
        var parts = httpContext.Request.Path.Value.Split('/');
        if (parts.Length < 3)
        {
            return Task.FromResult<ProviderCultureResult>(null);
        }
        var hasCulture = Regex.IsMatch(parts[2], @"^[a-z]{2}-[A-Z]{2}$");
        if (!hasCulture)
        {
            return Task.FromResult<ProviderCultureResult>(null);
        }

        var culture = parts[2];
        return Task.FromResult(new ProviderCultureResult(culture));
    }
}

最后启用本地化功能,包括将您的新提供商作为受支持提供商列表中的第一个提供商。由于它们是按顺序评估的,第一个 return 非空结果获胜,您的提供者将优先,下一个将是 the default ones(查询字符串、cookie 和 header)。

var localizationOptions = new RequestLocalizationOptions
{
    SupportedCultures = new List<CultureInfo>
    {
        new CultureInfo("de-DE"),
        new CultureInfo("en-US"),
        new CultureInfo("en-GB")
    },
    SupportedUICultures = new List<CultureInfo>
    {
        new CultureInfo("de-DE"),
        new CultureInfo("en-US"),
        new CultureInfo("en-GB")
    }
};
//Insert this at the beginning of the list since providers are evaluated in order until one returns a not null result
localizationOptions.RequestCultureProviders.Insert(0, new UrlCultureProvider());

//Add request localization middleware
app.UseRequestLocalization(localizationOptions, new RequestCulture("en-US"));