Razor
The SimpleW.Helper.Razor is a Razor templating module for the SimpleW web server. It enables server-side HTML rendering using Razor syntax (via RazorLight) and integrates with SimpleW’s routing/controller system by introducing a ViewResult that the module can render into an HTTP response.
This module is designed to feel familiar to ASP.NET MVC developers, while staying simple and minimal.
Features
It allows you to :
- Razor View Engine : powered by RazorLight (compile + render)
- Controller integration : RazorController provides a
View()helper - Models : pass a DTO or an anonymous object
- ViewBag :
dynamicViewBag supported (ExpandoObject) - Layouts :
Layout = "_Layout"+@RenderBody()/@RenderSection() - Partials :
@await Html.PartialAsync("Header")(returns non-escaped HTML content) - Html helper :
Html.PartialAsync()available in templates (ASP.NET Core-like) - Caching : compiled templates cached in memory (RazorLight memory cache)
- Error handling : compilation errors return HTTP 500 with details (HTML-escaped)
Requirements
- .NET 8.0
- SimpleW (core server)
- RazorLight package (automatically included)
Installation
$ dotnet add package SimpleW.Helper.Razor --version 26.0.0-rc.20260309-1542Configuration options
| Option | Default | Description |
|---|---|---|
| Enabled | true | Enables or disables the Razor module. When disabled, no Razor views are rendered. |
| ViewsPath | "Views" | Root directory where Razor view files are located. Relative paths are resolved from the application base directory. |
| Layout | "_Layout" | Default Razor layout name used when a view does not explicitly specify one. |
| ReloadOnChange | false | When enabled, Razor views are reloaded automatically when files change on disk (useful for development). |
| CacheCompiledViews | true | Enables caching of compiled Razor views. Disabling this forces recompilation on each request (debug / dev only). |
| FileExtensions | [".cshtml"] | List of file extensions treated as Razor views. |
| ModelTypeResolver | null | Optional delegate used to resolve the model type for a given view at runtime. If null, the default resolution logic is used. |
| OnBeforeRender | null | Optional hook executed before rendering a Razor view. Can be used to mutate the model or inject data into the view context. |
| OnAfterRender | null | Optional hook executed after rendering a Razor view. Useful for logging, metrics, or post-processing the output. |
Minimal Example
1. Folder Structure
Recommended layout :
/Views
/Home
Index.cshtml
About.cshtml
/Controllers
HomeController.cs
/Program.cs/Views
/Home
Index.cshtml
About.cshtml
/Program.csLayouts and Partials structure
If you use layouts and partials, a common (ASP.NET Core-like) structure is :
/Views
/Home
Index.cshtml
About.cshtml
/Shared
_Layout.cshtml
/Partials
_Header.cshtml
_Footer.cshtmlDefaults:
LayoutsPath = "Shared"PartialsPath = "Partials"
2. Rendering a view
You can return a ViewResult directly from a mapped route :
using System;
using System.Net;
using SimpleW;
using SimpleW.Modules;
namespace Sample {
class Program {
static async Task Main() {
var server = new SimpleWServer(IPAddress.Any, 8080);
server.UseRazorModule(options => {
options.ViewsPath = "Views"; // path of your views folder
});
server.MapControllers<RazorController>("/api");
await server.RunAsync();
}
}
[Route("/home")]
public sealed class HomeController : RazorController {
[Route("GET", "/index")]
public object Index() {
// the model can for strongly type or anonymous like in this example
var model = new { Title = "Home", H1 = "Welcome" };
return View("Home/Index", model);
}
}
}using System.Net;
using SimpleW;
using SimpleW.Modules;
namespace Sample {
class Program {
static async Task Main() {
var server = new SimpleWServer(IPAddress.Any, 8080);
server.UseRazorModule(options => {
options.Views = "Views" // path of your views folder
});
server.MapGet("/api/home/index", () => {
// the model can for strongly type or anonymous like in this example
var model = new { Title = "Home", H1 = "Welcome" };
return RazorResults.View("Home/Index", model);
});
await server.RunAsync();
}
}
}Layouts and Partials
Layouts
To use a layout, set Layout at the top of your view (similar to ASP.NET Core):
@{
Layout = "_Layout";
ViewBag.Title = "Home";
}
<h1>Hello</h1>
@section Scripts {
<script>console.log("home loaded");</script>
}Create the layout file in Views/Shared/_Layout.cshtml (default LayoutsPath = "Shared"):
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>@ViewBag.Title</title>
</head>
<body>
@await Html.PartialAsync("Header")
<main>
@RenderBody()
</main>
@RenderSection("Scripts", required: false)
</body>
</html>@RenderBody()injects the view HTML.@RenderSection(name, required: false)renders an optional section declared with@section.
Partials
Partials follow the usual underscore convention. Create a partial such as:
Views/Partials/_Header.cshtml (default PartialsPath = "Partials")
<header>
<h2>SimpleW</h2>
</header>Render it from a view or layout:
@await Html.PartialAsync("Header")PartialAsync() returns a non-escaped HTML content wrapper, so the browser receives real HTML (not <div>...).
3. Create a View
Views/Home/Index.cshtml :
@model dynamic
<!doctype html>
<html>
<head><title>@Model.Title</title></head>
<body>
<h1>@Model.H1</h1>
</body>
</html>View names
A view name is a path relative to ViewsPath.
Examples (with ViewsPath = "Views") :
"Home"→Views/Home.cshtml"Home/Index"→Views/Home/Index.cshtml
ViewBag Usage
ViewResult exposes a ViewBag (dynamic) using an ExpandoObject. Use WithViewBag():
Note: the module also injects an
Htmlhelper (ASP.NET Core-like) that currently exposesHtml.PartialAsync(...). If for any reason your template cannot resolveHtml, you can always useViewBag.Htmldirectly.
return RazorResults.View("Home.cshtml", new { Title = "Home" })
.WithViewBag(vb => {
vb.UserName = "Chris";
vb.Now = DateTimeOffset.UtcNow;
});In the Razor view :
<h2>Hello @ViewBag.UserName</h2>
<p>UTC: @ViewBag.Now</p>Html helper in templates
In RazorLight, ViewBag values are available at runtime, but symbols like Html must exist at compile time. That means you cannot rely on a @{ dynamic Html = ViewBag.Html; } block inside _ViewImports.cshtml (because _ViewImports is for directives like @using / @inherits, and code blocks may not run as you expect).
SimpleW injects an Html helper into ViewBag (from RazorModule):
ViewBag["Html"] = new SimpleHtmlHelper(...)
To be able to write ASP.NET Core-style calls:
@await Html.PartialAsync("Header")you have two options:
Option A (recommended): expose Html via a template base class
Create a base class that exposes Html as a real property:
using RazorLight;
using RazorLight.Razor;
namespace SimpleW.Helper.Razor;
public abstract class SimpleWTemplatePage<TModel> : TemplatePage<TModel>
{
public RazorModule.SimpleHtmlHelper Html
=> (RazorModule.SimpleHtmlHelper)ViewBag.Html!;
}Then ensure the project injects an @inherits import at compile time (works even if your RazorLight builder does not have ConfigureRazor / AddDefaultImports):
// inside SimpleWRazorProject.GetImportsAsync(...)
imports.Insert(0, new TextSourceRazorProjectItem(
key: "_SimpleW_HtmlImport",
content: "@inherits SimpleW.Helper.Razor.SimpleWTemplatePage<dynamic>"
));After that, every view/layout/partial can use:
@await Html.PartialAsync("Header")without repeating anything in each template.
Option B (fallback): define Html in each view/layout
If you do not want the base-class approach, you can define Html at the top of each template:
@{
dynamic Html = ViewBag.Html;
}Then use:
@await Html.PartialAsync("Header")Note: placing the
@{ ... }alias inside_ViewImports.cshtmlis not reliable.
Status code and content type
ViewResult lets you specify status code and content type.
return new ViewResult(
name: "Home/Index",
model: new { Title = "Home" },
statusCode: 200,
contentType: "text/html; charset=utf-8"
);Error handling
If Razor compilation fails, the module returns :
- HTTP 500
- an HTML page containing the compilation error (HTML-escaped)
This is helpful in development. In production you might want to replace it with a generic page.
Performance Considerations
- Template Caching : RazorLight caches compiled templates in memory
- First Request : Initial compilation has overhead, subsequent requests are fast
- Development : Consider disabling caching during development for live updates
- Memory : Each unique template is cached separately
