WebSurfer's Home

Filter by APML

Task.Status が Canceled に変わるタイミング

by WebSurfer 21. August 2025 12:41

Microsoft のドキュメント「タスクの一覧を取り消す」のサンプルコードを試して気づいた話ですが、CancellationTokenSource.Cancel メソッドを実行した際、CancellationToken を受け取る非同期メソッドの Task.Status が Canceled に変わるタイミングが .NET Framework 4.8.1 と .NET 8.0 / 9.0 では異なるということがあったので、備忘録として書いておきます。

サンプルコードはこの記事の下の方に載せておきます。ターゲットフレームワークを .NET 8.0 または .NET 9.0 とした場合と、.NET Framework 4.8.1 とした場合のサンプルコードの動きの違いは、ダウンロードの途中で Enter キーを押してキャンセルをかけた時、後者では Main メソッドの中の try - catch ブロックが実行されないことです。

その原因は、タスク cancelTask の中の s_cts.Cancel(); が実行された時、CancellationToken を受け取る非同期メソッドの Task.Status が Canceled に変わるタイミングが異なるからです。

.NET Framework 4.8.1 の場合、Cancel メソッドの実行で即 Task.Status が WaitingForActivation から Canceled に変わります。

一方、.NET 8.0 / 9.0 の場合、Cancel メソッドの実行では Task.Status は WaitingForActivation のまま変わりません。Task を await して初めて Status が Canceled に変わります。

以下にサンプルコードを検証した際の詳細を書いておきます。

.NET Framework 4.8.1

下の画像は、ターゲットフレームワークを .NET Framework 4.8.1 としたプロジェクトを Visual Studio 2022 でデバッグ実行した時のものです。

.NET Framework 4.8.1 の場合

ダウンロード途中で(すなわち、SumPageSizesAsync メソッド内の foreach ループが回っているとき)Enter キーを押した結果、タスク cancelTask の中の s_cts.Cancel(); が実行され、ProcessUrlAsync メソッド内の非同期メソッドの実行がキャンセルされ、Task.WhenAny メソッドでその引数に設定されたタスク cancelTask と sumPageSizesTask の内 sumPageSizesTask が先に完了と判断され、await による待機を抜け、次の行の if 文に制御が移ったところです。

その結果、if 文の条件 finishedTask == cancelTask は false となるので try - catch ブロックは実行されません。その際に気を付けなければならないのは、await sumPageSizesTask; は実行されないので OperationCanceledException はスローされないということです。

なので、下に載せたサンプルコードのように、OperationCanceledException を catch して何らかの処置を行いたい場合、そのためのコードを追加する必要があります。

ただし、ダウンロードが完了してから(すなわち SumPageSizesAsync メソッド内の foreach ループを抜けて CancellationToken を引数に取る非同期メソッドがすべて完了してから)タスク cancelTask の中の s_cts.Cancel(); が実行された場合は様子が違ってきます。タスク cancelTask の方が先に完了したと判断され、try - catch ブロックは実行されます。(実際にそれを試すには、foreach ループの後に 1 行 await Task.Delay(3000); を追加して、そこで止まっているときに Enter キーを押してみてください)

.NET 8.0 / 9.0

ターゲットフレームワークを .NET 8.0 または 9.0 としたプロジェクトで上と同様にデバッグを行うと以下の画像のようになります。

.NET 8.0 の場合

ダウンロード途中で Enter キーを押すとタスク cancelTask 内の s_cts.Cancel(); が実行されるのですが、その時点では ProcessUrlAsync メソッド内の非同期メソッドの実行がキャンセルされないのか、タスク sumPageSizesTask の Status は WaitingForActivation になっています。一方、タスク cancelTask の Status は RanToCompletion となり、Task.WhenAny メソッドでその引数に設定されたタスク cancelTask が先に完了したと判断され、await による待機を抜け、次の行の if 文に制御が移っています。

この場合、if 分の条件 finishedTask == cancelTask は true となるので try - catch ブロックが実行されます。try 句の中の await sumPageSizesTask; により OperationCanceledException がスローされ、catch 句の中のコードが実行されます。

