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

后端 未结 1 1721
花落未央
花落未央 2020-12-28 20:24

I am trying to override the culture of the current request. I got it working partly using a custom ActionFilterAttribute.



        
相关标签:
1条回答
  • 2020-12-28 20:44

    Update ASP.Net Core 1.1

    A new RouteDataRequestCultureProvider is coming as part of the 1.1 release, which hopefully means you won't have to create your own request provider anymore. You might still find the information here useful (like the routing bits) or you might be interested in creating your own request culture provider.


    You can create 2 routes that will let you access your endpoints with and without a culture segment in the url. Both /api/en-EN/home and /api/home will be routed to the home controller. (So /api/blah/home won't match the route with culture and will get 404 since the blah controller doesn't exists)

    For these routes to work, the one that includes the culture parameter has higher preference and the culture parameter includes a regex:

    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?}");                
    
    });
    

    The above routes will work with MVC style controller, but if you are building a rest interface using wb api style of controllers, the attribute routing is the favored way in MVC 6.

    • One option is to use attribute routing, but use a base class for all your api controllers were you can set the base segments of the url:

      [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" });
          }
      } 
      
    • A quick way of avoiding the base class without needing too much custom code is through the web api compatibility shim:

      • Add the package "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-rc1-final"
      • Add the shim conventions:

        services.AddMvc().AddWebApiConventions();
        
      • Make sure your controllers inherit from ApiController, which is added by the shim package
      • Define the routes including the culture parameter with te MapApiRoute overload:

        routes.MapWebApiRoute("apiLanguage", 
         "api/{language:regex(^[a-z]{{2}}-[A-Z]{{2}}$)}/{controller}/{id?}");
        
        routes.MapWebApiRoute("DefaultApi", 
         "api/{controller}/{id?}");
        
    • The cleaner and better option would be creating and applying your own IApplicationModelConvention which takes care of adding the culture prefix to your attribute routes. This is out of the scope for this question, but I have implemented the idea for this localization article

    Then you need to create a new IRequestCultureProvider that will look at the request url and extract the culture from there (if provided).

    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();
    });
    

    In order to implement the new IRequestCultureProvider you just need to:

    • Search for the culture parameter in the request url path.
    • If no parameter is found, return null. (If all the providers return null, the default culture will be used)
    • If a culture parameter is found, return a new ProviderCultureResult with that culture.
    • The localization middleware will fallback to the default one if it is not one of the supported cultures.

    The implementation will look like:

    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));
        }
    }
    

    Finally enable the localization features including your new provider as the first one in the list of supported providers. As they are evaluated in order and the first one returning a not null result wins, your provider will take precedence and next will come the default ones (query string, cookie and 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"));
    
    0 讨论(0)
提交回复
热议问题