Build a server-side web app with HTMX in C#

By FoxLearn 2/7/2025 7:36:54 AM   108
Combine .NET with C# and HTMX for a smooth development experience that delivers dynamic front-end interactivity without any JavaScript.

In this guide, we will develop a simple server-side web application using C# and enhance the user experience with HTMX for dynamic front-end updates without writing any JavaScript.

Create a .NET C# Project

First, ensure you have the .NET CLI installed. It's an easy process. Once installed, use the following command to create a new application:

$ dotnet new mvc -n ProductApp

This creates a new directory called /ProductApp and sets up the basic project structure. Now, let's run the project in development mode:

/ProductApp $ dotnet run

You can now visit localhost:5000 to see the default homepage.

If you need to adjust the port or allow connections from domains other than localhost, modify the ProductApp/Properties/launchSettings.json. For example, change the port to 4000 as follows:

"applicationUrl": "http://*:4000"

Model Class

Following the Model-View-Controller (MVC) pattern, we’ll create a model class to represent our product information. Inside a newly created /Models directory, create a Product.cs file:

// Models/Product.cs
namespace ProductApp.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

This model defines a Product class with Id, Name, and Price properties. Similar to Java, C# uses get; and set; to automatically generate getter and setter methods for each property.

Repository Class

Next, we’ll create a simple repository class to manage the products. In a real-world scenario, this class would interact with a database, but for this example, we’ll use an in-memory list:

// ProductRepository.cs
using ProductApp.Models;

namespace ProductApp
{
    public class ProductRepository
    {
        private static List<Product> _products = new List<Product>()
        {
            new Product { Id = 1, Name = "Laptop", Price = 999.99m },
            new Product { Id = 2, Name = "Smartphone", Price = 599.99m }
        };

        public List<Product> GetAll()
        {
            return _products;
        }

        public void Add(Product product)
        {
            product.Id = _products.Any() ? _products.Max(p => p.Id) + 1 : 1; 
            _products.Add(product);
        }
    }
}

Here, we have a static list of products and two methods: GetAll() and Add().

The GetAll() method returns the list of products, while the Add() method adds a new product, auto-generating an Id.

Controller Class

The controller is responsible for handling user requests. Here’s the ProductController to manage displaying and adding products:

// Controllers/ProductController.cs
using Microsoft.AspNetCore.Mvc;
using ProductApp.Models;

namespace ProductApp.Controllers
{
    public class ProductController : Controller
    {
        private ProductRepository _repository = new ProductRepository();

        public IActionResult Index()
        {
            var products = _repository.GetAll();
            return View(products);
        }

        [HttpPost]
        public IActionResult Add(string name, decimal price)
        {
            if (!string.IsNullOrEmpty(name) && price > 0)
            {
                var newProduct = new Product { Name = name, Price = price };
                _repository.Add(newProduct);

                return PartialView("_ProductItem", newProduct);
            }

            return BadRequest();
        }
    }
}

This controller has two actions: Index() to display the list of products and Add() to handle form submissions for adding new products. The Add() action returns a partial view, which will be used by HTMX to update the page dynamically.

View Class

In the Views/Product/Index.cshtml, we render the list of products and a form to submit new ones:

// Views/Product/Index.cshtml
@model List<ProductApp.Models.Product>

<h1>Products</h1>

<div>
    <ul id="productList">
        @foreach (var product in Model)
        {
            @await Html.PartialAsync("_ProductItem", product)
        }
    </ul>
</div>

<form 
    hx-post="/product/add" 
    hx-target="#productList" 
    hx-swap="beforeend">
    <input type="text" name="name" placeholder="Product Name" />
    <input type="number" name="price" placeholder="Product Price" step="0.01" />
    <button type="submit">Add Product</button>
</form>

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

This page lists all the products and provides a form to submit new products. The form uses HTMX attributes to submit via Ajax and insert new products without refreshing the page. The response from the Add() action is inserted directly at the end of the #productList element.

Partial View for Product Item

The partial view _ProductItem.cshtml will render each product in a list item:

// Views/Product/_ProductItem.cshtml
@model ProductApp.Models.Product

<li>
    @Model.Name - [email protected]
</li>

Main Program Configuration

The Program.cs file sets up the application and configures the routing:

// ProductApp/Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "product",
    pattern: "product/{action=Index}/{id?}",
    defaults: new { controller = "Product" });

app.Run();

This file configures the application's HTTP request pipeline and sets up routing for the default and Product controllers.

This simple application allows users to view a list of products and add new ones dynamically, all without writing any JavaScript. The combination of .NET, C#, and HTMX simplifies building a dynamic, modern web app while adhering to the MVC architecture. Try running the app with $ dotnet run to see it in action!