dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.51k stars 10.04k forks source link

Intercepting Blazor page events. #30115

Closed dotnetjunkie closed 1 year ago

dotnetjunkie commented 3 years ago

Blazor supports the notion of events, such as @onlick events, as shown here:

<button class="btn btn-danger" @onclick="DeleteUser">Delete</button>

This allows the DeleteUser method to be invoked in the page's @code section.

I'm looking for a mechanism that allows intercepting calls those 'code behind' methods, in order to be able to execute some infrastructure logic right before the method is invoked. If such mechanism is currently missing, I would urge the addition of a feature that makes this possible.

This question/discussion is related to my earlier issues #19642 and #29194 because I'm trying to find ways to integrate non-conforming DI Containers (such as Simple Injector) with the Blazor pipeline. As non-conforming containers don't replace the built-in DI Container, but merely live side-by-side, it is important to be able to start or continue a non-conforming container's Scope at the proper time.

Starting and continuing an existing scope can be done partially by:

This unfortunately leaves us with the invocation of Blazor events. When they are invoked, neither the IComponentActivator nor IHubActivator<T> is called, which causes that code to be executed outside the context of a non-conforming container's scope.

I might have overlooked the proper interception point for this in the Blazor code base. If there is such an interception point, please let me know. If there isn't, I would like to see it added.

javiercn commented 3 years ago

@dotnetjunkie thanks for contacting us.

Is this achievable with a Hub filter?

javiercn commented 3 years ago

/cc @BrennanConroy

BrennanConroy commented 3 years ago

Unless I'm misunderstanding the issue, Hub filters wont help. They are already using the IHubActivator to create a scope for the Hub.

the invocation of Blazor events

I don't know where this happens, but is there a scope being created by Blazor that isn't easily hooked into? This seems to be the ask.

javiercn commented 3 years ago

@BrennanConroy isn't it the case that hubfilters allow you to intercept each message a hub receives?

Unless I'm missing something about how this works, my understanding is that it should enable you to intercept the messages from the browser and run code at that point, which means they should be able to setup/restore their own scopes there?

That said, while I think this is possible I'm not very confortable with it, since it plugs into what we consider to be an implementation detail of how Blazor server operates.

For example, if we decide to change the messages that we send, I believe a solution like this would break.

dotnetjunkie commented 3 years ago

Hi @javiercn, @BrennanConroy is correct. An IHubFilter doesn't work. Although the IHubFilter.InvokeMethodAsync goes of prior to the call to the Blazor event, that event runs in a completely separate context (request?) which means that any applied AsyncLocal<T> value will be lost. Unless I'm doing something wrong. I'll explain below why we need to move data using AsyncLocal<T>.

I don't know where this happens, but is there a scope being created by Blazor that isn't easily hooked into? This seems to be the ask.

DI Containers like Simple Injector can't replace the built-in MS.DI Container and have to live side-by-side. This means that the need to start their own Scope where application components can be resolved from. With Simple Injector, however, scopes are stored in ambient state (using AsyncLocal<T>); this allows them to 'flow' across asynchronous operations and it allows user or integration code to simply call Container.GetInstance<T> (instead of Scope.GetInstance<T>) and the container automatically "knows" which scope it should use to resolve instances from.

To prevent confusion for Blazor users, however, a Simple Injector Scope needs to have the same lifestyle as MS.DI's IServiceScope. As you know, such IServiceScope can live from quite some time and will stay alive on the server for as long as the user is on the same page. All invoked events on that page run in the same IServiceScope. As the user (or its integration code) will not resolve their application components from the MS.DI IServiceScope but from the Simple Injector Container, the correct Simple Injector Scope must be set up as the 'current scope' for the given context.

I created a proof of concept for Simple Injector users here. In short, that PoC does the following:

// ScopeAccessor is a class from the PoC. ScopeAccessor allows storing
// the Simple Injector scope in the MS.DI IServiceScope state.
var accessor = requestServices.GetRequiredService<ScopeAccessor>();

if (accessor.Scope is null)
{
    // This instructs Simple Injector to create a new Scope
    accessor.Scope = AsyncScopedLifestyle.BeginScope(container);
    // This stores the MS.DI IServiceScope inside the Simple Injector scope
    accessor.Scope.GetInstance<ServiceScopeAccessor>().Scope = (IServiceScope)requestServices;
}
else
{
    // This instructs Simple Injector to set the scope pulled in from IServiceScope
    // as the current scope for the active (asynchronous) context. (stored inside a AsyncLocal<Scope>)
    lifestyle.SetCurrentScope(accessor.Scope);
}

In the PoC, this code is triggered by some infrastructure to the proper Blazor interception points (currently IComponentActivator and IHubActivator<T>) to make sure that, whatever the user does, the Simple Injector scope and MS.DI scope match up.

But the problem seems that there is no proper interception point that is triggered right before a Blazor event goes off in such way that when this interception points sets a value in a AsyncLocal<T>, the Blazor event code can read that value from the same AsyncLocal<T> instance.

