Skip to documentation
Heimdall Docs
Heimdall Docs

Invocations

Content invocations are server-side methods that return HTML. Heimdall triggers or Heimdall.invoke call them through the shared request pipeline, then apply the returned fragments and directives.

If pages handle routes, invocations handle interactions.

1. What an Invocation Is

An invocation is a method marked with [ContentInvocation]. Static methods can use request-injected service parameters; instance methods can share constructor dependencies. Both participate in discovery and normal request execution.

JSON
[ContentInvocationPrefix("counter")]
public sealed class CounterInvocations(ICounterStore counters)
{
    [ContentInvocation("increment")]
    public IHtmlContent Increment()
    {
        var count = counters.Increment();
        return FluentHtml.Div(div => div
            .Text($"Count: {count}"));
    }
}

2. Static Invocations Are Still Valid

Static invocations remain the smallest primitive. They are best for examples, pure rendering helpers, and simple endpoints that do not need constructor state.

Code
[ContentInvocation]
public static IHtmlContent Increment()
{
    return FluentHtml.Div(div => div
        .Text("Updated from the server."));
}

3. Invocation Feature Quick Reference

The newer Heimdall invocation model keeps invocation methods small, but it does honor the ASP.NET Core metadata and binding markers you expect. The attribute for explicitly marking the request payload is [ContentPayload].

Text
Required:
- static method or concrete instance method
- [ContentInvocation] or [ContentInvocation("custom.name")]
- any IHtmlContent implementation, including HtmlString, HtmlContentBuilder, or IHtmlAsyncContent
- Task<THtml> / ValueTask<THtml> when THtml implements IHtmlContent

Organization:
- [ContentInvocationPrefix("orders")] on invocation classes
- constructor DI for instance invocation classes
- globally unique resolved invocation ids

Parameters:
- one payload DTO
- [ContentPayload] to mark the payload explicitly
- [FromServices] to force DI binding
- HttpContext, CancellationToken, ClaimsPrincipal
- malformed JSON returns 400

Metadata:
- [Authorize] / [AllowAnonymous]
- [RequestTimeout] / [DisableRequestTimeout]
- [EnableRateLimiting("policy")] / [DisableRateLimiting]

4. How Invocations Are Triggered

Invocations are triggered from the browser using Heimdall attributes.

HTML
<button
  heimdall-content-click="counter.increment"
  heimdall-content-target="#result"
  heimdall-content-swap="inner">
  Click Me
</button>

5. Naming and Discovery

If no name is provided, Heimdall uses TypeName.MethodName. You can explicitly name an invocation, or put [ContentInvocationPrefix] on the invocation class to namespace every method in that class.

JSON
[ContentInvocationPrefix("orders")]
public sealed class OrderInvocations(IOrderRepository orders)
{
    [ContentInvocation("filter")]
    public IHtmlContent Filter(OrderFilter filter)
    {
        return OrderList.Render(orders.Search(filter));
    }

    [ContentInvocation]
    public IHtmlContent Summary()
    {
        return OrderSummary.Render(orders.GetSummary());
    }
}

// Resolved invocation ids:
// orders.filter
// orders.Summary

6. Uniqueness Is Resolved After Prefixing

Invocation ids are still globally unique. Prefixes reduce repetition, but Heimdall checks the final resolved invocation id and fails startup if two invocations resolve to the same name.

Code
[ContentInvocationPrefix("orders")]
public sealed class OrderInvocations
{
    [ContentInvocation("refresh")]
    public IHtmlContent Refresh() => OrderList.Render();
}

[ContentInvocation("counter.increment")]
public static IHtmlContent Increment()
{
    return FluentHtml.Div(d => d.Text("Count updated."));
}

// Both names are compared globally after resolution.

7. Return Types

Invocations must return HTML. Heimdall accepts any IHtmlContent implementation, including concrete framework types and custom IHtmlAsyncContent implementations. Asynchronous invocations can return Task<THtml> or ValueTask<THtml> when THtml implements IHtmlContent. When the invocation builds its markup with FluentHtml, the builder also has async callback overloads with the same element names.

C#
public HtmlString SyncConcrete()

public IHtmlAsyncContent AsyncContent()

public Task<HtmlString> AsyncConcrete()

public ValueTask<HtmlContentBuilder> ValueTaskConcrete()

public static async ValueTask<IHtmlContent> AsyncMarkup()
{
    return await FluentHtml.Div(async div =>
    {
        div.Text("Loaded asynchronously.");
        await div.Section(async section =>
        {
            section.Text("Nested builder work.");
        });
    });
}

