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

Blazor SSR: per-user state management #47796

Closed
rockfordlhotka opened this issue Apr 19, 2023 · 30 comments
Closed

Blazor SSR: per-user state management #47796

rockfordlhotka opened this issue Apr 19, 2023 · 30 comments
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-full-stack-web-ui Full stack web UI with Blazor ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Pillar: Complete Blazor Web Status: Resolved

Comments

@rockfordlhotka
Copy link

rockfordlhotka commented Apr 19, 2023

In .NET 6/7 the consistent way to manage per-user state, including things like the current user identity, but also any other per-user state, is to use a Scoped service.

This works because in Blazor server there's a consistent dependency injection (DI) scope on a per-user basis, and in Blazor wasm there's just the one "scope" running on the client. Relying on this DI scope approach is really quite elegant and enables us to build apps that maintain in-memory state much like any other smart-client UI technology, with the added benefit of the whole DI model for isolation.

Looking at the spectrum of Razor component rendering/hosting scenarios in .NET 8, I am concerned about how we developers will be able to consistently maintain per-user state over the lifetime of what the user will perceive as a "Blazor app".

In .NET 8 it appears we might have rendering/hosting options such as:

Server rendered

In this model the razor components are rendered on the server and delivered as html to the browser. On the surface it appears that this might be a stateless server model, which implies that no DI scope would exist being the rendering of a given page or component.

In this context, where would I put state? Presumably in old-fashioned Session? Or a database?

Any user interactivity uses postbacks, and so any state would need to be rematerialized before processing my Blazor code, otherwise I can't act on what was there before.

Side note: On the surface it appears that we might need to use HttpContext to access the current user identity? Entirely unlike current Blazor and its elegant DI scoped user identity service.

Streaming UI

This model appears to be a variation on server rendered, with basically the same state management limitations.

SignalR islands

In this model Blazor establishes a temporary SignalR channel for the page or component. It seems likely that during the lifetime of the SignalR channel that there'd be a DI scope like we have in .NET 6/7. However, that channel (and associated scope) probably goes away as soon as that island of UI goes away - so the DI scope might no longer be useful for anything?

In this model, HttpContext is invalid, but only for the components running within the SignalR/scope context - meaning that other parts of the same page won't have access to shared state?

"Normal" Blazor server

This model is like the .NET 6/7 model, where we can rely on a DI scope to exist per-user during the lifetime of the app, so we can maintain per-user state and access the current user identity via this scope.

In this model, HttpContext is invalid.

Blazor wasm islands

This model involves a mix of one or more server-side rendering models described above, plus some code running on the client in WebAssembly.

It seems that

  1. The server-side story will be a mess, depending on the mix of models being used
  2. The wasm client-side story will have a DI container/scope that can be used just like in .NET 6/7 - though it isn't obvious whether all components on the page will share the same wasm runtime and thus memory?
  3. It isn't clear how server-side state (assuming that problem is solved) and client-side state would be managed in any meaningful way?

Blazor wasm

This model is like the Blazor wasm model in .NET 6/7, where we can rely on a consistent in-memory environment and DI container where we can maintain per-user state and the current user identity.

Summary

In conclusion, it appears that the future of Blazor in .NET 8 could become extremely messy in terms of writing consistent code to access the current user identity and interacting with other per-user state that might exist over the lifetime of what the user perceives as a single Blazor app.

This blog post summarizes the situation in .NET 6/7 if you try to mix server-side Blazor with MVC and service workers in the same aspnetcore host app: https://blog.lhotka.net/2022/03/16/ASPNET-Core-and-Blazor-Identity-and-State

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label Apr 19, 2023
@javiercn
Copy link
Member

@rockfordlhotka thanks for the feedback.

These are all questions that we have in mind as we make progress on implementing these features. Here are some thoughts on it, that I hope helps bring clarity. Keep in mind, that we will be providing guidance in docs about how to choose between the different scenarios and the trade-offs each one has. With that said, here are some early thoughts:

Static server side rendered Blazor

Defaults to stateless, meaning state does not persist across browser navigations. Think of it as traditional MVC/Razor pages. If you want to maintain state, cookies or session are the way. Streaming UI shouldn't factor into this.