タスク sumPageSizesTask の Status が WaitingForActivation から Canceled に変わるのは try 句の中の await sumPageSizesTask; で await による待機が終わった時点になります。

以下の画像を見てください。上の画像の if 文でブレークポイントで止まったところからステップ実行して、await sumPageSizesTask; でスローされた OperationCanceledException を catch したところです。sumPageSizesTask の Status が Canceled に変わっています。

.NET 8.0 の場合

try 句の中の await sumPageSizesTask; の行で例外がスローされるので、その下の Console.WriteLine("Download task completed before cancel request was processed."); はスキップされます。このメッセージは「Enter キーが押されたがキャンセルが間に合わずダウンロードが完了してしまった」という意味なのですが、手動で Enter キーを押してこのメッセージを表示するのは無理があるので注意してください。

このメッセージが表示されることを試すには、SumPageSizesAsync メソッド内の foreach ループの後に 1 行 await Task.Delay(3000); を追加して、そこで止まっているときに Enter キーを押してみてください。その時点では CancellationToken を渡された非同期メソッドの実行は終わっているので、タスク cancelTask の中の s_cts.Cancel(); の実行では OperationCanceledException 例外はスローされません。結果、await sumPageSizesTask; の下の行の Console.WriteLine("Download task completed before cancel request was processed."); が実行されます。


.NET Framework 4.8.1 の場合の s_cts.Cancel(); の実行で CancellationToken を渡された非同期メソッドの実行が即キャンセルされるという動きの方が自分としては納得できるのですが、何らかの理由で .NET 8.0(もっと前からかも)で変更したのでしょうか?

そのあたりの説明がある Microsoft のドキュメントは見つけられませんでしたが、とにかく違いがあるということは覚えておいた方が良さそうと思って、備忘録として残しておくことにした次第です。


以下に、上に述べた検証に用いた Microsoft ドキュメントのサンプルコードを載せておきます。

using System.Diagnostics;

class Program
{
    static readonly CancellationTokenSource s_cts = new CancellationTokenSource();

    static readonly HttpClient s_client = new HttpClient
    {
        MaxResponseContentBufferSize = 1_000_000
    };

    static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dynamics365",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://learn.microsoft.com/system-center",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            // "https://learn.microsoft.com/maui" 404 エラーになるのでコメントアウト
    };

    static async Task Main()
    {
        Console.WriteLine("Application started.");
        Console.WriteLine("Press the ENTER key to cancel...\n");

        Task cancelTask = Task.Run(() =>
        {
            while (Console.ReadKey().Key != ConsoleKey.Enter)
            {
                Console.WriteLine("Press the ENTER key to cancel...");
            }

            Console.WriteLine("\nENTER key pressed: cancelling downloads.\n");
            s_cts.Cancel();
        });

        Task sumPageSizesTask = SumPageSizesAsync();

        Task finishedTask = await Task.WhenAny(new[] { cancelTask, sumPageSizesTask });
        if (finishedTask == cancelTask)
        {
            // wait for the cancellation to take place:
            try
            {
                await sumPageSizesTask;
                Console.WriteLine("Download task completed before cancel request was processed.");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("Download task has been cancelled.");
            }
        }

        Console.WriteLine("Application ending.");
    }

    static async Task SumPageSizesAsync()
    {
        var stopwatch = Stopwatch.StartNew();

        int total = 0;
        foreach (string url in s_urlList)
        {
            int contentLength = await ProcessUrlAsync(url, s_client, s_cts.Token);
            total += contentLength;
        }

        stopwatch.Stop();

        Console.WriteLine($"\nTotal bytes returned:  {total:#,#}");
        Console.WriteLine($"Elapsed time:          {stopwatch.Elapsed}\n");
    }

    static async Task<int> ProcessUrlAsync(string url, 
                                                 HttpClient client, 
                                                 CancellationToken token)
    {
        HttpResponseMessage response = await client.GetAsync(url, token);

        // .NET Framework 4.8.1 の ReadAsByteArrayAsync には
        // CancellationToken を引数に取るオーバーロードはない
        // ので下の引数 token は削除すること
        byte[] content = await response.Content.ReadAsByteArrayAsync(token);

        Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

        return content.Length;
    }
}

