要約
Linuxの場合は、環境変数TZ=Etc/UTC
として実行するとUTC時間として実行できます。
Windowsの場合、環境変数では変更できません。システム日時をGMTなどUTC相当のタイムゾーンにするのが正攻法です。邪道な方法としてTimeZoneInfo.Local
で使う値をリフレクションを使って無理矢理UTC時間に置き換える方法があります。ただし、TimeZoneInfo.Utc
をそのまま代入せず、TimeZoneInfo.CreateCustomTimeZone
を使ってUTCタイムゾーンの複製を作ってから置き換えましょう。そうしないと、TimeZoneInfo.ConvertTime
で任意のタイムゾーン値に変換する際に引数不正でエラーになってしまうことがあります。また、TimeZoneInfo.ClearCachedData
を呼び出すとリフレクションで上書きしたものが元に戻ってしまいます。同様にTimeZoneInfo.ConvertTime
にTicks==0なDateTimeを渡してもTimeZoneInfo.ClearCachedData
が呼ばれるので呼んではいけません。
以上の通り、Windowsの方法についてはちょっと問題を起こすので基本的に推奨しません。
そもそも、C#における日時はまずNodaTimeを検討し、よりカジュアルに使いたい場合はDateTimeOffsetを使えないか検討し、DateTimeはunspecifiedな日時(時差が無い日時)を扱いたいときの手段として使うのが良いのではないかと思います。
参考
以下のIssueでリフレクションを使う方法が紹介されている
サンプルコードと利用例
背景
タイムゾーンについて、開発環境と実行環境で異なるケースがあります。
例えばAzureにおいては基本UTCで動作するようになっていますが、任意のタイムゾーンに置き換えることはできるところはできます。VM等のIaaSならまずできますし、PaaSもサポートがあることがあります。しかし残念なことにLinuxのAzure Functionsはタイムゾーンを変更できませんでした。
そのため、任意タイムゾーンで動かすことは現実的ではないケースがありました。
で、あればUTCで扱うことにして、開発環境をUTC時間で扱えばよいでしょう。しかしWindowsではOS設定のシステム時計をGMT等のUTC相当のタイムゾーンに合わせる必要があるようでした。現実として開発兼業務PCが支給されている我々は普段は日本時間を使い、テスト時だけはUTCにしたいということがあります(ありませんか?)
なので、何とかできないか調べました。
方法
調査
以下のissueでWindowsの場合にリフレクションで置き換えるコメントがあります。これは内部実装によるのでランタイムのアップデートで動かなくなるリスクがあります。
Linuxは環境変数TZで簡単おきかえられるのでWindowsでもどうにかしてタイムゾーンを変えて起動できるようにしてくれないか、というissueも上がっていますが、まだ対応の見込みはなさそうです。
仕方がないので、上記のWindowsではリフレクションで無理やり置き換える方法を試しました。
UseUtcTimeZoneInfo()
というstatic methodが肝です。
TimeZoneInfo.Utcをそのまま使うと例外になるケースがありますが、TimeZoneInfo.CreateCustomTimeZoneでそれっぽい値を作ると回避できます。理由は後述します。
注意
このあたりの注意を克服できれば、使えるかもしれません。
UTC時間にするときはダミーのUTC時間を作ったほうがよさそう
TimeZoneInfo.Utc
をそのまま置き換えて、ConvertTimeで日本時間に変換しようとしたら、次のような例外が発生しました。
System.ArgumentException: The conversion could not be completed because the supplied DateTime did not have the Kind property set correctly. For example, when the Kind property is DateTimeKind.Local, the source time zone must be TimeZoneInfo.Local. (Parameter 'sourceTimeZone')
原因がよくわかってないですが、ReferenceEquals(DateTimeKind.Local, DateTimeKind.Utc)
という状態になっていると発生するようでした。
おそらく、リフレクションで無理やりTimeZoneInfo.Local == TimeZoneInfo.Utc
にすると、DateTimeのKindがLocalの場合に、SourceのTimeZoneInfoはLocalになりますが、ソースコード上該当箇所でTimeZoneInfoに対応するDateTimeKind値を返す処理でLocalを返すべきはずがUtcを返してしまいここのDateTimeのKindとの比較を通せず、例外になっているようです。
もちろん、もともとリフレクションを使って置き換えることが変なので、その先のことまで考えられていないというだけなんですが。
そのため、TimeZoneInfo.CreateCustomTimeZone
を使い、UTC時間の複製を作ってごまかしています。
TimeZoneInfo.ClearCachedDataが呼ばれるとリフレクションで置き換えたものが元に戻る
TimeZoneInfo.ClearCachedData
がpublic staticメソッドになっていますが呼んではいけません。書き換えたTimeZoneInfo.Localの値がリセットされてしまいます。
また、DateTime.Ticks == 0
な値(DateTime.MinValue, defaultなど)を書き換えたTimeZoneInfoのConvertTimeに渡してもTimeZoneInfo.ClearCachedData
メソッドが呼ばれてしまいます。(該当箇所)
この挙動が厄介で、単体テストで日時が影響を与えないことがわかっていてもDateTimeにはdefault値を使えません。ConvertTimeを呼ぶ前にはTicksが0でない事をチェックしたほうがよいでしょう。
想定FAQ
DateTimeではDateTimeKind.UtcもありUTCを明示的に扱えるのだからUTCで明示的に扱えばいいのでは?
その通りだと思います。
それができなかったのは、当時のNpgsqlの実装でtimestamp with time zone
からDateTime
のマッピングだとDateTimeZone.Local
を使ってしていたことによります。これは一見それっぽい挙動に見えますが、PostgreSQLがtimestamp with time zone
をUTC時間で保持している仕様であり本来の「タイムゾーン情報」は登録時に失われます。それをクライアントサイドのシステムのタイムゾーンを元にUTCから変換を行うのでミスマッチが起きるケースがあります。
単純に言えば「Npgsqlでtimestamp with time zone
をマッピングしたたDateTimeはLocalなので注意しましょう」という事です。これは思っていた以上に大変でした。
で、たどり着いたアイディアは「Local==Utc」となるようにすればいいのでは?というところでした。それで調べたところ、上記の方法にたどり着きました。
この件を含めて色々考えた結果、システム上ではUTC時間で統一するという方針として、もし日本時間が欲しければ明示的に変換するということにしました。もちろんロジック上で日本時間を扱うこともありますが、そこはテストとレビューで頑張ります。(海外向けのシステムではないので何とかなってます)
なおこの挙動はNpgsql 6系では修正され DateTimeZone.Utcを使うようになり、元の挙動はオプションで指定するようになりました。もっと早く来ていれば・・・
そもそもDateTimeを扱うのが良くないのでは?DateTimeOffsetやNodaTimeをつかうべきでは?
その通りです。
DateTimeOffsetはUTC時間でもLocal時間でも時間軸的に同じ時間であれば比較が可能です。
DateTimeOffsetはDateTimeよりも後発であり、DateTimeより数byteを多く消費する以外は、時差違いの比較も正しく行えますから、動作環境のタイムゾーンの違いに起因した問題はありえません。複数のタイムゾーンを扱うシステムでもとりあえずDateTimeOffsetで統一していればほとんどがうまくいくはずです。Npgsql 3でもDateTimeOffsetはそこそこうまく扱えた気がしますが、採用しませんでした。当時なぜDateTimeに固執していたかは今はもう思い出せません…
NodaTimeは考え方が少し難しいですが、コード上どういう時間を扱っているか明示されるので紛れが一切なくなります。もしタイムゾーンを跨ぐ何かがある場合、苦しんででも導入したほうがよいでしょう。DateTimeの扱いの楽さを考えるとNodaTimeはめんどくさすぎるのですが、これが日時の本当の姿だと私は思います。
そもそもWindowsの開発環境をやめるべきでは?仮想マシンやWSL2(WSL 1.0)でよくないか?
その通りです。Windows上で動作するアプリケーションを作らない限り、わざわざWindows上で開発するのは一種の縛りプレイといえるでしょう。また様々な方がおり、複数案件を経て色々汚染されたマシン上に共通の開発環境を作るのはかなり大変です。
VSCodeも発達してきているのでWSLやリモートVMで開発するのも良いでしょう。時間があれば移行したい(ない)