Blazor server: Stateful by default, with the "app like experience", state is maintained for the lifetime of the user session.

Blazor webassembly: Same as Blazor server but on the client.

Islands (with blazor server or webassembly components)

Stateful "while the user is on the page". If your app navigates away, the circuit will be stopped after the last server component has been removed from the page. At that point, your state is gone. Similarly for webassembly.

Accessing state

  • Blazor already offers primitives to access the user and other data in a "flavor" agnostic way. AuthenticationStateProvider and similar services will continue to work (they do so today, since server side rendered blazor is an extension to "static rendering, which we've supported since the beginning).
  • There will be mechanisms to transfer state from static rendering to interactive rendering (in fact, they exist today through persistent component state, but we are enhancing them).
  • You can write code that access HttpContext, but at that point:
    1. That component won't work in webassembly (you'll get a compile time error).
    2. The HttpContext will only be available during static rendering.
    3. This model is opt-in, similar to how in Maui you can write code that is specific to iOS or Android

How to think/plan about your app:

  • You can continue doing just pure blazor server or webassembly. Just make the top-level component in your app render as webassembly/server. The same options that you have today for rendering modes should be available (Server, ServerPrerendered, Webassembly, WebassemblyPrerendered, Static (and Auto) if we get time).
    • If you don't want complications, you can continue to use Server/Webassembly render modes and you won't have to care about any additional complexity.
  • If you want to opt-in to islands, then you'll have to think about how you maintain state across the boundaries that you define. Ideally you should make each island self-contained.
    • Services like authenticationstateprovider will most likely transfer state from the server to the browser, so if you authenticated on the server, you can have that same information when running on the client.

@rockfordlhotka
Copy link
Author

@javiercn this makes sense, and is pretty much what I expected.

I do think it is important that there be some way for my code to detect in which mode it is running, so that I can abstract state management into per-scenario providers, thus allowing my actual application code to not worry about the current model in which it is running.

I understand that Blazor itself may not provide this abstraction, but it is important that Blazor provide the information that makes it possible for us to implement abstractions so a typical UI or business developer doesn't need to waste mental energy and time dealing with this complexity.

@javiercn
Copy link
Member

I do think it is important that there be some way for my code to detect in which mode it is running

This is something that it is pretty much in our minds, and it is likely we end up providing something, but we have no concrete thoughts yet as we are focused on a lot of the static rendering features. Once we get to implementing more of the interactive and single project scenarios, we'll have a better sense.

@mkArtakMSFT mkArtakMSFT added the feature-full-stack-web-ui Full stack web UI with Blazor label Apr 24, 2023
@mkArtakMSFT mkArtakMSFT added this to the .NET 8 Planning milestone Apr 24, 2023
@ghost
Copy link

ghost commented Apr 24, 2023

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@mkArtakMSFT mkArtakMSFT added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Jun 6, 2023
@mkArtakMSFT mkArtakMSFT changed the title Blazor 8.0 per-user state management Blazor: per-user state management Jun 6, 2023
@mkArtakMSFT mkArtakMSFT changed the title Blazor: per-user state management Blazor SSR: per-user state management Jun 6, 2023
@mkArtakMSFT mkArtakMSFT modified the milestones: .NET 8 Planning, Backlog Jun 29, 2023
@ghost
Copy link

ghost commented Jun 29, 2023

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.

@mkArtakMSFT mkArtakMSFT modified the milestones: Backlog, BlazorPlanning Nov 5, 2023
@gabephudson
Copy link

I have recently ported a Blazor Server app to Blazor Web project and have run in to the issues outlined by the original poster. IMHO, there doesn't seem to be a clean, in the box way to communicate "session" level (not user level) state data between SSR and InteractiveServer modes. I have been forced to write my own custom session service (using a cookie) that can be accessed in both modes.

What I would love to see is to expand the standard ASP.NET session service to support InteractiveServer rendering so that, once configured, both SSR and InteractiveServer components can read and write to the same instance of the session store. I think this is a critical component for mixed render mode apps. Even if one is implementing their own state store, a "session id" readable between render both modes is needed at a minimum.

