Swap.Htmx 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Swap.Htmx --version 1.0.0
                    
NuGet\Install-Package Swap.Htmx -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Swap.Htmx" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Swap.Htmx" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Swap.Htmx" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Swap.Htmx --version 1.0.0
                    
#r "nuget: Swap.Htmx, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Swap.Htmx@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Swap.Htmx&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Swap.Htmx&version=1.0.0
                    
Install as a Cake Tool

Swap.Htmx

NuGet

HTMX + ASP.NET Core MVC, but ergonomic.

Swap.Htmx is a lightweight orchestration layer for HTMX‑powered ASP.NET Core apps. It gives you:

  • A SwapController base class and controller/PageModel extensions
  • A fluent response builder for coordinated partial updates, toasts, and triggers
  • SwapState – Strongly-typed state management with automatic model binding
  • A type‑safe event system and event chains
  • Built‑in real‑time support (WebSockets + Server‑Sent Events)
  • Observability hooks (logging + OpenTelemetry)

If you want the conceptual overview of Swap as a whole, see the root README.md. This document focuses specifically on the Swap.Htmx package.


When Should I Use Swap.Htmx?

Use Swap.Htmx when:

  • You're building an ASP.NET Core MVC / Razor Pages / Minimal API app with HTMX
  • You want to avoid scattered ViewData, magic IDs, and ad‑hoc headers
  • You need coordinated updates (multiple partials, toasts, triggers) per action
  • You want a central place to define “when X happens, update Y on the UI”
  • You’d like real‑time HTML updates without going full SPA

If you just need a few custom HTMX headers, the low‑level helpers here work fine; as your app grows, the fluent builder and event chains keep things sane.


Core Building Blocks

  • SwapController – Base controller that:

    • Auto‑detects HTMX vs full page requests
    • Chooses full views or partials appropriately
    • Exposes helpers like SwapView, SwapResponse, SwapEvent, ServerSentEvents, etc.
  • Controller / PageModel Extensions – Use Swap without inheriting:

    • this.SwapView(...) / this.SwapResponse() on Controller/ControllerBase
    • this.SwapResponse() on PageModel
    • SwapResults for Minimal APIs
  • Fluent Response Builder (SwapResponseBuilder)

    • WithView(viewName, model) – main response payload
    • AlsoUpdate(targetId, viewName, model, swapMode) – out‑of‑band swaps
    • WithSuccessToast(...), WithErrorToast(...), WithInfoToast(...), WithWarningToast(...)
    • WithTrigger(eventKeyOrName, payload?) – strongly‑typed or string events
    • All triggers and toasts are merged into a single HX-Trigger header.
  • Event System & Event Chains

    • Type‑safe EventKey and SwapEvents helpers
    • ISwapEventConfiguration for central “when X happens, update Y” definitions
    • Declarative chains that render partials, show toasts, and trigger additional events
  • Realtime

    • Server‑Sent Events helpers
    • WebSocket integration and connection registry
    • Optional Redis backplane (ISseBackplane) for multi‑server setups
  • Dev & Diagnostics

    • Dev endpoints (app.MapSwapHtmxDevEndpoints()) for inspecting chains and connections
    • Structured logs and OpenTelemetry hooks

Installation

Install the templates and scaffold a ready‑to‑go project:

dotnet new install Swap.Templates
dotnet new swap-mvc -n MyProject

This wires up Swap.Htmx, HTMX, dev tooling, and sample views for you. See templates/README.md and the demo apps under demo/ for patterns.

Option 2 – Manual Installation

Add the package to an existing ASP.NET Core app:

dotnet add package Swap.Htmx

Then follow the setup below.


Basic Setup

1. Register Services

In Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

// Registers core Swap services, event bus, middleware, etc.
builder.Services.AddSwapHtmx();

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();

// Enables Swap middleware (HTMX helpers, event handling, dev tooling hooks)
app.UseSwapHtmx();

app.MapControllers();

app.Run();

See: docs/GettingStarted.md for a step‑by‑step walkthrough.

2. Add Client Assets

In your main layout (e.g. Views/Shared/_Layout.cshtml), add Swap’s CSS and JS along with HTMX:

<head>
    

    
    <link rel="stylesheet" href="~/_content/Swap.Htmx/css/swap.css" />

    
    <script src="https://unpkg.com/htmx.org@2.0.8"></script>

    
    <script src="~/_content/Swap.Htmx/js/swap.client.js"></script>
</head>

For LibMan‑managed HTMX and assets, see the templates and docs/GettingStarted.md.


Using Swap in Controllers

1. Simple View (80% of cases)

Use SwapController or the SwapView extension to automatically handle full vs partial renders:

using Microsoft.AspNetCore.Mvc;
using Swap.Htmx;

