gRPCサービスと通信するWPFアプリの例を解説する。
これは国内でのみ使用する事を想定して、日付型はDateTimeだけ使用して作成している。
DateTimeは内部に自国とUTCを識別する情報を保有している。
(注意:2022年6月21日更新→.NET5.0は5月8日にサポート終了しました。サンプルコードは.NET6.0に更新しました)
Protobuf で日時をシリアライズ
また、gRPCサービスがシリアライズに使用するProtobufはUNIXタイムスタンプで日時をシリアライズする為、その日付はUTC(グリニッジ標準時)に変換してシリアライズする。
ProtobufのUNIXタイムスタンプの、C#API 上での正式な名称は、
Google.Protobuf.WellKnownTypes.Timestamp
となる。
DateTimeからTimestampへ変換する為には、Google.Protobuf.WellKnownTypes.Timestamp の中で定義されている関数を使用する。
DateTimeからTimestampへ変換するなら、FromDateTime(DateTime dateTime) メソッドを使用する。
Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime dateTime)
TimestampからDateTimeへ変換するなら、ToDateTime() メソッドを使用する。
Google.Protobuf.WellKnownTypes.Timestamp.ToDateTime()
同様の機能を持つメソッドとして、
DateTimeOffset用の FromDateTimeOffset(DateTimeOffset dateTimeOffset) メソッドと、ToDateTimeOffset() メソッドも存在する。
これは別記事で解説する。
DateTime から Timestamp へ変換する
クライアントアプリから、gRPCサービスへ、DateTime型を送信するなら、簡単サンプルコードで以下のように変換する。
static void Main(string[] args)
{
DateTime dateTime = new DateTime(2021, 2, 15, 14, 21, 11, DateTimeKind.Utc);
Google.Protobuf.WellKnownTypes.Timestamp timestamp;
timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(dateTime);
Console.WriteLine("Timestamp = {0}", timestamp.ToDiagnosticString());
}
実行結果>
Timestamp = "2021-02-15T14:21:11Z"
ちなみにこのコンソールアプリのサンプルは、以下の手順で作成できる。
Visual Studio 2019 の「新しいプロジェクトの作成」を選び、「コンソールアプリ(.NET Core)」で新規コンソールアプリのプロジェクトを作成する。
ソリューションエクスプローラーから、プロジェクトを右クリックして「NuGetパッケージの管理」をクリックして開く。
NuGetパッケージマネージャーから、「参照」タブを選択して「Google.Protobuf」を検索して選択し、画面右の「インストール」をクリックしてインストールすれば、利用可能になる。
あとは、Main関数に、上記のコードを書けば、ビルドして実行できる。
「Google.Protobuf」の簡単なテストに便利だ。
ちなみに、上記のソースコードの「DateTimeKind.Utc」を「DateTimeKind.Local」にして実行すると、以下のエラーが出て実行できない。
Conversion from DateTime to Timestamp requires the DateTime kind to be Utc
これが、DateTime が UTC でなければ Protobuf の Timestamp へ変換できない理由だ。
Timestamp から DateTime へ変換する
同様に、gRPCサービスからシリアライズで Timesramp を受け取り、DateTimeへ変換する場合は、以下のように書く。
DateTime resultDate = timestamp.ToDateTime();
Console.WriteLine("resultDate = {0}", resultDate.ToString());
先のサンプルコードに続けて書くとこうなる。
static void Main(string[] args)
{
DateTime dateTime = new DateTime(2021, 2, 15, 14, 21, 11, DateTimeKind.Utc);
Google.Protobuf.WellKnownTypes.Timestamp timestamp;
timestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(dateTime);
Console.WriteLine("Timestamp = {0}", timestamp.ToDiagnosticString());
DateTime resultDate = timestamp.ToDateTime();
Console.WriteLine("resultDate = {0}", resultDate.ToString());
}
実行結果>
Timestamp = "2021-02-15T14:21:11Z"
resultDate = 2021/02/15 14:21:11
このやり方は邪道である自覚を持つ
日本国内だけで使用するシステムならこのように日本標準時の DateTime を「UTCである」と欺いて使用するのが簡単だ。
但し、このやり方では後からシステムを拡張して、英国や台湾からもアクセスできるようにする事が不可能になる。
手作りで時差変更をする機能を作る事は可能だが、無駄に手間が掛かるのと、どうしてもタイムゾーンを間違える事になる。
日本標準時の DateTime を「UTCである」と欺いているので、後から本物の UTC 日付型を導入できない。
少しでも国際的に拡張する可能性があるなら、このやり方はやめておいた方が良い。
素直に、DateTiemOffset により UTC 基準を使用した、設計にすべきだろう。
.NET の日付型はタイムゾーンの間違いを検出する機能が標準装備されている。
タイムゾーンの管理は .NET の標準機能に任せた方が良い。
苦労して手作りする必要は無い。
Protobuf で時間差を表す
.NET にも、Protobuf にも、異なる二つの日付型の「時間差」を格納するクラスがある。
.NET で時間差を表す型には「TimeSpan」がある。
TimeSpan はタイムゾーンを表す時にも使用する。
Protobuf で時間差を表す型には「Duration」がある。
Google.Protobuf.WellKnownTypes.Duration
期間や時差や時間間隔やタイムゾーンを表す時は、この TimeSpan と Duration を使用する。
先ほどのサンプルと同じ、簡単なコンソールアプリで
「時差を TimeSpan で格納し、それを Duration に変換して、さらに TimeSpan に変換する」
というサンプルコードを書くと以下のようになる。
static void Main(string[] args)
{
DateTime dateTimeBgin =
new DateTime(2021, 2, 15, 14, 21, 11, DateTimeKind.Utc);
DateTime dateTimeEnd =
new DateTime(2021, 3, 31, 23, 59, 59, DateTimeKind.Utc);
//時差を算出する。
TimeSpan timeSpan = dateTimeEnd - dateTimeBgin;
Console.WriteLine("{0} - {1}", dateTimeEnd, dateTimeBgin);
Console.WriteLine(" = {0}, {1} s", timeSpan, timeSpan.TotalSeconds);
//Protobuf の Duration に変換する。
Google.Protobuf.WellKnownTypes.Duration duration;
duration = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(timeSpan);
Console.WriteLine("duration = {0}", duration.ToDiagnosticString());
//Duration を TimeSpan に変換する。
TimeSpan resultTimeSpan = duration.ToTimeSpan();
Console.WriteLine("resultTimeSpan = {0}, {1} s",
resultTimeSpan.ToString(), resultTimeSpan.TotalSeconds);
}
実行結果>
2021/03/31 23:59:59 - 2021/02/15 14:21:11
= 44.09:38:48, 3836328 s
duration = "3836328s"
resultTimeSpan = 44.09:38:48, 3836328 s
gRPC で通信する場合も、Protobuf の Duration に変換してシリアライズする。
以上、ここまでが Protobuf による簡単な、DateTime と TimeSpan の使い方の解説である。
次に、WPFアプリと gRPCサービスの通信サンプルによる、DateTime と TimeSpan の使い方の解説を行う。
DateTime を gRPCサービスで送受信する
WPFアプリと gRPCサービスの通信サンプルとして、以下のソースコードを公開する。
前回から使用しているgRPCサービスとWPFアプリの Visual Studio プロジェクトである。
それぞれ別のソリューションとして GitHub に登録している。
今回、解説するのは「ChangeTimeZone」メソッドである。
このサンプルの画面レイアウトは、このようになる。
この内、「ChangeTimeZone」はこの部分である。
gRPCサービスのサンプルコード
Sample_GrpcService プロジェクトの「greet.proto」ファイルに以下の message と rpc を追加している。
service Greeter {
// ChangeTimeZone function
rpc ChangeTimeZone (ReservationTime) returns (ReservationTime);
}
// The request and response for
message ReservationTime {
string subject = 1;
google.protobuf.Timestamp time = 2;
google.protobuf.Duration duration = 3;
google.protobuf.Duration timeZone = 4;
google.protobuf.Duration countryTimeZone = 5;
}
message ReservationTime は、DateTimeOffset の解説用サンプルコードの「Reserve (ReservationTime)」でも使用する message なので、今は使用しないメンバも含まれている。
「ChangeTimeZone」メソッドで使用するメンバは「time」と「timeZone」だけである。
やり方は以前解説したので、もう解説しない。
同 Sample_GrpcService プロジェクトの「GreeterService.cs」ファイルにも「ChangeTimeZone」の実装を追加している。
/// <summary>
/// 時差の変更
/// </summary>
/// <param name="request"></param>
/// <param name="context"></param>
/// <returns></returns>
public override Task<ReservationTime> ChangeTimeZone(ReservationTime request, ServerCallContext context)
{
//リクエストパラメータを取得する
DateTime requestDateTime = request.Time.ToDateTime();
TimeSpan timeZone = request.TimeZone.ToTimeSpan();
//時差を変更する
DateTime changeDateTime = requestDateTime.Add(timeZone);
//時差を変更した日時を設定する。
ReservationTime reservation = new ReservationTime();
reservation.Time = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(changeDateTime);
reservation.TimeZone = request.TimeZone;
//時差を変更した日時を返す。
return Task.FromResult(reservation);
}
ChangeTimeZone の機能
「ChangeTimeZone」メソッドの機能は、
google.protobuf.Timestamp time で与えられた UNIXタイムスタンプを、
google.protobuf.Duration timeZone で与えられたタイムゾーン(時差)の時刻に変換して返す、
という物だ。
他のプロパティは使用しない。
.NET の標準的なタイムゾーン管理機能を使用しないと、このような処理でタイムゾーンを変換する事になる。
現在のタイムゾーンが、グリニッジ標準時なのか、米国東部時間なのか、日本標準時なのかを示す情報は、存在しない。
プログラマーが自己管理する。
非常に間違いやすいので、あまりこのやり方はお勧めしない。
WPFアプリのサンプルコード
WPFアプリの側「Sample_gRPC_WpfApp」の側も、「greet.proto」ファイルを同様に変更している。
プロジェクトは「Sample_gRPC_ClassLibrary」になる。
gRPCサービスと同じ変更なので、ここには掲載しない。
XAMLコード
画面レイアウトには、以下の XAML を追加している。
<StackPanel Orientation="Vertical" Grid.Row="6" Height="60" VerticalAlignment="Top" Background="Peru">
<StackPanel Orientation="Horizontal" Height="25" VerticalAlignment="Center" Background="Peru">
<DatePicker x:Name="xSampleDatePicker" Width="120" Height="25" />
<TextBox Margin="20,0,0,0" x:Name="xSampleHourTextBox" Width="50" TextAlignment="Left"/>
<TextBlock Text="時刻(24時間制)" Width="100" TextAlignment="Left" />
<TextBox x:Name="xSampleMinuteTextBox" Width="50" TextAlignment="Left"/>
<TextBlock Text="分" Width="30" TextAlignment="Left" />
<TextBlock Text="タイムゾーン" Width="60" TextAlignment="Right" />
<ComboBox x:Name="xSampleTimeSpanComboBox" ItemsSource="{Binding TimeZone}" Width="120" />
<Button x:Name="ChangeTZButton" Content="時差変更" Width="100" Margin="30,0,0,0" Click="ChangeTZButtonButton_Click"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Height="25" VerticalAlignment="Bottom" Background="Peru">
<TextBlock Text="時差変更日時" Width="125" TextAlignment="Left" />
<TextBox x:Name="xChangeTextBox" Width="450" TextAlignment="Left"/>
</StackPanel>
</StackPanel>
xSampleDatePicker で日付を入力し、
xSampleHourTextBox と xSampleMinuteTextBox に「時分の値」を入力する。
xSampleTimeSpanComboBox でタイムゾーンを選択し、
ChangeTZButton で、gRPCサービスをリクエストする。
リプライは xChangeTextBox へ表示する。
他の機能と区別する為、背景色を「Background=”Peru”」にしている。
コードビハインド
ChangeTZButton のイベントを追加して、gRPCサービスを呼び出している。
private void ChangeTZButtonButton_Click(object sender, RoutedEventArgs e)
{
//選択していない場合は無視する。
if (this.xSampleDatePicker.SelectedDate == null)
return;
if (this.xSampleTimeSpanComboBox.SelectedValue == null)
return;
//時間を文字列から数値に変換する。
int hour, minute;
if (int.TryParse(this.xSampleHourTextBox.Text, out hour) == false) return;
if (hour >= 24)
{
this.xSampleHourTextBox.Text = "×24超";
return;
}
if (int.TryParse(this.xSampleMinuteTextBox.Text, out minute) == false) return;
if (minute >= 60)
{
this.xSampleMinuteTextBox.Text = "×60超";
return;
}
//タイムゾーンを設定
DateTime date = this.xSampleDatePicker.SelectedDate.Value;
TimeSpan timeZone = ((KeyValuePair<string, TimeSpan>)this.xSampleTimeSpanComboBox.SelectedValue).Value;
DateTime dateTime = new DateTime(date.Year, date.Month, date.Day, hour, minute, 0, DateTimeKind.Utc);
//ReservationTime 作成
ReservationTime reservationTime = new ReservationTime();
reservationTime.Time = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(dateTime);
reservationTime.TimeZone = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(timeZone);
// gRPC サービスを呼び出す。
var reply = this.grpcClient.GreeterClient.ChangeTimeZone(reservationTime);
// 時差日を表示する。
DateTime replyDateTime = reply.Time.ToDateTime();
TimeSpan timeSpan = reply.TimeZone.ToTimeSpan();
this.xChangeTextBox.Text = replyDateTime.ToString("yyyy年MM月dd日 H時m分") + " / TimeZone = " + timeSpan.ToString();
}
また、タイムゾーンをコンボボックスで選択する為に、タイムゾーンのコレクションの実体を宣言している。
private Dictionary<string, TimeSpan> _timeZone = null;
public Dictionary<string, TimeSpan> TimeZone { get { return this._timeZone; } }
private void InitTimeZone()
{
this._timeZone = new Dictionary<string, TimeSpan>();
var sysTimeZones = TimeZoneInfo.GetSystemTimeZones();
foreach (TimeZoneInfo timeZone in sysTimeZones)
{
string timeZoneId = timeZone.Id;
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
TimeSpan offset = timeZoneInfo.BaseUtcOffset;
this._timeZone.Add(timeZoneId, offset);
}
}
TimeZoneInfo はタイムゾーンを管理するクラスである。
GetSystemTimeZones() メソッドはシステムで管理するタイムゾーンの一覧を返す。
foreachでタイムゾーンの一覧をDictionaryに登録し、後でこれをComboBoxにバインドする。
コンストラクタで初期化する。
public MainWindow()
{
InitTimeZone();
InitializeComponent();
this.DataContext = this;
this.grpcClient = new SampleClient();
}
ChangeTZButtonButton_Click の中で、最初の方の処理は画面からパラメータ値を取り出しているだけである。
主要な日付型処理は、呼び出しの前処理が、以下になる。
DateTime dateTime = new DateTime(date.Year, date.Month, date.Day, hour, minute, 0, DateTimeKind.Utc);
//ReservationTime 作成
ReservationTime reservationTime = new ReservationTime();
reservationTime.Time = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(dateTime);
reservationTime.TimeZone = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(timeZone);
new DateTime で DateTimeKind.Utc を持つ DateTime型変数を作成している。
ここで UTC のDateTimeKind.Utc を持つ DateTime型に変換しないと、Timestamp.FromDateTime でエラーになる。
「Google.Protobuf.WellKnownTypes」を使用している部分が、Protobuf へのシリアライズをする処理である。
reservationTime.Time = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(dateTime);
DateTime から Timestamp に変換している。
reservationTime.TimeZone = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(timeZone);
こちらはタイムゾーンのTimeSpanを、Duration に変換している。
次に gRPCコールをする。
// gRPC サービスを呼び出す。
var reply = this.grpcClient.GreeterClient.ChangeTimeZone(reservationTime);
var reply に、結果が返ってくる。
リプライの処理が、
// 時差日を表示する。
DateTime replyDateTime = reply.Time.ToDateTime();
TimeSpan timeSpan = reply.TimeZone.ToTimeSpan();
this.xChangeTextBox.Text = replyDateTime.ToString("yyyy年MM月dd日 H時m分") + " / TimeZone = " + timeSpan.ToString();
となる。
DateTime replyDateTime = reply.Time.ToDateTime();
で、Timestamp から、DateTime へ変換する。
TimeSpan timeSpan = reply.TimeZone.ToTimeSpan();
で、Duration から、TimeSpan へ変換する。
xChangeTextBox.Text へ結果を表示する。
結果の日付は、gRPCサービス側で、時差が加算されて指定のタイムゾーンに変換されて表示される。
しかし、この日付はどちらも .NET には UTC(グリニッジ標準時)と認識されている。
このサンプルコードは日付型に時差を、加算減算しているだけのプログラムである。
以上が、DateTime だけで日付処理を行い、gRPCサービスを使用する方法の全解説である。
このやり方は邪道である
しつこいようだが、このやり方は「邪道」である。
複数のタイムゾーンを管理するなら、後の記事で解説する DateTimeOffset を使用した開発を行うべきである。
DateTimeOffset は内部にタイムゾーンの情報を保持しており、タイムゾーンの異なる DateTimeOffset を演算しようとするとエラーが出るので、プログラムのバグを発見しやすく「どのタイムゾーンを使用しているのか分からなくなる」事が無い。
タイムゾーンの管理も厳格に行える。
gRPCがシリアライズに使用する Protobuf はUNIXタイムスタンプでしか日時を保持できない。
UNIXタイムスタンプはグリニッジ標準時であり、経過秒数の部分はUTCと(ほぼ)同じである。
gRPC を使用する以上は、UTCとタイムゾーンを無視する事はできないと考えるべきだ。
DateTimeOffset を活用したシステム開発をお勧めする。
では、この記事はここまで。
お役に立てば幸いだ。
しつこいが、このやり方は「邪道」だ。