javiercn commented 3 years ago

@dotnetjunkie thanks for the additional details.

I think I have a better grasp of what's going on here now. Have you tried setting up the scope on a circuit handler instead of using the hub activator?

Blazor creates its own scope and circuit handlers run inside that scope context. I think that + async local might be enough for this to work.

What I think would happen is that you resolve the servicescopeaccessor at that point and set it to your custom scope there. From there I think things would flow "automagically" to other areas? I'm speculating quite a bit here BTW, this code is complicated and deals with a synchronization context and stuff, which always makes things more fun.

As I mentioned, I think you might be set if you do this inside a circuit handler, so it's worth giving it a shot.

dotnetjunkie commented 3 years ago

Have you tried setting up the scope on a circuit handler instead of using the hub activator?

Just checked, but the circuit handlers don't go off before the Blazor events; the seem to only get invoked when the connections go up and down, but not in between.

javiercn commented 3 years ago

@dotnetjunkie I was suggesting that maybe by setting it up when the circuit is started the async local would do the magic for the rest of the circuit. Otherwise we need an additional primitive to plug in at the event and JS interop levels.

dotnetjunkie commented 3 years ago

I was suggesting that maybe by setting it up when the circuit is started the async local would do the magic for the rest of the circuit

Could you show me an example?

javiercn commented 3 years ago
public class ContainerCircuitHandler : CircuitHandler
{
    public ContainerCircuitHandler(IServiceProvider provider)
    {
         _provider = provider;
    }
    public override Task OnCircuitOpened(Circuit circuit,  CancellationToken cancellationToken)
    {
        // Code to setup the non conforming scope for the circuit

        return Task.CompletedTask;
    }

   public override Task OnCircuitClosed(Circuit circuit, CancellationToken cancellationToken)
   {
   }
}

Then register it on DI with services.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler, ContainerCircuitHandler>()

dotnetjunkie commented 3 years ago

@javiercn, thank you for your example. Unfortunately, async local doesn't do "the magic" for the rest of the circuit; the scope of async local ends rather quickly. Your solution, unfortunately, doensn't work.

javiercn commented 3 years ago

@dotnetjunkie I see.

I'm going to move this so that we can discuss within the team.

ghost commented 3 years ago

Thanks for contacting us. We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost commented 3 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

beefydog commented 2 years ago

Interesting, this is the most often asked question (and probably most requested feature) on my youtube channel. Got 180,000 hits in Google. And it's backlogged.

javiercn commented 2 years ago

@beefydog if you can point us to the data, it would help us to prioritize this issue, but we haven't seen that interest reflected here.

inf9144 commented 1 year ago

Would also like to have this. If you could intercept all delegates (or their invocations) that get passed to EventCallbackFactory you could do things like structured exception handling or advanced logging or other aspects. Right now you need to boilerplate everything that needs to go in every event handler. :-/

inf9144 commented 1 year ago

To solve this in a local project (not a solution for frameworks) you can use Castle.DynamicProxy / IInterceptor together with an custom implementation of IComponentFactory. You can easily intercept OnInitialized(Async) OnParametersSet(Async) OnAfterRender(Async) and if you pass IHandleEvent as additional interface you can intercept IHandleEvent.HandleEventAsync and get access to all event delegates that get executed for your component :-)

Never the less - it would be much easier and more performant if there would be inbuild support to do sth like this.

inf9144 commented 1 year ago

@javiercn, thank you for your example. Unfortunately, async local doesn't do "the magic" for the rest of the circuit; the scope of async local ends rather quickly. Your solution, unfortunately, doensn't work.

Needed sth like this - you can implement it if you combine a SignalR IHubFilter together with a CircuitHandler. The HubFilter can control the AsyncLocal and put something like a value holder in InvokeMethodAsync and the CircuitHandler can access the circuit scope and other things and write it into your value holder. The values can than be read back and cached in your HubFilter so you can restore them in the next run. Only keep in mind that the cleaning of your cached values must be done by OnCircuitClosedAsync because instances can reconnect. And if u use the SignalR connection id to correlate that you need to handle the change in OnConnectionUp/OnConnectionDown in case of a reconnect.

javiercn commented 1 year ago

We believe this was addressed as part of https://github.com/dotnet/aspnetcore/pull/46968, if you still run into issues with this approach, please let us know.

FlukeFan commented 1 year ago

Sorry if I'm missing something, but the original request was "... I'm looking for a mechanism that allows intercepting calls those 'code behind' methods... "

also mentioned here: https://github.com/dotnet/aspnetcore/issues/30115#issuecomment-1378584075

However, the issue linked above appears to be for circuits (i.e., Blazor Server) only?

dotnetjunkie commented 1 year ago

In addition to @FlukeFan's response, I already mentioned above that circuit breakers won't solve the issue, except when https://github.com/dotnet/aspnetcore/pull/46968 chances what can be intercepted?

inf9144 commented 1 year ago

I also cannot see this closed. The implementation tackles only the connection aspect and not the event interception.