FluentHtml.Validation
0.2.0
dotnet add package FluentHtml.Validation --version 0.2.0
NuGet\Install-Package FluentHtml.Validation -Version 0.2.0
<PackageReference Include="FluentHtml.Validation" Version="0.2.0" />
<PackageVersion Include="FluentHtml.Validation" Version="0.2.0" />
<PackageReference Include="FluentHtml.Validation" />
paket add FluentHtml.Validation --version 0.2.0
#r "nuget: FluentHtml.Validation, 0.2.0"
#:package FluentHtml.Validation@0.2.0
#addin nuget:?package=FluentHtml.Validation&version=0.2.0
#tool nuget:?package=FluentHtml.Validation&version=0.2.0
FluentHtml — Server-Rendered HTML with a Fluent .NET API
Every ASP.NET team eventually faces the same choice: Razor mixes HTML and C# into a template language that resists refactoring. Blazor introduces a runtime, a component lifecycle, and hydration overhead that most line-of-business apps don't need. React/Vue/Angular require a separate language, a build pipeline, and a deployment story that doubles operational complexity. None of these are bad tools — they're just solving a bigger problem than most internal applications actually have.
FluentHtml takes a different approach. Instead of importing a foreign programming model, it makes HTML a native citizen of .NET — strongly typed, composable, IntelliSense-discoverable (in C#) — and keeps application logic on the server where ASP.NET Core excels. HTMX handles interactivity without a JavaScript framework. The result feels like writing idiomatic .NET, not porting another language's ideas.
Guiding principle: Never replace ASP.NET Core. Extend it with strongly typed HTML generation.
The Problem
// Razor — mixes template syntax with C#
<div class="card">
<h2>@Model.Title</h2>
@foreach (var item in Model.Items) {
<p>@item.Name</p>
}
</div>
// String concatenation — no IntelliSense, no refactoring, no safety
html += "<div class=\"card\">";
html += "<h2>" + title + "</h2>";
// Blazor — runtime overhead for simple server-rendered pages
<div class="card">
<h2>@Title</h2>
@foreach (var item in Items) {
<p>@item.Name</p>
}
</div>
Razor forces a template language that mixes two syntaxes in one file. String concatenation has no compile-time safety. Blazor adds a runtime, state management, and a rendering pipeline for pages that could be simple HTML. There is no lightweight, strongly typed, fluent way to generate HTML in .NET that integrates naturally with Minimal APIs and produces clean output.
FluentHtml eliminates the seam. It provides the strongly typed building blocks that make HTML generation a first-class .NET operation — not a template language, not a string, not a separate runtime.
How It Works
The entire component model lives in FluentHtml — pure .NET with no third-party dependencies. Targets net8.0 and net10.0. ASP.NET Core integration, HTMX support, and Bootstrap components are separate packages you opt into.
┌──────────────────────────────────────────────────────────────┐
│ FluentHtml │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Node │ │ Element │ │ Component │ │
│ │ .Children │ │ .TagName │ │ .Render() │ │
│ │ .AddChild() │ │ .Attributes │ │ .Build() │ │
│ │ │ │ .Classes │ │ │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ TextNode │ │ RawHtml │ │ Fragment │ │
│ │ .Content │ │ .Content │ │ (no wrapper) │ │
│ │ (encoded) │ │ (raw) │ │ │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ~120 HTML Elements (Div, Span, Button, ...) │ │
│ │ Fluent attributes: .Id() .Class() .HxPost() │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ FluentHtml.Http — IResult, Minimal API helpers │ │
│ │ FluentHtml.Htmx — hx-get, hx-post, hx-swap │ │
│ │ FluentHtml.Bootstrap — Card, Alert, Button, Modal │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Every element is immutable after construction — fluent methods return the same instance. Thread-safe by design. No defensive copies needed.
Not a Clone — An Idiom
FluentHtml is inspired by FastHTML's philosophy of composing HTML from function calls, but it is designed as an idiomatic .NET framework that follows .NET conventions rather than porting another language's patterns:
- Extension methods instead of static functions —
Button().Primary()reads like fluent .NET, not F# pipe operators - Generics and type constraints —
Component<T>with compile-time safety, not duck typing - Immutable element trees — built via constructors, not mutable builder chains
- ASP.NET Core native —
IResultintegration, Minimal API extensions, not a standalone renderer - Strong typing everywhere —
ButtonType.Submitinstead of"submit",InputType.Emailinstead of"email"
The result is a framework that feels like writing .NET, not translating another language's idioms.
Fluent API — Not a Template Language
// FluentHtml — strongly typed, IntelliSense-discoverable
Button("Save")
.Primary()
.Large()
.HxPost("/api/save")
.Target("#result")
// Instead of Razor's mixed syntax:
// <button class="btn btn-primary btn-lg"
// hx-post="/api/save"
// hx-target="#result">Save</button>
Typing Button(). reveals Primary(), Secondary(), Disabled(), Id(), Class(), HxPost(), HxGet(), MarginTop(), Width() — developers should rarely need documentation for common tasks.
Composition, Not Inheritance
// Assemble building blocks — don't extend base classes:
var card = Card(
H2("Customer Orders"),
Table(
Thead(new Tr(Th("Name"), Th("Amount"))),
Tbody(orders.Select(o =>
new Tr(new Td(o.Name), new Td(o.Amount.ToString("C")))
))
),
Button("New Order")
.Primary()
.HxGet("/orders/new")
.HxTarget("#dialog")
);
Components compose from elements. Elements compose from nodes. Pages compose from layouts. No inheritance hierarchies to navigate.
Server-Side Rendering — No Runtime Required
Browser → HTTP Request → ASP.NET Core Pipeline → Minimal API Route → FluentHtml Components → Renderer → HTML Response → Browser
FluentHtml owns only component creation and rendering. Everything before and after is standard ASP.NET Core — routing, DI, middleware, authentication, model binding, logging. HTMX requests behave exactly like normal requests; the only difference is partial HTML is returned instead of a complete page.
Use Cases
Internal Business Applications
app.MapGet("/customers", (CustomerService svc) =>
{
return CustomersPage(service.GetAll());
});
Dashboards, admin panels, CRM, ERP, inventory systems — applications where the UI serves the data, not the other way around. Server-rendered HTML with HTMX partial updates provides the interactivity without the JavaScript overhead.
CRUD Systems
app.MapGet("/users/new", () => CreateUserForm());
app.MapPost("/users", (CreateUserDto dto) =>
{
service.Create(dto);
return UsersTable(service.GetAll()).ToHtmlResult();
});
Strongly typed forms with InputFor(x => x.Name), validation via ASP.NET Core's existing infrastructure, and HTMX partial rendering for seamless updates.
Dashboards and Reporting
app.MapGet("/dashboard", (DashboardService svc) =>
{
return DashboardPage(
MetricCards(svc.GetMetrics()),
RecentOrders(svc.GetOrders(10)),
SalesChart(svc.GetSalesData())
);
});
Reusable components compose into complex layouts. Bootstrap integration provides the visual foundation.
Documentation and Portals
Clean, semantic HTML without the weight of a JavaScript framework. Server-side rendering provides excellent SEO and fast first-page load.
Technical Differentiators
| vs. | FluentHtml |
|---|---|
| Razor | No template language — pure C# with compile-time safety, IntelliSense, and easy refactoring |
| Blazor | No runtime, no hydration, no component lifecycle — just HTML generation and server responses |
| React/Vue/Angular | No JavaScript build pipeline, no client-side state management, no deployment complexity |
| String concatenation | Strongly typed elements, automatic HTML encoding, attribute management, composition |
| Tag Helpers | Fluent API with method chaining — not attribute-based template extensions |
| FastHTML | Idiomatic .NET — extension methods, generics, ASP.NET Core integration, not a language port |
Packages
| Package | Description |
|---|---|
| FluentHtml.Core | Core: Node, Element, Component<T>, Fragment, TextNode, RawHtml, Renderer, HtmlWriter, HtmlEncoder, ~120 HTML elements, fluent attributes, CSS helpers |
| FluentHtml.Http | HtmlResult (IResult), Minimal API endpoint extensions, Node.ToHtmlResult() |
| FluentHtml.Htmx | HxGet(), HxPost(), HxSwap(), HxTarget(), HxTrigger(), HxConfirm() and 20+ HTMX attribute extensions |
| FluentHtml.Bootstrap | Card, Alert, Button, Navbar, Modal, Accordion, Toast, Dropdown, Pagination, Badge, Breadcrumb, Spinner + CSS helper extensions |
| FluentHtml.Forms | Form, InputFor(), LabelFor(), SelectFor(), TextAreaFor(), CheckboxFor(), ValidationSummary() |
| FluentHtml.Validation | ValidationMessage, ValidationSummary, validation CSS helpers |
Installation
dotnet add package FluentHtml.Core
dotnet add package FluentHtml.Http
dotnet add package FluentHtml.Htmx
dotnet add package FluentHtml.Bootstrap
dotnet add package FluentHtml.Forms
dotnet add package FluentHtml.Validation
Quick Start
using FluentHtml.Elements;
using FluentHtml.Http;
using FluentHtml.Bootstrap.Components;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// A complete page — all .NET, no Razor, no templates
app.MapGet("/", () =>
{
return new HtmlElement(
new HeadElement(new TitleElement("FluentHtml Sample")),
new BodyElement(
new NavElement().Class("navbar navbar-dark bg-dark").Children(
new AnchorElement("FluentHtml").Href("/").Class("navbar-brand")
),
new MainElement().Class("container mt-4").Children(
Card(
new Heading1Element("Welcome"),
new ParagraphElement("Built with strongly typed .NET components."),
Button("Click Me")
.Primary()
.Large()
.HxPost("/api/click")
.HxTarget("#result")
),
new DivElement().Id("result")
)
)
);
});
// Partial rendering for HTMX
app.MapGet("/api/click", () =>
{
return AlertExtensions.Alert("Button clicked!")
.Success()
.ToHtmlResult();
});
app.Run();
Component Model
// Elements — one HTML tag each
var div = new DivElement(
new Heading1Element("Dashboard"),
new ParagraphElement("Welcome back.")
).Id("main").Class("container");
// Text nodes — auto-encoded
var p = new ParagraphElement("Hello <script>alert('xss')</script>");
// Renders: <p>Hello <script>alert('xss')</script></p>
// Fragments — siblings without a wrapper
var fragment = new Fragment(
new Heading1Element("Title"),
new ParagraphElement("Body")
);
// Raw HTML — trusted, not encoded (use sparingly)
var raw = new RawHtml("<strong>Bold text</strong>");
Fluent Attributes
Button("Save")
.Primary() // class="btn btn-primary"
.Large() // class="btn btn-primary btn-lg"
.Disabled() // disabled
.Id("save-btn") // id="save-btn"
.HxPost("/api/save") // hx-post="/api/save"
.HxTarget("#result") // hx-target="#result"
.HxConfirm("Save?") // hx-confirm="Save?"
HTMX Integration
using FluentHtml.Htmx;
Button("Load Data")
.HxGet("/api/data")
.HxTarget("#table")
.HxSwap("innerHTML")
.HxTrigger("click")
.HxIndicator("#spinner");
Button("Delete")
.Danger()
.HxDelete("/api/items/1")
.HxConfirm("Are you sure?")
.HxTarget("closest tr")
.HxSwap("outerHTML");
Forms with Model Binding
using FluentHtml.Forms;
var form = new FormElement(
new Heading2Element("Create User"),
new DivElement(
new LabelElement("Name").For("name"),
new InputElement().Type("text").Name("name").Id("name").Required()
).Class("mb-3"),
new DivElement(
new LabelElement("Email").For("email"),
new InputElement().Type("email").Name("email").Id("email").Required()
).Class("mb-3"),
Button("Create").Primary()
).Action("/users").Method("post");
Testing
using FluentHtml.Elements;
using FluentHtml.Rendering;
var renderer = new Renderer();
var card = new DivElement(
new Heading2Element("Test"),
new ParagraphElement("Content")
).Class("card");
var html = renderer.Render(card);
Assert.Equal("<div class=\"card\"><h2>Test</h2><p>Content</p></div>", html);
No web server required. Components are plain .NET objects — instantiate, render, assert.
Node Hierarchy
Node
├── Element — single HTML tag (Div, Span, Button, Form, Table, Input...)
├── TextNode — plain text, always HTML encoded
├── RawHtml — trusted HTML, not encoded (use sparingly)
├── Fragment — groups siblings without a wrapper element
└── Component — reusable UI, composes existing nodes
Every renderable object derives from Node. Elements own a tag name, attributes, and children. Components render other nodes. Fragments produce no wrapper HTML.
License
Apache License 2.0 — see LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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 is compatible. 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. |
-
net10.0
- FluentHtml.Core (>= 0.2.0)
-
net8.0
- FluentHtml.Core (>= 0.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Provides ValidateExtensions for System.ComponentModel.DataAnnotations validation, ValidationMessage component for inline error display, ValidationSummary component, and ValidationResult type.