Actions
Actions are server-side methods that return HTML. They are triggered by Heimdall attributes and are used to update parts of the UI without reloading the page.
If pages handle routes, actions handle interactions.
1. What an Action Is
An action is a method marked with [ContentInvocation]. It can be a static method for tiny helpers, or an instance method on an action class when you want constructor injection and MVC-style organization.
[ContentInvocationPrefix("counter")]
public sealed class CounterActions(ICounterStore counters)
{
[ContentInvocation("increment")]
public IHtmlContent Increment()
{
var count = counters.Increment();
return FluentHtml.Div(d =>
{
d.Text($"Count: {count}");
});
}
}2. Static Actions Are Still Valid
Static actions remain the smallest primitive. They are best for examples, pure rendering helpers, and simple endpoints that do not need constructor state.
[ContentInvocation]
public static IHtmlContent Increment()
{
return FluentHtml.Div(d =>
{
d.Text("Updated from the server.");
});
}Action Feature Quick Reference
The newer Heimdall action model keeps action 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].
Required:
- static method or concrete instance method
- [ContentInvocation] or [ContentInvocation("custom.name")]
- IHtmlContent / Task<IHtmlContent> / ValueTask<IHtmlContent>
Organization:
- [ContentInvocationPrefix("orders")] on action classes
- constructor DI for instance action 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]3. How Actions Are Triggered
Actions are triggered from the browser using Heimdall attributes.
<button
heimdall-content-click="counter.increment"
heimdall-content-target="#result"
heimdall-content-swap="inner">
Click Me
</button>4. Naming and Discovery
If no name is provided, Heimdall uses TypeName.MethodName. You can explicitly name an action, or put [ContentInvocationPrefix] on the action class to namespace every method in that class.
[ContentInvocationPrefix("orders")]
public sealed class OrderActions(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.Summary5. Uniqueness Is Resolved After Prefixing
Invocation ids are still globally unique. Prefixes reduce repetition, but Heimdall checks the final resolved action id and fails startup if two actions resolve to the same name.
[ContentInvocationPrefix("orders")]
public sealed class OrderActions
{
[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.6. Return Types
Actions must return HTML. Heimdall supports synchronous and asynchronous return types.
public IHtmlContent Sync()
public Task<IHtmlContent> Async()
public ValueTask<IHtmlContent> ValueTask()7. Parameter Binding
Heimdall automatically binds parameters from the request. This includes payloads, framework types, and services.
Payload DTO
public class IncrementRequest
{
public int Count { get; set; }
}
[ContentInvocation]
public IHtmlContent Increment(IncrementRequest req)
{
return FluentHtml.Div(d =>
{
d.Text($"Count: {req.Count + 1}");
});
}Supported Parameters
8. Service Injection
Actions can receive services directly from dependency injection, but constructor injection is usually cleaner for instance action classes. Heimdall activates unregistered action classes with ActivatorUtilities, or uses the registered action type if you add it to DI yourself.
public sealed class ClockActions(ISystemClock clock)
{
[ContentInvocation("clock.now")]
public IHtmlContent GetTime()
{
return FluentHtml.Div(d =>
{
d.Text($"Time: {clock.UtcNow}");
});
}
}9. [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.
using Microsoft.AspNetCore.Mvc;
[ContentInvocation("orders.filter")]
public IHtmlContent FilterOrders(
[ContentPayload] OrderFilter filter,
[FromServices] IOrderRepository orders)
{
return OrderList.Render(orders.Search(filter));
}10. The One Payload Rule
Each action can accept exactly one payload DTO. Additional parameters must come from the framework or dependency injection.
// Valid
Increment(MyDto dto, HttpContext ctx)
// Invalid
Increment(MyDto dto1, MyOtherDto dto2)11. Malformed JSON
If the request body cannot be parsed as JSON for the selected payload parameter, Heimdall returns 400 instead of running the action. The response includes a useful binding error so bad client payloads are visible during development and safe in production.
POST /__heimdall/v1/content/actions?action=orders.filter
body: { "status": true, }
-> 400 Bad Request
-> payload binding error
-> action is not invoked12. [Authorize], [AllowAnonymous], and [RequestTimeout]
Content actions honor normal ASP.NET Core metadata on both methods and action classes. You can protect an action with [Authorize], opt out with [AllowAnonymous], apply request timeouts, use named timeout policies, or disable a default timeout for a specific action.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Timeouts;
[Authorize(Roles = "Admin")]
[RequestTimeout(milliseconds: 2000)]
[ContentInvocationPrefix("admin")]
public sealed class AdminActions
{
[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")]13. Pages vs Actions
This distinction is fundamental to how Heimdall works.
Next Steps
Once you understand actions, the next step is understanding how their responses are applied to the DOM.