12

これを書いているのは、私たちのアプローチに関するコメントを集め、他の誰か (および私の記憶) を助けることを願っています。

シナリオ

  • 当社のすべてのデータベースはDateTime、タイム ゾーン情報のないデータ型を使用しています。
  • 内部的には、データベース内のすべての日付/時刻が UTC ではなく現地 (ニュージーランド) 時間であることを認識しています。Web アプリケーションの場合、これは理想的ではありませんが、他のシステム (会計、給与計算など) をサポートしているため、これらすべてのデータベースの設計を制御することはできません。
  • データ アクセスには Entity Framework (モデル ファースト) を使用しています。

私たちの問題

  • 特定のタイム ゾーン情報がないと、Breeze / Web Api / Entity Framework スタックは、時間がローカルではなく UTC であるという仮定を支持しているようです。これはおそらく最善ですが、アプリケーションには適していません。
  • Breeze は、日付を標準の UTC 形式で、特にクエリ文字列 (where句など) でサーバーに返すのが好きです。データベースからテーブルを IQueryable として直接公開する Breeze コントローラーを想像してみてください。Breeze クライアントは、日付フィルター (where) 句を UTC 形式でサーバーに渡します。Entity Framework はこれらの日付を忠実に使用して SQL クエリを作成しますが、データベース テーブルの日付がローカル タイム ゾーンであることはまったく認識されません。私たちにとっては、結果が必要な時間から 12 ~ 13 時間オフセットされていることを意味します (夏時間によって異なります)。

私たちの目的は、サーバー側のコード (およびデータベース) が一貫してローカル タイム ゾーンの日付を使用し、すべてのクエリが目的の結果を返すようにすることです。

4

3 に答える 3

15

私たちのソリューション パート 1: Entity Framework

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)));
    }
}

これにより、やや醜い 1 行のセッターが作成されますが、仕事は完了します。ヘルパー関数を使用して、日付を次の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 フィルター

次の問題は、Breeze がコントローラー アクションwhereに句を適用するときに UTC 日付を渡すことでした。IQueryableBreeze、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 の日付をローカル タイムゾーンに変換したいと考えていました。これを行うために、カスタムを作成し、JsonLocalDateTimeConverterBreeze がインストールする 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;
    }
}

それはそれについてです。すべてのコメントと提案を歓迎します。

于 2013-04-28T09:52:05.603 に答える
2

シナリオでこれを制御できない場合があることは認識していますが、この問題の別の解決策は、DateTime ではなく DateTimeOffset 型を使用して、エンティティ モデルで日付/時刻を表すことだと思います。

于 2015-07-06T13:41:48.093 に答える
1

あなたの記事にたどり着き、いくつかの情報を伝えたいと思いました。同僚があなたのソリューションを実装し、サーバーのタイム ゾーンのすべてのユーザーに対して適切に機能しました。残念ながら、サーバーのタイムゾーン外のユーザーには機能しませんでした。

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;
}
于 2014-09-05T18:03:30.470 に答える