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

Allow declaring render modes, and emit the corresponding markers into HTML #48190

Merged
merged 2 commits into from
May 16, 2023

Conversation

SteveSandersonMS
Copy link
Member

@SteveSandersonMS SteveSandersonMS commented May 11, 2023

This is the basis for including interactive (Server or WebAssembly) components at arbitrary locations within SSR output. This PR only implements the SSR pieces, i.e., emitting the markers into the HTML. This PR does not include the client-side code needed to start up a circuit or WebAssembly runtime, so it is not yet usable as an end-to-end feature.

Usage advice

Render modes are defined statically on component types. Where possible, components should not specify a rendermode at all. This means they are willing to work on any hosting platform, or that the developer knows they will only be used in a compatible hosting platform. For example:

  • If you're building a redistributable component to package in a Razor Class Library (RCL):
    • If possible, design the component to work across all render modes (SSR, Server, WebAssembly), and don't specify a rendermode. Your component will then be usable everywhere.
    • If your component is only capable of working on Server or WebAssembly due to its internal implementation, mark it with the corresponding rendermode. Then, application authors will only be able to use it in that mode (or SSR).
  • If you're building an application and implementing your own components for it:
    • Only specify a rendermode at locations where you wish to introduce a mode boundary, i.e., where you wish to switch from SSR into a particular interactive mode. For example, do this on a page component.
    • There is no need to specify a rendermode for components that you know will be inside a subtree that already has the desired rendermode. For example, if there's a Server/WebAssembly section of your site, there's is no need to mark all the components used in that section as Server/WebAssembly - they are going to have the desired mode anyway because of the location where they are being rendered. It is more flexible not to specify a rendermode on individual components except if that component represents the mode boundary.

Restrictions

