Showing posts with label routes. Show all posts
Showing posts with label routes. Show all posts

Monday, July 9, 2018

Return JWT token when user signs in rails

Leave a Comment

Having an issue with getting JWT token using devise gem and devise-jwt gem. This is how my confirmation looks like.

devise.rb

  Devise.setup do |config|      config.jwt do |jwt|       jwt.secret =  SECRETS.devise_jwt_secret_key       jwt.dispatch_requests = [ ['POST', %r{^/authentication_tokens/create$}] ]     end end  

user.rb

class User < ApplicationRecord      devise :database_authenticatable, :registerable,            :recoverable, :rememberable, :trackable, :validatable,            :jwt_authenticatable, jwt_revocation_strategy: Devise::JWT::RevocationStrategies::Null    end 

authentication_tokens_controller.rb

class Api::V1::AuthenticationTokensController < Devise::SessionsController   include Devise::Controllers::Helpers   skip_before_action :verify_authenticity_token     prepend_before_action :require_no_authentication, only: [:create]    before_action :rewrite_param_names, only: [:create]    def new     render json: { response: "Authentication required" }, status: 401   end    def create     self.resource = warden.authenticate!(auth_options)     sign_in(resource_name, resource)     yield resource if block_given?      render json: {success: true, jwt: current_token, response: "Authentication successful" }   end    private    def rewrite_param_names     request.params[:user] = {email: request.params[:email], password: request.params[:password]}   end    def current_token     request.env['warden-jwt_auth.token']   end  end 

routes.rb

   get 'home#secret'    devise_for :users    resources :tasks    #other routes for the website removed for brevity    namespace :api, defaults: { format: :json } do     namespace :v1 do       resources :users       devise_scope :user do         post '/authentication_tokens/create', to: "authentication_tokens#create"       end     end   end 

For some reason request.env['warden-jwt_auth.token'] returns null all the time, however, the user is authenticated. Is there anything that I need to add to get the JWT token when a user signs in?

Update - routes and namespacing

After days of debugging, I believe I have found the source of my problem. My app has a frontend which uses normal routes. The code above doesn't work however if I do something like the code below. All is good.

  scope :api, defaults: {format: :json} do     devise_for :users, controllers: {sessions: 'v1/authentication_tokens'}   end 

Is there a way of namespacing the devise_for for me API even though it has been used above for the website?

2 Answers

Answers 1

I've briefly looked on your issue and, it's probably wrong, but something for you to give a try:

looking on the following lines

def create   self.resource = warden.authenticate!(auth_options) end  def current_token   request.env['warden-jwt_auth.token'] end 

If you say that user is being authenticated even with nil returned from current_token method, so that means that jwt is passing correctly, but your way of fetching it is wrong.

Try to debug self.resource = warden.authenticate!(auth_options) line and see what contains inside auth_options, probably you can take JWT from there, or you just trying to get warden-jwt_auth.token in a wrong way. Try to debug this line as well and see if you should probably take "warden-jwt_auth.token" from request.headers["warden-jwt_auth.token"], or something like this. Just print out the whole response of your request and search by needed header.

I hope this helps!

Answers 2

You just need to make your route RESTful.

routes.rb

post '/authentication_tokens', to: "authentication_tokens#create" 

devise.rb

config.jwt do |jwt|   jwt.secret =  SECRETS.devise_jwt_secret_key   jwt.dispatch_requests = [ ['POST', %r{^/authentication_tokens$}] ] end 
Read More

Sunday, May 28, 2017

Define Cakephp Route to call specific controller if given action not exists

Leave a Comment

Using CakePHP v3.3.16

I want to write a fallback route in such a way that if URL is not connected to any action then it should go to that fallback.

Created routes for SEO friendly URL like this

   $routes->connect(         ':slug',         ['prefix'=>'website','controller' => 'Brands', 'action' => 'index'],         ['routeClass' => 'DashedRoute']     );     $routes->connect(         ':slug/*',         ['prefix'=>'website','controller' => 'Products', 'action' => 'index'],         ['routeClass' => 'DashedRoute']     ); 

But it's also inclute all the controller actions in it so if i try to call a controller ex: cart/index it's going to website/brands/index/index

If I have to remove exclude it, I have to create a route like this/

$routes->connect('/cart',['controller' => 'Cart'], ['routeClass' => 'DashedRoute']); 

And so on to the other controller to access.

Example: I have a controller CartController action addCart

CASE 1

if I access URL my_project/cart/addCart/ It should go to cart controller action

CASE 2

if I access URL my_project/abc/xyz/ and there is no controller named abc so it should go to BrandsController action index