Tags: , , , ,

.NET Framework

GeoCoordinateWatcher で緯度経度を取得

by WebSurfer 24. May 2025 19:13

.NET Framework 4.8/4.8.1 の GeoCoordinateWatcher クラスを使っての緯度経度データ取得について調べたことを備忘録として書いておきます。

Microsoft のドキュメント GeoCoordinateWatcher class の Remarks に書いてありますが、GeoCoordinateWatcher は位置プロバイダから座標データを取得して位置情報を提供するそうです。

位置プロバイダには GPS, Wi-Fi triangulation, cell phone tower triangulation などがあるそうです。cell phone tower triangulation は関係なさそうに思いましたが、ノート PC にはモバイルルーターを使わず直接携帯データネットワーク経由でインターネットにアクセスできるものがあって、そのような PC に搭載されているのかもしれません。(未確認&想像です)

(Microsoft 位置情報サービスは IP アドレスも位置情報を得るための手段の一つとして使うそうですが GeoCoordinateWatcher クラスが使うかどうかは不明です)

複数の位置プロバイダが搭載されている場合は優先付けがされますが、その優先順序は状況に応じて変わり、例えば GPS の電波が届かない場所に移動した場合は Wi-Fi triangulation プロバイダが使われるそうです。

それらのプロバイダのいずれも使えない場合、Windows OS の「既定の場所」から情報を取得できます。Windows 10 の場合[設定]⇒[プライバシー]⇒[位置情報]と進み、下の画像の[規定値に設定]ボタンをクリックして表示される画面で設定します。

既定の場所

ちなみに自分の Windows 10 ノート PC には Wi-Fi しか搭載されていませんので、GeoCoordinateWatcher クラスが使う位置プロバイダは Wi-Fi triangulation のみということになります。

なので、自分が検証できるのは Wi-Fi triangulation プロバイダを使った場合と、Wi-Fi をオフにして「既定の場所」の情報を使った場合だけですが、実際に GeoCoordinateWatcher クラスを使ったコンソールアプリを作って試してみました。アプリのコードはこの記事の下の方に載せておきます。

まず「既定の場所」を設定します。下の画像は[規定値に設定]ボタンをクリックして表示される画面で既定の位置を設定したところです。(Wi-Fi と違う結果になるのがすぐ分かるよう今回の検証では皇居に設定しています)

既定の位置

Wi-Fi をオンにした状態でアプリを走らせたコンソール出力は以下の通りとなりました。(緯度経度は自宅の位置になるので小数点以下を xxxx で隠しています)

Permission: Unknown, Status: NoData
Permission: Granted, Status: Ready
Lat: 35.xxxxxxxxxxxxx, Long: 139.xxxxxxxxxxx

アプリを走らせるたびに計測を行うようで結果は毎回微妙に異なりますが、Google Map に緯度経度を入力して表示されたポイントを見ると計測ごとの差は 10m もありませんでした。

自宅(PC が置いてある場所)の周りに適切な Wi-Fi サービス提供者のアクセスポイントが複数存在するということが精度に影響するのでしょうが、自宅の環境ですとかなり正確に Google Map 上で自宅の位置が特定できてしまいました。悪用されるとかなりヤバそうです。(汗)

ちなみに triangulation は三角測量という意味です。自宅(PC が置いてある場所)の近くに存在する複数の Wi-Fi サービス提供者のアクセスポイントを、PC に搭載された Wi-Fi triangulation プロバイダがスキャンしてその位置を知るためのデータ (MAC アドレスという記事を見ましたが真偽不明) と電波強度を取得し、電波強度からアクセスポイントとの距離を計算し、三角測量により PC の位置を特定するというプロセスだそうです。

Wi-Fi triangulation

そのプロセスの過程でアクセスポイントの位置情報(緯度経度とか)が必要になります。もし仮に、スキャンして得たデータがアクセスポイントの MAC アドレスだとすると、MAC アドレスとアクセスポイントの緯度経度がデータベースに登録してあって、プロバイダはアクセスポイントの緯度経度を得るためにそのデータベースに照会すると言った操作が必要になるはずです。しかし、そのデータベースというのは何で、どこにあって、どのように位置情報を得ているのかは調べても分かりませんでした。

