C#でビルド時にバージョン番号とか埋め込みたい時はAssemblyInfoに埋め込む

要約
-----------------

以下が非常に詳しいです。

https://tech.guitarrapc.com/entry/2019/04/30/213208

2022/07/21 追記: 日付を埋め込む場合は、次のmsbuildの[Property functions](https://docs.microsoft.com/en-us/visualstudio/msbuild/property-functions)を使ったやり方がよいでしょう。次のStackoverflowの回答もご覧ください。

https://stackoverflow.com/a/71928552


メモ
------------------

例えばWebAPIやアプリケーションのメタ情報として今のバージョンやビルドした日時やGitのハッシュ値が欲しくなる時があります。

アップデート前後でアクセス数の変化を見たり、問題が入り始めた時にモジュールのバージョンやビルドの日時やコミットIDがわかると原因の特定がしやすくなります。

C#(.Net)では`AssemblyInfo`を利用すると色々埋め込んだり取得できるらしいですが、色々ある上に色々注意が必要でした。




### Version

プロジェクトファイルを以下のようにします

``` MyProjectNameSpace.csproj
  <PropertyGroup>
    <Version>1.2.3.4</Version> <!-- これを追加 -->
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>
```

コードから読むときはこんな感じにします。クラス名はビルドで生成される成果物(dll等)に埋め込まれるクラス名にします。

``` MyProjectNameSpace/Application.cs
// これはダメらしくnullになる
// string Version = typeof(Application).Assembly.GetCustomAttribute<AssemblyVersionAttribute>()?.Version;
// こうする
string Version = typeof(Application).Assembly.GetName().Version.ToString();
```

`AssemblyVersionAttribute` で得られそうですが、nullになります。調べたところ、`Assembly.GetName()`を使えとのことでした。

https://stackoverflow.com/questions/1144525/getcustomattribute-returns-null-for-assemblyversionattribute

ToStringしていますが、Versionからはメジャーバージョン、マイナーバージョン等、細かく取ることもできます。

### InformationVersion

``` MyProjectNameSpace/Application.cs
string InformationVersion = typeof(Application).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
```

`Version`が`1.2.3.4` のとき、特に何もしていない場合は`InformationVersion`も`1.2.3.4`になります。
しかし`dotnet build /p:SourceRevisionId=asdf` とすると、`InformationVersion`は`1.2.3.4+asdf`が得られるという感じです。

CIのビルド時にチェックアウトしてきたGitのCommitIdを入れたりするとよさそうです。最近のCIサービスはGitのCommitIDぐらいは環境変数に入ってきます。


### ビルド日時とかは？

Assemblyにはないようです。以下の記事で見た感じ、いくつか方法がありそうです。

https://stackoverflow.com/questions/1600962/displaying-the-build-date

いくつかやり方はあるみたいですが、以下の2つを試しました。

- Assemblyのメタデータに埋め込んで取り出す
- リソースファイルとして埋め込む

#### Assemblyのメタデータに埋め込んで取り出す

多分これが一番楽だと思います。[StackOverflowの記事でも回答についています](https://stackoverflow.com/a/71928552)

msbuildの[Property functions](https://docs.microsoft.com/en-us/visualstudio/msbuild/property-functions)なる機能のおかげで簡単な式を書けるので、これでビルド時に評価させて埋め込むことができます。



``` Sample.csproj
  <ItemGroup>

    <!-- 追加 -->
    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
      <_Parameter1>BuildDateTime</_Parameter1>
      <_Parameter2>$([System.DateTime]::UtcNow.ToString("o"))</_Parameter2> <!-- 2021-07-21T00:00:00.000000Z -->
    </AssemblyAttribute>
  </ItemGroup>
```

とやってビルドし、そのAssemblyに対して以下のようなコードを書けば読みだせます。

```cs

// AssemblyはAssemblyにあるクラスのTypeからリフレクションで取り出せます
// 例: var assembly = typeof(MyClass).Assembly;

public static string LoadBuildDateTime(Assembly assembly){
    var metadata = assembly
        .GetCustomAttributes<AssemblyMetadataAttribute>()
        ?.Where(a => a.Key == "BuildDateTime") // Key は _Parameter1 の値と合わせる
        ?.FirstOrDefault();
    if (metadata != null)
    {
        return metadata.Value;
        // ParseしてDateTimeで返す場合は以下
        // return DateTime.ParseExact(metadata.Value, "o", CultureInfo.InvariantCulture);
    }
    return null;
}
```

#### リソースファイルを使うやり方


リソースファイルの埋め込み方は以下です。（厳密にバイナリが作られた日時ではないけど。）

- プロジェクトファイルにリソースファイルの場所を書く
- リソースファイルを用意する
- `Assembly.GetManifestResourceStream`を使って読み込む

プロジェクトファイルにリソースファイルの場所を書く

``` MyProjectNameSpace/MyProjectNameSpace.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <!-- 中略 -->
  <ItemGroup>
    <EmbeddedResource Include="Properties/BuildDate.txt" /><!-- ここ追加 -->
  </ItemGroup>
</Project>
```

リソースファイルを用意する。開発時はダミーでもいいのでこんな風に用意しておきます

``` MyProjectNameSpace/Properties/BuildDate.txt
1970-01-01T00:00:00,0000000000+09:00
```

`Assembly.GetManifestResourceStream`を使って読み込む（実際は初回１回だけ読むとかしたほうが良いかもしれない）

``` MyProjectNameSpace/Application.cs
        private readonly string BuildDate = ReadStringFromResource("MyProjectNameSpace.Properties.BuildDate.txt");

        private static string ReadStringFromResource(string resourceName)
        {
            using (var stream = new StreamReader(GetAssembly().GetManifestResourceStream(resourceName)))
            {
                return stream.ReadToEnd();
            }
        }
```


あとは`Properties/BuildDate.txt` をCI時に作ってやればよいという感じです。