1

I need to set user-specific culture for every web request sent to my web application written using ServiceStack 3 and MVC 4.

Each user's culture is stored in their profile in the database, which I retrieve into my own implementation of IAuthSession using a custom auth provider derived from CredentialsAuthProvider. So I don't care about the browser's AcceptLanguage header and instead want to set the current thread's culture to the Culture property of the auth session right after ServiceStack resolves it from the cache. This has to happen for both ServiceStack services and MVC controllers (derived from ServiceStackController).

What's the best way to accomplish the above?

UPDATE 1

I have found a way to do this, although I'm not convinced that this is the optimal solution.

In my base service class from which all services derive I overrode the SessionAs<> property as follows:

protected override TUserSession SessionAs<TUserSession>()
{
    var genericUserSession = base.SessionAs<TUserSession>();

    var userAuthSession = genericUserSession as UserAuthSession;
    if (userAuthSession != null && !String.IsNullOrWhiteSpace(userAuthSession.LanguageCode))
        System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(userAuthSession.LanguageCode);

    return genericUserSession;
}

where UserAuthSession is my custom implementation of ServiceStack's IAuthSession. Its LanguageCode property is set at login time to the user's chosen ISO culture code stored in the user's profile in the database.

Similarly, in my base controller class from which all my controllers derive I overrode the AuthSession property like so:

public override IAuthSession AuthSession
{
    get
    {
        var userAuthSession = base.AuthSession as UserAuthSession;
        if (userAuthSession != null && !String.IsNullOrWhiteSpace(userAuthSession.LanguageCode))
            System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(userAuthSession.LanguageCode);
        return userAuthSession;
    }
}

This seems to work fine because these two properties are used consistently whenever a service is invoked or a controller action is executed, so the current thread's culture gets set before any downstream logic is executed.

If anyone can think of a better approach please let me know.

UPDATE 2

Based on Scott's suggestion I created a custom AuthenticateAndSetCultureAttribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class AuthenticateAndSetCultureAttribute : AuthenticateAttribute
{
    public AuthenticateAndSetCultureAttribute() : base() { }
    public AuthenticateAndSetCultureAttribute(ApplyTo applyTo) : base(applyTo) { }
    public AuthenticateAndSetCultureAttribute(string provider) : base(provider) { }
    public AuthenticateAndSetCultureAttribute(ApplyTo applyTo, string provider) : base(applyTo, provider) { }

    public override void Execute(IHttpRequest req, IHttpResponse res, object requestDto)
    {
        base.Execute(req, res, requestDto);

        var session = req.GetSession() as UserAuthSession;
        if (session != null && session.IsAuthenticated && !String.IsNullOrWhiteSpace(session.LanguageCode))
            System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(session.LanguageCode);
    }
}

Because I only change the culture when the user is authenticated, it makes sense (in my mind anyways) to do it in the same place where we check for authentication.

I then decorated all my SS services and MVC controllers with this attribute instead of the original [Authenticate].

Now when a SS service is called the attribute's Execute method is executed, and the culture gets correctly set. However, Execute never gets executed when an MVC controller action is invoked, which is really puzzling because how then does MVC+SS know to redirect unauthenticated requests to the login page.

Any thoughts, anybody?

Scott
  • 21,211
  • 8
  • 65
  • 72
Caspian Canuck
  • 1,460
  • 1
  • 13
  • 19

2 Answers2

3

I would do this using a RequestFilter rather than overriding the SessionAs<T>. In your AppHost Configure method:

public override void Configure(Container container)
{
    RequestFilters.Add((httpReq, httpResp, requestDto) => {
        var session = httpReq.GetSession() as UserAuthSession;
        if(session == null || !session.IsAuthenticated || String.IsNullOrWhiteSpace(session.LanguageCode))
            return;

        System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(session.LanguageCode);
    });
}
Scott
  • 21,211
  • 8
  • 65
  • 72
  • Thanks for the suggestion! I took it a step further and created a custom authentication attribute (see my second update above). It works great for SS services but there's still a problem with MVC controllers. – Caspian Canuck Jun 11 '14 at 20:39
1

I ended up creating a custom MVC action filter that sets the request thread's culture based on the authenticated user's settings:

public class SetUserCultureAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);

        var baseController = filterContext.Controller as BaseController;
        if (baseController == null) return;

        var userAuthSession = baseController.UserAuthSession;
        if (userAuthSession != null && userAuthSession.IsAuthenticated && !String.IsNullOrWhiteSpace(userAuthSession.LanguageCode))
            System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(userAuthSession.LanguageCode);
    }
}

Then I decorated my BaseController class with this attribute, and secured my controllers/actions with ServiceStack's regular Authorize attribute.

The previously created AuthenticateAndSetCultureAttribute that I originally intended to work for both controllers and services now is used for SS services only.

The culture is getting set correctly on both the MVC and the SS side, so I'm happy!

Scott
  • 21,211
  • 8
  • 65
  • 72
Caspian Canuck
  • 1,460
  • 1
  • 13
  • 19