This blog contains some experiences working with Bolero.

Bolero is a set of free and open-source libraries and tools to write web applications in F#. With it, you can write fully dynamic client-side web applications from the comfort of a strongly-typed functional language, all with great performance.

Bolero is built on top of Blazor, which means it can run in one of two modes: either in WebAssembly directly in the browser, or on the server side with SignalR.

Bolero uses the Elmish Model-View-Update architecture.

You develop F# code for the client side (inside the browser), which is compiled into WebAssembly. The views are either F# codes or Html templates.

Server endpoint for Posts

I came across this topic trying to implement a file upload in Bolero. The It starts with the problem that <input type="file" />is not supported by Bolero.

Searching for a solution, I found this conversation about Bolero uploading files, which made the comment to create a ASP.NET Core endpoint. This post explores how to do this.

HTML

We use a standard html form with post to upload the file:

<form action="Uploader/PostFiles" method="post" enctype="multipart/form-data">
    <label>
        Data files
        <input type="file" multiple accept=".csv,.json" id="myfiles" name="myfiles"/>
    </label>
    <button type="submit">Upload files</button>
</form>

Remember the name myfiles. It will be used later on in the ASP.NET controller.

Note also that the form action does not start with a slash. This is particular important when deploying to an IIS server, when relative URL paths are used.

ASP.NET controller

It wasn’t easy to find the simplest tutorial on how to write an ASP.NET controller in F#. In the end, stackoverflow was helpful, as was this Microsoft introduction to getting started with web-app and f# and this tutorial MVC file-upload Here is our simple controller:

namespace HalloController.Server

open System
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging

[<ApiController>]
[<Route("[controller]")>]
type UploaderController (logger : ILogger<UploaderController>) =
    inherit ControllerBase()

    [<HttpGet>]         // match GET Uploader/
    member _.Get() =
        logger.LogInformation("Inside UploadController Get")
        "this is a simple GET"

    [<HttpGet("Info")>]     // match GET Uploader/Info
    member _.Info() =
        "this is a GET Info"

    // important: parameter name has to match id and name in Http <input>
    [<HttpPost("PostFiles")>]
    member _.PostFiles(myfiles: System.Collections.Generic.List<Microsoft.AspNetCore.Http.IFormFile>) =
        logger.LogInformation("Inside Uploader PostFiles")
        for f in myfiles do
            printfn "filename = %s" f.FileName
        "success"

It took me some time to understand how the routes are mapped.

  • The class name UploaderController provides the route part Uploader.
  • the member attribute provides the next route part Info or PostFiles.
  • So the complete routes are: http://localhost:12345/Uploader/Info.

It also took some time to discover that the name of the parameter of the post, myfiles must match exactly the name of the <input type="file" name="myfiles" /> for the parameter to be set.

Server side

So how do we make the server recognize our controller? This is where the magic of dependency injection kicks in. We just have to add it to the services and endpoints.

For the services, add the line services.AddControllers() to the ConfigureServices member.

For the endpoints, add the line endpoints.MapControllers() to the mapped endpoints.

type Startup() =

    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    member this.ConfigureServices(services: IServiceCollection) =
        services.AddControllers() |> ignore     // added
        services.AddMvc() |> ignore
        services.AddServerSideBlazor() |> ignore
        services
            .AddAuthorization()
            .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie()
                .Services
            .AddRemoting<BookService>()
            .AddBoleroHost()
#if DEBUG
            .AddHotReload(templateDir = __SOURCE_DIRECTORY__ + "/../HalloController.Client")
#endif
        |> ignore

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    member this.Configure(app: IApplicationBuilder, env: IWebHostEnvironment) =
        app
            .UseAuthentication()
            .UseRemoting()
            .UseStaticFiles()
            .UseRouting()
            .UseBlazorFrameworkFiles()
            .UseEndpoints(fun endpoints ->
#if DEBUG
                endpoints.UseHotReload()
#endif
                endpoints.MapControllers() |> ignore    // added
                endpoints.MapBlazorHub() |> ignore
                endpoints.MapFallbackToBolero(Index.page) |> ignore)
        |> ignore

That’s it!

Now when you run the server, and post the files, ASP.NET will log the following message:

