Skip to content

Hub filters! #21278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
May 20, 2020
Merged

Hub filters! #21278

merged 29 commits into from
May 20, 2020

Conversation

BrennanConroy
Copy link
Member

@BrennanConroy BrennanConroy commented Apr 28, 2020

Fixes #5353

Late feedback welcome!

public class HubLifetimeContext
{
    public HubLifetimeContext(HubCallerContext context, IServiceProvider serviceProvider, Hub hub);

    public HubCallerContext Context { get; }

    public Hub Hub { get; }

    public IServiceProvider ServiceProvider { get; }
}
public class HubInvocationContext
{
+   [Obsolete]
    public HubInvocationContext(HubCallerContext context, string hubMethodName, object[] hubMethodArguments);

+   public HubInvocationContext(HubCallerContext context, IServiceProvider serviceProvider, Hub hub, MethodInfo hubMethod, object[] hubMethodArguments);

+   [Obsolete]
    public string HubMethodName { get; }

    public HubCallerContext Context { get; }

    public IReadOnlyList<object> HubMethodArguments { get; }

+   public Hub Hub { get; }

+   public IServiceProvider ServiceProvider { get; }

+   public MethodInfo HubMethod { get; }
}
public static class HubOptionsExtensions
{
    public static void AddFilter(this HubOptions options, IHubFilter hubFilter);

    public static void AddFilter<TFilter>(this HubOptions options) where TFilter : IHubFilter;

    public static void AddFilter(this HubOptions options, Type filterType);
}
public interface IHubFilter
{
    ValueTask<object> InvokeMethodAsync(HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next);

    Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next) => next(context);

    Task OnDisconnectedAsync(HubLifetimeContext context, Exception exception, Func<HubLifetimeContext, Exception, Task> next) => next(context, exception);
}

@BrennanConroy BrennanConroy added the area-signalr Includes: SignalR clients and servers label Apr 28, 2020
@davidfowl
Copy link
Member

Spoke to @BrennanConroy about the design. Instead of building the pipeline dynamically per invocation we'll have a stable pipeline built once and creation of the specific HubFilters per invocation:

e.g.

var filters = new List<IHubFilter>()
{
   new AHubFilter(),
   new HubFilterFactory(typeof(BHubFilter)),
   new CHubFilter(),
   new FilterFactory(typeof(DHubFilter)),
};

Instead of storing a list of objects we can store a list of IHubFilter and have the HubFilterFactory create the HubFilter per call.

