Ramin Bateni
Ramin Bateni

Reputation: 17415

Where should I write the code to override Route data values of a MVC Request?

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?

Upvotes: 4

Views: 2632

Answers (2)

NightOwl888
NightOwl888

Reputation: 56869

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.

Upvotes: 3

Nicky
Nicky

Reputation: 468

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

Upvotes: 0

Related Questions