【C#】コンソールアプリでMessagePipeを使う

前回コンソールアプリ上で「Microsoft.Extensions.DependencyInjection」を使用した DI を環境を構築しましたが、今回は、この環境を使って Cysharp がリリースしているメッセージングライブラリの「MessagePipe」をコンソールアプリに導入して動作を確認します。

MessagePipe はメッセージングライブラリと書きましたが、簡単に説明すると、お互いに依存関係の無い別のオブジェクトに対してブロードキャスト的にイベントを発行すための仕組みを提供するライブラリです。

特に、同じプロセス内で使用する分にはかなり簡単にメッセージを飛ばすことができます。特に、Unity などで使用する場合ゲームは1つのプロセス内で動くことになるので簡単にメッセージをクラスやコンポーネント間などでやり取りすることができます。

確認環境

動作確認環境は以下の通りです。

  • VisualStudio2022
  • .NET6 + C#10.0
  • Microsoft.Extensions.DependencyInjection 6.0.0
  • Install-Package MessagePipe -Version 1.7.4

環境作成

パッケージマネージャーコンソールで以下コマンドを打ちます。

// メニュー
ツール > NuGet パッケージ マネージャー > パッケージ マネージャー コンソール

// コマンド(1)
// https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection
> Install-Package Microsoft.Extensions.DependencyInjection -Version 6.0.0

// コマンド(2)
// https://www.nuget.org/packages/MessagePipe/
> Install-Package MessagePipe -Version 1.7.4

これで、DI 用の「ServiceCollection」と、MessagePipe 用の「IPublisher」クラスが使用可能となります。

動作確認

MessagePipe と検証用の実装の登場人物は以下の通りです。

名前 説明
IPublisher<T> メッセージを発行するインターフェース
ISubscriber<T> メッセージを受け取るためのインターフェース
EventData 各<T>でメッセージでやり取りするデータ
Service IPublisher を使ってメッセージを発行するクラス
Client ISubscriber を使ってメッセージを受信するクラス

まずは、イベントで受け渡す型を以下の通り定義します。

// EventData.cs

// 受け渡し用のデータクラス
public readonly struct EventData
{
    // データの種類
    public readonly EventType Type;
    // データの値
    public readonly int Value;

    public EventData(EventType eventType, int value)
    {
        Type = eventType;
        Value = value;
    }

    // フィルター用の型
    public enum EventType
    {
        Type1,
        Type2,
    }
}

次に、イベントの発行側と受け取り側を以下の通り実装します。

IPublisher と ISubscriber は DI のコンストラクタインジェクションで受け取るようにします。

// イベント発行側の実装
public class Service
{
    readonly IPublisher<EventData> _publisher;

    public Service(IPublisher<EventData> publisher) // コンストラクタインジェクションで受け取り
    {
        _publisher = publisher;
    }

    public void Send()
    {
        _publisher.Publish(new EventData(EventData.EventType.Type2, 100));
    }
}

// イベント受け取り側の実装
public class Client : IDisposable
{
    readonly ISubscriber<EventData> _subscriber;
    readonly DisposableBagBuilder _bag = DisposableBag.CreateBuilder();

    public Client(ISubscriber<EventData> subscriber) // コンストラクタインジェクションで受け取り
    {
        _subscriber = subscriber;

        // ★全てのイベントを受け取る
        subscriber.Subscribe(OnEvent).AddTo(_bag);

        // ★条件に一致したイベントだけ受け取る
        subscriber.Subscribe(OnEvent,
            data => data.Type == EventData.EventType.Type2).AddTo(_bag);
    }

    // オブジェクトの開放処理
    public void Dispose()
    {
        using (_bag.Build()) { }
        GC.SuppressFinalize(this);
    }

    public void OnEvent(EventData data)
    {
        Console.WriteLine($"OnEvent. Type={data.Type} Value={data.Value}");
    }
}

実際に使用するときは、複数の種類のメッセージを扱うと思いますが、メッセージの種類は、(1) 違う型を使用する、(2) 同じ型だけど内容でフィルターするのどちらかが選択できます。(2) の実装の場合★の個所で Subscribe の第二引数に条件を記述することで、内容によって受信するかしないかをフィルターすることができます。

最後に、DI の設定とオブジェクトの使用をメインメソッドに以下の通り実装します。

using MessagePipe;
using Microsoft.Extensions.DependencyInjection;

internal class AppMain
{
    static readonly IServiceCollection _services = new ServiceCollection();
    static ServiceProvider _provider;
    
    private static void Main(string[] args)
    {
        // DIコンテナにMessagePipeの準備
        _services.AddMessagePipe();

        // DIコンテナに送受信するオブジェクトの準備
        _services.AddSingleton<Service>();
        _services.AddSingleton<Client>();

        // 依存関係を解決してくれるオブジェクトを取得
        _provider = _services.BuildServiceProvider();

        // 生成目的に読み捨て
        _provider.GetRequiredService<Client>();

        // 動作確認
        var service = _provider.GetRequiredService<Service>();
        service.Send();
        // Serviceでメッセージを発行するとClientが受信して以下メッセージが表示される
        // >OnEvent. Value=100
    }
}

これで以上です。DI は各実行環境で違うと思うので適宜変更する必要がありますが、メッセージを送信 → 受信するだけならこれで実装できます。