また、自分が試した限りですが、ネットに接続されているか否かは関係なく Wi-Fi がオンになっていれば正確な緯度経度データを取得できました。その時には外部のデータベースにはアクセスできないはずで、どのようにアクセスポイントの位置情報を得たのかは謎です。Windows の位置情報サービスとプライバシーの [場所の履歴] によると過去に取得した位置情報が一定時間 (Windows 10 では 24 時間) ローカルストレージに保存されるそうなので、そこから取得しているのかもしれません。(未検証、未確認です)

次に、Wi-Fi をオフにしてアプリを走らせるとコンソール出力は以下の通りとなります。下の Lat, Long の値を Google Map に入力して表示される場所を見ると「既定の場所」に設定した通り皇居となってました。

Permission: Unknown, Status: NoData
Permission: Granted, Status: Ready
Lat: 35.6839239255279, Long: 139.750550645143

以下に自分が検証に使ったサンプルコードを載せておきます。

using System;
using System.Device.Location;

namespace GeoCoordinateWatcherSample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            GetLocationProperty2();
        }

        static void GetLocationProperty2()
        {
            GeoCoordinateWatcher geoWatcher = new GeoCoordinateWatcher();
            Console.WriteLine($"Permission: {geoWatcher.Permission}, " +
                              $"Status: {geoWatcher.Status}");

            geoWatcher.Start();

            // geoWatcher.Position を取得する時点で Status がまだ NoData
            // の場合は位置情報は取得できないので Ready になるまで待つ。
            // geoWatcher.Permission != GeoPositionPermission.Denied も
            // while の条件に入れないと、位置情報の取得が有効になってな
            // い場合は無限ループに陥るので注意
            while ((geoWatcher.Status != GeoPositionStatus.Ready) &&
                (geoWatcher.Permission != GeoPositionPermission.Denied))
            {
                System.Threading.Thread.Sleep(100);
            }

            if (geoWatcher.Permission == GeoPositionPermission.Denied)
            {
                Console.WriteLine($"Permission: {geoWatcher.Permission}, " +
                                  $"Status: {geoWatcher.Status}");
            }
            else
            {
                Console.WriteLine($"Permission: {geoWatcher.Permission}, " +
                                  $"Status: {geoWatcher.Status}");
                GeoCoordinate coord = geoWatcher.Position.Location;

                if (coord.IsUnknown != true)
                {
                    Console.WriteLine("Lat: {0}, Long: {1}",
                        coord.Latitude,
                        coord.Longitude);
                }
                else
                {
                    Console.WriteLine("Unknown latitude and longitude.");
                }
            }
        }
    }
}

Microsoft のドキュメント GeoCoordinateWatcher Class のサンプルコードはそのままでは動かないので注意してください。(環境によっては動くこともあるようですが)

動かない理由は、GeoCoordinateWatcher.TryStart メソッドでデータの取得が始まり即 true が返されますが、その時点では GeoCoordinateWatcher.Status はまだ NoData だからです。緯度経度データを取得するには TryStart メソッド実行後 Status が Ready になるまで待つ必要があります。

Tags: , ,

.NET Framework

App.Debug.config と App.Release.config を使った構成ファイル書き換え

by WebSurfer 10. November 2024 18:31

.NET Framework 版の ASP.NET Web アプリは構成ファイルに Web.config を使っており、Visual Studio で開発を行う場合は運用環境にデプロイする際の書き換えを定義した Web.Release.config というファイルを作っておき、Visual Studio の発行ツールを使ってのデプロイ時に自動的に書き換えることが可能になっています。

詳しくはMicrosoft のドキュメント「Visual Studio を使用した ASP.NET Web 配置: Web.config ファイル変換」を見てください。

Windows Forms アプリや WPF アプリにはそのような機能は用意されていません。しかし、ネットの記事などを読むと、ASP.NET Web アプリと同様に、書き換え定義ファイルを作って構成ファイルを書き換えることができるそうです。