public class ProductsController : SwapController
{
    public IActionResult Details(int id)
    {
        var product = _db.Products.Find(id);
        return SwapView("Details", product);
        // Normal request  -> full view
        // HTMX request    -> partial view
    }
}

On a regular controller:

public class ProductsController : Controller
{
    public IActionResult Details(int id)
    {
        var product = _db.Products.Find(id);
        return this.SwapView("Details", product);
    }
}

See: docs/GettingStarted.md and docs/RazorPages.md for more patterns.

2. Coordinated Updates (Fluent Response Builder)

When an action needs to update multiple parts of the page, send a main view and out‑of‑band swaps in one chain:

public class CartController : SwapController
{
    public IActionResult AddToCart(int productId)
    {
        _cart.Add(productId);

        return SwapResponse()
            .WithView(CartViews.ProductAdded)
            .AlsoUpdate(CartElements.Count, CartViews.Count, _cart.Count, SwapMode.InnerHTML)
            .AlsoUpdate(CartElements.Total, CartViews.Total, _cart.Total)
            .WithSuccessToast("Added to cart!")
            .WithTrigger(CartEvents.Updated, new { itemCount = _cart.Count });
    }
}

Key APIs:

  • WithView(viewName, model) – main HTML payload
  • AlsoUpdate(targetId, viewName, model, swapMode?) – out‑of‑band swaps
  • WithSuccessToast(message) / WithErrorToast / WithWarningToast / WithInfoToast
  • WithTrigger(eventKeyOrName, payload?) – add HTMX triggers (merged into HX-Trigger)

Swap modes are strongly typed via SwapMode (OuterHTML, InnerHTML, BeforeBegin, AfterBegin, BeforeEnd, AfterEnd, Delete).

See: docs/OutOfBandSwaps.md.

3. Minimal APIs

SwapResults lets Minimal API endpoints return Swap responses:

using Swap.Htmx;

app.MapPost("/todo", (TodoItem item, ITodoService service) =>
{
    service.Add(item);

    return SwapResults.Response()
        .WithView("_TodoItem", item)
        .WithSuccessToast("Added!");
});

See: docs/MinimalApis.md.

4. Razor Pages

Use Swap directly from PageModel via extension methods:

using Microsoft.AspNetCore.Mvc.RazorPages;
using Swap.Htmx;

public class IndexModel : PageModel
{
    public IActionResult OnPostUpdate()
    {
        return this.SwapResponse()
            .WithView("_Partial", this)
            .Build();
    }
}

See: docs/RazorPages.md.

5. Composition Over Inheritance

All fluent APIs are available as extension methods on ControllerBase and PageModel, so you can opt‑out of inheriting from SwapController entirely:

using Microsoft.AspNetCore.Mvc;
using Swap.Htmx;

public class ReviewsController : Controller
{
    [HttpPost]
    public IActionResult Add(Review review)
    {
        if (!ModelState.IsValid)
        {
            return this.SwapValidationErrors(ModelState)
                .AlsoUpdate("review-form", "_ReviewForm", review)
                .Build();
        }

        _service.Add(review);

        return this.SwapResponse()
            .WithSuccessToast("Review added!")
            .WithTrigger(ReviewEvents.Added, review)
            .Build();
    }
}
``;

See: `Extensions/SwapControllerExtensions.cs`, `Extensions/SwapPageModelExtensions.cs`, `Extensions/SwapValidationExtensions.cs`.

---

## State Management with SwapState

SwapState provides strongly-typed state containers with automatic model binding:

