2018年1月8日月曜日

Protocol Buffers のIDL記述(名前空間関連)

Protocol Buffers で IDL をどのように記述するとよいのかを調べています。まずは名前空間から。C# でコーディングを行うときに名前空間や型をスムーズに記述できるような指定方法を探ります。



IDL のリファレンスはこちら。
Protocol Buffers 公式の C# コード生成ガイド
Proto3 Language Guide(和訳) - Qiita

なお、この記事の内容は Grpc.Tools 1.8.3 のコマンドラインツール protoc.exe を使用したときの結果です。

1.名前空間の指定


proto ファイル内にオプションで指定する。
option csharp_namespace = "名前空間";

proto ファイル内に package で指定する。
package 名前空間;

オプションとパッケージの両方を指定した場合、オプションが優先されます。オプションやパッケージを複数指定するとエラーになります。
パッケージで指定した場合、C# ではパスカルケースで変換されます。

protoc コマンドライン引数にも base_namespace という引数がありますが、これは生成する型の名前空間を指定するものではなく、出力ディレクトリを階層化するためのもののようです。
protoc
    protoファイル
    --proto_path=protoファイルの格納ディレクトリ
    --csharp_out=csファイルの出力ディレクトリ
    --csharp_opt=base_namespace=名前空間

コマンドライン引数で名前空間を指定した場合、指定しない場合と比べて次の違いがあります。
  • proto ファイル内にも名前空間を指定する必要があり、その名前空間はコマンドライン引数で指定した名前空間またはその配下に属している必要があります。
  • コマンドライン引数で指定した名前空間を起点として階層化されたサブフォルダが生成され、ソースファイルが出力されます。
  • gRPC 拡張によって生成される *Grpc.cs は出力ディレクトリ直下に出力されます。階層化されません。例えば、test.proto ファイル内にデータクラスとサービスクラスの定義を記述した場合、データクラスは名前空間に対応したサブフォルダの test.cs に出力されますが、サービスクラスは出力ディレクトリ直下の testGrpc.cs に出力されます。

名前空間 Sample.Models をコマンドライン引数で指定した場合

proto ファイルで指定した名前空間 ソースファイルの生成結果
Sample.Models 出力ディレクトリ直下に出力されます。
Sample.Models.Common 出力ディレクトリ\Common に出力されます。
Sample.Common 名前空間が一致しないためエラーが発生します。


2.入れ子の指定


message には入れ子の message を定義できます。入れ子の service は定義できません。
service には入れ子の message, service は定義できません。

namespase SampleApp
{
    public class SampleService
    {
        public class Request {}
        public class Response {}

        public Response Execute(Request request) {}
    }
}
というようなことがしたかったのですが難しいようです。


3.型の定義方針


現時点では次のように IDL を定義することにしました。

  • データクラスとサービスクラスは名前空間を分ける。
  • サービスクラスごとに名前空間を分ける。関連の深いサービスクラスは同じ名前空間にすることも検討する。
  • サービスクラスのメソッドの引数と戻り値はサービスクラスと同じ名前空間に定義する。複数のサービスで積極的に共有する型は共有型を定義する名前空間にまとめ、集中管理する。

[ Person.proto ]
syntax = "proto3";

option csharp_namespace = "Example.Models";

message Person {
 int32 id = 1;
 string name = 2;
}

[ PersonSearch.proto ]
syntax = "proto3";

import "Person.proto";

option csharp_namespace = "Example.Services.PersonSearch";

service PersonSearchService {
 rpc Search (PersonSearchRequest) returns (PersonSearchResponse){}
}

message PersonSearchRequest {
 string name = 1;
}

message PersonSearchResponse {
 repeated Person Persons = 1;
}

次のようなクラスが定義されます。リフレクション関連のクラスは除いています。
Example.Models.Person
Example.Services.PersonSearch.PersonSearchResult
Example.Services.PersonSearch.PersonSearchResponse
Example.Services.PersonSearch.PersonSearchService.PersonSearchServiceBase
Example.Services.PersonSearch.PersonSearchService.PersonSearchServiceClient

サービスの実装は次のようにコーディングできます。 PersonSearchService.PersonSearchServiceBase の記述は長いですが何度も記述するわけではないため問題にはならないと思います。サービスを意味する PersonSearch がプレフィクスとして含まれていますので、名前空間を省略しても衝突する可能性は低いと考えられます。
using Example.Models;
using Example.Services.PersonSearch;

internal class PersonSearchServiceImpl : PersonSearchService.PersonSearchServiceBase
{
    public override Task Search(PersonSearchRequest request, ServerCallContext context)
    {
        return base.Search(request, context);
    }
}

クライアントからの RPC 呼び出し実装は次のようにコーディングできます。 こちらも PersonSearchService.PersonSearchServiceClient の記述は長いですが何度も記述するわけではないため問題にはならないと思います。サービスを意味する PersonSearch がプレフィクスとして含まれていますので、名前空間を省略しても衝突する可能性は低いと考えられます。
using Example.Models;
using Example.Services.PersonSearch;

PersonSearchService.PersonSearchServiceClient client = new PersonSearchService.PersonSearchServiceClient(channel);

PersonSearchRequest request = new PersonSearchRequest();

PersonSearchResponse response = client.Search(request);

foreach (Person person in response.Persons)
{
}

0 件のコメント:

コメントを投稿

paiza のスキルチェックをやってみました

いまさら感はありますが、 paiza のスキルチェックをやってみました。指定された時間内にコードを書いてユニットテストにかけ、その結果を基に評価を数値化してくれるというものですが、ゲーム感覚で空き時間を見つけて進めていこうと考えています。 どうやら時間が短いほど高い評価を得...