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

Provide a mechanism for rendering Blazor pages statically even when the app is set up for interactive rendering #51046

Closed
danroth27 opened this issue Sep 30, 2023 · 41 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one Pillar: Complete Blazor Web Priority:1 Work that is critical for the release, but we could probably ship without
Milestone

Comments

@danroth27
Copy link
Member

danroth27 commented Sep 30, 2023

We provide a way to opt into interactivity on a per page or component level. When interactivity is enabled globally, there should similarly be a way to opt out of interactivity so that architectural choices made for one part of your app don't limit your choices in other parts of your app. For example, you might have an app that is setup to use interactive WebAssembly rendering globally, but you want to add a page that only renders statically from the server.

@SteveSandersonMS
Copy link
Member

The error page, which uses HttpContext to get the request ID

We already accounted for that when implementing error support. No matter how your app is set up, the error page will render non-interactively automatically.

Scaffolded CRUD pages that interact with EF Core to handle data interactions

Can you clarify? Why wouldn't you want those to be interactive? Those sorts of pages are full of reasons to have interactive behaviors. Shouldn't it be up to the developer to choose whether CRUD pages use interactive rendering or not?

The default Identity UI, that uses server based functionality for user identity and account management

I agree about this, although (1) we already have an adequate workaround, and (2) it's only a point-in-time limitation that the Identity UI pages are not coded in such a way that they work nicely in interactive mode. Longer term it would be best if we did make them work properly in interactive mode (and yes they will have to do something nonobvious to set cookies).

Please don't interpret this as rejecting the feature request. I totally agree developers should have the flexibility to do this anywhere that they want. I'm just trying to sharpen up the definition of the scenarios to make sure we don't optimize the design for the wrong things. My justification for the feature would be something like "in per-page mode you can opt into interactivity, so in global mode you should be able to opt out of interactivity - in both cases so that architectural choices made for one part of your app don't limit your choices in other parts of your app"

@danroth27
Copy link
Member Author

We already accounted for that when implementing error support. No matter how your app is set up, the error page will render non-interactively automatically.

How is this accomplished?

Can you clarify? Why wouldn't you want those to be interactive? Those sorts of pages are full of reasons to have interactive behaviors. Shouldn't it be up to the developer to choose whether CRUD pages use interactive rendering or not?

You're correct that interactivity may be desired. I was focusing on static server rendering to simplify the scenario and to get to parity with the scaffolding support we have for MVC & Razor Pages. Interactive server rendering should be relatively straightforward to support since the code is still running from the server. Interactive WebAssembly rendering however adds a bunch of complexity because it requires a different data access strategy (API endpoints, HTTP requests, etc).

it's only a point-in-time limitation that the Identity UI pages are not coded in such a way that they work nicely in interactive mode. Longer term it would be best if we did make them work properly in interactive mode (and yes they will have to do something nonobvious to set cookies).

In addition to figuring out a way to set the cookies presumably we'll need a way to abstract access to the identity related services and data. For some components, it does seem like supporting all render modes introduces a significant amount of complexity currently.

My justification for the feature would be something like "in per-page mode you can opt into interactivity, so in global mode you should be able to opt out of interactivity - in both cases so that architectural choices made for one part of your app don't limit your choices in other parts of your app"

Agreed. I'm happy to update the original post description accordingly.

@SteveSandersonMS
Copy link
Member

How is this accomplished?

Internal stuff, not exposed as public API. The renderer explicitly checks if the current HttpContext contains IErrorHandlingFeature or whatever it's called - the thing that's the error handling middlware places there when re-running the pipeline.

For some components, it does seem like supporting all render modes introduces a significant amount of complexity currently.

Definitely. Just handling SSR + InteractiveServer is not so much complexity (except in some edge cases where you have to be able to set cookies), but I definitely agree that InteractiveWebAssembly adds a whole extra dimension. Limiting the scaffolding to SSR + InteractiveServer would be a pretty reasonable option.

@mkArtakMSFT mkArtakMSFT added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Oct 2, 2023
@mkArtakMSFT mkArtakMSFT added this to the .NET 9 Planning milestone Oct 2, 2023
@ghost
Copy link

ghost commented Oct 2, 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.

@gabephudson
Copy link

This feature would be extremely useful for global InteractiveServer projects that want certain components/pages to render statically. In Blazor 8, it is currently tricky to do this.

I'm trying to convert a large(ish) Blazor Server 7 app to the new Blazor 8 web pattern and am having A LOT of issues unless I keep it globally InteractiveServer, especially with third party Blazor components.

This would allow me to carve out pages that can run statically. Currently, setting the rendermode on individual pages doesn't work, as it causes the circuit to drop and create a new one on each page (and the app relies on scoped services currently). :(

