日本国内のみ、C#のDateTimeだけで扱うgRPCサンプルコード

「C#によるgRPC通信のサンプルコード」に戻る

gRPCサービスと通信するWPFアプリの例を解説する。

これは国内でのみ使用する事を想定して、日付型はDateTimeだけ使用して作成している。

DateTimeは内部に自国とUTCを識別する情報を保有している。

(注意:2022年6月21日更新→.NET5.0は5月8日にサポート終了しました。サンプルコードは.NET6.0に更新しました)

DateTimeKind Enum (System)
Specifies whether a DateTime object represents a local time, a Coordinated Universal Time (UTC), or is not specified as ...
DateTime 構造体 (System)
特定の時点を表します。通常、日時形式で表されます。

Protobuf で日時をシリアライズ

また、gRPCサービスがシリアライズに使用するProtobufはUNIXタイムスタンプで日時をシリアライズする為、その日付はUTC(グリニッジ標準時)に変換してシリアライズする。

ProtobufのUNIXタイムスタンプの、C#API 上での正式な名称は、


Google.Protobuf.WellKnownTypes.Timestamp

となる。

Redirecting...

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 構造体 (System)
時間間隔を表します。

TimeSpan はタイムゾーンを表す時にも使用する。

Protobuf で時間差を表す型には「Duration」がある。


Google.Protobuf.WellKnownTypes.Duration

Redirecting...

期間や時差や時間間隔やタイムゾーンを表す時は、この 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 に登録している。

Release v1.7.1.0 · motoi-tsushima/Sample_GrpcService
.NET 8.0 により、リビルドしました。
Release v1.7.1.0 · motoi-tsushima/Sample_gRPC_WpfApp
.NET 8.0 により、リビルドしました。

今回、解説するのは「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 を活用したシステム開発をお勧めする。

では、この記事はここまで。

お役に立てば幸いだ。

しつこいが、このやり方は「邪道」だ。

「C#によるgRPC通信のサンプルコード」に戻る

タイトルとURLをコピーしました