redwarrior’s diary

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

ASP.NET Core 6でNLogを使用して、開発とプロダクションで別のnlog.configを読み込む方法

まず、以下のチュートリアルに従って、ログ出力の設定を行う。

github.com

開発用のnlog.configを読み込む

開発時の設定を記した「nlog.Development.config」を作成すれば良い。

Developmentは、環境変数 ASPNETCORE_ENVIRONMENT に設定した値。

プロダクション用のnlog.configを読み込む

元からあるnlog.configにプロダクション用の値を設定する。

仕組み

チュートリアルにある以下の行の LoadConfigurationFromAppSettings メソッドで上手いこと処理されているようだ。

var logger = NLog.LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();

(小ネタ).NET 6.0 Preview 5以降は、Visual Studio 2019 (Windows版)をサポートしていない

またもや、6か月も間が空いてしまいました。もう少しこまめに書けるようにならないとですね。

久しぶりなので小ネタを書きます。

.NET 6 のリリースがたぶん11月頃だと思いますが、今の時期に新しいプロジェクトが始まりました。

そこで悩むのが.NETのバージョンです。.NET 6はLTSバージョンなので、最終的には .NET 6のプロジェクトにしたいところです。 Visual Studio 2022は、まだプレビュー版なので、Visual Studio 2019で開発したい。

.NET 5のプロジェクトテンプレートで作成して、リリース後に.NET 6に移行するのが思いつきますが、5から6の変更に多少は対応が必要なんだろうなと思います。

それならば、.NET 6プレビュー版で開発を開始して、リリース後に置き換えた方が変更点が少ないのではないかと考えました。

VS 2022 Previewはインストールしてあったので、インストールされている.NET SDKを確認したところ、.NET 6.0 RC1がありました。 しかし、VS 2019 のプロジェクトテンプレートの選択肢には、.NET 6が出てきません。

ネットを検索すると、VS 2019の設定でプレビュー機能のチェックをつけると書いてあったので、試してみるも、表示されず。

Download .NET 6.0 (Linux, macOS, and Windows) を見ていると、なんとサポートバージョンに、VS 2019 がありません。 バージョンをさかのぼって行くと、Preview 5からサポートされていなかったようです。

参考にしたサイトを見返してみると、そちらはPreview 4でした。

という事で、現時点で.NET 6の開発をするには、以下の方法がありますが、どれにしようか困りますね。

やはり.NET 5で開発を始めて、途中で.NET 6に移行した方が良い気もしてきました。
以上。

(2021/10/13 追記) .NET 6 RC2が出ましたね。引き続き、VS 2019はサポートされていないため、状況は変わらずです。

opcdiary.net

(2021/10/14 追記) Visual Studio 2022のRC版が出ましたね。実務に使えるとの事なので、Visual Studio 2022 RCを使用して、.NET 6 RC2で開発を始めることにします。

Visual Studio 2022が11月8日にリリース - PC Watch

「Visual Studio 2022」は11月8日に一般公開 ~Go-Liveライセンス付きのRC版が公開 - 窓の杜

(小ネタ)Visual Studio Installer Projectsを使用し、アップデートに対応したインストーラーを作成する

Visual Studio Installer Projects拡張機能を使用すると、インストーラーを作成するためのプロジェクト(Setupプロジェクト)を作成できるようになる。

アプリケーションを更新するたびに、古いバージョンをアンインストールして、新しいバージョンをインストールするのは、ユーザーにとって手間なので、アップデートに対応したインストーラーを作成したい。

インターネットで調べるとやり方はヒットするのだが、なぜそのやり方になるのかという理由がなかなか見つからなかったので、調べた結果を残しておく。

アップデートの分類

デスクトップアプリケーションのアップデートは、3種類(Small Update、Minor Upgrade、Major Upgrade)に分類されている。

