脚本
>我们所有的数据库都使用没有时区信息的DateTime数据类型.
>在内部我们知道数据库中的所有日期/时间都在本地(新西兰)时间,而不是UTC.对于Web应用程序,这不是理想的,但是我们不支持所有这些数据库的设计,因为它们支持其他系统(会计,工资单等).
>我们正在使用实体框架(模型第一)进行数据访问.
我们的问题
>没有具体的时区信息,Breeze / Web Api / Entity Framework栈似乎赞成时间是UTC而不是本地的假设,这可能是最好的,但不适合我们的应用程序.
> Breeze喜欢以标准的UTC格式将日期传回服务器,特别是在查询字符串中(例如where子句).想象一下一个微风控制器,它直接从数据库中暴露出一个表作为IQueryable. Breeze客户端将以UTC格式将任何日期过滤器(where)子句传递到服务器.实体框架将忠实地使用这些日期来创建SQL查询,完全不知道数据库表日期在我们本地时区.对我们来说,这意味着结果与我们想要的结果相差12到13个小时(取决于夏令时).
解决方法
当实体框架从数据库中获取DateTime值时,将其设置为DateTimeKind.Unspecified.换句话说,本地或UTC.我们特意将日期标记为DateTimeKind.Local.
为了实现这一点,我们决定调整实体框架的生成实体类的模板.而不是我们的日期是一个简单的属性,我们引入了一个后台存储日期,并使用属性设置器将日期设置为本地,如果它是未指定的.
在模板(.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); }}}}",_code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
- } else {
- return string.Format(
- CultureInfo.InvariantCulture,_code.SpaceAfter(Accessibility.ForSetter(edmProperty)));
- }
- }
这造成了一个相当丑陋的一线设置者,但它完成了工作.它使用帮助函数将日期默认为本地,如下所示:
- 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过滤器
下一个问题是Breeze在将where子句应用于IQueryable控制器操作时通过UTC日期.在审查了Breeze,Web API和实体框架的代码后,我们决定最好的选择是拦截对我们的控制器操作的调用,并在本地日期的QueryString中交换UTC日期.
我们选择使用我们可以应用于我们的控制器操作的自定义属性来执行此操作,例如:
- [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并交换出了Json转换器Breeze的安装.
转换器如下所示:
- public class JsonLocalDateTimeConverter : IsoDateTimeConverter
- {
- public JsonLocalDateTimeConverter () : base()
- {
- // Hack is for the issue described in this post (copied from BreezeConfig.cs):
- // https://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;
- }
- }
就是这样欢迎所有意见和建议.