C# VisualStudioでprotoファイルからソースコードとAPI仕様書を生成する

## このドキュメントの内容

VisualStudio のプロジェクトテンプレートに「gRPCサービス」が追加され、gRPC を使ったアプリケーションの開発がしやすくなりましたが、このテンプレートはサービスアプリケーション用のものであるため、ライブラリを作成したい場合には無駄な実装が含まれてしまいます。ライブラリ用のテンプレートで proto ファイルからソースコードを生成する方法を説明します。加えて、`proto-gen-doc` を利用してAPI仕様書を出力する方法も説明します。

## 環境

使用した環境は次の通りです。
Microsoft Visual Studio Community 2019
Version 16.8.2

`Grpc.AspNetCore` のバージョンは 2.36.0 です。


## 手順

1. テンプレート「クラスライブラリ(.NET Core)」を選択して新規プロジェクトを作成します。

1. プロジェクトのプロパティを開き、対象のフレームワークを次の中から選択します。`Grpc.AspNetCore` は .NET Standard  や .NET Framework をサポートしていません。※
  * .NET Core 3.0 
  * .NET Core 3.1
  * .NET 5

:::note info
※ `Grpc.AspNetCore` の代わりに `Grpc.Tools` と `Google.Protobuf` を利用すれば、.NET Standard でもこの記事の内容を実現することができます。
:::

1. NuGet で `Grpc.AspNetCore` をインストールします。
 
1. プロジェクト内に proto ファイルを配置するフォルダを作成します。フォルダを作成するのは管理しやすくするためだけであり、フォルダ名に規約はありません。

1. proto ファイルを追加します。アイテムテンプレートに proto はないようです。「テキストファイル」でよいと思います。拡張子を proto とするとソリューションエクスプローラー上のアイコンが変わりますので、ファイルの種類を認識しているとは考えられます。

1. ファイルのビルドアクションを「Protobuf compiler」に変更します。リストアップされるプロパティ群が Protobuf に対応したものに変わります。
 
1. proto ファイルに IDL を記述します。

1. プロジェクトをビルドまたはリビルドすると IDL からソースコードが生成されます。cs ファイルはプロジェクトには追加されず、プロジェクトの obj フォルダ配下に出力されます。「カスタムツールの実行」では生成されませんでした。

1. ライブラリを使用するアプリケーションの参照アセンブリに dll ファイルを追加します。

## partial メンバーを追加するには

生成されたクラスに対して partial メンバーを追加するには、いったんビルドした後で partial クラスを追加します。


## Grpc.Tools で生成されるソースコードとの違い

全てのデータ型に対して確認してはいませんが、生成されるソースコードの実装は同じであるようです。
`Grpc.AspNetCore` は `Grpc.Core` には依存していませんが、`Grpc.Core.Api` と `Grpc.Tools` を利用しています。

```
Grpc.AspNetCore 2.36.0
  Google.Protobuf 3.15.5
  Grpc.AspNetCore.Server.ClientFactory 2.36.0
    Grpc.AspNetCore.Server 2.36.0
      Grpc.Net.Common 2.36.0
        Grpc.Core.Api 2.36.1
    Grpc.Net.ClientFactory 2.36.0
      Grpc.Net.Client 2.36.0
        Grpc.Net.Common 2.36.0
          Grpc.Core.Api 2.36.1
      Microsoft.Extensions.Http 3.0.3
  Grpc.Tools 2.36.1
```

## proto ファイルのインポート

ある proto ファイルの中で別のファイルに定義された型を参照するには、そのファイルをインポートする必要があります。

### 既知の型のインポート

Nuget でインストールされた Grpc.Tools (2.36.1) のパッケージフォルダ内には以下のファイルが組み込まれています。

* any.proto
* api.proto
* descriptor.proto
* duration.proto
* empty.proto
* field_mask.proto
* source_context.proto
* struct.proto
* timestamp.proto
* type.proto
* wrappers.proto

これらのファイルをインポートするには import を記述すればよいです。

```proto:sample.proto
import "google/protobuf/timestamp.proto";

message SampleMessage
{
  google.protobuf.Timestamp Time = 1;
}
```

### 独自の proto ファイルを無理やりパッケージフォルダに配置する

Grpc.Tools のパッケージフォルダに上記以外のファイルを配置してインポートすると、IDL 上のエラーにはならないものの生成されたソースコード内でコンパイルエラーが発生します。

次の GuidValue.proto を Grpc.Tools のパッケージフォルダに配置してインポートすると、

