.NetCoreアプリ(WPF)を.Net5へ移行して単一ファイルアプリ化した話

.NetCoreで作成したアプリを.Net5に移行してみたので手順やハマった部分などを残しておきます。

ソース：https://github.com/ambleside138/TimeRecorder
移行したアプリの説明記事：[こちら](https://qiita.com/ambleside138/items/e05e6d1fad91dd3c05c0)

## 結論
問題なく.Net5移行＆単一ファイル化できました！
ただし、下記２点は注意

- 単一ファイル化するとファイルサイズは100MB超えとなった（！）　＃元の.exeファイルサイズは数100KB
- PublishTrimmedオプションを利用すると実行時エラーとなったためサイズ削減は断念

## 開発環境
- Windows10 64bit
- VisualStudio 2019 Community
- 言語：C#
- WPFアプリ

## 移行手順

#### １. プロジェクトファイルを変更
.Net5移行するにはプロジェクトファイルの修正が必要のよう。
参考：[Windows フォーム デスクトップ アプリを .NET 5 に移行する方法](https://docs.microsoft.com/ja-jp/dotnet/desktop/winforms/migration/?view=netdesktop-5.0)

GUIからは変更できない（？）ので１つずつ.csprojファイルを変更していきました。
＃TargetFrameworkに実行環境（=windows）も記載するのがキモらしい

```diff
- <TargetFramework>netcoreapp3.1</TargetFramework>
+ <TargetFramework>net5.0-windows</TargetFramework>
```

#### ２. 利用ライブラリの最新化
１の状態でビルドするとなにやらエラーがでていたのでとりあえず参照しているライブラリを最新化してみることに
　→ 破壊的変更が含まれていたので少しソースを修正
　→ ビルドエラー解消
　★ここまでの手順で.Net5移行は完了！

#### ３. ビルドコマンドの修正
続いて単一ファイルアプリ化の作業。
単一ファイル化自体は.NetCoreでも可能だったのだが、実行時にTempフォルダにファイル展開される仕様だったために利用を見送っていました。.Net5でファイル展開せずに実行が可能になったということでトライしてみることに。
＃この記事↓が.NetCoreとの差分にも触れつつ詳しくまとまっていてわかりやすかったです
　[「dotnet-5.0のシングルファイルアプリについて」](https://qiita.com/skitoy4321/items/e8a43881456edc1f2c61)

リリースビルドではdotnetコマンドを利用しています。publishオプションを変更して対応しました。

```diff
- dotnet publish "../src/TimeRecorder.sln" -c Release --self-contained  -r win10-x64 -o "Release"
+ dotnet publish "../src/TimeRecorder/TimeRecorder.csproj" -c Release -r win10-x64 -p:PublishSingleFile=true --self-contained true -o "Release" 
```

- 単一ファイルビルドするために `-p:PublishSingleFile=true`を指定
- .sln指定してビルドすると、「実行ファイルじゃないとPublishSingleFileできないよ」と怒られたので実行プロジェクトの.csproj指定に変更


## ハマったこと
- ライブラリを更新したときに破壊的更新があったのに気づかず、実行時エラーが発生するようになっていて.Net5の不具合かと疑ってしばらく悩んでしまいました... ちゃんと順を追って作業すればいいだけの話なので気をつけましょう。

- 単一ファイルビルドすると.exeファイルが100MBを超えてしまいました.. さすがにちょっと..ということで`PublishTrimmed=true`オプションを利用してみましたが、削ってはいけない部分まで削ってしまっているよう（？）で実行時エラーが発生するようになりました。リフレクションを多用していたり、サードパーティ製のライブラリ利用時などはTrimするのはリスキーですね。

参考までに：`PublishTrimmed=true`でビルドしたモジュールでの実行時エラーOSイベントログ

```
Application: TimeRecorder.exe
CoreCLR Version: 5.0.220.61120
.NET Version: 5.0.2
Description: The process was terminated due to an unhandled exception.
Exception Info: System.Windows.Markup.XamlParseException: プロパティ 'System.Windows.ResourceDictionary.DeferrableContent' の Set で例外がスローされました。
 ---> System.TypeInitializationException: The type initializer for 'System.Windows.FrameworkElement' threw an exception.
 ---> System.TypeInitializationException: The type initializer for 'System.Windows.Documents.TextElement' threw an exception.
 ---> System.TypeInitializationException: The type initializer for 'System.Windows.Documents.Typography' threw an exception.
 ---> System.IO.FileNotFoundException: 
File name: 'System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
   at System.Windows.Media.TextFormatting.TextRunTypographyProperties.OnPropertiesChanged()
   at MS.Internal.Text.TypographyProperties.ResetProperties()
   at MS.Internal.Text.TypographyProperties..ctor()
   at System.Windows.Documents.Typography..cctor()
   --- End of inner exception stack trace ---
   at System.Windows.Documents.TextElement..cctor()
   --- End of inner exception stack trace ---
   at System.Windows.FrameworkElement..cctor()
   --- End of inner exception stack trace ---
   at System.Windows.FrameworkElement.FindResourceFromAppOrSystem(Object resourceKey, Object& source, Boolean disableThrowOnResourceNotFound, Boolean allowDeferredResourceReference, Boolean mustReturnDeferredResourceReference)
   at System.Windows.StaticResourceExtension.FindResourceInAppOrSystem(IServiceProvider serviceProvider, Boolean allowDeferredReference, Boolean mustReturnDeferredResourceReference)
   at System.Windows.StaticResourceExtension.FindResourceInEnviroment(IServiceProvider serviceProvider, Boolean allowDeferredReference, Boolean mustReturnDeferredResourceReference)
   at System.Windows.StaticResourceExtension.TryProvideValueImpl(IServiceProvider serviceProvider, Boolean allowDeferredReference, Boolean mustReturnDeferredResourceReference)
   at System.Windows.ResourceDictionary.SetOptimizedStaticResources(IList`1 staticResources, IServiceProvider serviceProvider, StaticResourceExtension staticResourceWorker)
   at System.Windows.ResourceDictionary.SetKeys(IList`1 keyCollection, IServiceProvider serviceProvider)
   at System.Windows.ResourceDictionary.SetDeferrableContent(DeferrableContent deferrableContent)
   at System.Windows.ResourceDictionary.set_DeferrableContent(DeferrableContent value)
   at System.Windows.Baml2006.WpfSharedBamlSchemaContext.<>c.<Create_BamlProperty_ResourceDictionary_DeferrableContent>b__297_0(Object target, Object value)
   at System.Windows.Baml2006.WpfKnownMemberInvoker.SetValue(Object instance, Object value)
   at MS.Internal.Xaml.Runtime.ClrObjectRuntime.SetValue(XamlMember member, Object obj, Object value)
   at MS.Internal.Xaml.Runtime.ClrObjectRuntime.SetValue(Object inst, XamlMember property, Object value)
   --- End of inner exception stack trace ---
   at System.Windows.Markup.XamlReader.RewrapException(Exception e, IXamlLineInfo lineInfo, Uri baseUri)
   at System.Windows.Markup.WpfXamlLoader.Load(XamlReader xamlReader, IXamlObjectWriterFactory writerFactory, Boolean skipJournaledProperties, Object rootObject, XamlObjectWriterSettings settings, Uri baseUri)
   at System.Windows.Markup.WpfXamlLoader.LoadBaml(XamlReader xamlReader, Boolean skipJournaledProperties, Object rootObject, XamlAccessLevel accessLevel, Uri baseUri)
   at System.Windows.Markup.XamlReader.LoadBaml(Stream stream, ParserContext parserContext, Object parent, Boolean closeStream)
   at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
   at TimeRecorder.App.InitializeComponent()
   at TimeRecorder.App.Main()
```


## 感想
（変なとこでつまづいてしまったのを除いて）.Net5移行はすごく簡単でした！
単一ファイル化については余計なライブラリファイルが目に見えなくなるのはいいことだなぁ〜と思いつつも、業務で使う場合はファイルサイズに気をつけないといけないと感じています。