(I tried setting rendermode option in _Imports.Razor file, but as you know, it doesn't function there. Dan suggests I try using the Router to do this.)

My simple goal is to have a default global "InteractiveServer" render mode (primarily to keep a single circuit open across the app), but to be able to specify components as static when needed.

I understand this will be less useful for new apps, but it will really help with converting existing apps.

@KennethHoff
Copy link

Good to see that my question from mid-August was reconsidered after all ^_^

@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.

@AbstractionsAs
Copy link

I'll add some complexity to this:

  • I would like to be able to differentiate on other parameters than just the page or component. For instance, a page might need interactivity if the user is logged in, but not otherwise. Or it might need it of the user has a certain permission.

  • The ability to switch between prerendered and non-prerendered interactivity based on the same parameters. You may want to enable prerenderering, but reasonably be able to say that most pages will never be prerendered. The way it is now, if you enable prerendering for a WebAssembly application, every single service needs to work on the server as well as the client to avoid prerendering causing a crash. Even if you try to differentiate based on state, inject-direcrives on pages will crash the application during prerendering if the service is not available. Even if I know this service will never be used or required during static rendering, I need it to be injectable, or the app will crash..

It seems to me like there are a couple of possible solutions that could solve all of the above.

Suggestions:

  • Add an interface that one could register in DI that could perform a discriminating function, It would have information about the current request/navigation action, and be used to determine behaviour, such as the rendermode, whether or not to pretender, whether or not to require injected services to exist.

  • Add a cascading root component that holds information about whether it is prerendering or not.

  • Add a way for a component to retrieve the current rendermode.

  • Add a simple way to switch layouts based on the above, enabling the use of a simpler layout for prerendering scenarios

  • Enable conditional injects

  • Enable optional injects

@danroth27
Copy link
Member Author

Thanks @AbstractionsAs for the feedback!

Add a cascading root component that holds information about whether it is prerendering or not.
Add a way for a component to retrieve the current rendermode.

I think these will be covered by #49401.

Enable optional injects

I believe in .NET 9 Preview 2 you can now do optional service dependencies in components using constructor injection.

@javiercn
Copy link
Member

javiercn commented Apr 3, 2024

@SteveSandersonMS I'm ok with proposal 1, but I think there might be better ways of achieving proposal 2.

Instead of having to do all the "shennanigans" with the multiple render modes. We could provide the render mode from the host as a cascading value that gets computed by looking at the endpoint metadata.
The way we would compute this value would be by using a global default value (server, webassembly, auto) that can be overriden by the page if it provides a concrete @rendermode StaticServer.

I feel a bit of impedance between having a different attribute as opposed to having a different render mode. If we proceed this way I would prefer if we name, the attribute differently without making any mention to the render mode. Like [DisableInteractivity] or something that can't be associated with the render mode.

If on the contrary we do something like what I suggested, I think that can end up being something like:

@code
{
  [CascadingValue] public RenderMode ResolvedRenderMode { get; set; }
}

<Router @rendermode="ResolvedRenderMode">
  ...
</Router>

Then the only thing to do remains defining the "default global render mode" which we could do in the call to MapRazorComponents(); as follows.

MapRazorComponents() -> Static|None
MapRazorComponents().AddServerComponents() -> Server
MapRazorComponents().AddWebAssemblyComponents() -> WebAssembly
MapRazorComponents().AddServerComponents().AddWebAssemblyComponents() -> Auto
MapRazorComponents(defaultRenderMode: RenderMode.Server).AddServerComponents().AddWebAssemblyComponents() -> Server

@javiercn
Copy link
Member

javiercn commented Apr 3, 2024

Problem with <Routes @rendermode="InteractiveServer" />
This is kind of obvious. On the initial page load it's going to ignore the per-page rendermode and just use InteractiveServer regardless, so @rendermode StaticServer simply doesn't work. This means people with existing global-interactivity sites have to do a nonobvious upgrade step and if they miss it, will struggle to understand why @rendermode StaticServer is ignored.

The router could get its render mode as a parameter and check the page its rendering to ensure it either doesn't define a render mode or defines a compatible one and throw an exception otherwise, couldn't it?

Problem with <Routes @rendermode="@PageRenderMode" />

If the render mode comes resolved from the host (and the SSR host just uses the global default + endpoint override) then this would work, because it would use the global default, isn't it?

@javiercn
Copy link
Member

javiercn commented Apr 3, 2024

The other aspect I think is that you would not get into a situation where you have <Router @rendermode="InteractiveServer" /> but your page doesn't render interactively without you knowing (think of a page coming from an RCL for example).

If we were to let the host resolve the render mode and have the server use the endpoint metadata to suggest a value, then at the point where you are passing the render mode to the router you can always put a breakpoint and know exactly what render mode is being used.

You also get reasonable behaviors in all combos (Server only, Wasm only, both enabled) and retain the ability to override on a per page basis (if the render mode is incompatible the entry doesn't get added to the client route table)

Finally, you get the benefit that you mentioned above where different groups of pages retain the ability to perform client navigations amongst themselves.

@KennethHoff
Copy link

KennethHoff commented Apr 3, 2024

Conceptually, I think Option 2 is the most consistent and "simple" way; There are three ways of rendering Razor Components:

  1. Interactivity & Statically(Prerendering) via WebSockets
    • @rendermode InteractiveServer
  2. Interactivity & Statically(Prerendering) via WebAssembly.
    • @rendermode InteractiveWebAssembly
  3. Purely statically.
    • Default behaviour if no other @rendermode is set anywhere in the tree, or can be restored using... @staticpage ...wait what? Why did this suddenly completely change?
    • To me, this @staticpage directive feels like @technically-not-a-rendermode-because-we-were-unable-to-implement-it-as-such

That said, I definitely understand the problems faced with @rendermode StaticServer, but the way it's shown tells me that all it requires is some (very) good documentation and some Roslyn Analyzers; Ensure that uses of @rendermode is "sane" according to whatever rules are needed for it to work reliably.

@javiercn
Copy link
Member

javiercn commented Apr 3, 2024

Another thing that I realized now is that option 1 is all or nothing, meaning that no component is going to be able to render interactively (for example in your layout) if your page requires static rendering.

With option 2 that's not the case, and it also leaves the door open for interactive components on the page to render interactively if they need to.

@gabephudson
Copy link

Regarding option 2 and the navigation render mode "pitfalls", what if you added a new default RenderMode "Inherit"? This would actually be the default render mode if not specified. For pages that have not yet been navigated to and that do NOT have a RenderMode set, then Inherit would default to Static.

I believe this would be fairly backward compatible, but would require the addition of 2 new RenderModes (Static and Inherit).

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Apr 3, 2024

Instead of having to do all the "shennanigans" with the multiple render modes. We could provide the render mode from the host as a cascading value that gets computed by looking at the endpoint metadata.
The way we would compute this value would be by using a global default value (server, webassembly, auto) that can be overriden by the page if it provides a concrete @rendermode StaticServer.

What I'm taking from this is the notion that, instead of resolving the rendermode via arbitrary code inside App.razor, we say "there is only one rule you can use, and it's: PageRenderMode ?? GlobalDefaultRenderMode", where GlobalDefaultRenderMode is another thing to configure in Program.cs.

That does have the payoff that we can use the same rule when building the interactive route table (at the cost that we have to communicate GlobalDefaultRenderMode into the circuit/wasm, via persistent component state or similar).

However, I'm concerned this still leaves us in the category of "it works if you use it like we say, but does weird stuff if you try to use it in ways we didn't plan for", which is one of the key challenges with rendermodes in general. For example, taking existing apps, if I'm interpreting correctly then I think:

  • They will have to change App.razor to put in the boilerplate logic, and move the place where they define the global rendermode from App.razor into a new place in Program.cs
  • Until they do this, they can't use @rendermode StaticServer as it will either be ignored or will be an error
  • It's going to be really weird that you even can have @rendermode="SomethingSpecific" in App.razor given that it wouldn't agree with the new rules around how we build the interactive route table
  • It continues to look like you can put arbitrary logic in App.razor to pick the rendermode dynamically (e.g., based on URL, some HTTP header, or whatever), which is a genuinely useful feature today. However, that arbitrary logic will not compose properly with @rendermode StaticServer or possibly other cross-mode navigations since the router will not be using that logic when it builds its route table.

I'm keen that we bias towards simplicity and not increasing the range of weird scenarios that we didn't anticipate, since the community is already stretched to understand what you can do in .NET 8 (and of course there's the support load). So unless there's super high payoff and really low chance of problems, I'm reluctant to add new combinations and upgrade steps 😄

Similarly all the stuff about being able to do client-side navigations between Server page groups and WebAssembly page groups is conceptually interesting but creates new inconsistencies/problems if you're reliant on this but then try to add some interactive component to the layout (we either have to destroy and lose its state when transitioning across the rendermode groups, because it's now a full-page load, or we simply can't transition across the rendermode groups and it we either ignore the specified rendermode, or it becomes an error to attempt that navigation). So it's also expanding the surface area of "stuff people can do but leads into them into weirdness".

Regarding option 2 and the navigation render mode "pitfalls", what if you added a new default RenderMode "Inherit"?

TBH I'm not clear on how that differs from not setting a rendermode, or if the problems would still remain on components that don't set a rendermode.

@gabephudson
Copy link

Regarding option 2 and the navigation render mode "pitfalls", what if you added a new default RenderMode "Inherit"?

TBH I'm not clear on how that differs from not setting a rendermode, or if the problems would still remain on components that don't set a rendermode.

It changes nothing from a functionality standpoint, but makes it clear to developers what the rendering behavior will be (which was a valid concern you outlined regarding option 2).

As you outlined, I see having a page "inherit" the rendermode from the previous page an advantage and not necessarily a pitfall. For example, I could have some public "static" pages in the app that the user could navigate to. If they navigate to them after already entering the interactive part of the app, they will render as interactive and (in the case of InteractiveServer) the circuit would not be dropped. (I am making an assumption based on my understanding of your description of option 2).

Both options work for me and there seems to be a lot of pros to option 2, but they come with some cons regarding complexity, backward compatibility, and perhaps some unexpected behavior (as you said). I was thinking by making the behavior you outlined more implicate with a specific new RenderMode, it would help clarify the communities understanding of the behavior you outlined in Option 2. Just a thought if you go that route. ;)

@gabephudson
Copy link

P.S. Of course, now that I say this, you then have essentially have two RenderMode contexts you have to track. The page's "default" rendermode and then the rendermode the page/component is currently operating under.

I believe there is another issue requesting an API for code to be able to "get" the current rendermode of a component.

@sbwalker
Copy link

sbwalker commented Apr 3, 2024

After recently going through the migration of a traditional Blazor application to the new Blazor approach in .NET 8 (2 months development effort, 500+ commits) I would like to request the following:

  • please focus on making the existing Blazor scenarios offered in .NET 8 work 100% properly BEFORE introducing new render modes or approaches
  • please stop changing the foundational aspects of Blazor applications ie. requiring changes to Startup, entry-point page / components, or root components on every major .NET release is time consuming and counter-productive to the vast majority of stakeholders
  • please focus on simplicity - there is already so much power and flexibility that it is very confusing for new developers to understand how to use the framework... especially the caveats of how various approaches are not compatible with one another and can lead to a lot of wasted time and energy
  • please do not ignore the fact that the Blazor router has been an extensibility point since Blazor was first introduced, and including new features which are dependent on new router behaviors means that all apps which are using custom routers cannot use the feature without a router modification (which is often difficult to accomplish because custom routers were not considered during the design phase of the feature)
  • please validate that this feature is actually needed before proceeding - it is already possible to configure an application for Static Rendering and then specify @rendermode = Interactive for the majority of your components - this is exactly what my application does as the home page is expected to be static but all of the administrative pages are expected to be interactive.

@gabephudson
Copy link

  • please validate that this feature is actually needed before proceeding - it is already possible to configure an application for Static Rendering and then specify @rendermode = Interactive for the majority of your components - this is exactly what my application does as the home page is expected to be static but all of the administrative pages are expected to be interactive.

All good points. Regarding this last point, the issue I have run in to (with InteractiveServer) is that, when one configures the app this way (rendermode specified on each page instead of globally), when navigating between pages, the SignalR circuit is destroyed, and one loses any scoped services and cascading values. This is a problem for apps the use scoped services to store user state across pages (which was a recommended/common approach in Blazor 7-).

(I know one can externalize the session state stores in other ways (Distributed Cache))

@sbwalker
Copy link

sbwalker commented Apr 3, 2024

@gabephudson it sounds like you are struggling with state management with mixed render modes. This is indeed a very big challenge in Blazor in .NET8 and is actually aligned with my first bullet point - “please make existing Blazor scenarios work 100%”. In Oqtane I am manually passing state as serializable parameters across the render mode boundary which is fairly simple to implement and works well. Maybe we just need some better guidance on this item as it seems to be the area where most developers are struggling… and it is potentially a better solution than introducing yet another render mode (with additional state management challenges)

@halter73
Copy link
Member

halter73 commented Apr 3, 2024

please validate that this feature is actually needed before proceeding

I think the presence of the following in App.razor in the .NET 8 Blazor project template if you have individual auth enabled is sufficient motivation for this feature:

<Routes @rendermode="RenderModeForPage" />
@* ... *@
@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
        ? null
        : InteractiveServer;
}

Having to check the HttpContext to see if the request path starts with /Account is very ugly. It's possible to have made this a little cleaner by checking the matched endpoint for a custom attribute or something, but that should be something that's built into the framework for a scenario as common as statically rendering a page on a mostly interactive site.

I think option 1 where we add a very limited @staticpage directive that causes the EndpointHtmlRenderer to ignore @rendermode values fits the bill perfectly. It doesn't require any modifications to the code other than adding the directive to the page. I want to get out of the business of writing complicated expressions to cumpute a @rendermode value.

Sure, it's less flexible than a fully blown render mode. @javiercn is right that it's all or nothing, but I think that's okay that no component including the layout is going to be able to render interactively if a page is explicitly labeled as a @staticpage. The whole point is that I'm overriding the defaults of the layout and subcomponents, and it's very easy to explain. If you really want the main page to be rendered statically without preventing other components from rendering dynamically, you pass a computed @rendermode to the router that's null for static pages like you do today.

I'm a fan of making it a directive rather than an attribute to highlight how special it is. We should also make sure we provide a clear error if someone tries to put @staticpage on a non-routable component.

Also, this is a bit of a tangent, but would it be possible to delay all rendering until the Task returned by OnInitializeAsync completes for pages marked with the @staticpage directive? Since it's a new directive, I would expect it to be pretty non-breaking. Sure, it might affect how existing components render, but so does ignoring @rendermode values.

@gabephudson
Copy link

@gabephudson it sounds like you are struggling with state management with mixed render modes. This is indeed a very big challenge in Blazor in .NET8 and is actually aligned with my first bullet point - “please make existing Blazor scenarios work 100%”. In Oqtane I am manually passing state as serializable parameters across the render mode boundary which is fairly simple to implement and works well. Maybe we just need some better guidance on this item as it seems to be the area where most developers are struggling… and it is potentially a better solution than introducing yet another render mode (with additional state management challenges)

Agreed that state management across render modes is indeed a challenge and I wish there was a recommended or "in the box" way to do this in 8. When moving to 8, this was the main challenge, and I was surprised that the dotnet session state provider (the logical place to look to solve this) does not seem to support Interactve pages. (Would love to hear more about how you have solved this issue by using serialized parameters).

That said, this is a related but separate issue from this. Don't want to derail this thread more. I was going to comment on how even the templates have to use a "workaround" so that Login and Error pages are rendered statically in a global interactive app, but @halter73 beat me to it. Bottomline, this issue is here because the current workaround is unintuitive and verbose.

Bottom line, sounds like you are a vote for Option 1; and you make good arguments for this. 👍

@MackinnonBuck
Copy link
Member

This is great, @SteveSandersonMS!

High-level thoughts

There are some really interesting ideas in this proposal and I especially appreciate how clearly the limitations for option 2 are conveyed.

Of the two options, proposed as they are, I would definitely prefer the first. There were a lot of nuances introduced in .NET 8 that can already be hard to wrap one's head around, so it seems less than ideal to introduce additional mechanisms that appear to be flexible but are actually constrained a small number of well-known usage patterns.

That said, I think it's worth exploring the advantages that the second approach provides and determining if there's a way to minimize/eliminate the downsides.

Additional option 2 considerations

However, the problems start if you do any of the following:

<Routes @rendermode="InteractiveServer" />
<Routes @rendermode="@PageRenderMode" />
<Routes @rendermode="@(PageRenderMode ?? StaticServer)" />

...

Correct me if I'm wrong, but I think even the expected usage...

<Routes @rendermode="@(PageRenderMode ?? InteractiveServer)" />

...is also susceptible to unintuitive behavior: If you navigate to a page with an explicit InteractiveWebAssembly render mode, then navigate to a page with no explicit render mode, then WebAssembly interactivity will continue to be used. This does not reflect what's specified in the expression used to compute the <Routes> render mode, which suggests that InteractiveServer should be used on any page without an explicit render mode.

It seems to me like the general problem is that the approach requires application code (the render mode expression in App.razor) to exactly match the logic that the interactive router uses when determining whether to reload the page. Having a framework feature that makes such a strong assumption about application logic seems problematic to me.

Option 2 alternative

I believe the functionality that option 2 provides can be summarized simply: Prefer the page's explicit render mode (including a new StaticServer render mode), with a default render mode for pages that don't specify one.

If that's the case, something like @javiercn's proposal has some great ideas. But I think we could probably make some subtle changes to reduce the likelihood of misusing the feature.

First, there wouldn't be any behavioral changes to how client-side routing works by default. Everything would be opt-in so all existing patterns (like the one Identity UI uses) are still valid.

If a customer wanted to use this feature, they would make the following changes to their app:

Program.cs

app.MapRazorComponents()
    .AddServerComponents()
    .AddWebAssemblyComponents()
+   .AddCascadingPageRenderMode(defaultRenderMode: InteractiveServer);

App.razor

-<Routes @rendermode="InteractiveServer" />
+<Routes @rendermode="PageRenderMode" />

...

@code {
+   [CascadingParameter]
+   public IComponentRenderMode PageRenderMode { get; set; }
}

If a defaultRenderMode does not get supplied to AddCascadingPageRenderMode(), then the default defaultRenderMode is always StaticServer. I think it would be too spooky to automatically select a default interactive render mode based on which interactive render modes are enabled.

Then the one additional bit of information the interactive router needs is whether the default page render mode is compatible with the current interactive render mode. If it is compatible, then allow interactive navigation to that page. If not, then reload when navigating to that page.

I believe this approach addresses most of the concerning scenarios presented so far:

@* This looks like sensible code, but the behavior is subtle and strange - see below *@
<Routes @rendermode="@PageRenderMode" />

The original problem with this was that interactively navigating to a page with no explicit render mode results in staying on the current render mode. This doesn't happen with the proposed alternative, because the interactive router is aware of whether the default render mode is compatible with its current render mode. If it's not compatible, a refresh will occur and the render mode will be re-evaluated on the server.

@* This also looks sensible (i.e., define SSR as the default), but again the behavior is NOT what you think *@
<Routes @rendermode="@(PageRenderMode ?? StaticServer)" />

The proposed alternative doesn't have this issue because the default gets expressed not through the @rendermode expression, but instead via the call to AddCascadingPageRenderMode(). As mentioned above, the client-side router knows when the specified default render mode is not compatible with the current one.

@* Existing global-interactivity apps *@
<Routes @rendermode="InteractiveServer" />

The concern with this was that the StaticServer render mode wouldn't work in this scenario without requiring changes to application code. This would also be true with the proposed alternative, but we could probably catch this case at runtime and throw a detailed error that suggests what the problem is.

From some of @SteveSandersonMS's recently-expressed concerns:

It's going to be really weird that you even can have @rendermode="SomethingSpecific" in App.razor given that it wouldn't agree with the new rules around how we build the interactive route table. It continues to look like you can put arbitrary logic in App.razor to pick the rendermode dynamically (e.g., based on URL, some HTTP header, or whatever), which is a genuinely useful feature today. However, that arbitrary logic will not compose properly with @rendermode StaticServer or possibly other cross-mode navigations since the router will not be using that logic when it builds its route table.

I agree with all of this, although it's not a problem by default - only if you use .AddCascadingPageRenderMode(). We might be able to create an analyzer that warns if it detects that you pass anything other than the cascading PageRenderMode to a component rendering a <Router> (but there's still a possibility that the analyzer misses a more sophisticated scenario, and we do have to pay the cost of maintaining the analyzer).