My Current routes.php looks like this

Router::defaultRouteClass(DashedRoute::class);  Router::scope('/', function (RouteBuilder $routes) {      $routes->connect('/', ['prefix'=>'website','controller' => 'Home', 'action' => 'index']);     $routes->connect('/trending-brands', ['prefix'=>'website','controller' => 'Brands', 'action' => 'trending']);     $routes->connect('/users/:action/*',['prefix'=>'website','controller' => 'Users'], ['routeClass' => 'DashedRoute']);      $routes->connect('/cart',['prefix'=>'website','controller' => 'Cart'], ['routeClass' => 'DashedRoute']);     $routes->connect('/cart/:action/*',['prefix'=>'website','controller' => 'Cart'], ['routeClass' => 'DashedRoute']);      $routes->connect(         ':slug',         ['prefix'=>'website','controller' => 'Brands', 'action' => 'index'],         ['routeClass' => 'DashedRoute']     );     $routes->connect(         ':slug/*',         ['prefix'=>'website','controller' => 'Products', 'action' => 'index'],         ['routeClass' => 'DashedRoute']     );     $routes->connect(':controller', ['prefix'=>'website'], ['routeClass' => 'DashedRoute']);     $routes->connect(':controller/:action/*', ['prefix'=>'website'], ['routeClass' => 'DashedRoute']);       $routes->fallbacks(DashedRoute::class); });  Router::prefix('website', function (RouteBuilder $routes) {     $routes->fallbacks(DashedRoute::class); });   Plugin::routes(); 

2 Answers

Answers 1

Your edits totally invalidate my first answer, so I decided to just post another answer.

What you want to achieve can not be done by the router because there is no way for router to tell if the controller/action exists for a particular route. This is because the router just work with url templates and delegates the task of loading Controllers up in the request stack.

You can emulate this feature though by using a Middleware that check if the request attribute params is set and then validate them as appropriate and changing them to your fallback controller if the target controller doesn't exist.

Just make sure to place the RoutingMiddleware execute before your middleware otherwise you will have no params to check because the routes wouldn't have been parsed.

Middlewares implement one method __invoke().