info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
    Route matched with {action = "PostFiles", controller = "Uploader"}. Executing controller action with signature Void PostFiles(System.Collections.Generic.List`1[Microsoft.AspNetCore.Http.IFormFile]) on controller HalloController.Server.UploaderController (HalloController.Server).

This example can be found on my Github/BoleroApp repository. It implements a simple AspNet MVC controller for file uploads.

Templating on Bolero Server

The standard scaffold for the Bolero server looks like this:

// index.fs
let page = doctypeHtml {
    head {
        meta { attr.charset "UTF-8" }
        meta { attr.name "viewport"; attr.content "width=device-width, initial-scale=1.0" }
        title { "Bolero Application" }
        ``base`` { attr.href "/" }
        link { attr.rel "stylesheet"; attr.href "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css" }
        link { attr.rel "stylesheet"; attr.href "css/index.css" }
    }
    body {
        nav {
            attr.``class`` "navbar is-dark"
            "role" => "navigation"
            attr.aria "label" "main navigation"
            div {
                attr.``class`` "navbar-brand"
                a {
                    attr.``class`` "navbar-item has-text-weight-bold is-size-5"
                    attr.href "https://fsbolero.io"
                    img { attr.style "height:40px"; attr.src "https://github.com/fsbolero/website/raw/master/src/Website/img/wasm-fsharp.png" }
                    "  Bolero"
                }
            }
        }
        div { attr.id "main"; comp<Client.Main.MyApp> } // rootComp
        boleroScript
    }
}

But how would you go about if you also want to put this into a html template?

You could replace the above code with a html template index.html:

<!DOCTYPE html>
<html>
  <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>
    <nav class="navbar is-dark" role="navigation" aria-label="main navigation">
      <div class="navbar-brand">
        <a class="navbar-item has-text-weight-bold is-size-5" href="https://fsbolero.io"><img style="height:40px" src="https://github.com/fsbolero/website/raw/master/src/Website/img/wasm-fsharp.png"/>&#xA0; Bolero</a>
      </div>
    </nav>
    ${Body}
    <script src="_framework/blazor.webassembly.js"></script>
  </body>
</html>

and then call it inside index.fs:

type IndexTemplate = Bolero.Template<"index.html">
let page = 
    let node = 
        div { attr.id "main"; comp<Client.Main.MyApp> }
    IndexTemplate()
        .Body(node)
        .Elt()

Note that the fsproj file has to be changed also:

  <ItemGroup>
    <Content Include="index.html">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
    <Compile Include="Index.fs" />
    <Compile Include="Startup.fs" />
  </ItemGroup>

If you now try to run in, you will get the following error:

Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
warn: Bolero.Templating.Server.Impl.Watcher[0]
      Bolero HotReload: failed to reload /Users/mmgreiner/Projects/Scrap/BoloeroStaticApp/src/BoloeroStaticApp.Client/index.html

We therefore also have to adjust Startup.fs to add the new template also to hot reload:

#if DEBUG
            .AddHotReload(templateDir = __SOURCE_DIRECTORY__ + "/../BoloeroStaticApp.Client")
            .AddHotReload(templateDir = __SOURCE_DIRECTORY__ + "/../BoloeroStaticApp.Server")
#endif

Input files

You develop F# code for the client side (inside the browser), which is compiled into WebAssembly. The views are either F# codes or Html templates.

The client contains predefined <input> types for text, numbers and booleans, but not for file uploads. How can this be done?

There are two potential approaches:

  1. Upload a file to the client and use the file’s data on the client.

  2. Upload a file to the client and pass it on to the server for more complex file data procedures.

Simple example

It contains predefined <input> types for text, numbers and booleans, but not for file uploads. How can this be done?

Client side

type Model = { Name: string }
let init = { Name = "" }

type Message =
    | SetName of string

let update message model =
    match message with
    | SetName n -> { model with Name = n }

let viewInput modelData setValue =
    input {
        attr.value modelData
        on.change (fun e -> setValue (unbox e.Value))
    }

let view model dispatch =
    div {
        input {
            attr.value = model.Name
            on.change (fun e -> dispatch (SetName (unbox e.Value)))
        }
        $"Hallo, {model.Name}!"
    }

Now the problem is, that there exist views for <input> types string, numbers and booleans, but not for file input.

Using Blazor component

  • [] TBD

Further information

See https://procodeguide.com/programming/file-upload-in-aspnet-core/ for an example how to implement file upload in Asp.Net Core 6.0.

Further tips

Deploying to IIS

If you deploy your Bolero app to IIS, and particularely to an IFS with a subfolder, your starting path will not be recognized. I ended up implementing this as follows:

let page (env: IWebHostEnvironment) =
    let root =
        if env.ContentRootPath.Contains("inetpub") then
            let s = env.ContentRootPath.Split(IO.Path.DirectorySeparatorChar)
            "/" + s.[s.Length-1] + "/"
        else
            "/"

And in the html template:

<head>
    <base href="${Root}">
</head>

Calling JavaScript

How to call JavaScript is somewhat hidden in the documentation, you can find it under Bolero-Blazor