I have an ASP.NET MVC 4 application that is targeting .NET Framework 4.7.1, with the problem that the culture is not shared between Controller and View, if the action contains async calls.
I am referencing the NuGet package Microsoft.AspNet.Mvc
5.2.3 (and can be reproduced in 5.2.4).
This is the code in the Controller:
public class CulturesTestController : Controller
{
public async Task<ActionResult> Index(string value)
{
Thread.CurrentThread.CurrentCulture =
CultureInfo.GetCultureInfo("fi-FI");
Thread.CurrentThread.CurrentUICulture =
CultureInfo.GetCultureInfo("fi-FI");
var model = new CulturesContainer
{
CurrentCulture = Thread.CurrentThread.CurrentCulture,
CurrentUICulture = Thread.CurrentThread.CurrentUICulture,
CurrentThreadId = Thread.CurrentThread.ManagedThreadId
};
Log.Write(Level.Info, "CurrentUICulture - Before Await - " +
"CurrentCulture: " +
$"{Thread.CurrentThread.CurrentCulture}, " +
"CurrentUICulture: "
${Thread.CurrentThread.CurrentUICulture} -> "+
"ThreadId: " +
$"{Thread.CurrentThread.ManagedThreadId}");
await GetAwait();
Log.Write(Level.Info, "CurrentUICulture - After Await - " +
"CurrentCulture: " +
$"{Thread.CurrentThread.CurrentCulture}, " +
"CurrentUICulture: " +
$"{Thread.CurrentThread.CurrentUICulture} -> " +
"ThreadId: " +
$"{Thread.CurrentThread.ManagedThreadId}");
return View("Index", model);
}
public class CulturesContainer
{
public CultureInfo CurrentCulture { get; set; }
public int CurrentThreadId { get; set; }
public CultureInfo CurrentUICulture { get; set; }
}
private async Task GetAwait()
{
await Task.Yield();
}
}
And this is the code in View:
@using System.Globalization
@using System.Threading
@model CultureTest.Controllers.CulturesTestController.CulturesContainer
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>title</title>
</head>
<body>
<div>
InitialCurrentCulture = {
<label>@Html.Raw(Model.CurrentCulture)</label>
} --
InitialCurrentUICulture = {
<label>@Html.Raw(Model.CurrentUICulture)</label>
} --
InitialThreadId = {
<label>@Html.Raw(Model.CurrentThreadId)</label>
}
<br />
ActualCurrentCulture = {
<label>@Html.Raw(CultureInfo.CurrentCulture)</label>
} --
ActualCurrentUICulture = {
<label>@Html.Raw(CultureInfo.CurrentUICulture)</label>
} --
ActualThreadId = {
<label>@Html.Raw(Thread.CurrentThread.ManagedThreadId)</label>
}
</div>
</body>
</html>
The logs are the following:
20180320-12:04:25.357-12 -INFO -CulturesTestController+<Index>d__0:
CurrentUICulture - Before Await -
CurrentCulture: fi-FI, CurrentUICulture: fi-FI -> ThreadId: 12
20180320-12:04:25.357-8 -INFO -CulturesTestController+<Index>d__0:
CurrentUICulture - After Await -
CurrentCulture: fi-FI, CurrentUICulture: fi-FI -> ThreadId: 8
While the web page shows:
InitialCurrentCulture = { fi-FI } --
InitialCurrentUICulture = { fi-FI } -- InitialThreadId = { 12 }
ActualCurrentCulture = { en-US } --
ActualCurrentUICulture = { en-US } -- ActualThreadId = { 9 }
There was an issue in .NET Framework < 4.6 that was fixed in 4.6, but it seems that the problem still persists in MVC.
If the thread is a thread pool thread that is executing a task-based asynchronous operation and the app targets the .NET Framework 4.6 or a later version of the .NET Framework, its UI culture is determined by the UI culture of the calling thread. (Source https://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.currentuiculture(v=vs.110).aspx)
Am I not correctly handling the CurrentCulture
, or this is how it is supposed to work? I couldn't find any post related to this issue, for .NET Framework 4.x.
You are not. The culture of the current UI thread needs to be set before going into your async action method. If you set the culture while you are inside of an async method, it has no effect on the UI thread (as you have discovered).
But async problems aside, it is too late in the application lifecycle of MVC to be setting the culture inside of an action method, since some important culture-sensitive features of MVC (i.e. model binding) run before that point.
One way to set the culture early enough in the lifecycle is to use an authorization filter, which ensures the culture is set before model binding takes place.
And to register it globally:
The above example assumes that you have setup routing to add the
culture
as a route value similar to this answer, but you could design a filter to get the culture from somewhere else, if desired.