Use hub filters in ASP.NET Core SignalR

Hub filters:

  • Are available in ASP.NET Core 5.0 or later.
  • Allow logic to run before and after hub methods are invoked by clients.

This article provides guidance for writing and using hub filters.

Configure hub filters

Hub filters can be applied globally or per hub type. The order in which filters are added is the order in which the filters run. Global hub filters run before local hub filters.

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>();
    });
}

A hub filter can be added in one of the following ways:

  • Add a filter by concrete type:

    hubOptions.AddFilter<TFilter>();
    

    This will be resolved from dependency injection (DI) or type activated.

  • Add a filter by runtime type:

    hubOptions.AddFilter(typeof(TFilter));
    

    This will be resolved from DI or type activated.

  • Add a filter by instance:

    hubOptions.AddFilter(new MyFilter());
    

    This instance will be used like a singleton. All hub method invocations will use the same instance.

Hub filters are created and disposed per hub invocation. If you want to store global state in the filter, or no state, add the hub filter type to DI as a singleton for better performance. Alternatively, add the filter as an instance if you can.

Create hub filters

Create a filter by declaring a class that inherits from IHubFilter, and add the InvokeMethodAsync method. There is also OnConnectedAsync and OnDisconnectedAsync that can optionally be implemented to wrap the OnConnectedAsync and OnDisconnectedAsync hub methods respectively.

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);
    }
}

Filters are very similar to middleware. The next method invokes the next filter. The final filter will invoke the hub method. Filters can also store the result from awaiting next and run logic after the hub method has been called before returning the result from next.

To skip a hub method invocation in a filter, throw an exception of type HubException instead of calling next. The client will receive an error if it was expecting a result.

Use hub filters

When writing the filter logic, try to make it generic by using attributes on hub methods instead of checking for hub method names.

Consider a filter that will check a hub method argument for banned phrases and replace any phrases it finds with ***. For this example, assume a LanguageFilterAttribute class is defined. The class has a property named FilterArgument that can be set when using the attribute.

  1. Place the attribute on the hub method that has a string argument to be cleaned:

    public class ChatHub
    {
        [LanguageFilter(filterArgument = 0)]
        public async Task SendMessage(string message, string username)
        {
            await Clients.All.SendAsync("SendMessage", $"{username} says: {message}");
        }
    }
    
  2. Define a hub filter to check for the attribute and replace banned phrases in a hub method argument with ***:

    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. Register the hub filter in the Startup.ConfigureServices method. To avoid reinitializing the banned phrases list for every invocation, the hub filter is registered as a singleton:

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

The HubInvocationContext object

The HubInvocationContext contains information for the current hub method invocation.

Property Description Type
Context The HubCallerContext contains information about the connection. HubCallerContext
Hub The instance of the Hub being used for this hub method invocation. Hub
HubMethodName The name of the hub method being invoked. string
HubMethodArguments The list of arguments being passed to the hub method. IReadOnlyList<string>
ServiceProvider The scoped service provider for this hub method invocation. IServiceProvider
HubMethod The hub method information. MethodInfo

The HubLifetimeContext object

The HubLifetimeContext contains information for the OnConnectedAsync and OnDisconnectedAsync hub methods.

Property Description Type
Context The HubCallerContext contains information about the connection. HubCallerContext
Hub The instance of the Hub being used for this hub method invocation. Hub
ServiceProvider The scoped service provider for this hub method invocation. IServiceProvider

Authorization and filters

Authorize attributes on hub methods run before hub filters.