4

I want to override value of an item in the RouteData that named lang based on a user claim in the User.Identity.

For example if the lang value in the route data is en (example.com/en/login), I want read the lang claim of the user and override the lang in the roue data by it. So other links in the login page should follow the new lang value (not en).

Where I set the language:

In my project I set the language of of the UiThread in Application_AuthenticateRequest. So I changed the lang route value there but it seems if has not the expected result (Why?):

requestContext.RouteData.Values["lang"] = langName;

Where should I write the code to override Route values of a request? and What is your suggestion?

Ramin Bateni
  • 16,499
  • 9
  • 69
  • 98
  • you should be able to do that in an `ActionFilter`. Referece [Understanding Action Filters (C#)](https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/controllers-and-routing/understanding-action-filters-cs) – Nkosi Jan 28 '18 at 11:35
  • It seems like a **really bad idea** to take control of the requested language away from the user who is requesting it. This will only result in frustration for those users who want to view the site in one language, but *it doesn't let them* because the language is not the same as what they are requesting in the URL. See [ASP.NET MVC 5 culture in route and url](https://stackoverflow.com/a/32839796/) and [ASP.NET MVC localization by session best choice](https://stackoverflow.com/a/36494941/181087). – NightOwl888 Jan 28 '18 at 11:54
  • @NightOwl888, I have a scenario in my site that I think the user want this change: _Sometimes the user come in the site with a language (not his favorite language for example: `en`) and goes to login and console. Now I check its language from his profile settings (for example `fr`) and set the lang in the route based on the settings. Now if the user want to come out of console with a link from inside the console then he goes to it but with his favorite language._ – Ramin Bateni Jan 28 '18 at 13:23
  • What specifically do you mean by console? Do you mean portal? – NightOwl888 Jan 28 '18 at 14:31
  • 1) The user will normally change the language before they login. 2) If you do want to "help" the user to change their culture when they login, you should do a redirect *one time, from the login action* to the localized URL and let the user change it back using the URL if they want to. But this isn't really helping (see #1). – NightOwl888 Jan 28 '18 at 14:37
  • @NightOwl888, My website has a user panel (console) and public pages (outside of console). It is possible that users come to the site with a link (include `en` in url) from outside of the site (for example from google) and they go fast to the login without change the language of the site to their favorite language (because they want go to the console fast or other reasons).The Console language is based on user setting (not route) but On the other hand inside the console I have several links to the outside of console. **In this case** I want the language of these link be based on user setting. – Ramin Bateni Jan 28 '18 at 14:51
  • @NightOwl888, The `ActionFilter` worked for me but I wrote a custom route drived from `RouteBase` instead of a `ActionFilter` because of your comment under the `Nicky` answer about `ModelBinder`. I decided to write a code like this in side the `GetVirtualPath` method: `if (requestContext.HttpContext.User?.Identity.IsAuthenticated ?? false) requestContext.HttpContext.Request.RequestContext.RouteData.Values["lang"] = getLangFromUserSettings();` and it works. Do you verify it? And is it possible to write an answer base on it to I mark it as accepted answer? – Ramin Bateni Jan 28 '18 at 15:09

2 Answers2

3

Application_AuthenticateRequest is too late in the MVC life cycle to have any effect on route values.

MVC uses value providers to resolve the values in the ModelBinder very early in the request before action filters run, so action filters are not a solution to this.

You basically have a couple of options.

  1. Create your own Route or RouteBase subclass. Routes are responsible for turning the request into route values, so you can put conditional logic here to build your request.
  2. Use a globally registered IAuthorizationFilter. Authorization filters run before the ModelBinder and can therefore affect the values that are passed into models and action methods.

However, as a dissatisfied user of other web sites that take control over the language selection process instead of just letting the user decide based on the URL/selection, I have to protest. You think you are helping the user by doing this, but in fact you are making your web site more frustrating to use.

  1. Most users will enter your site through a search engine or one of your site's marketing channels. In each of these cases, they will automatically enter your site in their own language because they are searching in their own language. At least that is what will happen if you have the culture in the URL and don't use any cookies or session state to block search engines from indexing your localized pages.
  2. Users who are returning through their browser history or bookmarks will also always come back in their own language.
  3. A (usually) very small fraction of users may enter your site by typing www.somesite.com, in which case they won't come in under their own language. They will have no problem recognizing that they are seeing the wrong language and will immediately look for some sort of language dropdown or flag icon to switch to their language. Unless you are using some sort of remember me feature, this step will always happen before they log in.