参考サイト:セットアッププロジェクトによるアップデート - .NET Tips (VB.NET,C#...)

アプリケーションのバージョンアップと言われたら、大体Minor Upgradeか、Small Updateを想定するだろう。

Major Upgrade以外は非対応

しかし、様々なサイトを見てみると、Major Upgradeのやり方ばかり書いてある。

これはなぜかというと、Setupプロジェクトで作成できるインストーラーは、ProductCode が同じままバージョンアップする方法(Minor Upgrade/Small Update)に対応していないからである。

参考サイト:Re[2]: msi作成について

ProductCode を変えずにインストーラーを作成すると、実行時に以下のメッセージが表示される。

f:id:redwarrior:20210216104807p:plain

Major Upgradeの設定のコツ

Major Upgradeは過去のバージョンと共存できることを想定しているため、同じフォルダを指定してインストールをしても、「プログラムと機能」に古いバージョンと新しいバージョンの両方のアプリケーションが表示されてしまう。

このため、SetupプロジェクトのRemovePreviousVersionsプロパティをTrueにして、古いバージョンをアンインストールすることで、アップデートしたように見せている。

以上

Dispatcher.InvokeとDispatcher.BeginInvoke、Dispatcher.InvokeAsyncの違い

WPFアプリケーションで、別スレッドから画面を更新したい場合は、DispatcherクラスのInvokeメソッドに、引数としてメソッドを渡して、UIスレッドで実行されるようにします。

ググってみると、DispatcherクラスにはInvokeメソッドの他に、BeginInvokeメソッド、InvokeAsyncメソッドがあるようです。

これらの違いが気になったので調べてみました。

Dispatcher.InvokeとDispatcher.BeginInvoke、Dispatcher.InvokeAsyncの違い

詳しく説明されているサイトがありました。

Learn more about how WPF Dispatcher works.(Invoke and InvokeAsync) | Neal's Blog

このサイトによると、Invokeが同期処理で、BeginInvokeとInvokeAsyncが非同期処理であり、InvokeAsyncが.NET Framework 4.5 で追加された新しいメソッドであるらしいです。

現在ではBeginInvokeとInvokeAsyncの実装は同じなので、InvokeAsyncを使うと良いそうです。

違いを確認する

文章で理解しただけだと「(* ̄- ̄)ふ~ん 」となって終わるので、コードを書いて確かめてみました。

MainWindow.xaml

<Window x:Class="DispatcherTestApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DispatcherTestApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="300">
    <StackPanel>
        <ListBox>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click" Width="140" Margin="10">Dispatcher.Invoke</Button>
                    <TextBlock Name="Text" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・①</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_1" Width="140" Margin="10">Dispatcher.BeginInvoke</Button>
                    <TextBlock Name="Text1" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・②</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_2" Width="140" Margin="10">Dispatcher.InvokeAsync</Button>
                    <TextBlock Name="Text2" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・③</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_3" Width="140" Margin="10">NoneDispatcher</Button>
                    <TextBlock Name="Text3" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・④</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_4" Width="140" Margin="10">await InvokeAsync</Button>
                    <TextBlock Name="Text4" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・⑤</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_5" Width="140" Margin="10">return value</Button>
                    <TextBlock Name="Text5" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・⑥</TextBlock>
                </StackPanel>
            </ListBoxItem>
        </ListBox>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace DispatcherTestApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly Stopwatch _stopwatch;

        public MainWindow()
        {
            _stopwatch = new Stopwatch();
            InitializeComponent();
        }

        #region Dispatcher.Invoke
        //①
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Text.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction);
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);
            //Text.Text = "456"; <--エラーになる

            Dispatcher.Invoke(ChangeText);

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region Dispatcher.BeginInvoke
        //②
        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            Text1.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction1);
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction1()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            Dispatcher.BeginInvoke(new Action(ChangeText1));

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText1()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text1.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region Dispatcher.InvokeAsync
        //③
        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            Text2.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction2);
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction2()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            Dispatcher.InvokeAsync(ChangeText2);

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText2()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text2.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region NoneDispatcher
        //④
        private async void Button_Click_3(object sender, RoutedEventArgs e)
        {
            Text3.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            await Task.Run(HeavyAction3);

            ChangeText3();

            //Console.WriteLine($"{stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction3()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            //Dispatcher.InvokeAsync(ChangeText3);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText3()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text3.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region await Dispatcher.InvokeAsync
        //⑤
        private void Button_Click_4(object sender, RoutedEventArgs e)
        {
            Text4.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction4);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private async Task HeavyAction4()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            await Dispatcher.InvokeAsync(ChangeText4);

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText4()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text4.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region return value
        //⑥
        private void Button_Click_5(object sender, RoutedEventArgs e)
        {
            Text5.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction5);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private async Task HeavyAction5()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            var result = await Dispatcher.InvokeAsync(ChangeText5);

            Thread.Sleep(100);

            Console.WriteLine(result
                ? $"{_stopwatch.ElapsedMilliseconds} action result=true"
                : $"{_stopwatch.ElapsedMilliseconds} action result=false");

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private bool ChangeText5()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text5.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");

            return _stopwatch.ElapsedMilliseconds % 2 == 0;
        }
        #endregion
    }
}

Dispatcher.Invoke

Dispatcher.Invokeは、同期処理です。サンプルは①です。このメソッドを基準として他のメソッドを確認していきます。
実行結果は以下の通りです。呼び出し順に処理が行われています。

0 event start
4 event end
6 action start
3007 ui change start
6007 ui change end
6108 action end

