4

I'm new to MVC (5). In order to add localization support to my website I added a "Language" field to my ApplicationUser : IdentityUser

What's the best approach to now store this information in the browser and ensure that it gets re-created even if the user manually deletes it?


TL; but I've got time

What I've tried until now:

I started creating a cookie in my method private async Task SignInAsync(ApplicationUser user, bool isPersistent) but I notice that:

  1. This method is not used if the user is already authenticated and automatically logs in using the .Aspnet.Applicationcookie and my language cookie could be meanwhile expired (or been deleted).

  2. A user could manually delete the cookie, just for fun.

I thought about checking its existence in the controller (querying the logged user and getting it from the db) and it works but I'd need to do it in EVERY controller. I'm not sure is the correct way to do this.

Any suggestion about how to approach this problem and guarantee that the application has a valid "language cookie" on every request?

Vland
  • 4,151
  • 2
  • 32
  • 43
  • There are loads of ways of solving this problem, I have a couple of questions I need clarified before I answer - 1. What do you actually do with the cookie when it's read in? 2. How is the language initially set by the user? – James Feb 11 '14 at 13:07
  • 1. I'd use that language cookie value to load the correct localization and globalization settings, before rendering the views. 2. I provide a default language for every new user since the field is not nullable – Vland Feb 11 '14 at 13:27
  • so you don't actually store this on the server-side? It's purely a client-side thing? I wonder why you need to add a property to `ApplicationUser` at all here. – James Feb 11 '14 at 13:34
  • I store it in my db, inside my `Users` table. The user can change his language if he wants too. I just don't like the idea of querying the db everytime I want to know that value and I read that creating a server side global variable is bad pratice, so I thought about putting it in a cookie. – Vland Feb 11 '14 at 13:50
  • using a cookie for this sort of thing is perfectly fine, my argument was if you are using a cookie why bother with the server-side storage at all. However, I can see why you might want to do that (i.e. persisting state across multiple machines). This then leads me to the other end of the spectrum - why bother with a cookie if it's loaded with the users profile? You need to load the user on every request *anyway* so why not just load the language along with it? – James Feb 11 '14 at 13:53
  • @James - Why would he need to load the user on every request? The authentication ticket contains the user name. The only checks that may be happening is a role check on certain action methods. Else, he may be pulling completely unrelated data and only checking that the user is authenticated (which is a cookie task and not a db task on each request). – Tommy Feb 11 '14 at 14:09
  • @James 1. AFAIK I don't have that information in my profile unless I specifically query the database and retrieve it. In my `HttpContext.User.Identity` I can only get the `Name` and `Id`. 2. the "language problem" is just an example. cookies can be used for anything and the questions about their existance-check remain – Vland Feb 11 '14 at 14:10
  • @Tommy I was assuming that the OP was loading the users profile *anyway*, their comment now confirms that they aren't. Just trying to get enough information before I posted an answer. – James Feb 11 '14 at 14:11
  • @James I could easily create some kind of global variable but as I said, I read that it's **bad** practice. I'm new to web development so I assume it is and trying to avoid it... – Vland Feb 11 '14 at 14:12
  • @James - gotcha. I have seen the misconceptions many times on SO that the db is checked on each request for checking authorization, wanted to make sure we werent going down that route :) – Tommy Feb 11 '14 at 14:12
  • @Vland if by global you mean `static`, then yeah it's a *really* bad idea. – James Feb 11 '14 at 14:14
  • @James yes, I meant static ;) – Vland Feb 11 '14 at 14:15

2 Answers2

6

It sounds to me like what you want here is a Custom Action Filter. You can override the OnActionExecuting method which means the logic is run before any action is called

public class EnsureLanguagePreferenceAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var langCookie = filterContext.HttpContext.Request.Cookies["LanguagePref"];
        if (langCookie == null)
        {
            // cookie doesn't exist, either pull preferred lang from user profile
            // or just setup a cookie with the default language
            langCookie = new HttpCookie("LanguagePref", "en-gb");
            filterContext.HttpContext.Request.Cookies.Add(langCookie);
        }
        // do something with langCookie
        base.OnActionExecuting(filterContext);
    }
}

Then register your attribute globally so it just becomes the default behaviour on every controller action

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
    filters.Add(new EnsureLanguagePreferenceAttribute());
}
James
  • 80,725
  • 18
  • 167
  • 237
2

To me, the easiest way would be to create your own Authorize attribute (since your language options are tied to an authenticated user account). Inside of your new authorize attribute, simply perform the check if the cookie exists. If it does, then life is good. Else, query the user's database profile and reissue the cookie with the stored value

public class MyAuthorization : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
         //no point in cookie checking if they are not authorized
        if(!base.AuthorizeCore(httpContext)) return false;
        var cookie = httpContext.Request.Cookies["LanguageCookie"];
        if (cookie == null) {
           CreateNewCookieMethod();
        }
        return true;
    }
}

To use, replace [Authorize] with [MyAuthorization] in your project.

If you don't want to mess with the [Authorize] attribute, you could create your own attribute that does the cookie checking and decorate your controller with that one as well.

One last alternative is to create your own Controller class that does the checking on the OnActionExecuting.

public class MyBaseController : Controller
{
    public string Language {get;set;}

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    var cookie = filterContext.HttpContext.Request.Cookies["LanguageCookie"];
    if(cookie == null){
       cookie = CreateNewCookieMethod();
       filterContext.HttpContext.Request.Cookies.Add(cookie);
    }
    Language = cookie.Value;
    base.OnActionExecuting(filterContext);
}

How to use (note that we inherit from MybaseController now)

public class HomeController : MyBaseController{
    public ActionResult Index(){
      //Language comes from the base controller class
      ViewBag.Language = Language;
      Return View();
        }

}

This method is neat because now that Language variable will be available in any controller that inherits from this new class.

Either of these will give you a single, cookie checking point. Additionally, you are only going back to the database only in the instance that the cookie does not exist.

Tommy
  • 39,592
  • 10
  • 90
  • 121
  • this means that basically every Controller (that needs to check the language) needs this Authorize attribute? – Vland Feb 11 '14 at 14:14
  • @Vland - if the language is part of a user row in your DB, then you are most likely already keeping those controllers under authorized (my assumption). If no, then my second option is to create your own DataAnnotation that checks that cookie. – Tommy Feb 11 '14 at 14:16
  • @Vland - proposed a secondary alternative – Tommy Feb 11 '14 at 14:24
  • thanks for the custom Authorize attribute example, it can be useful for other purposes – Vland Feb 11 '14 at 15:23
  • @Vland - no problem. I hope that between James and I, we have shown that there are a multitude of ways in the MVC framework to accomplish your goals without having to repeat code. – Tommy Feb 12 '14 at 04:58
  • @Tommy to ensure the cookie always exists I think that creating the base controller is a better approach than creating a new attribute +1 for you – vcRobe Jan 20 '16 at 15:44