```proto:GuidValue.proto
syntax = "proto3";
package example.protobuf;

message GuidValue
{
  string Value = 1;
}
```

```proto:sample.proto
import "google/protobuf/timestamp.proto";
import "google/protobuf/GuidValue.proto";

message SampleMessage
{
  google.protobuf.Timestamp Time = 1;
  example.protobuf.GuidValue id = 2;
}
```

次のような SampleMessage クラスのソースコードが生成されますが、Example.Protobuf.GuidValue クラスや Example.Protobuf.GuidValueReflection クラスが生成されず、コンパイルエラーになります。GuidValue.proto がプロジェクトに含まれていないためです。

```c#
public sealed partial class SampleMessage : pb::IMessage<SampleMessage>
{
    /// <summary>Field number for the "id" field.</summary>
    public const int idFieldNumber = 2;
    private global::Example.Protobuf.GuidValue id_;
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
    public global::Example.Protobuf.GuidValue id {
      get { return id_; }
      set {
        id_ = value;
      }
    }
}
```

### 独自の proto ファイルをプロジェクト内に配置する

前述の GuidValue.proto をプロジェクト内に配置します。

```console:プロジェクト構成
ProjectRoot
  proto
    sample.proto
    GuidValue.proto
```

GuidValue.proto をインポートする場合、フォルダ名/ファイル名 で指定します。

```proto:sample.proto
import "google/protobuf/timestamp.proto";
import "proto/GuidValue.proto";

message SampleMessage
{
  google.protobuf.Timestamp Time = 1;
  example.protobuf.GuidValue ID = 2;
}
```

ソースコードが生成され、ビルドも成功します。

### 複数のプロジェクトで型を共用する

前述の GuidValue のような汎用的な型は、複数のプロジェクトで共用できると便利です。
各プロジェクトに GuidValue.proto を含めると、それぞれのアセンブリに GuidValue 型が定義されます。それらのアセンブリを一つのプロジェクトから参照しようとすると、型の重複が発生してしまいます。
共用したい型を定義したプロジェクトを作り、アセンブリを参照させることで型を共用できます。

1. 共用したい型を定義するプロジェクトを作成します。CommonLibrary.csproj とします。

1. CommonLibrary.csproj に GuidValue.proto を追加します。

1. CommonLibrary.csproj をビルドします。

1. GuidValue 型を使用するプロジェクトを作成します。SampleLibrary.csproj とします。

1. SampleLibrary.csproj の参照に CommonLibrary.dll を追加します。

1. SampleLibrary.csproj に GuidValue.proto を追加します。GuidValue 型を生成対象から除外するため、GuidValue.proto のプロパティを変更します。
  * gRPC Stub Classes プロパティに Do not generate を設定します。
  * Compile Protobuf プロパティに false を設定します。

1. SampleLibrary.csproj をビルドします。

```text:CommonLibrary.csproj
CommonLibrary
  proto
    GuidValue.proto
```
```text:SampleLibrary.csproj
SampleLibrary
  proto
    sample.proto
    GuidValue.proto  // sample.proto のソースコード生成のためにファイルを追加。コンパイル対象外にします。
```

なお、上記の手順では各プロジェクトに GuidValue.proto ファイルがコピーされます。リンクファイルとしてプロジェクトに追加できるとよいのですが、リンクファイルは Grpc.Tools から読み取ることができないようで「ファイルが見つからない」エラーが発生してしまいます。

## API仕様書を出力する

proto ファイルで定義した内容をドキュメントに出力できると便利です。`proto-gen-doc` を利用することが多いと思いますが、これをビルド時に呼び出す方法を紹介します。proto-gen-doc に対応していそうなライブラリを Nuget で探してみましたが見つかりませんでしたので、プロジェクトのビルド後イベントを利用することにしました。