Dispatcher.BeginInvoke

Dispatcher.BeginInvokeは、非同期処理です。サンプルは②です。Delegateしか渡せないので、呼び出すときにわざわざActionをnewしなければなりません。
実行結果は以下の通りです。UI更新処理を呼び出した後も処理は続いています。

0 event start
1 event end
2 action start
3003 ui change start
3104 action end
6004 ui change end

Dispatcher.InvokeAsync

Dispatcher.InvokeAsyncは、非同期処理です。サンプルは③です。メソッドを直接渡しています。
実行結果は以下の通りです。流れはDispatcher.BeginInvokeと同じです。

0 event start
1 event end
2 action start
3003 ui change start
3103 action end
6004 ui change end

await Dispatcher.InvokeAsync

Dispatcher.BeginInvoke、Dispatcher.InvokeAsyncの返り値のDispatcherOperationクラスは、GetAwaiterメソッドを持っているので、awaitをつけることが出来ます。サンプルは⑤です。
実行結果は以下の通りです。awaitをつけて呼び出すと、渡したメソッドの実行終了後に、戻って後続の処理を実行するため、流れはDispatcher.Invokeと同じになります。

0 event start
1 event end
7 action start
3008 ui change start
6009 ui change end
6111 action end

return value

↑でawaitをつけると、Dispatcher.Invokeと同じ処理順になることがわかりましたが、awaitが必要になる場面もあります。 Dispatcher.InvokeAsyncは、Func<TResult>を渡すことができるので、awaitで渡したメソッドの返り値を取得することが出来ます。サンプルは⑥です。
実行結果は以下の通りです。画面更新メソッドの返り値によって、後続の処理の分岐をしています。

0 event start
1 event end
8 action start
3017 ui change start
6019 ui change end
6121 action result=false
6121 action end

まとめと方針

今までの内容をまとめて考えた結果、以下の方針で使ったら良いかなと思いました。

  • 画面更新が同期処理で問題なければ、Dispatcher.Invokeを使用する
  • 画面更新を非同期にしたければ、Dispatcher.InvokeAsyncを使用する
  • 画面更新を行うメソッドの返り値を使用したい場合は、Dispatcher.InvokeAsyncを使用する

補足

以下のサイトによると、例外の扱い方が違うらしいですが、画面更新で例外が発生する事はあまりないと思うので、こちらは確かめていません。

WPF Dispatcher.BeginInvokeとDispatcher.InvokeAsyncの違い - reflux flow

WPF Dispatcher BeginInvoke vs. InvokeAsync · jbe2277/waf Wiki · GitHub

(番外編)Dispatcherを使用せずに、画面を更新する

Dispatcherを使用せずに、画面を更新する方法がいくつかあるので、書いておきます。

awaitを使用して、別スレッドからの画面の更新をしない

別スレッドの処理をする場合に、Task.Run等を使うと思いますが、Task.Runの前にawaitを記述して、画面の更新処理を後ろの行に記述します。サンプルの④の処理です。

これで別スレッドの処理が終わってから、メインスレッド(UIスレッド)に戻ってくるので、画面の更新をしても例外は発生しません。

Prism等を使用して、Data Bindingによって画面を更新する

ViewModelの更新→Data BindingによるViewの更新という仕組みを使えば、別スレッドでViewModelを変更したとしても、直接画面のコントロールを触らないため、例外は発生しません。サンプルはありません。

以上です。

Prism のバージョンと Xaml.Behaviors.Wpf、System.Windows.Interactivity の対応状況を調査した

過去記事の Prismを使用したWPFアプリケーションのまとまったサンプルを作ってみた - redwarrior’s diary や、 WPFアプリのMainWindowの終了のキャンセルを、MVVMフレームワークを使用して実装する - redwarrior’s diary で使用している、prism:InvokeCommandAction ですが、含まれるパッケージが変更になったことを、以下のサイト等で知りました。

elf-mission.net

Prism の最新版を使うのであれば、パッケージを変更すれば良いのですが、諸事情により少し前のバージョンを使用する事になりました。

その時に、Prismのバージョンは下げたのですが、パッケージを戻すことを忘れてしまい、prism:InvokeCommandAction でエラーが発生して解決に時間をかけてしまったので、
後で見返せるようにPrismのバージョンと、各パッケージの関係をまとめました。

バージョン 名前空間 prism: InvokeCommandAction 必要なパッケージ
Prism 7.1 http://schemas.microsoft.com/expression/2010/interactivity OK なし(同梱)
http://schemas.microsoft.com/xaml/behaviors NG  
Prism 7.2 http://schemas.microsoft.com/expression/2010/interactivity OK なし(同梱)
http://schemas.microsoft.com/xaml/behaviors NG  
Prism 8 http://schemas.microsoft.com/expression/2010/interactivity NG  
http://schemas.microsoft.com/xaml/behaviors OK なし(同梱)

