dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.36k stars 9.99k forks source link

Introduce IRenderTreeBuilder to make blazor more flexible #48891

Closed albertwoo closed 1 year ago

albertwoo commented 1 year ago

Background and Motivation

With this new interface, we can change RenderFragment to below:

public delegate void RenderFragment(IRenderTreeBuilder builder);

Before it is like:

https://github.com/dotnet/aspnetcore/blob/9178b9b117e64ca75821db1927e4857714c5da97/src/Components/Components/src/RenderFragment.cs#L13

The benifit for this change is to let developer define their own implementation of RenderTreeBuilder, like define struct PureFragmentBuilder, so it can just write related markup content or element directly into a StringWriter or PipeWriter without all the overhead, and reduce allocation.

For example, we can use this in the dotnet 8 minimal api + AOT, and just let the razor source generator to generate fragments and write into the response body, so together with htmx I can build a very nice and efficient web app which is also very small in bundle size and very fast in speed. The idea is like razor slice which is building by @DamianEdwards and still investigating on support AOT. But just use razor, can it stay in pure static rendering or use component for complex logic if developers want.

Blogs.razor:

@code {
    public static RenderFragment CreateBlogList(IBlogService blogService) {
        var blogs = blogService.GetBlogs();
        return @<section>
            @foreach(var blog in blogs) {
                @CreateBlogCard(blog)
            }
        </section>;
    }

    static RenderFragment CreateBlogCard(Blog blog) => @<div>...</div>;
}

Use it in the minimal api:

app.MapGet("/", (BlogService blogService) => Results.Extensions.PureFragment(
    Layout.Create(
        Blogs.CreateBlogList(blogService))));

The Layout can be like this:

@code {
    public static RenderFragment Create(RenderFragment content, string? title = "foo title") =>
        @<html>
            <headers>
                 <title>@title</title>
            </headers>
            <body>
                 @content
                 <script src="https://unpkg.com/htmx.org@1.9.2"></script>
            </body>
        </html>;
}

The Results.Extensions.PureFragment is just a extension method which can create the PureFragmentBuilder to invoke the RenderFragment and write markup content, element and text into the HttpContext.Reponse.BodyWriter directly.

All of those stuff is very simple, just simple functions to compose RenderFragment.

Actually, I already can do above stuff in dotnet 8 preview 5 with the HtmlRenderer + AOT, but if we can introduce IRenderTreeBuilder, we can make it way more efficient.

Currently my Results extension just accept pure RenderFragment (Results.Extensions.PureFragment), because AOT is not fully working with razor component with minimal api now. But when the AOT works, I can still create Results.Extensions.RazorComponent. So in that time, for simple use cases I just call .PureFragment, for more complex logic, I can call .RazorComponent.

Proposed API

-public delegate void RenderFragment(RenderTreeBuilder builder);
+public delegate void RenderFragment(IRenderTreeBuilder builder);

Risks

Not sure, maybe @SteveSandersonMS and @danroth27 can provide suggestion. I also do not sure if my proposal is appropriate here, just exploring those for having fun 😊.

SteveSandersonMS commented 1 year ago

Thanks for the suggestion, @albertwoo.

This is the sort of thing we can't just do casually, as it would have massive impacts on (1) people immediately doing things that could go wrong in exceptionally complex ways, and (2) severely restrict our ability to evolve the framework in the future, since so much more of the internal semantics would become part of the public API contract and hence no longer possible to update.

As an example of the sort of ways this could immediately go wrong, your proposal of emitting markup directly to a StringWriter sounds appealing and lots of people would think they want to do that, but perhaps would not realise this breaks many of the behaviors and benefits of a real component model. Blazor components can render mode than once, and frequently do so even in SSR cases since (1) we have streaming rendering, and (2) the appearance of sibling or descendant components can create reasons to re-render (e.g., with sections, or child component discovery).

In summary, we definitely wouldn't do this any time soon and to be honest are unlikely ever to do it. But it is an interesting thought experiment! The closest I could imagine us coming to this would be some way to tell the Razor compiler that you want to use a different type instead of RenderTreeBuilder, then it would be up to you to provide an implementation and you would have to knowingly give up on all the semantics of Blazor and compatibility with all existing Blazor components. So it would really be a Razor compiler feature, and not actually be part of Blazor at all.

albertwoo commented 1 year ago

@SteveSandersonMS thanks for the quick response and detail explanation. Also consider the RenderFragment is public API, so to change its signature may also consider as a breaking changes. I will close this one.

I also checked the Razor repo and found it is quite complex to change, a lot of stuff are just hard coded.