use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Cake\Http\ControllerFactory; use Cake\Routing\Exception\MissingControllerException;  class _404Middleware {  public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next) {         //$params = (array)$request->getAttribute('params', []);          try {             $factory = new ControllerFactory();             $controller = $factory->create($request, $respond);         }      catch (\Cake\Routing\Exception\MissingControllerException $e) {       // here you fallback to your controllers/action or issue app level redirect       $request = $request->withAttribute('params',['controller' =>'brands','action' => 'index']);       }     return $next($request, $response); }  } 

Then attach in your App\Application see also attaching middlewares

$middlewareStack->add(new _404Middleware()); 

please remmber to clean up the code as I didn't test it under real dev't enviroment.

After thought: if your were looking to create an error page for all not found resources, then you don't need all that, instead you would just customise the error template in Template/Error/error404.ctp.

Answers 2

If I have to remove exclude it, I have to create a route like this/ And so on to the other controller to access.

There is no need to specify every controller/name in the pattern, instead you can create all your specify routes first and then place the following route below them to catch all unmatched routes.

$routes->connect(':/prefix/:controller/:action/*', [], ['routeClass' => 'DashedRoute']); 

And maybe another one without prefix

$routes->connect('/:controller/:action/*', [], ['routeClass' => 'DashedRoute']); 

And since you mentioned that you are using 3.3.* cake version, Your routes can be written taking advatage of routes scoping

Router::prefix('website', function ($routes) {  // All routes here will be prefixed with `/prefix`  // And have the prefix => website route element added.  $routes->fallbacks(DashedRoute::class);    $routes->connect(     '/:slug',     ['controller' => 'Brands', 'action' => 'index'] ); $routes->connect(     ':slug/*',     ['controller' => 'Products', 'action' => 'index']  });  
Read More

Thursday, February 2, 2017

Mapping multiple routes (controller) using a router

Leave a Comment

I am looking at danny vankooten router library here. This looks good (though not sure how will it handle project from medium to large, example an ecommerce site). Now, going by the example this is mapping

$router->map('GET','/', 'home.php', 'home'); $router->map('GET','/home/', 'home.php', 'home-home'); $router->map('GET','/plans/', 'plans.php', 'plans'); $router->map('GET','/about/', 'about.php', 'about'); $router->map('GET','/contact/', 'contact.php', 'contact'); $router->map('GET','/tos/', 'tos.html', 'tos'); 

Let say I have a scenario where my website has 20-30 static pages or near about 50 controllers with 2-3 action/method each.

How do I map them all. If I use the above method of mapping I will probably end up having more than 100 lines of mapping and this does not looks right.

I believe there should be a way or short cuts/wildcard like check if there is a page or controller available,then load it or else throw a 404.

How do I map all the routes the right way?.

PS. Giving out a bounty of 50 to anyone who is willing to answer on how to use the up said router using wildcard to match controller/method.

4 Answers

Answers 1

What you can do to lighten your router file is to move the route definition in a YAML file. You will still have many lines in your YAML but it will be more readable.

In your router.php file, use this code :

Don't forget to add the symfony YAML parser to your composer.json

use Symfony\Component\Yaml\Yaml; $yaml_file = 'routes.yaml'; $routes = Yaml::parse(file_get_contents($yaml_file)); foreach ($routes as $route_name => $params) {     $router->map($params[0],$params[1], $params[2].'#'.$params[3], $route_name); }   // match current request $match = $router->match(); 

Your file routes.yaml will look like this

index:      ["GET", "/", "home_controller", "display_item"] content:    ["GET", "/content/[:parent]/?[:child]?", "content_controller", "display_item"] article:    ["GET", "/article/[:page]", "article_controller", "display_item"] 

An other thing you can do to get smaller files is to separate your route definition in many small YAML files. For example, one for static files, one for admin area, one for front...

To do something like this, you have to change the router.php code to something like this :

use Symfony\Component\Yaml\Yaml; $yaml_files = ['front.yaml', 'static.yaml', 'admin.yaml']; foreach ($yaml_files as $yaml_file) {     $routes = Yaml::parse(file_get_contents($yaml_file));     foreach ($routes as $route_name => $params) {         $router->map($params[0],$params[1], $params[2].'#'.$params[3], $route_name);     }  }  // match current request $match = $router->match(); 

Danny Van Kooten also made PHP-Router which has a built-in support for YAML files. (If you look at the source code, you will see that he uses the Symfony parser so both methods are quite similar)

From the doc

YAML route definition

base_path: /blog  routes:   index: [/index, someClass.indexAction, GET]   contact: [/contact, someClass.contactAction, GET]   about: [/about, someClass.aboutAction, GET] 

Router.php

require __DIR__.'/vendor/autoload.php';  use PHPRouter\RouteCollection; use PHPRouter\Config; use PHPRouter\Router; use PHPRouter\Route;  $config = Config::loadFromFile(__DIR__.'/router.yaml'); $router = Router::parseConfig($config); $router->matchCurrentRequest(); 

Answers 2

in the library there is no function which do something like you wanted but there's an alternative way to do it, is to store those page on database by that you will have something like this below

//your query // "SELECT `method`, `route`, `page`, `name` FROM `static_pages` $pages = (query); //your executed query if(count($pages) > 0){   foreach($pages as $page){     $router->map($page['method'],$page['route'], $page['target'], (($page['name'] !== null || !empty($page['name']) ) ? $page['name'] : null));   } } 

I hope this helps

Answers 3

As part of the bootstrap phase tell your controllers to map themselves onto a router:

// bootstrap $router = new AltoRouter();  FooController::mapRoutes($router); BarController::mapRoutes($router); // etc. 

Each controller is allowed to produce however many routes it needs.

Answers 4

If you use a pre-built router like Silex you can do something like this:

$app->get('{route}', function($route) {     $dir = "/path/to/my/static/files/";     $file = $dir . $route . '.php';     if (file_exists($file)) {         require $file;     } else {         require '/path/to/custom/404.php';     } }); 

Note that Silex and many similar routers require routes to be by order of preference. When I need to use a router like this, I put it at the bottom so that it doesn't accidentally catch anything else.

Note that, if you want to build something like this custom, you can do this - keeping in mind I don't know what your code currently looks like or what logic you need - by substituting out your regular expression (in Silex's example, it is with curly brackets) and checking the $_SERVER['REQUEST_URI'] against your formulated regular expression.

If this is a custom application, you may not really need to over engineer this.

// routing class foreach ($routes as $route) {     if ($route != self::DEFAULT_ROUTE) {         // do your normal routing logic     } else {         // check if file exists (as with code above), else send to error 404     } } 

Then call with something like:

$router->map('GET',Router::DEFAULT_ROUTE, 'legacyRouter.php', 'legacy'); 
Read More

Sunday, January 22, 2017

Play 2.x: How to make an AJAX request with a common button

Leave a Comment

So I have successfully gotten ajax requests to work before but I have always had to use a form, and then at the end of the submit do return false so that it doesnt refresh the page.

I have also just recently moved my javascript into a seperate file this has caused my @ commands to fail. Because of this I do not no how to set my url to my route?

Html:

<button id="saveAsDefaultButton">Save as default</button> 

Playframework java code:

public static Result saveDefaultPhoneForUser(String handset) {     User currentUser = User.findByName(session("name"));     currentUser.lastControlledHandset = theHandset;     currentUser.save();     return ok(); } 

routes:

POST    /                           controllers.Application.saveDefaultPhoneForUser(handset : String) 

javascript:

$('#saveAsDefaultButton').click(function(evt) {         $('#errors').hide();         $.ajax({             type : 'POST',             url : "controllers.Application.saveDefaultPhoneForUser",             data : $('#controlledPhone option:selected').text(),             dataType : "text",             success : function(data) {                 //setError('Call succedded');                 //$('#test1').attr("src", data)             },             error : function(data) {                 setError('Make call failed');             }         });         return false;     }); 

Im sure there is a way to do this I am just having no luck finding anything. Any help greatly appreaciated.

5 Answers

Answers 1

For this job you should go with javascriptRoutes as it generates correct JS paths based on your routes.conf. You'll find usage sample in Zentask sample

Anyway, for now you can fix your AJAX call by changing the url to

url : '@routes.Application.saveDefaultPhoneForUser()', 

This way requires it to place the whole JS in template, which is wrong. It can or even should be moved to separate JS file and to make it possible you need to use javascriptRoutes.

More...

javascriptRoutes are not described yet in official documentation, but here's step-by-step introduction to it. Although the description looks sophisticated de facto using this way brings a lot of benefits.

1. Create the common routes

First you need to create common routes in conf/routes file:

GET     /item/:id     controllers.Application.getItem(id: Long) POST    /item/new     controllers.Application.newItem PUT     /item/:id     controllers.Application.updateItem(id: Long) 

Of course, you need to create at least these three actions in Application controller:

  • getItem(Long id){ ... }
  • newItem() { ... }
  • updateItem(Long id) { ... }

2. Create an action translating common routes to JS

  • place it somewhere, ie. in your Application controller
  • Let's call it javascriptRoutes()

In that action you'll point the existing routes from the conf/routes file

public static Result javascriptRoutes() {     response().setContentType("text/javascript");     return ok(         Routes.javascriptRouter("myJsRoutes",             routes.javascript.Application.getItem(),             routes.javascript.Application.newItem(),             routes.javascript.Application.updateItem(),             //inside somepackage             controllers.somepackage.routes.javascript.Application.updateItem()         )     ); } 

Note: Don't set any params in brackets.

3. Create a route for javascriptRoutes action and include it in your template

Route conf/routes

GET     /javascriptRoutes     controllers.Application.javascriptRoutes 

View in <head> part of /views/main.scala.html

<script type="text/javascript" src='@routes.Application.javascriptRoutes()'></script> 

4. Use javascriptRoutes where you want

Up from now you can use routes in JS to get the correct path without need to specify the url and type. For an example instead of:

 $('.getAjaxForThisContainer').click(function(e) {     var idToGet = $("#someField").val();     $.ajax({         type : 'GET',         url : '@routes.Application.getItem()',         data : {             id: idToGet         },         success : function(data) {             // ... some code on success         }     });     return false; }); 

you can use simplified version (myJsRoutes from point 2):

myJsRoutes.controllers.Application.getItem(idToGet).ajax({     success : function(data) { ... some code ... } }); 

or

myJsRoutes.controllers.Application.newItem().ajax({     success : function(data) { ... some code ... } }); 

etc...

  • you don't need to specify type: "POST" - JS router will use correct method according to conf/routes rule
  • you can set id of the record (or other params) to GET or PUT (or other methods) using routes-like syntax in pure JS
  • If your route rule contains all required params you can really minimize yours JS:

for route:

GET   /some/:a/:b/:c    controllers.Application.getABC(a: String, b: Integer, c: String) 

JS:

myJsRoutes.controllers.Application.getABC("a", 1, "b" ).ajax({}); 

Answers 2

Something to watch out for... when calling

Routes.javascriptRouter("myJsRoutes", 

Routes is imported from play.Routes, not play.api.Routes

Had an error when I implemented this code, because I assumed it would came from api (clearly I was wrong). Other than that, everything works great~!!

Answers 3

I like "old good" simple way:

1 In the page, with JQuery,

inside some even-handler

 $.get('/giveme', {'arg': value}, function(data) {                                      window.alert(data);                                    }  ); 

2 On the server/play side

2.1 Routes: GET /giveme controllers.Application.giveme(arg)

2.2 scala action:

object Application extends Controller {         def giveme(arg:String) = Action {         Ok(Json.toJson("hello," + arg ))       }      } 

Answers 4

There is a simpler solution, use Embedded router to generating Javascript router. You can generate Javascript router using @javascriptRouter helper directive, place the following code inside your html page head tag (maybe inside the main decorating template)

<head>     <title>Saeed Title</title>     <link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">     <link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")">     <script src="@routes.Assets.at("javascripts/jquery/v1.12.4/jquery.min.js")" type="text/javascript"></script>     @helper.javascriptRouter("jsRoutes")(         routes.javascript.MyController.getById,         routes.javascript.MyController.getByName     ) </head> 

Don't worry about syntax error (in Intellij) and don't use parenthesis after your action name. Now you can use jsRoutes.controllers.MyController.getById(id) in your ajax call code.

$.ajax(jsRoutes.controllers.MyController.getById(id))    .done( /*...*/ )    .fail( /*...*/ ); 

or use jsRoutes.controllers.MyController.getById(id).url to get url. more information.

Answers 5

Marcus' answer is very good, so anyone else having this issue should be able to use this.

I did have one issue though when trying to get this to work which took me a little while to get working. When I was making my ajax request it was routing to the wrong method.

This was because in my routes file I had the following:

POST    /                           controllers.Application.makeCall() POST    /                           controllers.Application.saveDefaultPhoneForUser(handset : String) 

By having two post methods with the same location /. It seemed to always go to make call. So just a tip for anyone don't do this otherwise it wont work.

I just needed to change it to:

POST    /                           controllers.Application.makeCall() POST    /saveDefaultPhoneForUser    controllers.Application.saveDefaultPhoneForUser(handset : String) 

Hope this helps someone.

Read More

Thursday, March 17, 2016

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

Leave a Comment

I am trying to override the culture of the current request. I got it working partly using a custom 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);     } } 

On the controller I use the LanguageActionFilter.

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

This works so far, but I have two issues with it:

  1. I don't like having to declare {culture} on every controller, as I am going to need it on every route.
  2. I having a default culture doesn't work with this approach, even if I declare it as [Route("api/{culture=en-US}/[controller]")] for obvious reasons.

Setting a default route results is not working neither.

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

I also investigated in a custom IRequestCultureProvider implementation and add it to the UseRequestLocalization method like

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

but then I don't have access to the routes there (I assume cause the routes are done later in the pipeline). Of course I could also try to parse the requested url. And I don't even know if I could change the route at this place so it would match the above route with the culture in it.

Passing the culture via query parameter or changing the order of the parameters inside the route is not an option.

Both urls api/en-us/products as we as api/products should route to the same controller, where the former don't change the culture.

The order in which the culture will be determined should be

  1. If defined in url, take it
  2. If not defined in url, check query string and use that
  3. If not defined in query, check cookies
  4. If not defined in cookie, use Accept-Language header.

2-4 is done via UseRequestLocalization and that works. Also I don't like the current approach having to add two attributes to each controller ({culture} in route and the [ServiceFilter(typeof(LanguageActionFilter))]).

Edit: I also like to limit the number of valid locales to the one set in SupportedCultures property of the RequestLocalizationOptions passed to the UseRequestLocalization.

IOptions<RequestLocalizationOptions> localizationOptions in the LanguageActionFilter above doesn't work as it returns a new instance of RequestLocalizationOptions where SupportedCultures is always null and not the one passed to the.

FWIW it's an RESTful WebApi project.

1 Answers

Answers 1

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

The other options lets you use the route table approach for the api controllers, using 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?}"); 

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

Right now, you don't have access to the route data inside the IRequestCultureProvider, you only have access to the HttpContext. This means you will neede to manually extract the culture parameter from the url path.

However in RC2 a new extension method HttpContext.GetRouteData has been added that lets you get the route data from the httpContext (internally this is done via adding a new http context feature IRoutingFeature after finding a route match but before running the route handler, see the udpated router middleware)

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

  • Search for the culture parameter in the request url path. (Remember in RC2 you can get access to the route data, otherwise you will need to manually extract the parameter)
  • 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 wont be needed in RC2 since an extension method GetRouteData has been added to HttpContext         //which means we could just get the the "language" parameter from the route data         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 Controllers.UrlCultureProvider());  //Add request localization middleware app.UseRequestLocalization(localizationOptions, new RequestCulture("en-US")); 
Read More