Revisiting option 1

While I think it's still worth discussing various directions to take option 2, I'm leaning toward option 1. That said, option 1 still has a potentially confusing bit of behavior, which is that @rendermode gets ignored/disabled. But if the naming/syntax made it clear enough that this was a switch completely disabling interactivity for the page, then this might be fine.

In addition, AFAICT, taking option 1 now doesn't necessarily prevent us from taking option 2 in the future if there's enough demand for it. Even if we add some variation of option 2 later, it might still be useful to have option 1 as a global override that completely disables render modes.

@halter73
Copy link
Member

halter73 commented Apr 4, 2024

If we do option 1, would it be sensible to throw if any part of the layout or any subcomponent specifies a non-null @rendermode for a page with the @staticpage directive rather than ignore it? It's a bit more restrictive, but makes it clear what's going on.

@sbwalker
Copy link

sbwalker commented Apr 4, 2024

@halter73 I actually do not agree that the default Blazor template with Auth enabled is validation that this feature is needed.

If you are developing globally interactive applications then it can be assumed all components are using Blazor interactivity… and you would actually prefer to use interactive Auth components as well… however you cannot because Microsoft only provided static versions of these components in .NET 8. If Microsoft had provided interactive Auth components there would be no need for ugly hacks in App.razor and there would be no need for the enhancement being debated in this issue.