という訳で、今さらながらですが、Visual Studio 2022 Community のテンプレートを使って作成したターゲットフレームワーク .NET Framework 4.8 の Windows Forms アプリおよび WPF アプリで試してみました。

(注: .NET をターゲットフレームワークにしたアプリでは App.config ではなくて appsettings.json を使うことが推奨されています。その場合の書き換え方法については先の記事「WinForms アプリで構成��報の上書き (CORE)」を見てください)

Windows Forms アプリ、WPF アプリは App.config を構成ファイルのベースに使います。App.config 自体はアプリが使う構成ファイルではないことに注意してください。書き換えを行わない場合は、App.config の内容がそのままコピーされた <プロジェクト名>.exe.config という名前のファイルが作成され、それが構成ファイルになります。

App.Debug.config と App.Release.config は、App.config との差分を指定してデバッグ用とリリース用の構成ファイルを作成するための変換ファイルになります。環境(Debug または Release)ごとに App.config の内容を指定された差分に従って書き換えて <プロジェクト名>.exe.config という名前の構成ファイルを作成し、bin\Debug または bin\Release フォルダに配置するようにします。

以下に手順を書きます。Windows Forms アプリと WPF アプリで同じ手順になります。

(1) App.Debug.config と App.Release.config の追加

App.config は Visual Stidio のテンプレートを使って作成した Windows Forms アプリ、WPF アプリのプロジェクトには最初から含まれています。

それに App.Debug.config と App.Release.config という名前の 2 つのファイルをプロジェクトに追加します。ソリューションエクスプローラーのプロジェクトノードを右クリックし、表示されたコンテキストメニューから[追加(D)]⇒[新しい項目(W)]⇒[アプリケーション構成ファイル]と進んで、App.Debug.config と App.Release.config を追加してください。

追加したらプロジェクトファイルを開いて、

<None Include="App.config" />
<None Include="App.Debug.config" />
<None Include="App.Release.config" />

<None Include="App.config" />
<None Include="App.Debug.config">
  <DependentUpon>App.config</DependentUpon>
</None>
<None Include="App.Release.config">
  <DependentUpon>App.config</DependentUpon>
</None>

に書き換えます。

これによりソリューションエクスプローラーで構成ファイルを見た時、下の画像のように階層表示されるようになります。

App.Debug.config と App.Release.config

なお、これをやらなくても、自分が試した限りですが、書き換えには影響ありませんでした。Visual Studio のソリューションエクスプローラーでの見た目の問題だけなのかもしれません。

(2) App.config の例

今回の例では App.config には以下のように接続文字列と基本情報を配置してみました。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
  </startup>
  
  <connectionStrings>
    <add name="Database"
      connectionString="Data Source=(localdb)\mssqllocaldb;Initial Catalog=Database;Integrated Security=True"
      providerName="System.Data.SqlClient" />
    <add name="MyDB"
      connectionString="Data Source=(localdb)\mssqllocaldb;Initial Catalog=Northwind;Integrated Security=True"
      providerName="System.Data.SqlClient" />
  </connectionStrings>
  <appSettings>
    <add key="FilePath" value="C:\Users\surfe\Documents"/>
    <add key="FileName" value="original.txt"/>
  </appSettings>
</configuration>

(3) App.Debug.config の例

App.Debug.config には App.config の FileName を original.txt から development.txt に変更するための変換定義を書きます。

上にも書きましたが、App.Debug.config と App.Release.config は書き換え方法を指定する XML ファイルです。 変換操作は xdt プレフィックスにマップされる XML-Document-Transform 名前空間で定義されている XML 属性を使用して指定します。

詳しい方法は Microsoft のドキュメント「Web アプリケーション プロジェクト配置の Web.config 変換構文」が参考になると思います。(日本語版は翻訳がアレなので英語版を読むことをお勧めします)

configuration 要素に xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform" という属性を追加し、書き換え方法を指定するコードを追加します。

以下の定義で FileName を original.txt から development.txt に変更します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <appSettings>
    <add key="FileName" value="development.txt"
         xdt:Transform="Replace"
         xdt:Locator="Match(key)"/>
  </appSettings>