@rockfordlhotka
Copy link
Author

@gabephudson I've been working on a solution. You can see what I have so far in the Blazor8State repo, and in the near future I'll write it up as a blog post.

There are some tricky aspects, because there aren't events to tell your code when the user is navigating away to another page (and potentially another render mode). The best way seems to be IDisposable, with the trick being that the page isn't disposed until the next page is rendered - so transitioning from a wasm render mode back to a server render mode (and transferring state from wasm to server) is challenging.

If you don't use wasm rendering in an app, then it is relatively straightforward.

@ghost
Copy link

ghost commented Dec 20, 2023

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@kevon-vems
Copy link

Without the ability to maintain state across all render modes, any attempt to create an application using the InteractiveRender mode seems futile. This should be a higher priority than .Net 9

@Webreaper
Copy link

How can it be "higher priority than .net 9", when 8 is already released?!

@kevon-vems
Copy link

Service patch?

@rockfordlhotka
Copy link
Author

fwiw, I did finish the sample and Blazor 8 state management blog post I mentioned back in November, and that can provide a good model for a solution. I have modified that solution to be a part of the upcoming CSLA 8 release, and have confidence that it is a workable solution overall.

Yes, it would be ideal if a solution was built into Blazor! I'm not sure we should rush the team into a solution when workarounds are practical.

@mkArtakMSFT
Copy link
Member

Thanks for confirming that there is a solution.
We will close this as we have separate issue tracking the work to expose an API for detecting the current render mode: #49401

@mkArtakMSFT mkArtakMSFT closed this as not planned Won't fix, can't repro, duplicate, stale Jan 17, 2024
@gabephudson
Copy link

Thanks for confirming that there is a solution. We will close this as we have separate issue tracking the work to expose an API for detecting the current render mode: #49401

mkArtakMSFT, #49401 you linked to is a proposal for detecting render mode at runtime. This seems to be a completely different issue than this discussion, which is having a standard framework solution for maintaining state across render modes. Can you clarify why you have closed this discussion?

@rockfordlhotka
Copy link
Author

Agreed. While yes, I have a type of solution to the problem, in reality this is something that should be addressed within the Blazor platform itself, and the issue you link to doesn't address this at all.

@Andrzej-W
Copy link

Hello @mkArtakMSFT I think you have closed this issue to fast. Please read the two comments above. I'm reopening in hope the problem can be solved in Blazor framework itself.

@Andrzej-W Andrzej-W reopened this Feb 17, 2024
@arc95
Copy link

arc95 commented Mar 3, 2024

Any update on a resolution to this issue? Thank you.

@rockfordlhotka
Copy link
Author

Carl Franklin and I did a Blazor Train episode on this issue about a week ago, and I think Carl was on Jeff Fritz's twitch stream talking about it as well.

I have a workaround that is now built into CSLA .NET, and a prototype that I described in the blog mentioned earlier in this thread.

At this point I don't expect the product team to resolve this until .NET 9. Hopefully at some point anyway!

@gabephudson
Copy link

Carl Franklin and I did a Blazor Train episode on this issue about a week ago, and I think Carl was on Jeff Fritz's twitch stream talking about it as well.

I have a workaround that is now built into CSLA .NET, and a prototype that I described in the blog mentioned earlier in this thread.

At this point I don't expect the product team to resolve this until .NET 9. Hopefully at some point anyway!

Great episode Rocky! Thanks for your work around library and your detailed issue posting above. I hope the Blazor team creates a nice, in the box solution for session level state management for a mixed render mode Blazor app in 9 (or 8.11).

"Just use Redis" is not the answer. ;) Even that requires some kind of session ID, which requires a cookie, and those are easy to read in SSR, tricky to read in Interactive Server (no HttpContext), and I'm assuming very difficult to read in WebAssembly.

@mkArtakMSFT mkArtakMSFT removed this from the .NET 9 Planning milestone Mar 13, 2024
@mkArtakMSFT
Copy link
Member

@rockfordlhotka what would be the minimum set of features that you think of, that can solve the problems you're facing?

