ASP.NET Core 6でNLogを使用して、開発とプロダクションで別のnlog.configを読み込む方法
まず、以下のチュートリアルに従って、ログ出力の設定を行う。
開発用の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の開発をするには、以下の方法がありますが、どれにしようか困りますね。
- Visual Studio 2022 Previewを使用して、.NET 6 RC1で開発をする
- Visual Studio 2019 を使用して、.NET 6 Preview4で開発をする
- Visual Studio 2019 for Mac を使用して、.NET 6 RC1で開発をする
やはり.NET 5で開発を始めて、途中で.NET 6に移行した方が良い気もしてきました。
以上。
(2021/10/13 追記) .NET 6 RC2が出ましたね。引き続き、VS 2019はサポートされていないため、状況は変わらずです。
(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 を変えずにインストーラーを作成すると、実行時に以下のメッセージが表示される。
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 ですが、含まれるパッケージが変更になったことを、以下のサイト等で知りました。
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なのです。
どうせならば、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()で十分だと思います。
動作環境
- .NET Framework 4.8
- Prism 8.0.0.1909
- Visual Studio 2019 Professional
以上
MSIXアプリケーションをIISで公開する時の設定
MSIXアプリケーションをIISで公開する設定は、公式サイトに説明があります。
必要な部分だけ抜き出すと、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" />
以上