1. `proto-gen-doc` を [GitHub](https://github.com/pseudomuto/protoc-gen-doc/releases) から入手し、適当なフォルダに展開します。

1. バッチファイル proto-gen-doc.bat を作成し、プロジェクトのビルド後イベントに次のコマンドを記述します。

```bat:ビルド後イベント
proto-gen-doc.bat $(Protobuf_ProtocFullPath) $(Protobuf_StandardImportsPath) $(ProjectDir)
```

```bat:proto-gen-doc.bat
rem protoc.exe の絶対パス。
rem プロジェクトが参照している Grpc.Tools に対応するパスがマクロ $(Protobuf_ProtocFullPath) によって引き渡されます。
set PROTOC=%1

rem 標準サポートされているインクルードファイルの格納ディレクトリの絶対パス。
rem プロジェクトが参照している Grpc.Tools に対応するパスがマクロ $(Protobuf_StandardImportsPath) によって引き渡されます。
set WELLKNOWN_PROTO_DIR=%2

rem proto-gen-doc の格納ディレクトリ。
rem マクロではサポートされていないため直接指定しています。
set PROTOC_GEN_DOC_DIR=E:\ProgramFiles\proto-gen-doc

rem proto-gen-doc の実行ファイル。
set PLUGIN_PROTOC_GEN_DOC=protoc-gen-doc.exe

rem proto-gen-doc のカスタムテンプレート。
set MARKDOWN_TEMPLATE=CustomMarkdownTemplate.tmpl

rem プロジェクトディレクトリの絶対パス。マクロ $(ProjectDir) によって引き渡されます。
set PROJECT_DIR=%3

rem 出力対象の proto ファイル。
rem プロジェクトディレクトリ配下の proto フォルダに格納されている proto ファイルを対象とします。
set PROTO_FILE=%PROJECT_DIR%proto\*.proto

rem ドキュメント出力先のパス。プロジェクトディレクトリ配下の doc フォルダとしました。
set OUT_DIR=%PROJECT_DIR%doc

rem proto-gen-doc ディレクトリをカレントディレクトリに設定します。
rem テンプレートファイルを絶対パスで指定する方法がわからず、
rem 実行ファイルとテンプレートファイルを同じフォルダに配置し、
rem カレントディレクトリにする方法をとりました。
cd %PROTOC_GEN_DOC_DIR%

rem ドキュメント出力ディレクトリを生成します。
if not exist %OUT_DIR% mkdir %OUT_DIR%

rem 個々の proto ファイルごとにドキュメントを出力する場合
for /f "usebackq tokens=* delims=0123456789 eol=" %%i in (`dir /B /S %PROTO_FILE%`) do (

  rem html形式で出力します
  call %PROTOC% -I=%WELLKNOWN_PROTO_DIR% -I=%PROJECT_DIR% -I=%%~dpi --doc_out=html,%%~ni.html:%OUT_DIR% "%%i" --plugin=protoc-gen-doc="%PLUGIN_PROTOC_GEN_DOC%"

  rem markdown形式で出力します
  call %PROTOC% -I=%WELLKNOWN_PROTO_DIR% -I=%PROJECT_DIR% -I=%%~dpi --doc_out=markdown,%%~ni.md:%OUT_DIR% "%%i" --plugin=protoc-gen-doc="%PLUGIN_PROTOC_GEN_DOC%"

  rem カスタムテンプレートを使用して出力します
  call %PROTOC% -I=%WELLKNOWN_PROTO_DIR% -I=%PROJECT_DIR% -I=%%~dpi --doc_out=%MARKDOWN_TEMPLATE%,%%~ni.custom.md:%OUT_DIR% "%%i" --plugin=protoc-gen-doc="%PLUGIN_PROTOC_GEN_DOC%"

)

rem 一つのドキュメントに出力する場合

rem html形式で出力します
call %PROTOC% -I=%WELLKNOWN_PROTO_DIR% -I=%PROJECT_DIR% --doc_out=html,all.html:%OUT_DIR% %PROTO_FILE% --plugin=protoc-gen-doc="%PLUGIN_PROTOC_GEN_DOC%"

rem markdown形式で出力します
call %PROTOC% -I=%WELLKNOWN_PROTO_DIR% -I=%PROJECT_DIR% --doc_out=markdown,all.md:%OUT_DIR% %PROTO_FILE% --plugin=protoc-gen-doc="%PLUGIN_PROTOC_GEN_DOC%"

rem カスタムテンプレートを使用して出力します
call %PROTOC% -I=%WELLKNOWN_PROTO_DIR% -I=%PROJECT_DIR% --doc_out=%MARKDOWN_TEMPLATE%,all.custom.md:%OUT_DIR% %PROTO_FILE% --plugin=protoc-gen-doc="%PLUGIN_PROTOC_GEN_DOC%"
```



## まとめ

任意のプロジェクトテンプレートからも proto ファイルからのソースコードを生成することができることを確認しました。
コマンドラインツールを使わずに Visual Studio 上の操作だけで処理できるのは便利ですが、できることは制限されるようです。

## 参考リンク

https://qiita.com/ayatothos/items/ea05a70856d8812c644e

https://coffee-nominagara.com/2018-07-17-114005

https://chromium.googlesource.com/external/github.com/grpc/grpc/+/HEAD/src/csharp/BUILD-INTEGRATION.md