(methodInvoked, result) = await ExecuteHubMethod(methodExecutor, hub, arguments, connection, scope.ServiceProvider);
if (!methodInvoked)
{
await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems overly defensive to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BrennanConroy After re-thinking this can we just support a way to return an invocation error instead of inferring anything? We can treat it like HubException? We support a way that you can opt into sending a custom InvocationError but we don't do anything else.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just support a way to return an invocation error

That's what the HubResult is for

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undo

@davidfowl
Copy link
Member

I think it's time to undraft this PR and finish up. It's looking pretty good.

@davidfowl
Copy link
Member

One more thing we need to think about is non-conforming containers or @dotnetjunkie won't let me hear the end of it 😉

@dotnetjunkie
Copy link

Thanks for pinning me, @davidfowl.

As far as I can see, non-conforming containers would be able to plug in to this system, because:

  • Filters are essentially a list of singletons managed outside the container, allowing the creation of a factory/proxy (like your internal HubFilterFactory).
  • IHubFilter implementations are provided with a context object, which allows a non-conforming container to pull in its own scope, to resolve hub filters from.

Using Pure DI in this model will perhaps be more complicated, but certainly possible.

Here's a sample implementation for Simple Injector:

// Setup
services.AddSignalR(options =>
{
    options.AddFilter(new SimpleInjectorHubFilterProxy<CustomHubFilter>(container));
})

class SimpleInjectorHubFilterProxy<THubFilter> : IHubFilter
    where THubFilter : IHubFilter
{
    private readonly InstanceProducer<THubFilter> producer;

    public SimpleInjectorHubFilterProxy(Container container) =>
        producer = Lifestyle.Scoped.CreateProducer<IHubFilter, THubFilter>(container);
        
    private IHubFilter GetFilter() => this.producer.GetInstance();

    public async ValueTask<object> InvokeMethodAsync(
        HubInvocationContext context, Func<HubInvocationContext, ValueTask<object>> next) =>
        this.GetFilter().InvokeMethodAsync(context, next);

    public async Task OnConnectedAsync(
        HubLifetimeContext context, Func<HubLifetimeContext, Task> next) =>
        this.GetFilter().OnConnectedAsync(context, next);

    public async Task OnDisconnectedAsync(
        HubLifetimeContext c, Exception ex, Func<HubLifetimeContext, Exception, Task> next) =>
        this.GetFilter().OnDisconnectedAsync(c, ex, next);
}

Do note that this implementation doesn't use the provided HubLifetimeContext, because Simple Injector's scoping mechanism is ambient in nature. Other non-conforming implementations, however, might need to pull their own scope object from this HubLifetimeContext.

@davidfowl
Copy link
Member

@dotnetjunkie Thanks for reviewing, yes that pattern works well!

@BrennanConroy BrennanConroy marked this pull request as ready for review May 14, 2020 21:43
@BrennanConroy BrennanConroy added this to the 5.0.0-preview6 milestone May 14, 2020
@BrennanConroy BrennanConroy added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label May 15, 2020
@ghost
Copy link

ghost commented May 15, 2020

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

public string HubMethodName { get; }

/// <summary>
/// Gets the arguments provided by the client.
/// </summary>
public IReadOnlyList<object> HubMethodArguments { get; }
public IReadOnlyList<object> HubMethodArguments { get => Arguments; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public IReadOnlyList<object> HubMethodArguments { get => Arguments; }
public IReadOnlyList<object> HubMethodArgument => Arguments;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about just Arguments for the public property?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not make another breaking change

/// </summary>
public Type HubType { get; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we retain this property to avoid a breaking change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not breaking because this was added in 5.0

Copy link
Contributor

@analogrelay analogrelay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks swanky!

public string HubMethodName { get; }

/// <summary>
/// Gets the arguments provided by the client.
/// </summary>
public IReadOnlyList<object> HubMethodArguments { get; }
public IReadOnlyList<object> HubMethodArguments { get => Arguments; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about just Arguments for the public property?

hubActivator = scope.ServiceProvider.GetRequiredService<IHubActivator<THub>>();
hub = hubActivator.Create();

if (!await IsHubMethodAuthorized(scope.ServiceProvider, connection, descriptor, hubMethodInvocationMessage.Arguments, hub))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make sure to call out the ordering of Filters and Authorization in docs. I do remember @rynowak cautioning us against the complexity of moving things like AuthZ into filters, so I won't suggest that, but we should at least doc the order.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah we did that in the old signalr as well but I don’t see the need here

Copy link
Contributor

@analogrelay analogrelay May 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, but we need to doc the fixed "ordering" we have so people know when filters run.

@pranavkm pranavkm added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels May 18, 2020
namespace Microsoft.AspNetCore.SignalR
{
/// <summary>
/// Methods to add <see cref="IHubFilter"/>'s to Hubs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Methods to add <see cref="IHubFilter"/>'s to Hubs.
/// Methods to add <see cref="IHubFilter"/> instances to Hubs.

@BrennanConroy BrennanConroy merged commit 2ad8121 into master May 20, 2020
@BrennanConroy BrennanConroy deleted the brecon/testpipeline branch May 20, 2020 05:05
@davidfowl
Copy link
Member

Great work @BrennanConroy!

public System.Collections.Generic.IReadOnlyList<object> HubMethodArguments { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
[System.ObsoleteAttribute("This property is obsolete and will be removed in a future version. Use HubMethod.Name instead.")]
public string HubMethodName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we decided in API review that we were not going to obsolete HubMethodName.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, maybe. No one commented that so it was forgotten. Fortunately we can easily fix this :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-signalr Includes: SignalR clients and servers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consider introducing a single "hub pipeline" abstraction for cross-cutting concerns
7 participants