-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
Summary
Scoped injection of services should operate per-session rather than per "request."
Motivation and goals
In a classic website, it makes sense to have scoped DI inject a new service per HTTP request. HTTP is a stateless protocol, so the implementation matches the reality of the way it's used.
This is not the case in Blazor. A single-page application is not conceptually a web request; it's conceptually an application that happens to be running in a browser, and applications are not stateless. When using server-side rendering, scoped DI injects new instances of services you've used previously at unexpected times, resulting in errors because your application state does not stay where you left it.
This occurs because scoped DI is still scoped per-request, which may be correct on a very pedantic technical level, but is entirely unhelpful since this isn't the use case it was designed for. For Blazor, there needs to be a way to inject services per-session, that will remain around for the entire usage of the SPA, either by modifying the scoped DI implementation or by adding a new option.
In scope
If Blazor code injects a scoped service, the entire SPA should see that same object instance forever, until the user leaves/reloads the SPA or the server shuts down, no matter how many different "requests" and navigation events occur along the way.
Out of scope
No effort needs to be made to make this application state persist between multiple sessions by the same user. A reload is still a reload.
Risks / unknowns
If existing code depends on the current behavior in some way, it could cause unexpected consequences. So adding an explicit session-scoped option might be the better course of action.
Examples
This all arose out of a Discord discussion of a crash bug in my project. I have a service that's loading some data from the database. It's being called in the layout, and if I'm opening a page that also wants data from the database, I get:
InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext.
And due to the design of Blazor, throwing this exception brings down the entire SPA.
This occurs no matter what efforts are made to get this data only once and persist it in memory, because the memory in question keeps getting replaced out from under me. This needs to stop happening. I'm building an application, and application state needs to stay where the developer left it.
Some people in chat suggested various solutions involving caching user data per-user, but this is not a persuasive suggestion.
- Users can leave, and then you have stale data bloating up the cache.
- Setting up timed cache eviction policies can bring you right back to where you started, with application state vanishing out from under you, if the user steps away from the SPA for a few minutes. There's a reason this is called one of the Hard Problems Of Computer Science.
- Even if invalidation wasn't an issue, now every service that deals with application state becomes significantly more complex as it has to interact with the cache for everything, rather than just keeping it inside its own object.
What is needed is proper scoping that actually works with the SPA model.