Mediator - ソース ジェネレーターを使用した .NET でのメディエーター パターンの高パフォーマンス実装。

(A high performance implementation of Mediator pattern in .NET using source generators.)

Created at: 2021-03-31 20:30:30
Language: C#
License: MIT

GitHub ワークフローのステータス
抽象化 NuGet current ソースジェネレータNuGet current
抽象化 NuGet プレリリース ソースジェネレータ NuGet プレリリース

仲立ち

これは、.NET 5 で導入されたソース ジェネレーター機能を使用したメディエーター パターンの高パフォーマンスの .NET 実装です。 APIと使用法は主に優れたMediatRライブラリに基づいており、パフォーマンスを向上させるためにいくつかの逸脱があります。 パッケージは .NET Standard 2.1 と互換性があります。

メディエーターパターンは、横断的な懸念(ロギング、メトリックなど)を実装し、多くのサービスが注入されるために「太った」コンストラクタを回避するのに最適です。

このライブラリの目標

  • 高性能
    • ランタイムパフォーマンスは、ランタイムリフレクションとソースジェネレータベースのアプローチの両方で同じですが、後者の場合は最適化が簡単です
  • AOTフレンドリー
    • MSはさまざまなAOTシナリオに時間を費やしており、たとえばiOSではAOTコンパイルが必要です
  • ランタイムエラーの代わりにビルド時エラー
    • ジェネレータには診断機能が含まれており、要求に対してハンドラが定義されていない場合、警告が発せられます。

特に、このライブラリのソースジェネレータは、

  • DI 登録用のコードを生成する
  • 実装用のコードを生成する
    IMediator
    • 要求/コマンド/クエリメソッドは単形式化され(Tあたり1メソッド)、ジェネリックメソッドはこれらに依存します
      Send
      ISender.Send
    • あなたはボットハンドを使うことができます、後者はより良いパフォーマンスを可能にします
      IMediator
      Mediator
  • 診断関連のメッセージとメッセージ ハンドラーを生成する

手記

私は現在メディエーターの2.0バージョンに取り組んでいますが、この時点でプレビューリリースを使用することをお勧めします。このバージョンは現在プレビュー段階であり、多くの改善が含まれています。


目次

2. ベンチマーク

このベンチマークは、ライブラリのパフォーマンス オーバーヘッドを公開します。 メディエーター (このライブラリ) メソッドと MediatR メソッドは、それぞれのメディエーター実装のオーバーヘッドを示します。 また、MessagePipeライブラリも優れたパフォーマンスを備えているため、含めました。

  • <SendRequest | Stream>_Baseline
    : ハンドラクラスへの単純なメソッド呼び出し
  • <SendRequest | Stream>_Mediator
    : このライブラリによって生成された具象クラス
    Mediator
  • <SendRequest | Stream>_MessagePipe
    :メッセージパイプライブラリ
  • <SendRequest | Stream>_IMediator
    : このライブラリのインターフェイスを介して呼び出します
    IMediator
  • <SendRequest | Stream>_MediatR
    :MediatRライブラリ

測定の詳細については、ベンチマークコードを参照してください。

要求のベンチマーク

ストリームベンチマーク

3. 使用法と抽象化

このライブラリを使用するために必要な NuGet パッケージは 2 つあります。

  • Mediator.SourceGenerator
    • 実装と依存関係の挿入のセットアップを生成します。
      IMediator
  • 仲立ち
    • メッセージ型 (,)、ハンドラー型 (,)、パイプライン型 (
      IRequest<,>
      INotification
      IRequestHandler<,>
      INotificationHandler<>
      IPipelineBehavior
      )

ソースジェネレータパッケージをエッジ/最も外側のプロジェクト(つまり、コアアプリケーション、バックグラウンドワーカープロジェクト)ASP.NET インストールします。 次に、メッセージ型とハンドラーを定義する場所でパッケージを使用します。 標準メッセージ ハンドラーは自動的に取得され、生成されたメソッドの DI コンテナーに追加されます。パイプラインの動作は手動で追加する必要があります。

