WebSurfer's Home

Filter by APML

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

interface メンバーの「既定の実装」

by WebSurfer 17. January 2024 14:54

C# 8.0 以降で、interface のメンバーに「既定の実装 (default implementation)」を設定できるようになり、それに関連してアクセス修飾子に private, protected, internal などを設定することが可能になりました。

ちなみに、C# 8.0 より前 (.NET Core 3 より前、.NET Framework はすべて) では interface のメンバーのアクセス修飾子は public しか許されておらず、継承する class 側でメンバーを実装する際にもアクセス修飾子に public と明示的に指定する必要がありました。

public 以外が許されなかった理由は、自分が調べたことの要約ですが、以下のようなことと理解しています。 (理由が明確に書いてある Microsoft のドキュメントは見つかりませんでした)

  • class が interface を継承すると、その class が必ず interface に定義されているメンバーを実装して公開するという外部との契約となる。(Interface の仕様に "An interface defines a contract. A class or struct that implements an interface shall adhere to its contract." と書いてあります)
  • 別の言い方をすると、そもそも interface に指定されるメンバーを実装して class を利用する外部に公開するのが目的なのに、private とか protected で隠ぺいするのは理にかなってない。

上記にもかかわらず、C# 8.0 以降で interface のメンバーに private や protected などを設定できるようになったのは何故か、その理由に興味があったので調べてみました。以下に調べたことを備忘録として書いておきます。

調べたことを簡単に書くと、C# 8.0 で interface に「既定の実装」という機能を追加する際に、ついでに public 以外のあらゆるアクセス修飾子を設定できるようにし、「既定の実装」に対するアクセスコントロールを可能にするというのが目的らしいです。

そのあたりのことが書いてあったドキュメントと、関係する部分の抜粋を以下に書いておきます。

  1. アクセス修飾子 (C# プログラミング ガイド) の「その他の型」
    "インターフェイス メンバー宣言には、あらゆるアクセス修飾子を含めることができます。 そのことは、クラスを実装するあらゆるもので必要になる共通実装を静的メソッドから与えるときに最も役に立ちます。Interface member declarations may include any access modifier. This is most useful for static methods to provide common implementations needed by all implementors of a class."
  2. interface (C# リファレンス)
    "インターフェイスによってメンバーの既定の実装を定義できます。 共通の機能を 1 回で実装する目的で static メンバーも定義できます。 An interface may define a default implementation for members. It may also define static members in order to provide a single implementation for common functionality."
  3. default interface methods
    "Add support for virtual extension methods - methods in interfaces with concrete implementations. A class or struct that implements such an interface is required to have a single most specific implementation for the interface method, either implemented by the class or struct, or inherited from its base classes or interfaces."
  4. インターフェイスのデフォルト実装
    "メソッド、プロパティ、インデクサー、イベントのアクセサーの実装を持てるようになった。アクセシビリティを明示的に指定できるようになった。静的メンバーを持てるようになった・・・中略・・・狭義にはこの1番目の機能こそが「デフォルト実装」です。 ただ、これのついでに実装されたものなので2番目、3番目には具体的な名前がついていません"

以下に、interface のメンバーを「既定の実装」とし、アクセス修飾子に public, private, protected, internal を使ったサンプルを載せておきます。説明はコード中にコメントとして書きましたのでそちらを見てください。

namespace ConsoleAppInterface
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Sample sample = new();
            ((ISampleDerived)sample).PublicMethod();
            sample.PublicMethod2();
            sample.Protected3();
            //((ISampleDerived)sample).Protected2();  // アクセス不可
        }
    }

    // C# 8 以降で、interface のメンバーに「既定の実装 (default
    // implementation)」を設定できるようになり、関連してアクセス修飾子に
    // private, protected, internal などを設定することが可能になった
    interface ISample
    {
        // デフォルトで public なのは以前と同じ。なので、アクセス修飾子を付
        // けない場合は public になる
        void Public()
        {
            Console.WriteLine("ISample.Public");
            Private();    // private メンバーにアクセス
        }

        internal void Internal()
        {
            Console.WriteLine("ISample.Internal");
        }

        protected void Protected()
        {
            Console.WriteLine("ISample.Protected");
        }

        // private の場合「既定の実装」は必須。無いと以下のエラー:
        // エラー CS0501 'ISample.Private()' は abstract、extern、または
        // partial に指定されていないため、本体を宣言する必要があります
        private void Private()
        {
            Console.WriteLine("ISample.Private");
        }
    }

    interface ISampleDerived : ISample
    {
        void PublicMethod()
        {
            // interface で interface を継承する場合、継承元の public, 
            // internal, protected メソッドを呼べる。「既定の実装」の有無
            // も関係なく呼べる
            Public();
            Internal();
            Protected();
            // Private();  private はもちろんダメ
        }

        void Default()
        {
            Console.WriteLine("ISampleDerived.Defualt");
        }

        // 派生先から protected メンバーにアクセスできるのは interface
        // だけ。class から呼ぶことはできない。呼ぶとエラーになる。下の
        // Sample の実装の PublicMethod2 メソッド内の説明を参照
        protected void Protected2()
        {
            Console.WriteLine("ISampleDerived.Protected2");
        }

        // 以下のような「既定の実装」がない場合、継承する class 側で実装が
        // 必要。ただし、継承する class 側では public にしないとエラー
        protected void Protected3();
    }

    public class Sample : ISampleDerived
    {
        // 継承元の ISampleDerived 内で「既定の実装」がされているメソッド
        // (この例では PublicMethod, Default, Protected2)は継承するクラ
        // スでの実装が無くてもエラーにならない

        public void PublicMethod2()
        {
            // Default を呼ぶには 1 段キャストが必要。単に Default(); とし
            // たのではエラー
            ((ISampleDerived)this).Default();

            // interface と違って class では protected なものは呼べない。

            //((ISampleDerived)this).Protected2();

            // ・・・とすると以下のエラーとなる:
            // エラー CS1540 'ISampleDerived' 型の修飾子をとおしてプロ
            // テクト メンバー 'ISampleDerived.Protected2()' にアクセスす
            // ることはできません。修飾子は 'Sample' 型、またはそれから派
            // 生したものでなければなりません
        }

        // ISampleDerived に「既定の実装」がない Protected3() があるので
        // class側で実装が必要。

        public void Protected3()
        {
            Console.WriteLine("Sample.Protected3");
        }

        // ただし、アクセス修飾子を public にしないと以下のエラー:
        // エラー CS0737 'Sample' は、インターフェイス メンバー
        // 'ISampleDerived.Protected3()' を実装していません。
        // 'Sample.Protected3()' は public ではないため、インターフェイス
        // メンバーを実装できません。

        // と言って class 側で Protected3 を実装しないと以下のエラー:
        // エラー CS0535 'Sample' はインターフェイス メンバー
        // 'ISampleDerived.Protected3()' を実装しません
    }
}

