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

2021年2月16日火曜日

技術的備忘録

t f B! P L


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


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

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

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

https://docs.microsoft.com/ja-jp/dotnet/api/system.datetimekind?view=net-5.0

https://docs.microsoft.com/ja-jp/dotnet/api/system.datetime?view=net-5.0

 

Protobuf で日時をシリアライズ

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

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


Google.Protobuf.WellKnownTypes.Timestamp

となる。

https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/timestamp#todatetime

 

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」がある。

https://docs.microsoft.com/ja-jp/dotnet/api/system.timespan?view=net-5.0

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

 

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


Google.Protobuf.WellKnownTypes.Duration

https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/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 に登録している。

 

https://github.com/motoi-tsushima/Sample_GrpcService/releases/tag/1.3.0.0

https://github.com/motoi-tsushima/Sample_gRPC_WpfApp/releases/tag/1.3.1.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通信のサンプルコード」に戻る


このブログを検索

Translate

人気の投稿

自己紹介

自分の写真
オッサンです。実務経験は Windows環境にて C#,VB.NET ,SQL Server T-SQL,Oracle PL/SQL,PostgreSQL,MariaDB。昔はDelphi,C,C++ など。 趣味はUbuntu,PHP,PostgreSQL,MariaDBかな ?基本無料のやつ。

QooQ