Closed dotnetjunkie closed 1 year ago
@dotnetjunkie thanks for contacting us.
Is this achievable with a Hub filter?
/cc @BrennanConroy
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.
@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.
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.
@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.
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.
@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.
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?
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>()
@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.
@dotnetjunkie I see.
I'm going to move this so that we can discuss within the team.
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.
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.
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.
@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.
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. :-/
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.
@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.
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.
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?
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?
I also cannot see this closed. The implementation tackles only the connection aspect and not the event interception.
Blazor supports the notion of events, such as
@onlick
events, as shown here: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:
IComponentActivator
) to start/continue a scopeIHubActivator<T>
) to start/continue a scopeThis unfortunately leaves us with the invocation of Blazor events. When they are invoked, neither the
IComponentActivator
norIHubActivator<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.