// 結果は:
// ISample.Public
// ISample.Private
// ISample.Internal
// ISample.Protected
// ISampleDerived.Defualt
// Sample.Protected3

Tags: , ,

.NET Framework

.NET Framework での Dependency Injection

by WebSurfer 13. April 2023 15:12

ASP.NET Core で Dependency Injection (DI) に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類は .NET Framework でもバージョン 4.6.1 以降であれば利用できるそうですので、.NET Framework 4.8 のコンソールアプリで試してみました。

先の記事「.NET Core での Dependency Injection」でターゲットフレームワーク .NET 5.0 のコンソールアプリに DI 機能を実装してみましたが、その .NET Framework 4.8 版です。

まず、Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET Framework 4.8 でコンソースアプリを作成し、NuGet から Microsoft.Extensions.DependencyInjection をインストールします。この記事を書いた時点での最新版 7.0.0 をインストールしました。

NuGet でインストール

他の Micosoft.Bcl.AsyncInterfaces などのパッケージは Microsoft.Extensions.DependencyInjection をインストールした時に同時に自動的にインストールされたものです。

検証に使ったコードは以下の通りです。先の記事「.NET Core での Dependency Injection」のものと同じです。

using Microsoft.Extensions.DependencyInjection;
using System;

namespace ConsoleAppDependencyInjection
{
    internal class Program
    {
        static void Main(string[] args)
        {
            IServiceCollection services = new ServiceCollection();

            services.AddTransient<IOrderRepository, SqlOrderRepository>();
            services.AddSingleton<ILogger, Logger>();
            services.AddScoped<IEventPublisher, EventPublisher>();
            services.AddTransient<CancelOrderHandler>();

            var provider = services.BuildServiceProvider();
            var handler = provider.GetRequiredService<CancelOrderHandler>();

            var orderId = Guid.NewGuid();
            var command = new Order { OrderId = orderId };
            handler.Handle(command);
        }
    }

    public class CancelOrderHandler
    {
        private readonly IOrderRepository repository;
        private readonly ILogger logger;
        private readonly IEventPublisher publisher;

        // Use constructor injection for the dependencies
        public CancelOrderHandler(IOrderRepository repository,
                                  ILogger logger,
                                  IEventPublisher publisher)
        {
            this.repository = repository;
            this.logger = logger;
            this.publisher = publisher;
        }

        public void Handle(Order command)
        {
            this.logger.Log($"Cancelling order {command.OrderId}");
            var order = this.repository.GetById(command.OrderId);
            order.OrderStatus = "Cancelled";
            this.repository.Save(order);
            this.publisher.Publish(order);
        }
    }

    public interface IOrderRepository
    {
        Order GetById(Guid orderId);

        void Save(Order order);
    }

    public class SqlOrderRepository : IOrderRepository
    {
        private readonly ILogger logger;

        // Use constructor injection for the dependencies
        public SqlOrderRepository(ILogger logger)
        {
            this.logger = logger;
        }

        public Order GetById(Guid orderId)
        {
            this.logger.Log($"Getting Order {orderId}");

            // Retrieve from db.・・・のつもり
            var order = new Order
            {
                OrderId = orderId,
                ProductName = "911-GT3",
                OrderStatus = "Ordered"
            };

            return order;
        }

        public void Save(Order order)
        {
            this.logger.Log($"Saving order {order.OrderId}");
            // Save to db.
        }
    }

    public interface ILogger
    {
        void Log(string log);
    }

    public class Logger : ILogger
    {
        public void Log(string log)
        {
            Console.WriteLine(log);
        }
    }

    public interface IEventPublisher
    {
        void Publish(Order order);
    }

    public class EventPublisher : IEventPublisher
    {
        public void Publish(Order order)
        {
            Console.WriteLine($"Publish order {order.OrderId}, " +
                $"{order.ProductName}, {order.OrderStatus}");
        }
    }

    public class Order
    {
        public Guid OrderId { get; set; }

        public string ProductName { get; set; }

        public string OrderStatus { get; set; }
    }
}

実行結果は以下の画像のようになります。先の記事の .NET 5.0 版と同様に期待した結果になっています。

実行結果

Tags: ,

.NET Framework

About this blog

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

Calendar

<<  February 2025  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
242526272812
3456789

View posts in large calendar