Mediator
AddMediator

実装例については、/samplesフォルダーを参照してください。 より現実的な設定については、ASP.NET サンプルを参照してください。

3.1. メッセージタイプ

  • IMessage
    - マーカーインターフェース
  • IStreamMessage
    - マーカーインターフェース
  • IBaseRequest
    - リクエストのための市場インターフェース
  • IRequest
    - 要求メッセージ、戻り値なし (
    ValueTask<Unit>
    )
  • IRequest<out TResponse>
    - 応答付きの要求メッセージ (
    ValueTask<TResponse>
    )
  • IStreamRequest<out TResponse>
    - ストリーミング応答を含む要求メッセージ (
    IAsyncEnumerable<TResponse>
    )
  • IBaseCommand
    - コマンドのマーカーインターフェイス
  • ICommand
    - コマンド メッセージ、戻り値なし (
    ValueTask<Unit>
    )
  • ICommand<out TResponse>
    - 応答付きのコマンド・メッセージ (
    ValueTask<TResponse>
    )
  • IStreamCommand<out TResponse>
    - ストリーミング応答を含むコマンド メッセージ (
    IAsyncEnumerable<TResponse>
    )
  • IBaseQuery
    - クエリ用のマーカーインターフェイス
  • IQuery<out TResponse>
    - 応答を含むクエリ メッセージ (
    ValueTask<TResponse>
    )
  • IStreamQuery<out TResponse>
    - ストリーミング応答を含むクエリ メッセージ (
    IAsyncEnumerable<TResponse>
    )
  • INotification
    - 通知メッセージ、戻り値なし (
    ValueTask
    )

ご覧のとおり、リクエスト、コマンド、クエリでもまったく同じことを実現できます。ただし、たとえば CQRS パターンを使用する場合や、何らかの理由でアプリケーションで名前付けを優先する場合など、名前付けの区別は便利です。

3.2. ハンドラ型

  • IRequestHandler<in TRequest>
  • IRequestHandler<in TRequest, TResponse>
  • IStreamRequestHandler<in TRequest, out TResponse>
  • ICommandHandler<in TCommand>
  • ICommandHandler<in TCommand, TResponse>
  • IStreamCommandHandler<in TCommand, out TResponse>
  • IQueryHandler<in TQuery, TResponse>
  • IStreamQueryHandler<in TQuery, out TResponse>
  • INotificationHandler<in TNotification>

これらの型は、上記のメッセージ型と相関して使用されます。

3.3. パイプラインタイプ

  • IPipelineBehavior<TMessage, TResponse>
  • IStreamPipelineBehavior<TMessage, TResponse>
public sealed class GenericHandler<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
    where TMessage : IMessage
{
    public ValueTask<TResponse> Handle(TMessage message, CancellationToken cancellationToken, MessageHandlerDelegate<TMessage, TResponse> next)
    {
        // ...
        return next(message, cancellationToken);
    }
}

public sealed class GenericStreamHandler<TMessage, TResponse> : IStreamPipelineBehavior<TMessage, TResponse>
    where TMessage : IStreamMessage
{
    public IAsyncEnumerable<TResponse> Handle(TMessage message, CancellationToken cancellationToken, StreamHandlerDelegate<TMessage, TResponse> next)
    {
        // ...
        return next(message, cancellationToken);
    }
}

3.4. 設定

メディエーターを構成する方法は 2 つあります。これはソースジェネレータであるため、コンパイル時に構成値が必要です。

  • コンフィギュレーションのアセンブリ レベル属性:
    MediatorOptionsAttribute
  • オプション構成デリゲートの関数がありません。
    AddMediator
services.AddMediator(options =>
{
    options.Namespace = "SimpleConsole.Mediator";
    options.DefaultServiceLifetime = ServiceLifetime.Transient;
});

// or