</configuration>

(4) App.Release.config の例

App.Release.config には、App.config の接続文字列を本番用に変更し、FileName を release.txt に変更し、さらにリリース版でのみ必要な情報 AdditionalInfo を追加するための変換定義を書きます。

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add name="Database"
      connectionString="Data Source=Release;Initial Catalog=DB1;Integrated Security=True"
      xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
    <add name="MyDB"
      connectionString="Data Source=Release;Initial Catalog=DB2;Integrated Security=True"
      xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
  </connectionStrings>

  <appSettings>
    <add key="FileName" value="release.txt"
      xdt:Transform="SetAttributes" xdt:Locator="Match(key)"/>
    <add key="AdditionalInfo" value="release version specific info"
      xdt:Transform="Insert"/>
  </appSettings>
</configuration>

(5) プロジェクトファイルに書き換え指示を追加

プロジェクトファイルの下の方にある、

<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

の下に以下のコードを追加します。追加する場所に制約があるようで、場所を間違えると変換されないので注意してください。

<UsingTask TaskName="TransformXml"
  AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" Condition="exists('app.$(Configuration).config')">
  <TransformXml Source="app.config"
    Destination="$(IntermediateOutputPath)$(TargetFileName).config"
    Transform="app.$(Configuration).config" />
  <ItemGroup>
    <AppConfigWithTargetPath Remove="app.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
</Target>

上のコードの意味については、この記事の下の方にオマケとして書きましたので興味があれば見てください。

(6) 書き換え結果

以上で、Visual Studio でプロジェクトをビルドすると、構成マネージャーの「構成」(Debug または Release)の設定に応じて、

構成マネージャ

App.config の内容が、変換ファイル App.Debug.config または App.Release.config に従って書き換えられて <プロジェクト名>.exe.config という名前の構成ファイルが作成され、bin\Debug または bin\Release フォルダに 配置されます。下の画像はプロジェクト名が WindowsFormsApp3 という名前の Windows Forms アプリのものです。

bin フォルダの構成ファイル

この記事の例で、bin\Relese フォルダに生成される WindowsFormsApp3.exe.config は以下のようになります。期待通り App.config の内容が App.Release.config に従って書き換えられています。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
  </startup>
  
  <connectionStrings>
    <add name="Database" 
      connectionString="Data Source=Release;Initial Catalog=DB1;Integrated Security=True" 
      providerName="System.Data.SqlClient"/>
    <add name="MyDB" 
      connectionString="Data Source=Release;Initial Catalog=DB2;Integrated Security=True" 
      providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <appSettings>
    <add key="FilePath" value="C:\Users\surfe\Documents"/>
    <add key="FileName" value="release.txt"/>
    <add key="AdditionalInfo" value="release version specific info"/>
  </appSettings>
</configuration>

(7) アプリの実行例

もちろんアプリのコードで構成ファイルから取得する情報には上の書き換え結果が反映されます。例えば、Windows Forms アプリの場合、以下のコードを、

using System.Configuration;
using System.Windows.Forms;

namespace WindowsFormsApp3
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            this.label1.Text = ConfigurationManager
                               .ConnectionStrings["Database"]
                               .ConnectionString;

            this.label2.Text = ConfigurationManager
                               .ConnectionStrings["MyDB"]
                               .ConnectionString;

            this.label3.Text = ConfigurationManager
                               .AppSettings["FilePath"];

            this.label4.Text = ConfigurationManager
                               .AppSettings["FileName"];
            
            this.label5.Text = ConfigurationManager
                               .AppSettings["AdditionalInfo"];
        }
    }
}

Visual Studio の構成マネージャーの「構成」を Release にして実行すると以下の結果になります。

アプリを実行して構成情報を取得


以下はオマケの情報です。上の「(5) プロジェクトファイルに書き換え指示を追加」に書いたコードについて調べていろいろ分かったことがあったので、備忘録として残しておくことにしました。100% 間違いないところまで深く調べたわけではなく、想像と Copilot に聞いた話が混じっていますが。

