Entity framework 我们如何在微风吹拂的当地时区生存

Entity framework 我们如何在微风吹拂的当地时区生存,entity-framework,datetime,asp.net-web-api,timezone,breeze,Entity Framework,Datetime,Asp.net Web Api,Timezone,Breeze,我写这篇文章是为了收集对我们方法的评论,希望能帮助其他人(和我的记忆) 脚本 我们所有的数据库都使用没有时区信息的DateTime数据类型 在内部,我们知道数据库中的所有日期/时间都是当地(新西兰)时间,而不是UTC时间。对于web应用程序来说,这并不理想,但我们无法控制所有这些数据库的设计,因为它们支持其他系统(会计、工资单等) 我们使用实体框架(模型优先)进行数据访问 我们的问题 在没有特定时区信息的情况下,Breeze/Web Api/Entity Framework堆栈似乎支持时间

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

脚本
  • 我们所有的数据库都使用没有时区信息的
    DateTime
    数据类型
  • 在内部,我们知道数据库中的所有日期/时间都是当地(新西兰)时间,而不是UTC时间。对于web应用程序来说,这并不理想,但我们无法控制所有这些数据库的设计,因为它们支持其他系统(会计、工资单等)
  • 我们使用实体框架(模型优先)进行数据访问
我们的问题
  • 在没有特定时区信息的情况下,Breeze/Web Api/Entity Framework堆栈似乎支持时间是UTC而不是本地的假设,这可能是最好的,但不适合我们的应用程序
  • Breeze喜欢以标准UTC格式将日期传递回服务器,特别是在查询字符串中(例如
    where
    子句)。设想一个Breeze控制器直接将数据库中的表公开为IQueryable。Breeze客户端将以UTC格式向服务器传递任何日期筛选器(where)子句。实体框架将忠实地使用这些日期来创建SQL查询,完全不知道数据库表日期在本地时区。对我们来说,这意味着结果与我们想要的结果相差12到13个小时(取决于夏令时)
我们的目标是确保服务器端代码(和数据库)始终使用本地时区中的日期,并且所有查询都返回所需的结果。

我们的解决方案第1部分:实体框架 当实体框架从数据库中获取
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); }}}}",
            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)));
    }
}
这创造了一个相当丑陋的单线设置器,但它完成了任务。它确实使用了一个helper函数将日期默认为
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;
    }
}
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;
    }
}

我们的解决方案第2部分:可更换过滤器 下一个问题是在将
where
子句应用于我们的
IQueryable
控制器操作时传递UTC日期。在查看Breeze、Web API和Entity Framework的代码后,我们决定最好的选择是拦截对控制器操作的调用,并将
QueryString
中的UTC日期替换为本地日期

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

[UseLocalTime]
public IQueryable<Product> Products()
{
    return _dc.Context.Products;
}

我们的解决方案第3部分:Json 这可能更具争议性,但我们的web应用程序用户也完全是本地人:)

我们希望发送给客户端的Json在默认情况下包含本地时区中的日期/时间。我们还希望从客户端接收到的Json中的任何日期都转换为本地时区。为此,我们创建了一个自定义的
JsonLocalDateTimeConverter
,并替换了Json转换器

转换器如下所示:

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;
    }
}
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;
    }
}
public类CustomBreezeConfig:Breeze.WebApi.BreezeConfig
{
受保护的重写JsonSerializerSettings CreateJsonSerializerSettings()
{
var baseSettings=base.CreateJsonSerializerSettings();
//更换breeze随我们自己的产品安装的标准IsoDateTimeConverter
var timeConverter=baseSettings.Converters.OfType().SingleOrDefault();
if(timeConverter!=null)
{
baseSettings.converter.Remove(时间转换器);
}
添加(新的JsonLocalDateTimeConverter());
返回基本设置;
}
}


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

我收到了你的文章,想传递一些信息。一位同事实现了您的解决方案,它对服务器时区中的任何用户都很有效。不幸的是,对于服务器时区以外的用户,它不起作用

我已经修改了您的转换器类以使用TimeZoneInfo。代码如下:

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 - coming from DB, then treat is as UTC - user's UTC Offset. All our dates are saved in user's proper timezone. Breeze will Re-add the offset back
            var userdateTime = (DateTime)value;
            if (userdateTime.Kind == DateTimeKind.Unspecified)
            {
                userdateTime = DateTime.SpecifyKind(userdateTime, DateTimeKind.Local);
                var timeZoneInfo = ApplicationContext.Current.TimeZoneInfo;
                var utcOffset = timeZoneInfo.GetUtcOffset(userdateTime);
                userdateTime = DateTime.SpecifyKind(userdateTime.Subtract(utcOffset), DateTimeKind.Utc);
            }

            base.WriteJson(writer, userdateTime, 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)
        {
            var utcDateTime = (DateTime)result;
            if (utcDateTime.Kind != DateTimeKind.Local)
            {
                // date is UTC, convert it to USER's local time
                var timeZoneInfo = ApplicationContext.Current.TimeZoneInfo;
                var utcOffset = timeZoneInfo.GetUtcOffset(utcDateTime);
                result = DateTime.SpecifyKind(utcDateTime.Add(utcOffset), DateTimeKind.Local);
            }
        }

        return result;
    }
}
这里的关键是:

var timeZoneInfo = ApplicationContext.Current.TimeZoneInfo;
此变量在登录时在用户上下文中设置。当用户登录时,我们在登录请求上传递jsTimezoneDetect的结果,并将该信息放在服务器上用户的上下文中。因为我们有一个Windows服务器,jsTimezoneDetect将吐出一个IANA时区,我们需要一个Windows时区,所以我在我们的解决方案中导入了noda time nuget,使用以下代码,我们可以将IANA时区转换为Windows时区:

// This will return the Windows zone that matches the IANA zone, if one exists.
public static string IanaToWindows(string ianaZoneId)
{
    var utcZones = new[] { "Etc/UTC", "Etc/UCT" };
    if (utcZones.Contains(ianaZoneId, StringComparer.OrdinalIgnoreCase))
        return "UTC";

    var tzdbSource = NodaTime.TimeZones.TzdbDateTimeZoneSource.Default;

    // resolve any link, since the CLDR doesn't necessarily use canonical IDs
    var links = tzdbSource.CanonicalIdMap
      .Where(x => x.Value.Equals(ianaZoneId, StringComparison.OrdinalIgnoreCase))
      .Select(x => x.Key);

    var mappings = tzdbSource.WindowsMapping.MapZones;
    var item = mappings.FirstOrDefault(x => x.TzdbIds.Any(links.Contains));
    if (item == null) return null;
    return item.WindowsId;
}

虽然我意识到您可能无法在场景中控制这一点,但我相信解决此问题的另一个方法是使用DateTimeOffset类型而不是DateTime来表示实体模型中的日期/时间

啊!我还想不出更好的选择。Breeze.NET组件(或BreezeJs)如何让您更轻松。。。没有真正为你做这件事?在我们看来,保持UTC的一切正常运行仍然是最好的默认设置。。。正如你似乎同意的那样。但你不会是唯一一个在这个绑定中的人。@Ward,谢谢你阅读本文和你的评论。我认为微风正在做它应该做的事情。时区只是一种痛苦。如果Breeze客户端可以选择在javascript日期的时区内发送日期,那就太好了,但是你不能仅仅使用浏览器的
date.toISOString()
,这会增加很多代码(而且不是那么“标准”)。一点