Using Htmx with Asp.Net Razor Components(Blazor)


Asp.Net Web CSharp

This article is focused on using Htmx with Asp.Net backend, and using Blazor(Razor Components) as the templating language.

We are using blazor to generate static pages, not using any websocket or webasm rendering mode, so it may also work with razor pages and mvc, but I haven’t test it.

1. Write Frontend

Create a new blazor project.

dotnet new blazor -o BlazorHtmx

Adding the Htmx script to the body of Components/App.razor

<script src="https://unpkg.com/[email protected]"></script>

(If you want to use a bundler to bundle other scripts and css, I may write a artitle about how to use vite with asp.net later.)

Delete the pages in project and create our new test page.

Just ignore the ”…” class name for styling in the html code below, I’m using it as a placeholder. (Actually I’m using Tailwindcss to style my example in the preview screenshot, but the long class name is a distraction to this article’s topic).

@page "/"

<div class="...">
    <h1 class="...">Hello World, HTMX!</h1>
    <button class="...">Click!</button>
</div>
page preview Page preview ⬆️

Add Htmx attributes

<div class="...">
    <h1 class="...">Hello World, HTMX!</h1>
    <p class="..."></p>
    <button class="..." hx-get="/htmx/rand-num" hx-target="#rand-num">
        Click!
    </button>
</div>

2. Implement Backend with Reflection

Now let’s implement HTMX in backend.

Add a custom “Htmx” attribute

namespace BlazorHtmx;

[AttributeUsage(AttributeTargets.Class)]
public abstract class HtmxAttribute : Attribute {
    public string Route { get; set; }

    public HtmxAttribute(string route) {
        Route = route;
    }
}

[AttributeUsage(AttributeTargets.Class)]
public class HtmxGetAttribute : HtmxAttribute {
    public HtmxGetAttribute(string route) : base(route) {}
}

[AttributeUsage(AttributeTargets.Class)]
public class HtmxPostAttribute : HtmxAttribute {
    public HtmxPostAttribute(string route) : base(route) {}
}

Create a new folder Components/Htmx, create a new template file RandNum.razor

@inherits BlazorHtmx.HtmxBase
@attribute [HtmxGet("rand-num")]  @* use our defined attribute *@

<span>Random Number: @GetRandInt()</span>

@code {
    static Random rand = new();
    static int GetRandInt() {
        return rand.Next();
    }
}

Now, we can map all razor components with the Htmx attribute with simple reflection. Let’s create a extension method.

using System.Reflection;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http.HttpResults;

namespace BlazorHtmx;

public static class HtmxWebappExtends {
    public static void MapHtmx(this WebApplication app) {
        var types = Assembly.GetExecutingAssembly().GetTypes();
        foreach (var type in types) {
            if (!type.IsAssignableTo(typeof(IComponent))) {
                continue;
            }
            var attr = Attribute.GetCustomAttribute(type, typeof(HtmxAttribute));
            switch (attr) {
                case null:
                    continue;
                case HtmxGetAttribute hxGet:
                    app.MapGet($"/htmx/{hxGet.Route}", 
                        () => new RazorComponentResult(type));
                    break;
                // we will implement post later
            }
        }
    }
}

In program.cs :

app.MapHtmx();

By this, we can write any razor component and add this attribute, and then it will be automatically mapped to “/htmx/{route}”

Run the application.

We can now test the HTMX endpoint.

HTTP GET http://localhost:PORT/htmx/rand-num
Test Endpoint Open the page in browser and click button: Test Endpoint

3.Post

Let’s write a simple form

<div class="...">
    <form hx-post="/htmx/test-post" hx-target="#recieved-result">
        <li class="...">
            <ul>
                <label class="...">
                    <span class="...">Name:</span>
                    <input type="text" name="Name" class="...">
                </label>
            </ul>
            <ul>
                <label class="...">
                    <span class="...">Email:</span>
                    <input type="email" name="Email" class="...">
                </label>
            </ul>
            <ul class="...">
                <button type="submit" class="...">Submit</button>
            </ul>
        </li>
    </form>
    <p id="recieved-result" class="..."></p>
</div>
html form preview Preview⬆️

Our htmx template:

@inherits BlazorHtmx.HtmxBase
@attribute [HtmxPost("test-post")]

<span>
    Recieved Data: {"Name" = "@Name", "Email" = "@Email"}
</span>

@code {
    [Parameter]public string? Name {get; init;}
    [Parameter]public string? Email {get; init;}
}

For razor component with parameters, we need to construct RazorComponentResult like this:
(learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpresults.razorcomponentresult)

public RazorComponentResult(Type componentType, System.Collections.Generic.IReadOnlyDictionary<string,object?> parameters);
//or
public RazorComponentResult(Type componentType, object parameters);

In our HtmxWebappExtends static class, create a new case in the switch: (I don’t want a too long inline function, so I create a new function called mapHtmxPost)

case HtmxPostAttribute hxPost:
    app.MapPost($"/htmx/{hxPost.Route}",
        async (HttpRequest request) => await mapHtmxPost(componentType, request));
    break;
static async Task<IResult> mapHtmxPost(Type componentType, HttpRequest request) {
    var form = await request.ReadFormAsync();   //get http post form from request bodt
    var pRequests = form    //all the Parameters from post as Dictionary
        .Select(pair => (pair.Key, pair.Value[0]))   //(we are taking the first value currently)
        .ToDictionary();
    var pNeeds = componentType
        .GetProperties()
        .Where(p => p.GetCustomAttribute<ParameterAttribute>() is not null)
        .Select(p => p.Name);   //all the Parameters we need for our compoent
    //I don't know if there's better way to check if all Parameters are valid.
    //I think this may not be the best option.
    var pResult = new Dictionary<string, object?>();
    foreach (var pNeedName in pNeeds) {
        if (pRequests.TryGetValue(pNeedName, out var pStr)) {
            pResult.Add(pNeedName, pStr);
        }
        else {
            return Results.BadRequest($"Invalid Format. Parameter '{pNeedName}' not provided");
        }
    }
    // make sure call the right overload of the construct (the dict one, not the object one).
    return new RazorComponentResult(componentType, pResult.AsReadOnly());
}

Now let’s test our form:  form result preview Now it Works!