[assembly: MediatorOptions(Namespace = "SimpleConsole.Mediator", DefaultServiceLifetime = ServiceLifetime.Transient)]
  • Namespace
    - 実装が生成される場所
    IMediator
  • DefaultServiceLifetime
    - DI サービスの有効期間
    • Singleton
      - (デフォルト値)シングルトンとして登録されたすべてのもの、最小割り当て
    • Transient
      - 一時ハンドラ、///まだシングルトンとして登録されているハンドラ
      IMediator
      Mediator
      ISender
      IPublisher
    • Scoped
      - スコープとして登録されたメディエーターとハンドラー

4. はじめに

このセクションでは、メディエーターの使用を開始し、サンプルを確認します アプリケーションでメディエーター パターンを使用できるさまざまな方法を示します。

実行可能な完全なサンプル コードについては、SimpleEndToEnd サンプルを参照してください。

4.1. パッケージの追加

dotnet add package Mediator.SourceGenerator --version 1.0.*
dotnet add package Mediator.Abstractions --version 1.0.*

又は

<PackageReference Include="Mediator.SourceGenerator" Version="1.0.*">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.Abstractions" Version="1.0.*" />

4.2. DI コンテナへのメディエーターの追加

同等の呼び出し(構成されていない限り、デフォルトの名前空間は構成されています)。 これにより、以下のハンドラーが登録されます。

ConfigureServices
AddMediator
MediatorOptions
Mediator

using Mediator;
using Microsoft.Extensions.DependencyInjection;
using System;

var services = new ServiceCollection(); // Most likely IServiceCollection comes from IHostBuilder/Generic host abstraction in Microsoft.Extensions.Hosting

services.AddMediator();
var serviceProvider = services.BuildServiceProvider();

4.3. クリエイトタイプ
IRequest<>

var mediator = serviceProvider.GetRequiredService<IMediator>();
var ping = new Ping(Guid.NewGuid());
var pong = await mediator.Send(ping);
Debug.Assert(ping.Id == pong.Id);

// ...

public sealed record Ping(Guid Id) : IRequest<Pong>;

public sealed record Pong(Guid Id);

public sealed class PingHandler : IRequestHandler<Ping, Pong>
{
    public ValueTask<Pong> Handle(Ping request, CancellationToken cancellationToken)
    {
        return new ValueTask<Pong>(new Pong(request.Id));
    }
}

メッセージタイプをコーディングするとすぐに、ソースジェネレータは自動的にDI登録を追加します(内部)。 PS - あなたは自分でコードを検査することができます - プロジェクトからVSを開く ->依存関係 ->アナライザー ->メディエーター.ソースジェネレータ ->メディエーター.ソースジェネレーター.メディエータージェネレーター、 またはコードを介してF12だけです。

AddMediator
Mediator.g.cs

4.4. パイプラインビヘイビアを使用する

以下のパイプライン動作は、すべての受信メッセージを検証します。 現在、パイプラインの動作は手動で追加する必要があります。

Ping

services.AddMediator();
services.AddSingleton<IPipelineBehavior<Ping, Pong>, PingValidator>();

public sealed class PingValidator : IPipelineBehavior<Ping, Pong>
{
    public ValueTask<Pong> Handle(Ping request, CancellationToken cancellationToken, MessageHandlerDelegate<Ping, Pong> next)
    {
        if (request is null || request.Id == default)
            throw new ArgumentException("Invalid input");

        return next(request, cancellationToken);
    }
}

4.5. オープンジェネリックによる制約メッセージ
IPipelineBehavior<,>

メディエーターを通過するメッセージのすべてまたはサブセットを処理するためのオープン汎用ハンドラーを追加します。 このハンドラーは、メッセージ ハンドラー (,,) からスローされたエラーをログに記録します。 また、通知ハンドラーがエラーに対応できるようにする通知も発行します。

IRequest
ICommand
IQuery

services.AddMediator();
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(ErrorLoggerHandler<,>));

public sealed record ErrorMessage(Exception Exception) : INotification;
public sealed record SuccessfulMessage() : INotification;