Prism 8 で Xaml.Behaviors.Wpf パッケージに置き換わったようですね。

以上

ProgressBarの表示をMVVMアーキテクチャで実装する

ProgressBarの表示って、非同期処理が入るので、意外と難しいですよね。

表示の仕方は、以下のサイトで分かりやすく説明されているのですが、素のWPFなのです。

anderson02.com

どうせならば、MVVMアーキテクチャで実装したいなと思いやってみました。

まず、Visual Studioに「Prism Template Pack拡張機能をインストールします。

次に、「Prism Blank App」プロジェクトテンプレートを使用して、プロジェクトを作成します。

そして、ProgressBarコントロールと実行ボタンをXAMLに書きます。

MainWindow.xaml

<Window x:Class="PrismProgressBarApp3.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="{Binding Title}" Height="350" Width="525">
    <Grid>
        <StackPanel Orientation="Horizontal" Grid.Row="0">
            <ProgressBar Height="30" Width="300" Margin="80,0,0,0"
                         Minimum="0" Maximum="100"
                         Value="{Binding ProgressValue}" />
            <Button Height="30" Width="60" Margin="30,0,0,0" Content="Run"
                    Command="{Binding RunCommand}" />
        </StackPanel>
    </Grid>
</Window>

さらに、ProgressBarとBindingする値や、Runボタンをクリックした時のコマンドをViewModelに作成します。

MainWindowViewModel.cs

using Prism.Commands;
using Prism.Mvvm;

namespace PrismProgressBarApp3.ViewModels
{
    public class MainWindowViewModel : BindableBase
    {
        private string _title = "Prism Application";
        private int _progressValue;

        public string Title
        {
            get => _title;
            set => SetProperty(ref _title, value);
        }

        public int ProgressValue
        {
            get => _progressValue;
            set => SetProperty(ref _progressValue, value);
        }

        public DelegateCommand RunCommand { get; set; }

        public MainWindowViewModel()
        {
            RunCommand = new DelegateCommand(ExecuteMethod);
        }

        private void ExecuteMethod()
        {
            throw new System.NotImplementedException();
        }
    }
}

さて、これでExecuteMethodメソッドで、ProgressValueプロパティの値を増やす処理を書けば進捗状況が更新されていきます。

ただし、上記サイトの例でもそうですが、ProgressValueの値の更新は非同期に行う必要があります。さらに、非同期処理の中では、コントロールを操作できないため、UIスレッドで変更する必要がありました。

しかし、MVVMアーキテクチャで実装すると、片方の問題は気にしなくて済みました。

private void ExecuteMethod()
{
    Task.Run(() =>
    {
        for (var i = 0; i < 10; i++)
        {
            Thread.Sleep(500);
            ProgressValue += 10;
        }
    });
}

Task.Run()で別スレッドにするのですが、データバインディングによるUIへの反映がUIスレッドで行われるため、Dispatcherを使う必要がなく、簡単に作成できました。

最後に、スリープのためにTask.Delay()を使ったりすると、await/asyncの知識が必要になるため、今回の用途ではThread.Sleep()で十分だと思います。

なお、ソースコードは、GitHubで公開しています。

github.com

動作環境

以上

MSIXアプリケーションをIISで公開する時の設定

MSIXアプリケーションをIISで公開する設定は、公式サイトに説明があります。

docs.microsoft.com

必要な部分だけ抜き出すと、IISで公開するためにはMIMEの構成を追加します。MSIXアプリケーションのフォルダの親フォルダに web.config を作成し、以下を記述します。

<system.webServer>
    <!--This is to allow the web server to serve resources with the appropriate file extension-->
    <staticContent>
      <mimeMap fileExtension=".appx" mimeType="application/appx" />
      <mimeMap fileExtension=".msix" mimeType="application/msix" />
      <mimeMap fileExtension=".appxbundle" mimeType="application/appxbundle" />
      <mimeMap fileExtension=".msixbundle" mimeType="application/msixbundle" />
      <mimeMap fileExtension=".appinstaller" mimeType="application/appinstaller" />
    </staticContent>
</system.webServer>

後はMSIXアプリケーションのフォルダにある index.html を開いてインストールが出来ます。

補足

テスト用に自己署名証明書を使用している場合は、公開証明書をインストールする場合があります。

上記の設定だけだと、アプリケーションのインストール画面で、公開者証明書をクリックしたら、404になってしまったので、以下も追加する必要がありました。

<mimeMap fileExtension=".cer" mimeType="application/pkix-cert" />

以上