redwarrior’s diary

C#, ASP.NET, WEB, G*などの雑多な情報

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の設定が必要らしいです。

techcommunity.microsoft.com

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で書かれているため、読むのがちょっと大変でした。

docs.microsoft.com

この記事は、MSDN Magagine 2019 の記事の一部ですが、PDF版もダウンロード可能です。PDF版はWeb版で省略された図や画面も含まれるので、PDF版を見た方が理解しやすいです。

docs.microsoft.com

コマンドラインでのビルドが出来ているので、それをタスクに置き換えます。

ただし、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でした。

stackoverflow.com

ビルドエージェントを動かしているPCには、Visual Studio 2019がインストールされていて、エラーになる直前にアップデートした事を思い出しました。

サイトを参考にビルド定義を編集して「NuGet Tool インストーラー」で4.4.1としていたところを5.5.0に変更したところ、ビルドが成功するようになりました。

インストールされているNuGetのバージョンを確認するには、Microsoft Visual Studio のバージョン情報を見れば良いようです。

blog.house-soft.info

以上

追伸

つい最近、Visual Studio 2019 を 16.8以降にアップデートした後も、ビルドが失敗しました。その時のエラーメッセージは、Error NETSDK1005 netcoreapp3.1と出ていました。

エラーメッセージで調べると以下のサイトがヒットしました。

https://developercommunity.visualstudio.com/content/problem/1248649/error-netsdk1005-assets-file-projectassetsjson-doe.html

.NET 5対応のために、修正が入ったのかなと思い、こちらもビルド定義の「NuGet Tool インストーラー」のバージョンを5.5.0から5.8.0にしたところビルドが成功するようになりました。

ClickOnceで使用できたコード署名が、MSIXで使用できない理由を調査し、対応した

背景

この間、ClickOnceの発行をする時に、使用したコード署名証明書(コードサイニング証明書)をPCの証明書ストアにインストールしたので、 .NET Coreアプリケーションの配布形式であるMSIXでも試してみました。

MSIX形式で配布する方法を調べると、以下のサイトに詳しくのっていました。

devlog.grapecity.co.jp

サイトを参考に、.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は依存パッケージが設定されているため、一つインストールすれば必要なものが全て入るようになっている。

www.nuget.org

以上

TFS 2018 で ClickOnce の発行を行う

少し前に作成したWPFアプリケーションは、対象OSとしてWindows 8.1も含まれるため、フレームワークとして.NET Framework 4.5.1を使用しました。

Webアプリ(ASP.NET MVC)は、TFS上でビルド(XAMLではない)、リリースが出来るようにしたので(下記参照)

redwarrior.hateblo.jp

今回はデスクトップアプリケーションのビルド、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変数は、変数タブで作成し、値としてプロジェクト名、ソリューション名を登録しました。

つまづいた点

最初はファイルのコピータスクで、配布用フォルダへのコピーまでやろうとしたが、ソースフォルダ(リポジトリのルートパス)からのコピーになってしまい、階層が深くなってしまったので、分けることにしました。

成果物の公開タスクで、ファイルが圧縮されてしまわないか心配しましたが大丈夫でした(フォルダの内容がそのままコピーされました)

以上

Entity Framework CoreのInsert高速化 & InsertOrUpdateを行うライブラリ

数万件単位のレコードを登録するバッチアプリケーションを作成することになった。

公開されているWeb APIからデータを取得して、かなり編集をしてから登録するため、ストアドプロシージャは適していないし、生のSQLでやるのもミスが多そうなので避けたい。となると、Entity Frameworkを使うことになる。最新は Entity Framework Coreという名前になっているので、こちらを使った。

登録用データの作成までは良かったが、実際に動かしてみると、それなりに時間がかかった。スケジューラーに組み込んでいる他の処理との関係で、登録にかかる時間は短ければ短いほど良いので、大きく変えずに高速化する方法を調べると、公式サイトに以下のページがあった。

docs.microsoft.com

github.com

ここで紹介されているライブラリ「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フレームワークを使用して実装する方法を調べました。

MVVMフレームワークは、Prismを使用します。

自作のボタンを押して画面を閉じる

自作のボタンを押して画面を閉じるやり方です。これは、以下で紹介されているCommandParameter属性を使用する方法にしました。WindowクラスをViewModelが参照するので、若干気になる部分ではありますが、引数なので良しとし、コードビハインドを使用しないという方向で決めました。

c# - Close Window from ViewModel - Stack Overflow

Windowにx:Nameで名前を定義するのではなく、RelativeSourceを使用して自分の親Windowを取得するようにします。

XAML

<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リポジトリに追加したので、全体像を見たい方はどうぞ。

github.com

以上。