public sealed class ErrorLoggerHandler<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
    where TMessage : IMessage // Constrained to IMessage, or constrain to IBaseCommand or any custom interface you've implemented
{
    private readonly ILogger<ErrorLoggerHandler<TMessage, TResponse>> _logger;
    private readonly IMediator _mediator;

    public ErrorLoggerHandler(ILogger<ErrorLoggerHandler<TMessage, TResponse>> logger, IMediator mediator)
    {
        _logger = logger;
        _mediator = mediator;
    }

    public async ValueTask<TResponse> Handle(TMessage message, CancellationToken cancellationToken, MessageHandlerDelegate<TMessage, TResponse> next)
    {
        try
        {
            var response = await next(message, cancellationToken);
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error handling message");
            await _mediator.Publish(new ErrorMessage(ex));
            throw;
        }
    }
}

4.6. Use notifications

We can define a notification handler to catch errors from the above pipeline behavior.

// Notification handlers are automatically added to DI container

public sealed class ErrorNotificationHandler : INotificationHandler<ErrorMessage>
{
    public ValueTask Handle(ErrorMessage error, CancellationToken cancellationToken)
    {
        // Could log to application insights or something...
        return default;
    }
}

4.7. Polymorphic dispatch with notification handlers

We can also define a notification handler that receives all notifications.

public sealed class StatsNotificationHandler : INotificationHandler<INotification> // or any other interface deriving from INotification
{
    private long _messageCount;
    private long _messageErrorCount;

    public (long MessageCount, long MessageErrorCount) Stats => (_messageCount, _messageErrorCount);

    public ValueTask Handle(INotification notification, CancellationToken cancellationToken)
    {
        Interlocked.Increment(ref _messageCount);
        if (notification is ErrorMessage)
            Interlocked.Increment(ref _messageErrorCount);
        return default;
    }
}

4.8. Notification handlers also support open generics

public sealed class GenericNotificationHandler<TNotification> : INotificationHandler<TNotification>
    where TNotification : INotification // Generic notification handlers will be registered as open constrained types automatically
{
    public ValueTask Handle(TNotification notification, CancellationToken cancellationToken)
    {
        return default;
    }
}

4.9. Use streaming messages

Since version 1.* of this library there is support for streaming using .

IAsyncEnumerable

var mediator = serviceProvider.GetRequiredService<IMediator>();

var ping = new StreamPing(Guid.NewGuid());

await foreach (var pong in mediator.CreateStream(ping))
{
    Debug.Assert(ping.Id == pong.Id);
    Console.WriteLine("Received pong!"); // Should log 5 times
}

// ...

public sealed record StreamPing(Guid Id) : IStreamRequest<Pong>;

public sealed record Pong(Guid Id);

public sealed class PingHandler : IStreamRequestHandler<StreamPing, Pong>
{
    public async IAsyncEnumerable<Pong> Handle(StreamPing request, [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        for (int i = 0; i < 5; i++)
        {
            await Task.Delay(1000, cancellationToken);
            yield return new Pong(request.Id);
        }
    }
}

5. 診断

これはソースジェネレータであるため、診断も含まれています。以下の例

  • 要求ハンドラーがありません

要求ハンドラーがありません

  • 複数の要求ハンドラーが見つかりました

複数の要求ハンドラーが見つかりました

6.メディアットとの違い

これは、このライブラリとMediatRの違いに関する進行中のリストです。

  • RequestHandlerDelegate<TResponse>()
    ->
    MessageHandlerDelegate<TMessage, TResponse>(TMessage message, CancellationToken cancellationToken)
    • これは、過剰なクロージャ割り当てを回避するためです。コストが単にメッセージとキャンセルトークンを渡すことであるとき、私はそれが価値があります。
  • いいえ
    ServiceFactory
    • このライブラリはに依存しているため、これらの抽象化と統合されるDIコンテナでのみ機能します。
      Microsoft.Extensions.DependencyInjection
  • 既定のシングルトン サービスの有効期間
    • MediatRと組み合わせてデフォルトで一時的なサービス登録を行うため、多くの割り当てが発生します。シングルトンの有効期間用に構成されている場合でも、サービスは一時的 (構成不可) として登録されます。
      MediatR.Extensions.Microsoft.DependencyInjection
      IMediator
      ServiceFactory