```csharp
using Swap.Htmx.State;

// 1. Define your state
public class InventoryState : SwapState
{
    public string Tab { get; set; } = "all";
    public int Page { get; set; } = 1;
    public string? Search { get; set; }
}

// 2. Bind automatically in actions
public IActionResult Grid([FromSwapState] InventoryState state)
{
    var items = _service.GetItems(state);
    
    return this.SwapResponse()
        .WithView("_Grid", items)
        .WithState(state)  // Auto-updates state via OOB swap
        .Build();
}

<swap-state state="Model.State" />

<button hx-get="/Inventory/Grid"
        hx-target="#results"
        hx-include="#inventory-state">
    Load
</button>

Benefits:

  • Strongly-typed – No magic strings for hidden fields
  • Automatic binding[FromSwapState] handles model binding
  • Auto-sync.WithState() updates hidden fields via OOB swap
  • Change trackingstate.HasChanges, state.ChangedProperties

See: docs/SwapState.md for full documentation.


Form Validation

Use the <swap-validation> tag helper and SwapValidationErrors() for seamless server-side validation:


<div class="form-group">
    <label asp-for="Name"></label>
    <input asp-for="Name" />
    <swap-validation for="Name" />
</div>

<div class="form-group">
    <label asp-for="Email"></label>
    <input asp-for="Email" />
    <swap-validation for="Email" />
</div>
[HttpPost]
public IActionResult Create(CreateDto dto)
{
    if (!ModelState.IsValid)
    {
        return this.SwapValidationErrors(ModelState)
            .WithView("_CreateForm", dto)
            .Build();
    }
    
    var item = _service.Create(dto);
    return this.SwapRedirect("/Items", "Item created!");
}

The validation errors are automatically displayed in the corresponding <swap-validation> elements.

See: docs/Validation.md for full documentation.


CRUD Toast Presets

Standard success messages for common operations:

// After creating
.WithCreatedToast("Product", product.Name)  // "Product 'Widget' created successfully"

// After updating
.WithUpdatedToast("Settings")               // "Settings updated"

// After deleting
.WithDeletedToast("User", "john@test.com")  // "User 'john@test.com' deleted"

// Generic CRUD toast
.WithCrudToast(CrudOperation.Archived, "Record")  // "Record archived"

See: docs/CrudToasts.md for all operations.


State Coordination with swap-include-state

Simplify hx-include by referencing state containers by ID:


<button hx-get="/Items/Search"
        hx-include="#filter-tab, #filter-search, #filter-page">


<button hx-get="/Items/Search"
        swap-include-state="filter-state">

Multiple state containers:

<div hx-get="/Report" swap-include-state="filter-state, sort-state">

The JavaScript automatically expands this to the proper hx-include selector.


Event System & Event Chains

As your UI grows, you can centralize "when event X happens, refresh Y and show Z toast" declarations.

Configuration

Create a config class implementing ISwapEventConfiguration and register it via AddSwapHtmx:

using Swap.Htmx;
using Swap.Htmx.Events;

public static class ProductViews
{
    public const string List = "_ProductList";
    public const string Count = "_ProductCount";
}

public static class ProductElements
{
    public const string List = "product-list";
    public const string Count = "product-count";
}

public class ProductEventConfig : ISwapEventConfiguration
{
    public void Configure(SwapEventBusOptions events)
    {
        events.When(SwapEvents.Entity.Created("Product"))
              .RefreshPartial(ProductElements.List, ProductViews.List, ctx => GetProducts(ctx))
              .RefreshPartial(ProductElements.Count, ProductViews.Count, ctx => GetProductCount(ctx))
              .SuccessToast("Product created!");
    }
}

// Program.cs
builder.Services.AddSwapHtmx(options =>
{
    options.AddConfig<ProductEventConfig>();
});

From a controller, just emit the event:

public class ProductsController : SwapController
{
    public IActionResult Create(Product product)
    {
        _db.Products.Add(product);
        _db.SaveChanges();

        return SwapEvent(SwapEvents.Entity.Created("Product"));
    }
}

Async model factories avoid thread starvation for DB work:

events.When(ProductEvents.StockChecked)
      .RefreshPartialAsync(ProductElements.Stock, ProductViews.Stock, async ctx =>
      {
          var service = ctx.RequestServices.GetRequiredService<IProductService>();
          return await service.GetStockAsync();
      });

See: docs/Events.md and docs/EventChains.md.


Realtime: SSE & WebSockets

Swap.Htmx includes primitives for streaming HTML over SSE and WebSockets.

Server‑Sent Events (SSE)

Basic SSE endpoint from a SwapController:

public const string NotificationView = "_Notification";

[HttpGet("/sse/notifications")]
public IActionResult StreamNotifications()
{
    return ServerSentEvents(async (connection, ct) =>
    {
        while (!ct.IsCancellationRequested)
        {
            var html = await RenderPartialToStringAsync(NotificationView, GetLatestNotification());
            await connection.SendEventAsync("notification", html);
            await Task.Delay(5000, ct);
        }
    });
}

Enhanced SSE with rooms and subscriptions:

public static class SseRooms
{
    public const string Dashboard = "dashboard";
}

public static class SseEventNames
{
    public const string MetricsUpdated = "metrics-updated";
}

[HttpGet("/sse/dashboard")]
public IActionResult Dashboard()
{
    return EnhancedServerSentEvents(async (builder, ct) =>
    {
        var connection = builder.Connection;

        connection.JoinRoom(SseRooms.Dashboard);
        connection.SubscribeToEvent(SseEventNames.MetricsUpdated);

        while (!ct.IsCancellationRequested)
        {
            await Task.Delay(30000, ct);
        }
    });
}

To broadcast from services, inject ISseEventBridge:

public class OrderService
{
    private readonly ISseEventBridge _sse;

    public OrderService(ISseEventBridge sse) => _sse = sse;

    public async Task CompleteOrder(int orderId)
    {
        await _sse.BroadcastAsync("OrderCompleted", new { id = orderId });
        await _sse.SendToUserAsync("user-123", "Notification", "Your order is ready!");
        await _sse.SendToRoomAsync("admin-dashboard", "StatsUpdated", new { id = orderId });
    }
}

Configure in Program.cs:

// Single‑server, in‑memory
builder.Services.AddSwapHtmx()
                .AddSseEventBridge();

// With Redis backplane
builder.Services.AddSwapHtmx()
                .AddSseEventBridge()
                .AddSwapRedisBackplane(options =>
                {
                    options.Configuration = "localhost:6379";
                    options.ChannelName = "my-app-events";
                });

See: docs/ServerSentEvents.md, docs/WebSockets.md, docs/Realtime.md, and docs/RedisBackplane.md.


Low‑Level HTMX Helpers

If you just need to set headers manually, use the HttpRequest/HttpResponse extensions in Swap.Htmx.Extensions:

if (Request.IsHtmxRequest())
{
    Response.HxTrigger("todoCreated");
    Response.HxTrigger("todoCreated", new { id = 123 });
    Response.HxRedirect("/todos");
    Response.HxRefresh();
}

There are also helpers for HX-Location, HX-Reswap, and more. See: Extensions/SwapHtmxExtensions.cs.


Dev Tooling & Observability

  • Dev endpoints – expose Swap dev UIs:

    app.MapSwapHtmxDevEndpoints(); // /_swap/dev/*
    

    Useful for inspecting event chains, SSE/WebSocket connections, and troubleshooting.

  • Logging & TelemetrySwapTelemetry and SwapLog integrate with ASP.NET logging and OpenTelemetry.

    • To see verbose logs, add e.g. to appsettings.Development.json:

      "Logging": {
        "LogLevel": {
          "Swap.Htmx": "Debug"
        }
      }
      

See: docs/DebuggingAndLogging.md.


Demos & Templates

This repo ships with several demos that exercise different parts of Swap.Htmx:

  • demo/SwapMinimal – minimal API + Swap example
  • demo/SwapShop – e‑commerce style MVC app showing controllers, events, and chains
  • demo/TaskFlow – team task management with realtime features
  • demo/SwapWebSockets, demo/SwapRedisDemo, demo/SwapPhase15, etc. – focused samples for realtime and orchestration features

For a production‑ready starting point, use the swap-mvc template from Swap.Templates.


Documentation Map

All library docs live under docs/ in this folder:

Core Concepts

  • Getting Starteddocs/GettingStarted.md
  • Migration Guidedocs/MigrationGuide.mdNEW
  • Multi-Component Coordinationdocs/MultiComponentCoordination.mdNEW
  • State Managementdocs/StateManagement.mdNEW
  • Anti-Patternsdocs/AntiPatterns.mdNEW

Events & Updates

  • Events & Triggersdocs/Events.md
  • Event Chainsdocs/EventChains.md
  • Out‑of‑Band Swapsdocs/OutOfBandSwaps.md

Realtime

  • Realtime Overviewdocs/Realtime.md
  • Server‑Sent Eventsdocs/ServerSentEvents.md
  • WebSocketsdocs/WebSockets.md
  • Redis Backplanedocs/RedisBackplane.md

Framework Integration

  • Minimal APIsdocs/MinimalApis.md
  • Razor Pagesdocs/RazorPages.md
  • Source Generatorsdocs/SourceGenerators.md
  • User Context & Identitydocs/UserContext.md

Development

  • Debugging & Loggingdocs/DebuggingAndLogging.md

For a higher‑level view of all Swap packages (Swap.Htmx, Swap.Testing, templates, etc.), see the root‑level README.md.


License

MIT

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.6 112 12/11/2025
1.0.5 118 12/11/2025
1.0.4 144 12/10/2025
1.0.3 138 12/9/2025
1.0.2 193 12/5/2025
1.0.1 680 12/2/2025
1.0.0 521 11/27/2025
0.14.0 164 11/26/2025
0.13.0 163 11/26/2025
0.12.0 166 11/26/2025
0.11.4 170 11/25/2025
0.11.3 175 11/24/2025
0.11.2 344 11/21/2025
0.11.1 275 11/21/2025
0.11.0 300 11/21/2025
0.10.0 341 11/21/2025
0.9.1 385 11/20/2025
0.9.0 379 11/20/2025
0.8.2 386 11/20/2025
0.8.1 388 11/20/2025
0.8.0 379 11/20/2025
0.7.1 381 11/20/2025
0.7.0 385 11/20/2025
0.6.0 386 11/20/2025
0.5.1 384 11/19/2025
0.5.0 317 11/17/2025
0.4.1 321 11/17/2025
0.4.0 266 11/16/2025
0.3.5 242 11/14/2025
0.3.4 281 11/12/2025
0.3.3 264 11/12/2025
0.3.2 275 11/11/2025
0.3.1 189 11/6/2025
0.3.0 190 11/3/2025
0.2.0-dev 120 11/1/2025
0.1.0 177 10/30/2025