Share via


ASP.NET Core SignalR でハブ フィルターを使う

ハブ フィルターの特長:

  • ASP.NET Core 5.0 以降で使用できます。
  • ハブ メソッドがクライアントによって呼び出される前後でロジックを実行できます。

この記事では、ハブ フィルターの作成と使用に関するガイダンスを提供します。

ハブ フィルターを構成する

ハブ フィルターは、グローバルに、またはハブの種類ごとに適用できます。 フィルターを追加する順序は、フィルターが実行される順序になります。 グローバル ハブ フィルターは、ローカルのハブ フィルターの前に実行されます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR(options =>
    {
        // Global filters will run first
        options.AddFilter<CustomFilter>();
    }).AddHubOptions<ChatHub>(options =>
    {
        // Local filters will run second
        options.AddFilter<CustomFilter2>();
    });
}

ハブ フィルターは、次のいずれかの方法で追加できます。

  • 具象型によるフィルターの追加:

    hubOptions.AddFilter<TFilter>();
    

    これは、依存関係の挿入 (DI) またはアクティブ化された型から解決されます。

  • ランタイム型によるフィルターの追加:

    hubOptions.AddFilter(typeof(TFilter));
    

    これは、DI またはアクティブ化された型から解決されます。

  • インスタンスによるフィルターの追加:

    hubOptions.AddFilter(new MyFilter());
    

    このインスタンスは、シングルトンのように使われます。 すべてのハブ メソッドの呼び出しで同じインスタンスが使われます。

ハブ フィルターは、ハブの呼び出しごとに作成および破棄されます。 グローバル状態をフィルターに格納する場合、または状態を指定しない場合は、パフォーマンスを向上させるために、ハブ フィルターの種類をシングルトンとして DI に追加します。 または、可能な場合は、フィルターをインスタンスとして追加します。

ハブ フィルターを作成する

IHubFilter から継承するクラスを宣言してフィルターを作成し、InvokeMethodAsync メソッドを追加します。 また、必要に応じて OnConnectedAsyncOnDisconnectedAsync を実装し、それぞれ OnConnectedAsyncOnDisconnectedAsync のハブ メソッドをラップすることもできます。

public class CustomFilter : IHubFilter
{
    public async ValueTask<object> InvokeMethodAsync(
        HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next)
    {
        Console.WriteLine($"Calling hub method '{invocationContext.HubMethodName}'");
        try
        {
            return await next(invocationContext);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Exception calling '{invocationContext.HubMethodName}': {ex}");
            throw;
        }
    }

    // Optional method
    public Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next)
    {
        return next(context);
    }

    // Optional method
    public Task OnDisconnectedAsync(
        HubLifetimeContext context, Exception exception, Func<HubLifetimeContext, Exception, Task> next)
    {
        return next(context, exception);
    }
}

フィルターはミドルウェアとよく似ています。 next メソッドは、次のフィルターを呼び出します。 最後のフィルターは、ハブ メソッドを呼び出します。 また、フィルターでは、待機している next からの結果を格納したり、ハブ メソッドが呼ばれた後 next から結果が返される前にロジックを実行したりできます。

フィルターでハブ メソッドの呼び出しをスキップするには、next を呼び出す代わりに、型 HubException の例外をスローします。 結果が求められていた場合、クライアントはエラーを受け取ります。

ハブ フィルターを使う

フィルター ロジックを記述するときは、ハブ メソッド名をチェックするのではなく、ハブ メソッドの属性を利用して汎用性を持たせるようにします。

ハブ メソッドの引数に禁止語句がないかチェックして、見つかった語句を *** に置き換えるフィルターについて考えてみます。 この例では、LanguageFilterAttribute クラスが定義されているとします。 クラスには、属性を利用するときに設定できる FilterArgument という名前のプロパティがあります。

  1. この属性を、クリーニング対象の文字列引数を持つハブ メソッドに設定します。

    public class ChatHub
    {
        [LanguageFilter(filterArgument = 0)]
        public async Task SendMessage(string message, string username)
        {
            await Clients.All.SendAsync("SendMessage", $"{username} says: {message}");
        }
    }
    
  2. ハブ フィルターを定義して属性をチェックし、ハブ メソッドの引数の禁止語句を *** に置き換えます。

    public class LanguageFilter : IHubFilter
    {
        // populated from a file or inline
        private List<string> bannedPhrases = new List<string> { "async void", ".Result" };
    
        public async ValueTask<object> InvokeMethodAsync(HubInvocationContext invocationContext, 
            Func<HubInvocationContext, ValueTask<object>> next)
        {
            var languageFilter = (LanguageFilterAttribute)Attribute.GetCustomAttribute(
                invocationContext.HubMethod, typeof(LanguageFilterAttribute));
            if (languageFilter != null &&
                invocationContext.HubMethodArguments.Count > languageFilter.FilterArgument &&
                invocationContext.HubMethodArguments[languageFilter.FilterArgument] is string str)
            {
                foreach (var bannedPhrase in bannedPhrases)
                {
                    str = str.Replace(bannedPhrase, "***");
                }
    
                var arguments = invocationContext.HubMethodArguments.ToArray();
                arguments[languageFilter.FilterArgument] = str;
                invocationContext = new HubInvocationContext(invocationContext.Context,
                    invocationContext.ServiceProvider,
                    invocationContext.Hub,
                    invocationContext.HubMethod,
                    arguments);
            }
    
            return await next(invocationContext);
        }
    }
    
  3. Startup.ConfigureServices メソッドでハブ フィルターを登録します。 すべての呼び出しでの禁止語句リストの再初期化を避けるために、ハブ フィルターはシングルトンとして登録されます。

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSignalR(hubOptions =>
        {
            hubOptions.AddFilter<LanguageFilter>();
        });
    
        services.AddSingleton<LanguageFilter>();
    }
    

HubInvocationContext オブジェクト

HubInvocationContext には、現在のハブ メソッド呼び出しに関する情報が含まれます。

プロパティ 説明 Type
Context HubCallerContext には接続に関する情報が格納されます。 HubCallerContext
Hub このハブ メソッドの呼び出しに使われているハブのインスタンス。 Hub
HubMethodName 呼び出されているハブ メソッドの名前。 string
HubMethodArguments ハブ メソッドに渡される引数のリスト。 IReadOnlyList<string>
ServiceProvider このハブ メソッド呼び出しのスコープを持つサービス プロバイダー。 IServiceProvider
HubMethod ハブ メソッドの情報。 MethodInfo

HubLifetimeContext オブジェクト

HubLifetimeContext には、OnConnectedAsyncOnDisconnectedAsync ハブ メソッドの情報が格納されています。

プロパティ 説明 Type
Context HubCallerContext には接続に関する情報が格納されます。 HubCallerContext
Hub このハブ メソッドの呼び出しに使われているハブのインスタンス。 Hub
ServiceProvider このハブ メソッド呼び出しのスコープを持つサービス プロバイダー。 IServiceProvider

認可とフィルター

ハブ フィルターの前に実行されるハブ メソッドの認可属性