【Dynamics CRM】DateTimeをローカライズする

でもFilteredViewをSQLで覗いてみるとちゃんとローカライズされた値になっています。この辺の仕組みも含めて、ちょっとまとめておきます。

UTCをローカルタイムに変換する(クライアントマシン依存)

ざっくりとしたサンプルコード。

var entity = _serviceProxy.Retrieve(entityName, id, new ColumnSet("createdon"));
Console.WriteLine(entity.GetAttributeValue<DateTime>("createdon"));

このcreatedonをローカルタイムに変換する場合。一つはDateTime.ToLocalTimeメソッドを使う方法が考えられます。

var entity = _serviceProxy.Retrieve(entityName, id, new ColumnSet("createdon"));
Console.WriteLine(entity.GetAttributeValue<DateTime>("createdon").ToLocalTime());

「国内でしか使わねーよ」という場合はこれだけでOKです。お疲れ様でした。

UTCをローカルタイムに変換する(ユーザ依存)

「うちのは世界各国で使うの!クラウドなの!」という方は非常にかわいそうですが、もう4〜5手間ぐらい追加されます。

FilteredViewを見てもわかる通り、Dynamicのユーザとタイムゾーンは紐付けられています。SQLの結果を書き換えられるぐらいですので、当然テーブル、っていうかエンティティが用意されています。UserSettingsエンティティです。

ここのtimezonecodeを使うことでユーザごとに動的にDateTimeをローカライズすることが可能になります。

まずはそいつを取ってきましょう。

var query = new QueryExpression("usersettings")
    {
        ColumnSet = new ColumnSet("timezonecode"),
        Criteria = new FilterExpression
        {
            Conditions = new ConditionExpression("systemuserid", ConditionOperator.EqualUserId)
        }
    }

var timezoneCode = _serviceProxy.RetrieveMultiple(query)
                                .Entities
                                .Select(x => x.GetAttributeValue<int>("timezonecode"))
                                .First();

ConditionOperatorのEqualUserIdを使うことでログイン中のユーザのGUIDを自動で判別してくれます。UserSettingsにはBusinessUnitIdもあるので、部署単位でローカライズする場合はEqualBusinessIdを使えばOKです。

次に、UTCからローカライズしたいDateTimeと、先ほど取得したtimezonecodeを使ってLocalTimeFromUtcTimeRequestを作成します。

LocalTimeFromUtcTimeRequestをExecuteで投げるとLocalTimeFromUtcTimeResponseが返って来ます。そこのLocalTimeプロパティが、お目当てのものです。

一連の流れをまとめてみましょう。

var createdon = _serviceProxy.Retrieve(entityName, id, new ColumnSet("createdon"))
                             .GetAttributeValue<datetime>("createdon");

var query = new QueryExpression("usersettings")
    {
        ColumnSet = new ColumnSet("timezonecode"),
        Criteria = new FilterExpression
        {
            Conditions = new ConditionExpression("systemuserid", ConditionOperator.EqualUserId)
        }
    }

var timezoneCode = _serviceProxy.RetrieveMultiple(query)
                                .Entities
                                .Select(x => x.GetAttributeValue<int>("timezonecode"))
                                .First();

var req = new LocalTimeFromUtcTimeRequest
    {
        TimeZoneCode = timezoneCode,
        UtcTime = createdon
    };

var res = (LocalTimeFromUtcTimeResponse)_serviceProxy.Execute(req);

Console.WriteLine(res.LocalTime.ToString("yyyy/MM/dd HH:mm:ss"));

</datetime>

当然このLocalTimeFromUtcTimeRequestに指定できるUtcTimeは一つです。なんていうかもう、なんでこんな設計にしちゃったんでしょうね。

こんな拡張メソッドを作ってもいいかもしれません。

public static Datetime ToLocalTime(this Datetime utcTime, int timezoneCode, OrganizationServiceProxy proxy)
{
    var req = new LocalTimeFromUtcTimeRequest
    {
        TimeZoneCode = timezoneCode,
        UtcTime = utcTime
    };
    
    return ((LocalTimeFromUtcTimeResponse)proxy.Execute(req)).LocalTime;
}

UTCをローカルタイムに変換する(SQL

素直にDateAddかFilteredViewを使ったらどうでしょうか…。だめ?

SWITCHOFFSET関数を使うと変換できます。

肝心のtime_zoneの値なんですが、UserSettingsエンティティのtimezonebiasを-1倍することで求めることができます。

こんな感じになると思います。もっとスマートかつ汎用的なSQLが書けると思いますが、自分で考えてください。

DECLARE @offset int;

SELECT
 @offset = TimeZoneBias
FROM
 UserSettings
WHERE
 --使いたいユーザのGUID
 SystemUserId = '';

SELECT
 CreatedOn
 ,CONVERT(datetime, SWITCHOFFSET(CONVERT(datetimeoffset, CreatedOn), (@offset * -1))) LocalCreatedon
FROM
 Contact;

まとめ

正直ものすごく面倒な割に、知らないとハマるので、早いうちに何らかの関数なりメソッドなりをさくっと作っておいたほうがいいと思います。

参考