So, there is little point in "overriding" the culture when the user logs in. They will have already selected the right culture in 99.999% of all cases that reach this stage.

If you still insist on "helping" your users by doing this, don't override the culture from the URL. Put a RedirectToAction on the post portion of your login action to redirect them to a URL with the new culture exactly once and only when logging in. This will allow your (now frustrated) users to be able to override your override and change back to the language they intend to view the site in.

If you give them no option to view the site in the language they want by choosing it in the URL or in some sort of language selector, don't be surprised when they don't return.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    // This doesn't count login failures towards account lockout
    // To enable password failures to trigger account lockout, change to shouldLockout: true
    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            // Change to the culture of the signed in user by replacing the
            // first segment of the URL.
            returnUrl = ChangeCultureInUrl(returnUrl);
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

Again, my recommendation is to just let the user select their culture, but if you still think for some reason this is necessary the above solution is far better than hijacking the culture and taking control from the user.

Community
  • 1
  • 1
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • First I should say to you thank you a lot for your comments and answer and the time that you spend to answer to me and others with these details. I always like to learn of someone like you. Next, _in this case_ you recommend one Redirect to match the language right after login. yes? – Ramin Bateni Jan 28 '18 at 19:03
  • More detail about my project: I use lang in the user settings in console area (console links have not lang section) and for out of console My _first_ criterion is `route` value and _second_ is `cookie` value. This means when the user is in a console page the route value of lang is null and use default lang (en) to make the links to the out of console! In this case I think the user like to see the other pages of the site right with the language that he selected in his settings in the console. So I want custom the links to the out of console with lang based on user language in console. – Ramin Bateni Jan 28 '18 at 19:07
  • 1
    I see. In that case, I would probably put a query string parameter on the console links to identify them as such. The main site would have a cookie that always is set to the selected culture from the URL. This cookie would never be retrieved *unless* the query string parameter is present and then do a redirect to the same page in user's culture (without the query string parameter). That should probably be done in a globally-registered authorization filter that takes place before the `AuthorizeAttribute`. Then all of your console links will work whether the user is logged in or not. – NightOwl888 Jan 28 '18 at 19:16
  • Is your solution need that the console routes have lang section to? We wont lang section in our console URLs. Additional note: the custom route class that I created and mentioned to it in one of my comments under question works now. (I know that we are speaking about _the better way than hijacking the culture_) – Ramin Bateni Jan 28 '18 at 19:16
  • 1
    I mean have a query string parameter `&console=yes` on the links in your console back to your localized site. When the localized site receives a request with that query string param, read the language cookie, and use it to build a URL to redirect the current request to the same page *with* a language and *without* a query string `console=yes`. – NightOwl888 Jan 28 '18 at 19:20
  • OK,I understand the scenario. But I did not update the cookie with the console language (it was synced just with lang value in the route of the last page viewed out of console) and now I think in the new scenario I should sync in with the console language too. In this case I can retrieve the console language from the cookie when the URL has a query string like `console=yes`. Is it possible to write this comment of yours in the answer too? Also please write about checking the query string out of console: _A globally-registered authorization filter that takes place before the AuthorizeAttribute_ – Ramin Bateni Jan 28 '18 at 19:33
0

Create a class that implements IActionFilter, override "lang" value in it, and configure MVC to use that IActionFilter

using System.Web.Mvc;

namespace WebApplication1.Filters
{
    public class LangOverrideFilter : IActionFilter
    {
        public void OnActionExecuted(ActionExecutedContext filterContext)
        {
        }

        public void OnActionExecuting(ActionExecutingContext filterContext)
        {
            filterContext.RouteData.Values["lang"] = "en";
            //Access userclaims through filterContext.HttpContext.User.
        }
    }
}

In App_Start/FilterConfig.cs:

using System.Web;
using System.Web.Mvc;
using WebApplication1.Filters;

namespace WebApplication1
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new LangOverrideFilter());
            filters.Add(new HandleErrorAttribute());
        }
    }
}

My App_Start/RouteConfig.cs for testing:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace WebApplication1
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{lang}/{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}
Nicky
  • 428
  • 3
  • 13
  • 2
    Action filter is **too late** in the request lifecycle to have any effect on the ModelBinder. In order for it to work, the language would have to be set to the requested culture in an [authorization filter](https://stackoverflow.com/a/32839796/181087), and of course, the "override" would need to happen before that (either in the same authorization filter, one globally registered before it, or in a [RouteBase subclass](https://stackoverflow.com/a/31958586/181087)). – NightOwl888 Jan 28 '18 at 11:58