TFS 2018 で MSIXの発行を行う
Visual Studio 2019からのMSIX形式のアプリパッケージの作成は、他サイトを参考にして出来るようになったので、TFS上でビルドする方法を調査しました。
一応、今回は以下の状態で始めました。
- .NET Core 3.1の32bitのWPFアプリケーション
- Visual Studio 2019の画面操作で、アプリパッケージの作成が一度成功している
- 配布方法は、サイドローディング
- コード署名証明書は、証明書ストアから選択している
- アプリケーション バンドルの生成は、常に行う
まずは、コマンドラインでビルドをしてみましたが、さっそく以下のエラーが発生。
error NETSDK1047: 資産ファイル 'C:\Users\(省略)\obj\project.assets.json' に '.NETCoreApp,Version=v3.1/win-x86' のターゲットがありません。 復元が実行されたこと、および 'netcoreapp3.1' がプロジェクト の TargetFrameworks に含まれていることを確認してください。 プロジェクトの RuntimeIdentifiers に 'win-x86' を組み込む必要が生じる可能性もあります。 [C:\Users\(省略).csproj]
MSIXパッケージでは、自己完結型デプロイメントをするため、RuntimeIdentifiersの設定が必要らしいです。
csprojファイルに以下を追加します。
<RuntimeIdentifiers>win-x86</RuntimeIdentifiers>
以下のコマンドで、コマンドラインからの作成が出来ました。(Developer PowerShell for VS 2019で実行しました)
> msbuild (プロジェクト名).wapproj /p:Configuration=Release /p:AppxBundlePlatforms=x86
プロパティなしで実行することも可能で、その場合はDebug構成のx86プラットフォームが選択されていました。指定しなかったプロパティは、wapprojファイルやappxmanifestファイルの値が使用されるようです。パッケージの作成とコード署名証明書による署名も行われていました。
TFSでのビルド、発行
コマンドラインからのビルドが出来たので、いよいよTFSでのビルドに入ります。
このサイトが参考になりました。対象がTFSではなく、後継のAzure DevOps(Azure Pipelines)なので、設定ファイルがYAMLで書かれているため、読むのがちょっと大変でした。
この記事は、MSDN Magagine 2019 の記事の一部ですが、PDF版もダウンロード可能です。PDF版はWeb版で省略された図や画面も含まれるので、PDF版を見た方が理解しやすいです。
コマンドラインでのビルドが出来ているので、それをタスクに置き換えます。
ただし、TFS 2018 だと「Visual Studioのビルド」タスクが、VS 2017までしか対応していないため、MSBuildタスクを使用し、ビルドを実行するPCのMSBuild へのパスを設定する必要があったので、以下のように設定しました。
MSBuildタスク
プロジェクト **/*.wapproj MSBuild へのパス C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe プラットフォーム x86 構成 Release MSBuild 引数 /p:AppxPackageDir=$(build.artifactstagingdirectory)\AppxPackages /p:AppxBundlePlatforms=x86
最後に、AppxPackagesフォルダを、配布用フォルダにコピーします。発行場所は「TFS」ではなく「A file share」を指定しました。ここはClickOnceの発行と同じですね。
成果物の公開タスク
発行するためのパス $(build.artifactstagingdirectory)\AppxPackages 成果物名 AppxPackagesの中身が格納されるフォルダ名 ファイル共有パス 成果物が格納されるフォルダの親フォルダ
動作環境
- TFS 2018 Update 3
- .NET Core 3.1
- ビルドマシン:Visual Studio 2019 Professional
以上
TFS 2018 のビルドが NuGet restore タスクで失敗したので調査する
少し前にソースコードの修正や、ビルド定義の編集をしていないのに、TFSのビルドが失敗するようになっていたので調査しました。
エラーメッセージ
MSBuild auto-detection: using msbuild version '16.5.0.12403' from 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\bin'. Use option -MSBuildVersion to force nuget to use a specific version of MSBuild. NuGet.CommandLine.CommandLineException: Error parsing solution file at C:\TfsBuildsAgents\VstsAgent2\_work\1\s\XXXProduct.sln: 呼び出しのターゲットが例外をスローしました。 The project file could not be loaded. ファイルまたはアセンブリ 'Microsoft.Build.Framework, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'、またはその依存関係の 1 つが読み込めませんでした。 指定されたファイルが見つかりません。C:\TfsBuildsAgents\VstsAgent2\_work\1\s\XXXProduct.sln
エラーメッセージで調べてみると、MSBuildのバージョンに対して、指定しているNuGetのバージョンが古いらしい。使用しているのは、NuGet 4.4.1でした。
ビルドエージェントを動かしているPCには、Visual Studio 2019がインストールされていて、エラーになる直前にアップデートした事を思い出しました。
サイトを参考にビルド定義を編集して「NuGet Tool インストーラー」で4.4.1としていたところを5.5.0に変更したところ、ビルドが成功するようになりました。
インストールされているNuGetのバージョンを確認するには、Microsoft Visual Studio のバージョン情報を見れば良いようです。
以上
追伸
つい最近、Visual Studio 2019 を 16.8以降にアップデートした後も、ビルドが失敗しました。その時のエラーメッセージは、Error NETSDK1005 netcoreapp3.1
と出ていました。
エラーメッセージで調べると以下のサイトがヒットしました。
.NET 5対応のために、修正が入ったのかなと思い、こちらもビルド定義の「NuGet Tool インストーラー」のバージョンを5.5.0から5.8.0にしたところビルドが成功するようになりました。
ClickOnceで使用できたコード署名が、MSIXで使用できない理由を調査し、対応した
背景
この間、ClickOnceの発行をする時に、使用したコード署名証明書(コードサイニング証明書)をPCの証明書ストアにインストールしたので、 .NET Coreアプリケーションの配布形式であるMSIXでも試してみました。
MSIX形式で配布する方法を調べると、以下のサイトに詳しくのっていました。
サイトを参考に、.NET CoreのWPFアプリケーションを作成し、Windows アプリケーション パッケージ プロジェクト(以下、パッケージプロジェクト)をソリューションに追加しました。
パッケージプロジェクトを右クリックして、「公開」⇒「アプリ パッケージの作成」と進んで、「署名方法の選択」画面から、「Store から選択」をクリックします。
この後、証明書の選択ウィンドウが出てくるのですが、画面には「証明書がありません」のメッセージ。
??と思い、現在のソリューションを一旦終了して、.NET Frameworkプロジェクトを開き、プロジェクトのプロパティから署名メニューを選択すると、コード署名証明書はあります。「ストアからの選択」にも表示されます。
本題
ClickOnceで使用できたコード署名が、MSIXで使用できない理由を調べました。
パッケージプロジェクトの画面では、テスト用の証明書を作成する事が出来ます。作成したファイルは、プロジェクトにpfxファイルとして保存されます。
テスト用証明書を作成して、それを証明書ストアにインストールしてみました。その後、「Store から選択」を押してみると、インストールしたテスト用証明書が選択できます。
また、対象のコード署名証明書をファイルにエクスポートして、「ファイルから選択」でエクスポートしたファイルを選択してみました。すると「選択された証明書はコード署名には無効です。」というエラーメッセージが表示されました。
という事は、ClickOnceの発行に使用したコード署名証明書に問題がありそうです。エラーメッセージから検索したり、対象のコード署名証明書とテスト用証明書のフィールドを見比べると以下がわかりました。
フィールド:基本制限(basic constraints, oid=2.5.29.19)の値に Subject Type=End Entity が設定されていない。
アプリケーション パッケージへの署名 (Windows ストア アプリ) | Microsoft Docs
パッケージ署名用証明書を作成する - MSIX | Microsoft Docs
以前にルート証明書について調べたときに、この値はCAかどうかを指定すると書いてあったので、ルート証明書ではないので、Subject Typeは空欄にしていました。前述のようにClickOnceでは使えていました。
以下のように、PowerShellのNew-SelfSignedCertificateコマンドレットを使用して、基本制限を設定したテスト用証明書その2を作成したところ、「Store から選択」に表示されました。
> New-SelfSignedCertificate -FriendlyName "証明書を人が区別するための名前" -DnsName "団体名" -TextExtension @("2.5.29.19={text}") -Type CodeSigningCert -CertStoreLocation "Cert:\CurrentUser\My" -Subject "CN=xxx,O=yyy,OU=zzz,C=JP"
対応
テスト用ではないコード署名証明書は、Active Directory証明書サービス(Active Directory Certificate Services:AD CS)で発行されるため、修正方法を調べて発行しなおしてもらいました。
やったことは、AD CS上でのテンプレートの修正です。具体的には、互換性タブでの対象OSの変更と、拡張機能の基本制限の「有効にする」をチェックしました。
修正後に証明書を更新してもらったところ、新しいコード署名証明書が「Store から選択」に表示されました。
以上
Entity Framework で SQLite を使用する
NuGetでSystem.Data.SQLite
パッケージをインストールし、以下をApp.configに追加する。
<entityFramework> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> <provider invariantName="System.Data.SQLite" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" /> <provider invariantName="System.Data.SQLite.EF6" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" /> </providers> </entityFramework> <system.data> <DbProviderFactories> <remove invariant="System.Data.SQLite.EF6" /> <add name="SQLite Data Provider (Entity Framework 6)" invariant="System.Data.SQLite.EF6" description=".NET Framework Data Provider for SQLite (Entity Framework 6)" type="System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6" /> <remove invariant="System.Data.SQLite" /> <add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".NET Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" /> </DbProviderFactories> </system.data>
リンク先を見てもらえばわかるが、System.Data.SQLiteは依存パッケージが設定されているため、一つインストールすれば必要なものが全て入るようになっている。
以上
TFS 2018 で ClickOnce の発行を行う
少し前に作成したWPFアプリケーションは、対象OSとしてWindows 8.1も含まれるため、フレームワークとして.NET Framework 4.5.1を使用しました。
Webアプリ(ASP.NET MVC)は、TFS上でビルド(XAMLではない)、リリースが出来るようにしたので(下記参照)
今回はデスクトップアプリケーションのビルド、ClickOnceによる発行をTFS上で試しました。
はじめに
TFSはビルド、リリースに分かれていて、ビルドが対象のビルドを行い、リリースで各環境への配置を行います。 ASP.NET MVCでは、リリースでIISの設定を行うため、ビルドとリリースに分けて使いました。
しかし、ClickOnceは、MSBuildのパラメータでビルド/発行を切り替えるし、発行先は1か所なので、リリースを使用せず、ビルドだけを使用しました。
作業開始
まず、「.NET デスクトップ」テンプレートを使用してビルド定義を作成しました。
WPFアプリケーションで、Prismを使用するためにPrism Template Packでプロジェクトを作成したところ、NuGetパッケージの管理方式に「PackageReference」を使用しており、リポジトリにpackagesフォルダが存在せず、リポジトリに使用するDLLを含めなくなりました。
そのため、NuGet関連タスクは削除せずに残すことにして、早速、ビルドを試しました。これはテンプレートのままで上手くいきました。
次に、ClickOnceの発行を試しました。「ソリューションのビルド」タスクのMSBuild引数で/target:publish
とすると、ClickOnce用のファイルが、app.publishフォルダに作成されます。
VSの画面から発行した場合とは違い、サーバへのファイルコピーは行われません。
以前にコマンドラインから実行したときは、csprojファイルを編集して、コピー処理を追加しました。これを今回はTFSのビルド定義のタスクで行う必要があります。
いろいろと試してみた結果、最終的にはファイルのコピー用に、二つのタスクを作成しました。
ファイルのコピータスク
ビルド結果から、app.publishフォルダだけをコピーします
ソースフォルダ $(system.defaultworkingdirectory) コンテンツ **\bin\$(BuildConfiguration)\app.publish\** !**\bin\$(BuildConfiguration)\app.publish\$(VsProject).exe ターゲット フォルダー $(build.artifactstagingdirectory)
成果物の公開タスク
app.publishフォルダを、配布用フォルダにコピーします。発行場所は、「TFS」ではなく「A file share」を指定しました。
発行するためのパス $(build.artifactstagingdirectory)\$(VsSolution)\bin\$(BuildConfiguration)\app.publish 成果物名 app.pubslihの中身が格納されるフォルダ名 ファイル共有パス 成果物が格納されるフォルダの親フォルダ
VsProject、VsSolution変数は、変数タブで作成し、値としてプロジェクト名、ソリューション名を登録しました。
つまづいた点
最初はファイルのコピータスクで、配布用フォルダへのコピーまでやろうとしたが、ソースフォルダ(リポジトリのルートパス)からのコピーになってしまい、階層が深くなってしまったので、分けることにしました。
成果物の公開タスクで、ファイルが圧縮されてしまわないか心配しましたが大丈夫でした(フォルダの内容がそのままコピーされました)
- TFS 2018 Update 3
- .NET Framework 4.5.1
以上
Entity Framework CoreのInsert高速化 & InsertOrUpdateを行うライブラリ
数万件単位のレコードを登録するバッチアプリケーションを作成することになった。
公開されているWeb APIからデータを取得して、かなり編集をしてから登録するため、ストアドプロシージャは適していないし、生のSQLでやるのもミスが多そうなので避けたい。となると、Entity Frameworkを使うことになる。最新は Entity Framework Coreという名前になっているので、こちらを使った。
登録用データの作成までは良かったが、実際に動かしてみると、それなりに時間がかかった。スケジューラーに組み込んでいる他の処理との関係で、登録にかかる時間は短ければ短いほど良いので、大きく変えずに高速化する方法を調べると、公式サイトに以下のページがあった。
ここで紹介されているライブラリ「EFCore.BulkExtensions」を導入すると、Insert高速化 & InsertOrUpdateが出来るようになった。具体的な記録は取っていないけど10万レコードで試した所、確かに速くなっているように感じた。InsertOrUpdate機能も要件にマッチしていたので、これを採用することにした。
サイトの方に使い方がのっているが、簡単なサンプルを残しておく。削除は要件になかったので、使用せず。
// EntityクラスBookを作成(POCO) Book book = new Book { // プロパティを設定 } // Insert Or Update処理。SQLのMERGE文が呼び出される // contextは、Entity Frameworkで作成したDbContextのサブクラス // 1レコードでもリストを作成する context.BulkInsertOrUpdate(new List<Book> { book });
実際に使用した時は、リストに全て含めると、内1件でエラーが発生した時にどこでエラーになったかがSQLを見てもわからないため、数件ずつのリストに分割して呼び出すようにした。
以上
WPFアプリのWindowの閉じる・開くを、MVVMフレームワークを使用して実装する
WPFアプリケーションで、自作のボタンを押して画面を閉じて、新しい画面を表示する処理をMVVMフレームワークを使用して実装する方法を調べました。
自作のボタンを押して画面を閉じる
自作のボタンを押して画面を閉じるやり方です。これは、以下で紹介されているCommandParameter属性を使用する方法にしました。WindowクラスをViewModelが参照するので、若干気になる部分ではありますが、引数なので良しとし、コードビハインドを使用しないという方向で決めました。
c# - Close Window from ViewModel - Stack Overflow
Windowにx:Nameで名前を定義するのではなく、RelativeSourceを使用して自分の親Windowを取得するようにします。
<Button Width="120" Height="30" Command="{Binding OpenCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"> Open New Window </Button>
ViewModel
public DelegateCommand<Window> OpenCommand { get; set; } public MainWindowViewModel() { OpenCommand = new DelegateCommand<Window>(ExecuteOpen); } private void ExecuteOpen(Window window) { window?.Close(); }
新しい画面を表示する
続いて新しい画面を表示する方法です。新しい画面を表示しないとアプリケーションが終了してしまいますからね。
実は、(呼び出し元の)ViewModelから(呼び出し先の)Windowをnewして、Showメソッドを呼び出すだけで画面表示そのものは出来てしまいます。PrismがViewModelの紐づけや、DIを行ってくれます。
しかし、メソッド内で特定のView(Window)をnewして、Showメソッドまで呼び出しているので、上とは違って、このままではMVVMとして良くありませんし、単体テストも難しいです。
以下のサイトを参考にして、解決しました。
c# - Open a new Window in MVVM - Stack Overflow
Opening new window in MVVM WPF
WindowをnewしてShowメソッドを呼び出す処理をメソッドにし、画面表示用のクラスを用意して、そこにメソッドを移動します。
ViewModelからは、画面表示用クラスのメソッドを呼び出します。画面表示用のクラスは、DIで取得します。
画面表示用クラス
public class RouteService : IRouteService { private readonly IContainerExtension _container; public RouteService(IContainerExtension container) { _container = container; } public void ShowBrandNewWindow(string username) { var brandNewWindow = _container.Resolve<BrandNewWindow>(); brandNewWindow.Show(); } }
ViewModel
public DelegateCommand<Window> OpenCommand { get; set; } public MainWindowViewModel(IRouteService routeService) { _routeService = routeService; OpenCommand = new DelegateCommand<Window>(ExecuteOpen); } private void ExecuteOpen(Window window) { _routeService.ShowBrandNewWindow(Username); window?.Close(); }
開発環境:
.NET Core 3.1
Prism 7.2.0.1422
おまけ
今回と一つ前の内容をサンプルプロジェクトとして、GitHubリポジトリに追加したので、全体像を見たい方はどうぞ。
以上。