ASP.NET Integration

Wire MonorailCSS into an ASP.NET app with the Discovery package or the CssFramework API

MonorailCSS is a JIT compiler — it only emits CSS for the classes your app actually uses. The MonorailCss.Discovery package brings that into ASP.NET: it scans at startup, watches your source tree, and serves CSS from middleware. CSS regenerates as you edit. If its scanning model doesn't fit, you can call CssFramework directly instead — see rolling your own at the end.

The shape is simple: every place a class name can hide is scanned, the discovered names are compiled against your theme by CssFramework, and the result is a single stylesheet. The diagram below traces that flow — the four class sources at the top fan into the scan, app.css configures the engine, and one CSS file comes out.

From source to stylesheetHow discovery feeds CssFrameworkcandidate classestheme + utilitiescompiled CSSRazor & C#.razor · .csReferenced assembliesclass strings in ILStatic web assets_content/**/*.jsDiscovery scancollect + validateapp.css@theme · @utility · @applyCssFrameworkJIT compileapp.cssserved at /_monorail/app.cssCLASS SOURCES

The input: app.css

Discovery reads a Tailwind v4-style CSS file:

css
/* wwwroot/app.css */
@import "tailwindcss";
  
@theme {
    --color-brand: oklch(60% 0.2 250);
    --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.1);
    --radius-card: 0.625rem;
}

@custom-variant dark (&:where(.dark, .dark *));
  
@utility scrollbar-hide {
    scrollbar-width: none;
}

@layer base {
    body {
        @apply text-foreground bg-background;
    }
}

@import follows imports recursively, @theme defines design tokens, @utility registers custom utilities, @custom-variant registers custom variants, @apply resolves utility composition, and plain CSS passes through.

The CSS-file-as-config angle has a second benefit: editor tooling like the Tailwind CSS IntelliSense extension parses the same file and gives you autocomplete, hover previews, and color squares for every theme token in your .razor and .cs source.

MonorailCss.Discovery

Install:

bash
dotnet add package MonorailCss.Discovery

Wire it up in Program.cs:

csharp
using MonorailCss.Discovery;
  
var builder = WebApplication.CreateBuilder(args);
  
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddMonorailCss();
  
var app = builder.Build();
  
app.UseStaticFiles();
app.UseMonorailCss();
  
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.Run();

Point your layout at the served stylesheet:

html
<link rel="stylesheet" href="/_monorail/app.css" />

With no configuration AddMonorailCss auto-detects wwwroot/app.css, scans every non-BCL referenced assembly for class strings, scans JavaScript shipped by component packages as static web assets, watches your source tree for changes in Development, and serves the result at /_monorail/app.css. Edit a class in a .razor file under dotnet watch and the browser sees the new CSS on the next HEAD poll.

Under dotnet watch, this extends across projects: editing a .razor/.cs file in a referenced project — a component library you're tweaking while running the app that consumes it — regenerates CSS too, even though that source lives outside the running app's content root. The referenced project's directory is located from its build PDB and added to the watch set automatically (gated on the DOTNET_WATCH environment variable; see WatchReferencedProjectSources below).

Configuration

Pass a callback to AddMonorailCss to override defaults:

csharp
builder.Services.AddMonorailCss(opt =>
{
    opt.ExcludeAssemblies.Add("BadIdeas.Icons.FontAwesome");
    opt.ExtraSafelist.Add("bg-red-500");
    opt.CssEndpoint = "/css/app.css";
});

The options you'll actually reach for:

  • ExcludeAssemblies — skip libraries whose IL strings would inflate the candidate set without contributing real utilities. Icon packs that bake thousands of class-shaped tokens into metadata are the usual culprits. MonorailCSS itself and BCL assemblies (System.*, Microsoft.*) are excluded automatically.
  • ExtraSafelist — force-include classes static scanning can't reconstruct, e.g. anything built at runtime via $"bg-{color}-500".
  • ScanStaticWebAssets — on by default; reads classes out of JavaScript that referenced packages/RCLs ship under _content/<Package>/. Those files live in the NuGet cache — outside your source tree and the assembly IL — so nothing else reaches them; a component whose modal markup is built in scripts.js needs this. Narrow what's read with StaticWebAssetExtensions (default .js, .mjs), or suppress a package's assets by adding it to ExcludeAssemblies. In Development the source watcher also watches these script extensions inside your watched directories, so live edits to a .js/.mjs file that carries class strings regenerate CSS the same as a .razor edit (the startup manifest scan is read once, so the live signal comes from the watcher).
  • SourceCssPath — path to your entry CSS file. Auto-detected as wwwroot/app.css when unset.
  • CssEndpoint — the URL the middleware serves CSS at, default /_monorail/app.css.
  • Framework — supply a pre-configured CssFramework when you need to seed prose configuration or register utilities programmatically. See configuration. The CSS file processing layers on top.

There are a couple of less-common options (SourceCss for in-memory CSS, WriteToFile to mirror the output to disk, WatchSourceDirectories for non-standard layouts, WatchReferencedProjectSources to force cross-project watching on or off rather than letting it follow DOTNET_WATCH) on MonorailDiscoveryOptions; the defaults are right for most projects.

Owning the endpoint

The built-in middleware handles ETag, If-None-Match, and HEAD, and exposes a JSON diagnostics view at {CssEndpoint}/diagnostics (handy when a class isn't appearing and you want to confirm whether it failed to discover or failed to compile).

When the built-in middleware isn't enough — you want auth in front of the CSS, custom cache directives, or to mirror to a CDN — register discovery without the middleware and inject IClassRegistry into your own endpoint:

csharp
builder.Services.AddMonorailClassDiscovery(opt =>
{
    opt.ExcludeAssemblies.Add("BadIdeas.Icons.FontAwesome");
});
  
var app = builder.Build();
  
app.MapGet("/css/app.css", (IClassRegistry registry) =>
    Results.Text(registry.Css, "text/css", Encoding.UTF8));

IClassRegistry exposes Css (the assembled stylesheet), Version (a content-derived ETag, already wrapped in quotes per RFC 7232 — don't re-quote it), and GetClasses() (the validated class set).

Rolling your own

The Discovery package is a wrapper around CssFramework.Process. If its scanning model doesn't fit — you have your own class collector, you're generating CSS in a non-ASP.NET host, you're driving everything from a build step that produces inputs by some other route — call the framework directly:

csharp
var framework = new CssFramework();
var classes = GetClassesSomehow();
var css = framework.Process(classes);

See getting started for the bare-API walkthrough. The hard part isn't calling MonorailCSS — it's reliably discovering which classes your application uses. Discovery solves that for the ASP.NET case.

© 2026 MonorailCss · A Tailwind 4.3 compatible CSS engine written in .NET