以下にコードを再掲して説明を書きます。

<UsingTask TaskName="TransformXml"
  AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="AfterCompile" Condition="exists('app.$(Configuration).config')">
  <TransformXml Source="app.config"
    Destination="$(IntermediateOutputPath)$(TargetFileName).config"
    Transform="app.$(Configuration).config" />
  <ItemGroup>
    <AppConfigWithTargetPath Remove="app.config" />
    <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config">
      <TargetPath>$(TargetFileName).config</TargetPath>
    </AppConfigWithTargetPath>
  </ItemGroup>
</Target>

UsingTask の AssemblyFile 属性には書き換えに使うアセンブリ Microsoft.Web.Publishing.Tasks.dll へのパスが指定されており、それを用いて書き換えを行っています。Web という名前のフォルダにあるということは、もともとは ASP.NET Web アプリの web.config を書き換えるのに用いているもののようです。

Target は、変換ファイル app.$(Configuration).config が存在する場合はコンパイル後に書き換えを実行するという設定をしています。$(Configuration) は構成オプションによって Debug または Release になります。

TransformXml は、プロジェクトの App.config ファイルを変換ファイル app.$(Configuration).config をベースに書き換えて、中間出力ディレクトリ(通常 obj\Debug または obj\Release)に $(TargetFileName).config という名前で保存するという設定になっています。

$(Configuration) は構成オプションによって Debug または Release になります。$(TargetFileName) はビルドで生成される exe ファイルの名前で、<プロジェクト名>.exe となります。

ItemGroup の中の最初の AppConfigWithTargetPath Remove="app.config" は最終出力フォルダ(bin\Debug または bin\Release)にオリジナルの App.config が書き込まれないことを確実にするためだそうです。

copilot に聞いた話ですが、AppConfigWithTargetPath は bin\Debug または bin\Release フォルダを意味するわけではないそうです。copilot によると "AppConfigWithTargetPath doesn't directly refer to the bin/Debug or bin/Release folders themselves, but it is related to how the app.config file is processed and used within those output directories." とのことでした。

(自分が AppConfigWithTargetPath Remove="app.config" を削除してビルドして試した限りでは、App.config が bin\Debug または bin\Release に書き込まれることはなかったのですが・・・)

ItemGroup の中の 2 つ目の AppConfigWithTargetPath Include=... は、中間出力ディレクトリに置かれた $(TargetFileName).config ファイルを、最終出力フォルダ(bin\Debug または bin\Release)に <プロジェクト名>.exe.config という名前で配置するという設定になっています。


もう一つオマケを書きます。

x64 Native Tools Command Prompt を使ってプロジェクトファイルの $(MSBuildExtensionsPath) などの値を調べる方法です。

x64 Native Tools Command Prompt は Visual Studio Installer で「C++ によるデスクトップ開発」のワークロードを追加し、インストールの詳細で「最新の v143 ビルドツール用 C++ ATL」と「最新の v143 ビルドツール用 C++ MFC」にチェックを入れてインストールすれば、Windows の「スタート」メニュ��に現れるようになります。

x64 Native Tool

そのツールを使ってプロジェクトファイルの $(MSBuildExtensionsPath) などの値を調べるには、プロジェクトファイルに以下の様なコードを追加します。

<Target Name="PrintProperties">
  <Message Text="MSBuildExtensionsPath="$(MSBuildExtensionsPath)""/>
  <Message Text="VisualStudioVersion="$(VisualStudioVersion)""/>
  <Message Text="Configuration="$(Configuration)""/>
  <Message Text="IntermediateOutputPath="$(IntermediateOutputPath)""/>
  <Message Text="TargetFileName="$(TargetFileName)""/>
  <Message Text="AppConfigWithTargetPath="$(AppConfigWithTargetPath)""/>
</Target>

そして x64 Native Tools Command Prompt を起動し、プロジェクトファイルのあるディレクトリに移動してから、msbuild <プロジェクトファイル名> /t:PrintProperties とタイプして実行すれば、以下のように結果が表示されます。

Command Prompt

Tags: , , , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  November 2025  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar