我们如何使用Breeze使用当地时区生存

Bre*_*dan 12 timezone datetime entity-framework asp.net-web-api breeze

我写这篇文章是为了收集关于我们方法的评论,希望能帮助别人(和我的记忆).

脚本

  • 我们所有的数据库都使用DateTime没有时区信息的数据类型.
  • 在内部,我们知道数据库中的所有日期/时间都在本地(新西兰)时间,而不是UTC.对于Web应用程序,这并不理想,但我们不控制所有这些数据库的设计,因为它们支持其他系统(会计,工资单等).
  • 我们正在使用Entity Framework(模型优先)进行数据访问.

我们的问题

  • 如果没有特定的时区信息,Breeze/Web Api/Entity Framework堆栈似乎倾向于假设时间是UTC,而不是本地,这可能是最好的,但不适合我们的应用程序.
  • Breeze喜欢以标准UTC格式将日期传递回服务器,特别是在查询字符串(例如where子句)中.想象一下Breeze控制器直接将数据库中的表作为IQueryable公开.Breeze客户端将以UTC格式将任何日期过滤器(where)子句传递给服务器.实体框架将忠实地使用这些日期来创建SQL查询,完全不知道数据库表日期在我们的本地时区.对我们而言,这意味着结果在12到13个小时之间,偏离了我们想要的结果(取决于夏令时).

我们的目标是确保我们的服务器端代码(和数据库)始终在我们的本地时区中使用日期,并且所有查询都返回所需的结果.

Bre*_*dan 15

我们的解决方案第1部分:实体框架

当Entity Framework DateTime从数据库获取值时,它将它们设置为DateTimeKind.Unspecified.换句话说,既不是本地的也不是UTC的.我们特别希望将日期标记为DateTimeKind.Local.

为实现这一目标,我们决定调整实体框架的生成实体类的模板.相反,我们的日期是一个简单的属性,我们推出了后备存储日期和使用属性setter作出之日起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)));
}
Run Code Online (Sandbox Code Playgroud)

...... ......

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)));
    }
}
Run Code Online (Sandbox Code Playgroud)

这创造了一个相当丑陋的单线制定者,但它完成了工作.它确实使用辅助函数将日期默认为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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我们的解决方案第2部分:IQueryable过滤器

下一个问题是Breeze在将where句子应用于我们的IQueryable控制器操作时传递UTC日期.在查看了Breeze,Web API和Entity Framework的代码之后,我们决定最好的选择是拦截对我们的控制器操作的调用,并在QueryString本地日期中交换UTC 日期.

我们选择使用我们可以应用于控制器操作的自定义属性来执行此操作,例如:

[UseLocalTime]
public IQueryable<Product> Products()
{
    return _dc.Context.Products;
}
Run Code Online (Sandbox Code Playgroud)

实现此属性的类是:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们的解决方案第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):
        // 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

最后为了安装上面的转换器,我们创建了一个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;
    }
}
Run Code Online (Sandbox Code Playgroud)

就是这样.欢迎提出所有意见和建议.