@mkArtakMSFT mkArtakMSFT added the Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. label Mar 14, 2024
@gabephudson
Copy link

@rockfordlhotka what would be the minimum set of features that you think of, that can solve the problems you're facing?

The ASP.NET Core Session State Provider/Service should work in all Blazor render modes out of the box and retain its "state" across render mode boundaries.

@rockfordlhotka
Copy link
Author

@rockfordlhotka what would be the minimum set of features that you think of, that can solve the problems you're facing?

tl;dr - we need a way to know that the user is navigating away from the current page so state can be persisted or transferred for use in the destination page. The current page should have a lifecycle concept like OnLeaving or something like that.

--

I think the primary issue remaining for implementing a solution is that there's not a good way to know that the user is navigating from an interactive-wasm page to a different render mode (server-static or server-interactive). It is this transition where any changed client-side state needs to be transferred to the server.

My current solution is to have every wasm page implement IDisposable so the state can be transferred to the server in the Dispose method. This is problematic, because the Dispose method isn't called until after the destination page has started to render. For a server-static page this only occurs when (a) the page finished rendering; (b) the page is streaming.

Assuming that the destination page can't render without the state, the fact that the destination page needs to render before it can get the state leads to a deadlock. The page can't render without the state, but the state can't become available until the page renders 🙀

@dotnet-policy-service dotnet-policy-service bot added Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. and removed Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. labels Mar 14, 2024
@rockfordlhotka
Copy link
Author

@rockfordlhotka what would be the minimum set of features that you think of, that can solve the problems you're facing?

The ASP.NET Core Session State Provider/Service should work in all Blazor render modes out of the box and retain its "state" across render mode boundaries.

I have not found that this transfers state to a wasm-interactive client, and then back from the wasm environment to a server-static or server-interactive page.

@gabephudson
Copy link

I have not found that this transfers state to a wasm-interactive client, and then back from the wasm environment to a server-static or server-interactive page.

Sorry for the confusion Rocky, I was answering the question. It should read...
The ASP.NET Core Session State Provider/Service DOES NOT work in all Blazor render modes out of the box and retain its "state" across render mode boundaries. IT SHOULD. :)

I've had to write my own provider that initiates a session ID cookie in the App.Razor (SSR) and then (painstakingly) statefully retains that ID across render mode boundaries so it can interact with a server-side key/value state service. It would be nice if this "just worked" for the Asp.net Core Session Provider.

@rockfordlhotka
Copy link
Author

To be clear, my goal is to have an equivalent to Blazor 6/7 per-user state capabilities that translates into the awesome Blazor 8 InteractiveAuto render mode model.

In 6/7 it was possible to maintain state in a DI scoped service. Extremely elegant.

In 8 that no longer works because DI scopes have become ephemeral. They often come and go page to page.

That's fine, but the concept can be retained, and I have a blog and sample on github that does exactly that. However, it is very sketchy when it comes to transferring state from a wasm-interactive page to any server page, because there's no reliable way to know that the user is about to navigate to another page (or render mode).

I absolutely agree that it would be ideal if such a solution was "in the box". I suspect that won't happen in the near future, so my contention is that we need this OnLeaving notification in the component lifecycle so it is possible to easily implement our own solutions.

@MackinnonBuck
Copy link
Member

MackinnonBuck commented Mar 22, 2024

@rockfordlhotka I recall you mentioning that LocationChanged isn't sufficient to initiate the transfer of state from the client to the server, and that's because there's a race condition between the state transfer and the rendering of the new page. This race condition could be overcome by having the server delay the rendering of the new page until the state transfer completes. It's still a non-trivial task to make this work; for example, the client would need to somehow communicate to the server that it has state that needs transferring. This might be achievable via some logic in the browser that sets a cookie depending on whether there is state to sync.

After #48766 gets implemented, another approach might be to use location changing handlers, since they have the capability to block navigations arbitrarily. This would eliminate the race condition problem.

