Skip to content
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

Intercepting Blazor page events. #30115

Closed
dotnetjunkie opened this issue Feb 11, 2021 · 23 comments
Closed

Intercepting Blazor page events. #30115

dotnetjunkie opened this issue Feb 11, 2021 · 23 comments
Labels
affected-few This issue impacts only small number of customers area-blazor Includes: Blazor, Razor Components Blazor ♥ SignalR This issue is related to the experience of Signal R and Blazor working together enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-circuit-lifecycle Issues to do with blazor server lifecycle events severity-major This label is used by an internal tool
Milestone

Comments

@dotnetjunkie
Copy link

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:

  • intercepting the creation of Blazor components (using IComponentActivator) to start/continue a scope
  • intercepting the creation of SignalR hubs (using IHubActivator<T>) to start/continue a scope

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
Copy link
Member

@dotnetjunkie thanks for contacting us.

Is this achievable with a Hub filter?

@javiercn
Copy link
Member

/cc @BrennanConroy

@javiercn javiercn added the Blazor ♥ SignalR This issue is related to the experience of Signal R and Blazor working together label Feb 11, 2021
@BrennanConroy
Copy link
Member

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
Copy link
Member

@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
Copy link
Author

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
Copy link
Member

@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
Copy link
Author

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
Copy link
Member

@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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

@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
Copy link
Member

@dotnetjunkie I see.

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

@javiercn javiercn added this to the Next sprint planning milestone Feb 12, 2021
@ghost
Copy link

ghost commented Feb 12, 2021

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.

@SteveSandersonMS SteveSandersonMS added affected-few This issue impacts only small number of customers enhancement This issue represents an ask for new feature or an enhancement to an existing one severity-major This label is used by an internal tool labels Feb 19, 2021 — with ASP.NET Core Issue Ranking
@javiercn javiercn added the feature-circuit-lifecycle Issues to do with blazor server lifecycle events label Apr 20, 2021
@ghost
Copy link

ghost commented Jul 20, 2021

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
Copy link

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
Copy link
Member

@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
Copy link

inf9144 commented Jan 11, 2023

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
Copy link

inf9144 commented Jan 13, 2023

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
Copy link

inf9144 commented Jan 13, 2023

@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
Copy link
Member

We believe this was addressed as part of #46968, if you still run into issues with this approach, please let us know.

@FlukeFan
Copy link

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: #30115 (comment)

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

@dotnetjunkie
Copy link
Author

dotnetjunkie commented Oct 24, 2023

In addition to @FlukeFan's response, I already mentioned above that circuit breakers won't solve the issue, except when #46968 chances what can be intercepted?

@inf9144
Copy link

inf9144 commented Nov 6, 2023

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

@dotnet dotnet locked as resolved and limited conversation to collaborators Dec 6, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
affected-few This issue impacts only small number of customers area-blazor Includes: Blazor, Razor Components Blazor ♥ SignalR This issue is related to the experience of Signal R and Blazor working together enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-circuit-lifecycle Issues to do with blazor server lifecycle events severity-major This label is used by an internal tool
Projects
None yet
Development

No branches or pull requests

8 participants