Разный формат привязки входных параметров даты при GET и POST запросах в ASP.NET MVC

Очередная статья являющаяся развёрнутым ответом на вопрос заданный недавно на форумах MSDN. Вопрос примерно следующий: почему при отправке значения даты на сервер, в зависимости от метода GET или POST, формат его может различаться? Сразу замечу, что это не глюк, а так и задумано. Но поведение, в некоторой степени, странное и наталкивающее на подобные мысли. Теперь постараемся разобраться в этом более подробно.  Для этого, создадим пустое приложение ASP.NET MVC 4, хотя и в старых версиях приложений поведение аналогично. Добавим контроллер HomeController
namespace MvcApplication.Controllers
{
  public class HomeController : Controller
  {
    public ActionResult Index(Nullable<DateTime> date)
    {
      return View(date ?? DateTime.Now);
    }
  }
}
и представление Index
<!DOCTYPE html>
<html>
<head>
  <title>Index</title>
</head>
<>
  <div>
    @model DateTime
    <a href="/?date=3.04.2013">Отправить</a>
    <form action="/" method="post">
      <input id="date" name="date" value="23.03.2013" />
      <button type="submit" name="button">Отправить</button>
    </form>
    Полученное значение: @Model.ToString()
  </div>
</>
</html>
для обработки маршрута по умолчанию (для тех, кто хочет разобраться в этом более детально, есть специально подготовленный мною проект исходного кода и описанный в этой статье. Можно скачать, поместив показанный высше код в проект MvcApplication, и пройтись по нему отладчиком для более полного разбора). И так, если запустить приложение и поочерёдно выполнить запросы, можно увидеть, что в случае с запросом GET, в нижней строке дата отображается неверно (значение месяца и дня меняются местами), даже если глобально определить значение культуры в файле конфигурации приложения:
<system.web>
    <globalization uiCulture="ru" culture="ru-RU" />
    ...
</system.web>
В чём дело, почему так происходит? Всё дело в провайдерах значений (подробно о механизме связывания моделей очень хорошо написано вот тут). Всего их по умолчанию определено шесть:
namespace System.Web.Mvc
{
  public static class ValueProviderFactories
  {
    private static readonly ValueProviderFactoryCollection _factories 
      = new ValueProviderFactoryCollection()
        {
            new ChildActionValueProviderFactory(),
            new FormValueProviderFactory(),
            new JsonValueProviderFactory(),
            new RouteDataValueProviderFactory(),
            new QueryStringValueProviderFactory(),
            new HttpFileCollectionValueProviderFactory(),
        };
 
    public static ValueProviderFactoryCollection Factories
    {
      get { return _factories; }
    }
  }
}
Параметры POST запроса предосталяет фабрика FormValueProviderFactory
namespace System.Web.Mvc
{
  public sealed class FormValueProviderFactory : ValueProviderFactory
  {
    private readonly UnvalidatedRequestValuesAccessor _unvalidatedValuesAccessor;
 
    public FormValueProviderFactory()
      : this(null)
    {
    }
 
    // For unit testing
    internal FormValueProviderFactory(UnvalidatedRequestValuesAccessor unvalidatedValuesAccessor)
    {
      _unvalidatedValuesAccessor = unvalidatedValuesAccessor ??
        (cc => new UnvalidatedRequestValuesWrapper(cc.HttpContext.Request.Unvalidated()));
    }
 
    public override IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
      if (controllerContext == null)
      {
        throw new ArgumentNullException("controllerContext");
      }
 
      return
        new FormValueProvider(controllerContext, _unvalidatedValuesAccessor(controllerContext));
    }
  }
}
которая реализует всего один контракт
namespace System.Web.Mvc
{
  public abstract class ValueProviderFactory
  {
    public abstract IValueProvider GetValueProvider(ControllerContext controllerContext);
  }
}
и возвращает экземпляр FormValueProvider
namespace System.Web.Mvc
{
  public sealed class FormValueProvider : NameValueCollectionValueProvider
  {
    public FormValueProvider(ControllerContext controllerContext)
      : this(controllerContext, 
      new UnvalidatedRequestValuesWrapper(controllerContext.HttpContext.Request.Unvalidated()))
    {
    }
 
    // For unit testing
    internal FormValueProvider(ControllerContext controllerContext, 
      IUnvalidatedRequestValues unvalidatedValues)
      : base(controllerContext.HttpContext.Request.Form, 
      unvalidatedValues.Form, CultureInfo.CurrentCulture)
    {
    }
  }
}
который в единственном открытом конструкторе (используя вызов внутреннего конструктора) устанавливает текущую культуру: CultureInfo.CurrentCulture, а её мы задали глобально в файле конфигурации. Поэтому в случае POST запроса, всё работает верно. А вот провайдер значений QueryStringValueProvider не использует текущую культуру, а устанавливает её как CultureInfo.InvariantCulture (такое же поведение и у RouteDataValueProvider, просто в данном случае мы не определи входной параметр как параметр маршрута). Как же быть тогда? Имея исходные коды и полную свободу действий можно придумать много способов выхода из данной ситуации. Наилучшим, на мой взгляд была бы возможность наследования и переопредления вызова конструктора QueryStringValueProvider, но это невозможно, так как клас запечатан, а связанные с ним некоторые типы являются внутренними. Но ни что не мешает нам создать свою реализацию фабрики провайдера ValueProviderFactory
public class MyQueryStringValueProviderFactory : ValueProviderFactory
{  
  public override IValueProvider GetValueProvider(ControllerContext controllerContext)  
  {  
    if (controllerContext == null)  
    {  
      throw new ArgumentNullException("controllerContext");  
    }  
 
    return new MyQueryStringValueProvider(controllerContext);  
  }  
}

и провайдера значений QueryStringValueProvider
public class MyQueryStringValueProvider : NameValueCollectionValueProvider
{  
  public MyQueryStringValueProvider(ControllerContext controllerContext)  
    : base(controllerContext.HttpContext.Request.QueryString, null, CultureInfo.CurrentCulture)  
  {  
  }  
}
и установить его по умолчанию
protected void Application_Start()
{    
  //...    
  ValueProviderFactories.Factories[4] = new MyQueryStringValueProviderFactory();    
}
прописав в класс приложения в файл Global.asax. Сделав всё высшесказанное и запустив проект заново, можно убедиться что все работает как нужно.