8. Parameter Binding

Heimdall automatically binds parameters from the request. This includes payloads, framework types, and services.

Payload DTO

C#
public class IncrementRequest
{
    public int Count { get; set; }
}

[ContentInvocation]
public IHtmlContent Increment(IncrementRequest req)
{
    return FluentHtml.Div(div => div
        .Text($"Count: {req.Count + 1}"));
}

Supported Parameters

Payload DTO (exactly one)
HttpContext
CancellationToken
ClaimsPrincipal
Services from DI

9. Service Injection

Invocations can receive services directly from dependency injection, but constructor injection is usually cleaner for instance invocation classes. Heimdall activates unregistered invocation classes with ActivatorUtilities, or uses the registered invocation type if you add it to DI yourself.

C#
public sealed class ClockInvocations(ISystemClock clock)
{
    [ContentInvocation("clock.now")]
    public IHtmlContent GetTime()
    {
        return FluentHtml.Div(div => div
            .Text($"Time: {clock.UtcNow}"));
    }
}

10. [ContentPayload] and [FromServices]

When a type could be interpreted more than one way, make the binding explicit. [ContentPayload] marks the request body parameter, and [FromServices] forces a parameter to come from dependency injection.

C#
using Microsoft.AspNetCore.Mvc;

[ContentInvocation("orders.filter")]
public IHtmlContent FilterOrders(
    [ContentPayload] OrderFilter filter,
    [FromServices] IOrderRepository orders)
{
    return OrderList.Render(orders.Search(filter));
}

11. The One Payload Rule

Each invocation can accept exactly one payload DTO. Additional parameters must come from the framework or dependency injection.

Text
// Valid
Increment(MyDto dto, HttpContext ctx)

// Invalid
Increment(MyDto dto1, MyOtherDto dto2)

12. Malformed JSON

If the request body cannot be parsed as JSON for the selected payload parameter, Heimdall returns 400 instead of running the invocation. The response includes a useful binding error so bad client payloads are visible during development and safe in production.

Text
POST /__heimdall/v1/content/invocations
X-Heimdall-Content-Invocation: orders.filter
Content-Type: application/json
RequestVerificationToken: <valid-token>

body: { "status":

-> 400 Bad Request
-> payload binding error
-> invocation is not invoked

13. [Authorize], [AllowAnonymous], and [RequestTimeout]

Content invocations honor normal ASP.NET Core metadata on both methods and invocation classes. You can protect an invocation with [Authorize], opt out with [AllowAnonymous], apply request timeouts, use named timeout policies, or disable a default timeout for a specific invocation. If cookie authentication challenges a protected invocation, the browser runtime navigates to sign-in and rewrites the return URL back to the current page instead of the internal Heimdall invocation endpoint.

C#
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Timeouts;

[Authorize(Roles = "Admin")]
[RequestTimeout(milliseconds: 2000)]
[ContentInvocationPrefix("admin")]
public sealed class AdminInvocations
{
    [ContentInvocation("refresh")]
    public async Task<IHtmlContent> RefreshAdminPanel(
        HttpContext ctx,
        CancellationToken ct)
    {
        var model = await AdminData.LoadAsync(ctx.User, ct);
        return AdminPanel.Render(model);
    }

    [AllowAnonymous]
    [DisableRequestTimeout]
    [ContentInvocation("preview")]
    public IHtmlContent PublicPreview()
    {
        return PreviewPanel.Render();
    }
}

// Also supported:
// [RequestTimeout("SearchTimeout")]

Auth redirect:
/signin?ReturnUrl=/__heimdall/v1/content/invocations
-> /signin?ReturnUrl=/current-page

14. Pages vs Invocations

This distinction is fundamental to how Heimdall works.

Invocation Transport and Compatibility

The canonical transport names match ContentInvocation and Heimdall.invoke. Prefer rendered triggers or the public browser API so antiforgery, headers, cancellation, and response processing remain consistent.

Text
POST /__heimdall/v1/content/invocations
X-Heimdall-Content-Invocation: company.editor.save

Heimdall.invoke("company.editor.save", payload, {
    target: "#editor-status",
    sync: "replace"
});

Request/provider/event contexts use invocationId and the content-invocation request kind. The old /content/actions route and X-Heimdall-Content-Action header remain compatibility aliases; actionId remains a compatibility field where emitted. New code uses the invocation names.