All that said, the overall idea of transferring state between the client and the server in this manner has limitations/challenges that are difficult to solve with a generalized abstraction. For example:

  • How would one generally handle the case where WebAssembly and Server interactive components exist on the page at the same time? They have their own copies of state, which means conflicts would need to be somehow resolved by the server when a navigation happens.
  • What should the lifetime of this state be? Should it get cleared on page reload? When the tab gets closed? When the user logs out? Or should it last indefinitely? The "right" answer to this question may heavily depend on the app's requirements and implementation. Maybe the state should get stored in a database, or an in-memory cache, or something else.

A built-in general and intuitive solution that takes all these variables into account is very tricky to do correctly.

@rockfordlhotka
Copy link
Author

@MackinnonBuck You bring up some very good points about the complexity. Another one we just encountered is multiple tabs against the same app, with each tab hosting wasm pages - so at least two "active" copies of the state exist.

The idea of an in-memory cache is what I've been doing in a sense, but the granularity of the data is still challenging. My simple example has granularity of "all state", which is problematic. Having a granularity of "each field or value" is probably better, but leads to a very chatty API. And in any case, notifying all potential users (server and wasm components) of changes is complex.

So I hear you and agree that this isn't an easy problem space to address.

At the same time, for some scenarios and application requirements, I continue to think it important that the events exist from Blazor page lifecycle or navigation such that we have some hope of implementing solutions.

I recall you mentioning that LocationChanged isn't sufficient to initiate the transfer of state from the client to the server, and that's because there's a race condition between the state transfer and the rendering of the new page. This race condition could be overcome by having the server delay the rendering of the new page until the state transfer completes. It's still a non-trivial task to make this work; for example, the client would need to somehow communicate to the server that it has state that needs transferring. This might be achievable via some logic in the browser that sets a cookie depending on whether there is state to sync.

The specific issue today is that the only way to know for sure that a user has navigated away from a page is for the page to implement IDisposable, and to save the state in the disposed method.

The problem is that Dispose isn't invoked until after the next page has been rendered. This seems to work fine for server-interactive pages, and also for server-static streaming pages.

Server-static pages (non-streaming) cause the deadlock because they haven't rendered when any of the page lifecycle events occur (initialized, etc.), and they don't invoke the after render event (of course). The result is that any use of a non-streaming server-static page causes a deadlock when navigating from a wasm-interactive page.

@MackinnonBuck
Copy link
Member

@rockfordlhotka

I continue to think it important that the events exist from Blazor page lifecycle or navigation such that we have some hope of implementing solutions.

Thanks for the reply. If I'm understanding correctly, it sounds like we agree that it's reasonable to consider this type of solution to be out of scope as a built-in Blazor feature. However, having a way to be notified that the current page is about to be disposed is something Blazor could provide, and this would enable the community to implement their own solutions to the problem. Please let me know if my interpretation is correct!

If so, I believe that implementing #48766 would meet that need. Note that the issue tracks enabling location changing handlers (not the LocationChanged event, which already works) for Blazor Web apps. Those handlers would get invoked before the navigation happens and allow delaying/cancelling the navigation arbitrarily, which would allow apps to preserve any state they need to before allowing the navigation to continue. If you agree that this would be sufficient, is it fine with you if we close out this issue as a duplicate of #48766?

@rockfordlhotka
Copy link
Author

@MackinnonBuck Assuming that #48766 works in all cases (NavigationManager and <a href tags) and doesn't suffer the IDisposable timing/deadlock issue, I agree that it provides the necessary hook.

@mkArtakMSFT
Copy link
Member

@MackinnonBuck Assuming that #48766 works in all cases (NavigationManager and <a href tags) and doesn't suffer the IDisposable timing/deadlock issue, I agree that it provides the necessary hook.

That's the plan - to raise the event in all the cases.
At his point I'm closing this issue as a dupe of the linked one, given there is no other action pending from us here.

@mkArtakMSFT mkArtakMSFT closed this as not planned Won't fix, can't repro, duplicate, stale Mar 26, 2024
@mkArtakMSFT mkArtakMSFT added ✔️ Resolution: Duplicate Resolved as a duplicate of another issue and removed Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. labels Mar 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-full-stack-web-ui Full stack web UI with Blazor ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Pillar: Complete Blazor Web Status: Resolved
Projects
None yet
Development

No branches or pull requests

9 participants