Razor Integration
Heimdall works with the Razor surfaces you already use: MVC views, Razor Pages, partials, View Components, Tag Helpers, and normal server-rendered forms.
Razor remains responsible for composing HTML. Heimdall describes the interaction in that HTML and handles the request, response, and DOM-swap lifecycle.
1. Razor Is A Rendering Surface
Heimdall does not require a particular server-side templating style. It only needs the final HTML to contain the Heimdall attributes that describe the interaction. Razor can therefore remain responsible for composition while Heimdall handles the request and swap lifecycle.
Razor view or partial
-> rendered HTML with Heimdall attributes
-> browser runtime sends a content request
-> invocation returns HTML
-> declared target is updated2. Register The Razor Host
Use the normal ASP.NET Core registration for the Razor host you have. AddHeimdallMvc adds the MVC view services used when a content invocation renders a Razor partial. The Blazor adapter is documented separately because it has a different ownership model.
builder.Services.AddControllersWithViews();
// Or, for Razor Pages:
builder.Services.AddRazorPages();
builder.Services.AddHeimdall();
builder.Services.AddHeimdallMvc();
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{invocation=Index}/{id?}");
app.MapRazorPages();
app.UseHeimdall();3. Use Heimdall Attributes In A Razor View
Native attributes are often the clearest choice in a .cshtml file. They can sit beside normal Razor expressions, tag helpers, forms, partials, and layout markup.
@model OrderFilter
<form
id="order-filter"
heimdall-content-submit="orders.filter"
heimdall-payload-from="closest-form"
heimdall-content-target="#orders-table"
heimdall-content-swap="inner">
<input name="Search" value="@Model.Search" />
<button type="submit">Apply</button>
</form>
<div id="orders-table">
@await Html.PartialAsync("_OrderList", Model)
</div>4. Razor Pages Keep Their Normal Endpoint Model
Razor Pages can use Heimdall attributes in the page markup and can keep ordinary OnGet or OnPost handlers for normal navigation and form posts. Heimdall content invocations remain named HTML-returning interactions, so they do not need to be forced into a PageModel handler shape.
@page
@model OrdersModel
<section id="orders-panel">
<button
heimdall-content-click="orders.refresh"
heimdall-content-target="#orders-panel"
heimdall-content-swap="outer">
Refresh
</button>
@await Html.PartialAsync("_OrderList", Model.Orders)
</section>5. Return Partials From Content Invocations
Partials are the simplest reusable fragment boundary. Render the same partial during the initial page request and from the content invocation so both paths use the same markup and Heimdall attributes.
[ContentInvocationPrefix("orders")]
public sealed class OrderInvocations(
IOrderRepository orders,
IHeimdallMvcRenderer views)
{
[ContentInvocation("filter")]
public async Task<IHtmlContent> Filter(
OrderFilter filter,
CancellationToken ct)
{
var results = await orders.SearchAsync(filter, ct);
return await views.PartialAsync(
"_OrderList",
results,
ct);
}
}6. Partials Can Compose Partials
The MVC renderer uses the real view engine. A returned partial can use nested partials, layout-owned helpers, Razor dependency injection, tag helpers, ViewData, and TempData according to the normal MVC rendering rules.
// Views/Shared/_OrderList.cshtml
@model IReadOnlyList<OrderRow>
<div class="order-list">
@foreach (var order in Model)
{
@await Html.PartialAsync("_OrderRow", order)
}
</div>7. View Components Are Still Available
Use an MVC View Component when a reusable server-rendered region owns its own query and composition logic. It can be invoked from a Razor view or partial like any other View Component. Heimdall can trigger an invocation that returns a partial containing that invocation.
// In a Razor view or partial.
<aside id="cart-summary">
@await Component.InvokeAsync(
"CartSummary",
new { CartId = Model.CartId })
</aside>
// A content invocation can return a partial such as
// _CartSummaryHost.cshtml, which invokes the same
// View Component.
return await views.PartialAsync(
"_CartSummaryHost",
cart,
ct);8. Tag Helpers And Dependency Injection
Tag Helpers run as part of Razor rendering. They can generate the markup that Heimdall consumes, and they can coexist with explicit Heimdall attributes. Razor views and partials can also continue to inject application services in the normal way.
@inject ICurrencyFormatter Currency
<form
method="post"
asp-page-handler="Save"
heimdall-content-submit="profile.save"
heimdall-payload-from="closest-form"
heimdall-content-target="#profile-form"
heimdall-content-swap="outer">
<input asp-for="Model.DisplayName" />
<span>@Currency.Format(Model.Balance)</span>
<button type="submit">Save</button>
</form>9. Controller-Local Invocations
In an MVC-heavy application, a content invocation can live beside the controller and view it serves. Mark it NonAction so MVC conventional routing does not expose it as a normal MVC controller action.
[ContentInvocationPrefix("orders")]
public sealed class OrdersController(
IOrderRepository orders,
IHeimdallMvcRenderer views) : Controller
{
public IActionResult Index() => View();
[NonAction]
[ContentInvocation("filter")]
public async Task<IHtmlContent> FilterRows(
OrderFilter filter,
CancellationToken ct)
{
var results = await orders.SearchAsync(filter, ct);
return await views.PartialAsync(
"_OrderList",
results,
ct);
}
}10. Native And Heimdall-Managed Forms
A Razor view or Razor Page can keep a normal browser form, or it can add Heimdall attributes to progressively enhance the submit. A native form keeps its normal method, invocation, navigation, and browser behavior. A Heimdall-managed form is intercepted and receives returned HTML instead.
<!-- Normal browser submission -->
<form method="get" invocation="/orders">
<input name="search" />
<button type="submit">Search</button>
</form>
<!-- Heimdall-managed submission -->
<form
heimdall-content-submit="orders.filter"
heimdall-payload-from="closest-form"
heimdall-content-target="#orders-table"
heimdall-content-swap="inner">
<input name="Search" />
<button type="submit">Filter</button>
</form>Next Steps
Choose the integration page that matches who owns the route and render lifecycle. Then use the invocations, forms, state, and patterns pages to shape the interactions around each HTML boundary.