Skip to documentation
Heimdall Docs
Heimdall Docs

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.

Text
Razor view or partial
    -> rendered HTML with Heimdall attributes
    -> browser runtime sends a content request
    -> invocation returns HTML
    -> declared target is updated

2. 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.

C#
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.

Text
@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.

Text
@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.

Code
[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.

Text
// 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.

C#
// 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.

Text
@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.

JSON
[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.

HTML
<!-- 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>