I actually do not understand why the Auth components were developed as static components… as the type of functionality they represent is a perfect example of the scenario where you would choose to use an interactive component within a statically rendered application. The Oqtane Framework (https://www.oqtane.org) has interactive components for all Auth scenarios (ie, login, register, etc…) which work perfectly whether you choose to use global interactivity or static rendering in your app.

@gabephudson To clarify, I am not voting for option 1 or 2… I am suggesting that this feature should not be implemented at all.

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Apr 4, 2024

Thanks everyone for the additional thoughts! Especially @MackinnonBuck for the very detailed analysis.

The "option 2" refinements that @javiercn and @MackinnonBuck are describing do indeed address some of the more severe weirdnesses of my original option 2. My sense is that we could make something along those lines work if we considered it important enough, albeit with remaining costs/challenges:

  • It necessitates an upgrade step and some new, fairly unattractive boilerplate code in every project (and until the upgrade step is done, attempting to use StaticServer rendermode won't work)
  • There would remain the need for lots of explanations about why we've moved the place where you define the global rendermode, and under which circumstances you can and can't use StaticServer rendermode (e.g., can't use it on @rendermode=... attributes)

However some of the following feedback backs up my instinct that it would be a wrong direction right now:

  • [sbwalker] "BEFORE introducing new render modes or approaches" ... "stop changing the foundational aspects of Blazor applications ie. requiring changes to Startup, entry-point page / components, or root components" ... "focus on simplicity" ... "especially the caveats of how various approaches are not compatible with one another"
  • [mackinnonbuck] "While I think it's still worth discussing various directions to take option 2, I'm leaning toward option 1" "taking option 1 now doesn't necessarily prevent us from taking option 2 in the future"
  • [many people] "Option 1 sounds good and fits the bill"

After hearing all this, I'm sold on the idea that "requiring upgrade steps" on its own is enough to invalidate a design (I don't mean that universally, but for this feature). It would be OK if the upgrade steps were innately obvious (e.g., "to use a new DI service, first register it in your DI container"), but it's definitely not obvious that using @rendermode StaticServer on one place also requires arbitrary-looking boilerplate changes in App.razor. The ecosystem does not have an appetite for learning about more combinations that do and don't work together, or for debugging problems that happen if you didn't upgrade your app properly.

The all-or-nothing limitation

[javiercn] Another thing that I realized now is that option 1 is all or nothing, meaning that no component is going to be able to render interactively (for example in your layout) if your page requires static rendering.
[mackinnonbuck] option 1 still has a potentially confusing bit of behavior, which is that @rendermode gets ignored/disabled

This is an excellent point - thanks for bringing it up, @javiercn! I thought about this a bunch more and think it can be mitigated in a quite satisfying way. That is, we would define the meaning of @attribute [StaticPage] or @staticpage as follows:

"Suppresses any @rendermode directives in your root component"

Now I haven't actually prototyped this so maybe I'm missing something, but this small tweak to the definition seems to evade the entire limitation. I know at first glance it feels a bit arbitrary, but there's nothing hard to understand about it, and AFAICT gives exactly the behaviors everyone would want:

  • It just suppresses "global interactivity" assuming you're following typical conventions
  • You're still free to embed whatever combination of interactive components you want on your layout or inside the SSR pages themselves, so you still have the same capabilities and behaviors of per-page interactivity.

I know in theory people could structure their site in more unexpected ways, for example having some combination of other components wrapped around their router, with @rendermode in one of them. In my opinion the high benefit of fixing the "all or nothing" problem is worth that, since there's still a simple and clear definition of "static page" that should be understandable.

If we were hyper-determined to give even more options, we could have some further enum parameter like StaticPageMode.SuppressRootRenderModes (default) or StaticPageMode.SuppressAllRenderModes, but that's obviously something we could layer on in the future if enough demand emerged.

Proposal

Here's what I now propose to do:

  • Implement @attribute [StaticPage]
    • ... and not new Razor syntax like @staticpage because the whole point of supporting @attribute is to avoid incurring the costs of new syntax for every feature. We could later add an equivalent shorthand syntax, but right now I don't see a reason to think this is more syntax-worthy than @attribute [StreamRendering]
  • Make it cause full-page navigations when you go from an interactive page to one of these pages
  • Make it suppress rendermodes defined in the root component
  • Update the auth-enabled template to use it

If we were going further, the next highest-priority thing would be something like an analyzer that fails if you try to use this on a non-page component, or if you try to use it on a page component that itself has @rendermode. However on a cost-benefit basis I don't expect to implement such an analyzer, but it's something we could come back to late in the product cycle if by that stage we have evidence that it's top of our priority queue at that point.

@gabephudson
Copy link

I actually do not understand why the Auth components were developed as static components…

I was wondering this at well when first exposed to the templates. I have come to the following conclusions (total assumptions by me)...

  1. Many of the authentication components need access to HttpContext to function, and this is only available directly in static rendering.
  2. Login pages are visible to the public. If a user navigates to an InteractiveServer login page (or is directed to one upon logging out), a circuit is created. If they just leave the browser open and never interact, you have a circuit open consuming resources on your server. (I'm not aware of a true "user" inactivity timeout in Blazor, but perhaps there is?). I have gotten around this by having a JS timer redirect the user to a static page after a 20-minute timeout expires.

@javiercn
Copy link
Member

javiercn commented Apr 5, 2024

If you are developing globally interactive applications then it can be assumed all components are using Blazor interactivity… and you would actually prefer to use interactive Auth components as well… however you cannot because Microsoft only provided static versions of these components in .NET 8. If Microsoft had provided interactive Auth components there would be no need for ugly hacks in App.razor and there would be no need for the enhancement being debated in this issue.

I actually do not understand why the Auth components were developed as static components… as the type of functionality they represent is a perfect example of the scenario where you would choose to use an interactive component within a statically rendered application. The Oqtane Framework (https://www.oqtane.org) has interactive components for all Auth scenarios (ie, login, register, etc…) which work perfectly whether you choose to use global interactivity or static rendering in your app.

You need to set a cookie and that's done via a traditional form post. These components need to work without JavaScript enabled, which might not be a constraint that you have, but most other people do.

@sbwalker
Copy link

sbwalker commented Apr 5, 2024

@javiercn you are correct that Oqtane does not have the requirement to support non-JavaScript clients (its interactive login component uses JS Interop to call a JS method which posts values to a razor page endpoint which uses standard Identity to set the cookie and redirect). However, I would still suggest that the majority of the Identity Auth components could have been offered as interactive components. The exception is Login which could have been implemented as a page component which does not specify @rendermode so that it inherits the render mode from the project (either static or interactive)... and the Login component could use the static form onsubmit approach which works in all render modes. However I am sure there would then be complaints from the purists who want a pure static solution - unfortunately you can never satisfy everyone.

@mkArtakMSFT
Copy link
Member

Thanks for the writeup, @SteveSandersonMS.

I'm trying to understand the experience in a situation, where a component has defined a render mode, but it's being use from a page, that is defined as static. Here is a diagram to visualize the scenario:

flowchart TD
    P[Static Page] --> C1[Component 1 - no rendermode defined]
    P --> C2[Component 2 - InteractiveWebAssembly]
    C1 --> C3[Component 3 - InteractiveServer]

I assume the following is going to be the case, but I would like to get confirmation:

  1. Component 1 will inherit it's rendermode from the page, so it will be rendered statically
  2. The interactivity preferences (rendermode) for Component 2 and Component 3 will be ignored, and they will be rendered statically.

Can you please confirm my understanding?

Assuming this is the case, I think we should produce some warnings at runtime indicating, that the developer is not going to get the experience they actually intend to and that the rendermode defined for Component 2 and 3 will be ignored.

@akorchev
Copy link

akorchev commented Apr 7, 2024

Sharing my feedback on introducing a new rendering mode from the position of a Blazor Component vendor who speaks daily with lots of Blazor developers.

I am backing @sbwalker on all his points. I too think that this feature shouldn't be implemented.

Some Blazor developers (especially the new ones) are having a hard time understanding the existing rendering modes. I can only imagine what a new rendering mode would do to them. As a Blazor UI component vendor we are still seeing the question "why aren't your components firing any events". Of course that's because static rendering mode doesn't support interactivity. Some recent examples:
https://forum.radzen.com/t/radzen-profile-menu-not-showing-up/16893
https://forum.radzen.com/t/radzen-blazor-button-click-not-firing/3853/5
https://forum.radzen.com/t/layout-icon-sidebar-example-not-reacting-to-user-input/16649

To this day I don't know why static rendering mode was picked as the default. A comparison with React SSR was made but it isn't 100% equivalent - React SSR still hydrates to client-side and supports events (interactivity) just fine. Also I don't think React SSR is the default React mode and the getting started tutorial about events doesn't mention anything about rendering modes as they are advanced and opt-in concept.

Another major point where I back @sbwalker is that authentication could have been implemented with interactive components and without HttpContext access. Our tool Radzen has been doing this since Blazor 1. Yes it needs a cookie to be set - we set it in a controller action method which the login form posts to. It doesn't even require JavaScript. I am starting to think that static rendering mode was implemented to support HttpContext access and the new authentication components.

@michelkommers
Copy link

As a Blazor developer that has been working with this tech since its beta releases, I will add my two cents to this discussion because this was quite frustrating issue recently to me and why the first option is more important than you guys imagine.

Imagine the scenario, you are developing a consumer facing website, with lots of users daily, so, your first thought would be, lets use SSR as the main rendering mode, but than you want to add more complex UI functionality on some 'lnternal/logged in' area only, so you switch to auto with per page/component, you develop the internal area with a lot of functionality and decide that those internal pages should render as interactiveWebassembly only, but than, every single route on the 'internal' area always hit the server before loading the page,even after the wasm was download, and you think 'this is uggly, if its wasm already, why not only load the page without going to the server', but thats by design, without the global render being InteractiveWebassembly every route will go to the server.

With proposal 1, I could for example use a global 'InteraciveWebassembly' render mode, and only on the most consumed pages (home and others not internal) use the attribute [StaticPage], literally saving me from turning the entire 'internal pages' in a single page with lots of components, because i don't want my wasm routes on internal pages to hit the server every single time the user clicks some link on the wasm.

And this my guys, is what blazor devs are after, some dumb pages with SSR, and some smart ones in interactive wasm, but without the routes hitting the server after is loaded. That simple, and should actually be available as soon as possible because devs are going to do things the wrong way to overcome those limitations/issues and when you release this, they will be pissed to have to rework everything again and say 'why was not like this from the start?'.

So that is my 2 cents, hope it help you guys decide in the best approach for this.

@SteveSandersonMS
Copy link
Member

Done in #55157

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 Pillar: Complete Blazor Web Priority:1 Work that is critical for the release, but we could probably ship without
Projects
None yet
Development

Successfully merging a pull request may close this issue.