We're trying to keep the feature surface small where possible to begin with. So for .NET 8, we expect that:

  • We will not support changing the rendermode in a nested way.
    • The SSR renderer allows Server/WebAssembly/Auto modes, but once a subtree has one of those modes, any other modes nested within that subtree are ignored (because within that subtree, we're just prerendering, and the subtree will be replaced completely once the app starts running in interactive mode anyway).
      • If we wanted, we could go further and respect nested prerender=false instructions (to stop prerendering within a given subtree) but that's not implemented here.
    • The WebAssembly renderer only allows WebAssembly and Auto modes, both of which are no-ops. Any other mode will be rejected as an error.
    • The Server renderer only allows Server and Auto modes, both of which are no-ops. Any other mode will be rejected as an error.
  • We don't support setting a rendermode at a call site. The rendermode can only be set statically on a component type. We have considered the scenarios extensively and think this is already perfectly fine for almost all cases, given the usage advice above.

API design

A rendermode is a class that implements IComponentRenderMode. The built-in ones are ServerRenderMode, WebAssemblyRenderMode, AutoRenderMode. These are the only rendermodes recognized by the built-in renderers for SSR, Server, and WebAssembly.

Since rendermodes are arbitrary types, they can hold additional parameters (e.g., to control whether to prerender).

The set of rendermodes is not fixed as far as M.A.Components is concerned. In fact, the core doesn't know about any specific render modes. The ones we support today are all defined in M.A.Components.Web. Platforms with custom renderers can implement support for their own rendermodes by overriding ResolveComponentForRenderMode.

Note that the existing Html.RenderComponentAsync and <component> tag helpers already have their own different RenderMode enum. This PR automatically maps that to the new IComponentRenderMode types, so existing APIs continue to work. Given the intended API design, I don't think the naming clash will affect real-world use, but we can see how this goes.

Example usage

Final syntax (not yet supported by Razor compiler):

@rendermode Server // (or WebAssembly or Auto)

Temporary syntax until Razor compiler is updated:

@attribute [ServerRenderMode] // or [WebAssemblyRenderMode] or [AutoRenderMode]

Note that the ServerRenderMode/WebAssemblyRenderMode/AutoRenderMode attributes can all be removed once the Razor compiler is updated, because we can cause @rendermode SOME_EXPRESSION_HERE to compile as:

[__GeneratedRenderMode]
class SomeComponent : ComponentBase
{
    private class __GeneratedRenderModeAttribute : RenderModeAttribute
    {
        public override IComponentRenderMode Mode => SOME_EXPRESSION_HERE;
    }
}

... and then the project template's _Imports.razor can have using static Microsoft.AspNetCore.Components.Web.RenderMode so that it is legal to simply write @rendermode Server. This allows the cleanest possible syntax without coupling the Razor compiler to any particular M.A.C.Web render modes or naming conventions.

Behavior

Currently, it emits <!--Blazor:{...}--> markers, including the serialized parameters, in a form compatible with the existing blazor.server.js and blazor.webassembly.js. The existing prerendering logic has been updated to use the new rendering mechanism.

When we implement interactive component support in blazor.web.js, we can change this marker format if we want (but then would also have to update blazor.server.js and blazor.webassembly.js).

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label May 11, 2023
@@ -5,6 +5,7 @@
using System.Diagnostics.CodeAnalysis;
Copy link
Member Author

@SteveSandersonMS SteveSandersonMS May 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conceptually, the changes in this file are:

  • Extend the existing per-component-type cache to also track the render mode declared on the component type, so there's no new runtime cost for determining this (not even an extra cache lookup)
  • When a nonnull rendermode is found, use the new ResolveComponentForRenderMode to do the component instantiation. This is how a rendermode gets translated into concrete, platform-specific behavior.

{
var result = new Dictionary<string, object>();
var result = new Dictionary<string, object?>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nullability annotation was wrong before.

if (frame.ComponentStateField != null)
{
throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}");
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check was redundant since InstantiateChildComponentOnFrame already does that.

// marker types into a single thing for auto mode (the code below emits both separately for auto).
// It may be better to use a custom element like <blazor-component ...>[prerendered]<blazor-component>
// so it's easier for the JS code to react automatically whenever this gets inserted or updated during
// streaming SSR or progressively-enhanced navigation.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this PR I'm not trying to express a final opinion on the exact format of the interactive component markers that we output. We can determine that when implementing the client-side pieces, based on what's best for the client-side code (e.g., whether it should change from being a comment tag to a custom element).

The goal for this PR is just to emit the output in some sensible format. The specific format used in this PR happens to be exactly what we already output, and hence is compatible with blazor.server.js etc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with emitting a custom element is that it will affect the layout, isn't it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that is a risk, and is why we haven't done it before. It is a bit of an edge case though, since <my-custom-container>some markup</my-custom-container> will normally render exactly the same as some markup, if there are no CSS rules associated with my-custom-container. The cases where it would make a different would be:

  • if you have very specific CSS rules that try to locate things in some markup based on them being immediate children of some other element
  • or if the parent is a special element type like table that can only have certain child types
  • There may be other such cases too.

We could decide that rendermode boundaries are rare enough that we are willing to tell people not to place them at critical locations (like as immediate children of <table>), and not to write CSS rules that are so specific in this case.

It's a tradeoff between compatibility and simplicity of implementation, and warrants some open discussion. Overall I suspect we'll end up saying we want to maximize compatibility even if it makes our implementation harder. But we are free to discuss this and be open to other outcomes.

/// A component that describes a location in prerendered output where client-side code
/// should insert an interactive component.
/// </summary>
internal class SSRRenderModeBoundary : IComponent
Copy link
Member Author

@SteveSandersonMS SteveSandersonMS May 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having the boundary be an actual component in the hierarchy means:

  • It automatically captures the parameters that we need to pass to the interactive child, without any extra logic in the prerendering or rendering systems
  • We can encapsulate things like "doing prerendering or not" with a trivial "if" condition here
  • We can trivially use other DI services like Data Protection from here without adding noise to the renderer
  • We can easily see if rendermodes are nested by scanning the ancestor hierarchy.

@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/component-rendermode branch 2 times, most recently from a3c0de4 to 390d7c3 Compare May 12, 2023 10:53
Comment on lines +43 to +51
Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute
Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.RenderModeAutoAttribute() -> void
Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.RenderModeAutoAttribute(bool prerender) -> void
Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute
Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.RenderModeServerAttribute() -> void
Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.RenderModeServerAttribute(bool prerender) -> void
Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute
Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.RenderModeWebAssemblyAttribute() -> void
Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.RenderModeWebAssemblyAttribute(bool prerender) -> void
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these can be removed when the Razor compiler is updated to support the @rendermode directive.

Comment on lines +63 to +65
override Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode!
override Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode!
override Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode!
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can also be removed once we have @rendermode

@DamianEdwards
Copy link
Member

Are we still considering supporting setting render mode via file extension too, e.g. App.razor, Comments.wasm.razor, Login.server.razor?

@SteveSandersonMS
Copy link
Member Author

Are we still considering supporting setting render mode via file extension too, e.g. App.razor, Comments.wasm.razor, Login.server.razor?

Interesting idea. Previously I thought that the naming convention would only be to influence conditional compilation. But we can make the Razor compiler do whatever we like.

Copy link
Member

@MackinnonBuck MackinnonBuck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me!

builder.CloseComponent();
}

public (ServerComponentMarker?, WebAssemblyComponentMarker?) ToMarkers(HttpContext httpContext)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so I'm understanding this correctly - in the case of the "Auto" render mode, we'll emit markers of both types? And on the client side, we'll have some logic to detect if any given component has multiple markers and treat that as "auto" mode?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in the auto case, the client has to make the choice about what mode to use (at least, if it's based on "is the WebAssembly .NET runtime ready to start"). We are free to change the format of these markers and perhaps come up with a format that merges the server and WebAssembly information into a single structure, but this PR is only about emitting markers in the existing format. We should design a new format, if we want one, based on the needs of the client-side code when we implement that.

@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/component-rendermode branch from ea22b9b to 466f62c Compare May 16, 2023 09:36
@SteveSandersonMS SteveSandersonMS enabled auto-merge (squash) May 16, 2023 09:51
@SteveSandersonMS SteveSandersonMS merged commit ec9a2d8 into main May 16, 2023
@SteveSandersonMS SteveSandersonMS deleted the stevesa/component-rendermode branch May 16, 2023 12:40
/// a fixed rendering mode when the component is incapable of running in other modes.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public abstract class RenderModeAttribute : Attribute
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to allow multiple for this?

Copy link
Member Author

@SteveSandersonMS SteveSandersonMS May 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. What would we do if there were multiple?

This is about specifying a required rendermode, not a set of allowed rendermodes (which, if implemented in the future, would be a separate API).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean AllowMultiple = false

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the default. If I'm missing something and it has to be specified explicitly in this case, please let me know!

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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants