Ответ 1
Наше решение часть 1: структура сущностей
Когда Entity Framework получает значения DateTime
из базы данных, она устанавливает их в DateTimeKind.Unspecified
. Другими словами, ни локальный, ни UTC. Мы специально хотели отметить наши даты как DateTimeKind.Local
.
Для этого мы решили настроить шаблон Entity Framework, который генерирует классы сущностей. Вместо того, чтобы наши даты были простым свойством, мы ввели дату хранилища и использовали средство настройки свойств, чтобы сделать дату Local
, если она была Unspecified
.
В шаблоне (файл .tt) мы заменили...
public string Property(EdmProperty edmProperty)
{
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} {2} {{ {3}get; {4}set; }}",
Accessibility.ForProperty(edmProperty),
_typeMapper.GetTypeName(edmProperty.TypeUsage),
_code.Escape(edmProperty),
_code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
_code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
}
... с...
public string Property(EdmProperty edmProperty)
{
// Customised DateTime property handler to default DateKind to local time
if (_typeMapper.GetTypeName(edmProperty.TypeUsage).Contains("DateTime")) {
return string.Format(
CultureInfo.InvariantCulture,
"private {1} _{2}; {0} {1} {2} {{ {3}get {{ return _{2}; }} {4}set {{ _{2} = DateKindHelper.DefaultToLocal(value); }}}}",
Accessibility.ForProperty(edmProperty),
_typeMapper.GetTypeName(edmProperty.TypeUsage),
_code.Escape(edmProperty),
_code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
_code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
} else {
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} {2} {{ {3}get; {4}set; }}",
Accessibility.ForProperty(edmProperty),
_typeMapper.GetTypeName(edmProperty.TypeUsage),
_code.Escape(edmProperty),
_code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
_code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
}
}
Это создает довольно уродливый однострочный сеттер, но он выполняет свою работу. Он использует вспомогательную функцию для значения по умолчанию для даты Local
, которая выглядит следующим образом:
public class DateKindHelper
{
public static DateTime DefaultToLocal(DateTime date)
{
return date.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(date, DateTimeKind.Local) : date;
}
public static DateTime? DefaultToLocal(DateTime? date)
{
return date.HasValue && date.Value.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(date.Value, DateTimeKind.Local) : date;
}
}
Наше решение часть 2: фильтры IQueryable
Следующая проблема заключалась в том, что Бриз передавал даты UTC при применении предложений where
к нашим действиям контроллера IQueryable
. Просмотрев код для Breeze, Web API и Entity Framework, мы решили, что лучшим вариантом было перехват вызовов на наши действия с контроллером и изменение дат UTC в QueryString
с локальными датами.
Мы решили сделать это с помощью настраиваемого атрибута, который мы могли бы применить к нашим действиям контроллера, например:
[UseLocalTime]
public IQueryable<Product> Products()
{
return _dc.Context.Products;
}
Класс, который реализовал этот атрибут:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Http.Filters;
using System.Text.RegularExpressions;
using System.Xml;
namespace TestBreeze.Controllers.api
{
public class UseLocalTimeAttribute : ActionFilterAttribute
{
Regex isoRegex = new Regex(@"((?:-?(?:[1-9][0-9]*)?[0-9]{4})-(?:1[0-2]|0[1-9])-(?:3[0-1]|0[1-9]|[1-2][0-9])T(?:2[0-3]|[0-1][0-9]):(?:[0-5][0-9]):(?:[0-5][0-9])(?:\.[0-9]+)?Z)", RegexOptions.IgnoreCase);
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
// replace all ISO (UTC) dates in the query string with local dates
var uriString = HttpUtility.UrlDecode(actionContext.Request.RequestUri.OriginalString);
var matches = isoRegex.Matches(uriString);
if (matches.Count > 0)
{
foreach (Match match in matches)
{
var localTime = XmlConvert.ToDateTime(match.Value, XmlDateTimeSerializationMode.Local);
var localString = XmlConvert.ToString(localTime, XmlDateTimeSerializationMode.Local);
var encoded = HttpUtility.UrlEncode(localString);
uriString = uriString.Replace(match.Value, encoded);
}
actionContext.Request.RequestUri = new Uri(uriString);
}
base.OnActionExecuting(actionContext);
}
}
}
Наше решение, часть 3: Json
Это может быть более спорным, но наша аудитория веб-приложений полностью локальна:).
Мы хотели, чтобы Json отправил клиенту количество дат/времени в нашем локальном часовом поясе по умолчанию. Также мы хотели, чтобы любые даты в Json, полученные от клиента, были преобразованы в наш часовой пояс. Для этого мы создали пользовательский JsonLocalDateTimeConverter
и поменяли установление BZZ конвертера Json.
Конвертер выглядит так:
public class JsonLocalDateTimeConverter : IsoDateTimeConverter
{
public JsonLocalDateTimeConverter () : base()
{
// Hack is for the issue described in this post (copied from BreezeConfig.cs):
// http://stackoverflow.com/questions/11789114/internet-explorer-json-net-javascript-date-and-milliseconds-issue
DateTimeFormat = "yyyy-MM-dd\\THH:mm:ss.fffK";
}
// Ensure that all dates go out over the wire in full LOCAL time format (unless date has been specifically set to DateTimeKind.Utc)
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is DateTime)
{
// if datetime kind is unspecified then treat is as local time
DateTime dateTime = (DateTime)value;
if (dateTime.Kind == DateTimeKind.Unspecified)
{
dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
}
base.WriteJson(writer, dateTime, serializer);
}
else
{
base.WriteJson(writer, value, serializer);
}
}
// Ensure that all dates arriving over the wire get parsed into LOCAL time
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var result = base.ReadJson(reader, objectType, existingValue, serializer);
if (result is DateTime)
{
DateTime dateTime = (DateTime)result;
if (dateTime.Kind != DateTimeKind.Local)
{
result = dateTime.ToLocalTime();
}
}
return result;
}
}
И, наконец, чтобы установить вышеуказанный конвертер, мы создали класс CustomBreezeConfig
:
public class CustomBreezeConfig : Breeze.WebApi.BreezeConfig
{
protected override JsonSerializerSettings CreateJsonSerializerSettings()
{
var baseSettings = base.CreateJsonSerializerSettings();
// swap out the standard IsoDateTimeConverter that breeze installed with our own
var timeConverter = baseSettings.Converters.OfType<IsoDateTimeConverter>().SingleOrDefault();
if (timeConverter != null)
{
baseSettings.Converters.Remove(timeConverter);
}
baseSettings.Converters.Add(new JsonLocalDateTimeConverter());
return baseSettings;
}
}
Что об этом. Все комментарии и предложения приветствуются.