1
votes

Context: Bootstrapping an F# app that uses server sider blazor/ razor components BUT does not use razor to define components. Instead I use FsBoleros DSL.

Everything works as expected but there is still on .cshtml file (_Host.cshtml) that I'd like to replace with just (F#) code for a few reasons:

  • All my code will be written in F# - but if I need to change things/ add code to the host razor file I need to mix in C#. While this works it's not optimal and I'd rather just have F# code and no special razor file.

  • F# Projects depend on file ordering. The razor file behaves a bit strange in that regard (I cant just reorder it like other files in my project using IDE tooling).

So the goal is to get rid of the _Host.cshtml file and replace it with just code.

I've already invested 2 days in trying to do this. There seems to be little to no content on how to replace a razor page with just code or how to call a tag helper from non razor code. A little guidance on how to approach this would be highly appreciated, otherwise I'll burn more time on this.

My current understanding of what I have to do is:

Render a html response that includes

  • the server side blazor framework
  • the prerendered root component annotated with a special comment

Is this correct ?

If so:

Generating the needed html page is trivial except for the prerendered annotated component. Is calling a tag helper the right approach to achieving that (as I'm having some issues with that) ?

Other Component rendering classes (and interfaces) are internal so I suspect that I should not use them. I also assume that I have to use the tag helper in order to get the html correctly annotated.

note: It's not required to actually replace it with code that does exactly the same (eg. is a razor page) but rather does what is required to bootstrap blazor in a maintainable fashion (so no magic string plumbing).

(This is the _Host.cshtml file to replace)

@page "/"
@namespace X.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Bolero Application</title>
    <base href="~/">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
    <link rel="stylesheet" href="css/index.css">
</head>

<body>
<h1>Blazor Host</h1>

<component type="typeof(App)" render-mode="ServerPrerendered"/>

<script src="_framework/blazor.server.js"></script>
</body>
</html>
2

2 Answers

0
votes

Look at the generated code file _Host.cshtml.g.cs and convert that to F#

I don't do F#, so can't comment on how difficult that will be, but reviewing the generated code should at least answer your questions about how to call taghelpers etc...

0
votes

EDIT: This also seems to be possible using the IHtmlHelper after opening the right namespaces.

task {  
    let htmlHelper = container.GetService<IHtmlHelper>()
    
    (htmlHelper :?> IViewContextAware).Contextualize(ViewContext(HttpContext = httpContext))

    // RenderComponentAsync is an extension from "Microsoft.AspNetCore.Mvc.Rendering"
    let! componentHtmlContent = htmlHelper.RenderComponentAsync<App>(RenderMode.ServerPrerendered)
    
    let componentHtml: string =
        using (new StringWriter()) (fun writer ->
            componentHtmlContent.WriteTo (writer, HtmlEncoder.Default)
            writer.Flush()
            writer.ToString()
        )
}

Old answer is below:

The solution I've found works BUT involves calling methods on internal types that are not exposed by ASP.NET Core.

Module to call IComponentRenderer.RenderComponentAsync via reflection.

[<RequireQualifiedAccess>]
module ComponentRenderer =
    open System
    open System.Reflection
    open System.Threading.Tasks
    open Microsoft.AspNetCore.Html
    open Microsoft.AspNetCore.Mvc.Rendering
    
    let private ``reflected IComponentRenderer``: Type  =
        let assemblyName = "Microsoft.AspNetCore.Mvc.ViewFeatures"
        let assembly = Assembly.Load assemblyName
        let componentRendererInterface = assembly.GetType $"{assemblyName}.IComponentRenderer"
        componentRendererInterface
   
    let private getRef (serviceContainer: IServiceProvider) : obj =
        serviceContainer.GetService ``reflected IComponentRenderer`` 
        
    let private getMethodRef () : MethodInfo =
        let flags = BindingFlags.Public ||| BindingFlags.Instance 
        let method = ``reflected IComponentRenderer``.GetMethod ("RenderComponentAsync", flags)
        method

    let callRenderComponentAsync
       (serviceContainer: IServiceProvider,
        viewContext: ViewContext,
        componentType: Type,
        renderMode: RenderMode,
        parameters: obj) : Task<IHtmlContent> =
        
        let renderer = getRef serviceContainer
        let method = getMethodRef ()
        let result = method.Invoke(renderer, [| viewContext; componentType; renderMode; parameters |])
        
        result :?> Task<IHtmlContent>

Basically all that's required is a HTML response that contains:

  • the server side blazor framework
  • the prerendered root component annotated with a special comment

There are multiple ways to get to that desired result, here is a simple example handler.

module HttpHandlers =

    let htmlTemplate (comp: string) =
        $"""
        <!DOCTYPE html>
        <html lang="en">

        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
            <title>Bolero Application</title>
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
            <link rel="stylesheet" href="css/index.css">
        </head>

        <body>
            {comp}
            <script src="_framework/blazor.server.js"></script>
        </body>
        </html>
        """
        
    let index (container: IServiceProvider) (httpContext: HttpContext) : Task =
        task {
            let! componentHtmlContent =
                ComponentRenderer.callRenderComponentAsync (
                    container,
                    ViewContext(HttpContext = httpContext),
                    typeof<App>,
                    RenderMode.ServerPrerendered,
                    null
                )
                
            let componentHtml: string =
                using (new StringWriter()) (fun writer ->
                    componentHtmlContent.WriteTo (writer, HtmlEncoder.Default)
                    writer.Flush()
                    writer.ToString()
                )
            
            let! _ =
                (htmlTemplate componentHtml)
                |> Encoding.UTF8.GetBytes
                |> ReadOnlyMemory
                |> httpContext.Response.BodyWriter.WriteAsync
                
            return ()
        } :> _


The resulting html response looks should now look like this:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
    <link rel="stylesheet" href="css/index.css">
</head>

<body>
    <!--Blazor:{"sequence":0,"type":"server","prerenderId":"de4cefeb845c4241810b39a9fa6fb09d","descriptor":"CfDJ8B1PPZs/SbNIpyDpD4CgsZTZ9MSJjiAAdULVygxQGjm2QdNlb19sKvxXV\u002B0ZZh\u002Br43icwKyOdOw0RCK6auj2fziShHMZNiE/kNp0XnqJywdEpAAGuNROLgewx4NSSwKJ5lUUWq\u002BiuwKOnKNwQEJA1BAQ1B0IJbE6gKqHkKPMK3vYVjnB/jgpX01f2DS7djVlH\u002Bc/D8hr2jjuqt08527OrAPky7Fm71HejVDjEwZApZUj853dq3sDpmyNO2uWJaTRufSeBX1UISwofgBwDobZ8RBSVTfzMP8HPJ\u002BKBJxt\u002BNXueXpxcXXQwva9n5tqWKyFEahW4lOQFLrr3/Gvh9mRY1EExZapEiO/b5qHc9CtwgqDQN8U7fwtH2il8uPBs3Hsdg=="}-->
    <div>
        <p>Hello World and welcome to my app!</p>
    </div>
    <!--Blazor:{"prerenderId":"de4cefeb845c4241810b39a9fa6fb09d"}-->

    <script src="_framework/blazor.server.js"></script>
</body>
</html>