Skip to documentation
Heimdall Docs
Heimdall Docs

Invocation Libraries

Distribute reusable Heimdall content through ordinary .NET libraries, NuGet packages, and Razor Class Libraries.

Register a library's invocation assembly explicitly, keep normal ASP.NET Core dependency injection and options, and let the consuming application own framework setup, page routes, and browser installation.

What a reusable library contributes

The server-side extension point is invocation discovery. A package can also provide services, rendering helpers, options, routes, and static assets through familiar ASP.NET Core conventions.

ConcernLibrary authorConsuming application
Invocation methodsDeclare [ContentInvocation] methods with stable IDs.Include the library's assembly in Heimdall discovery.
Service dependenciesExpose a normal AddCompanyFeature extension.Provide application dependencies and choose configuration/lifetimes.
Rendered HTMLProvide ordinary IHtmlContent render helpers or an appropriate MVC/Blazor integration.Compose the library content inside application pages/layouts.
Page routesOptionally expose an ordinary endpoint-mapping extension.Choose route patterns, host layout, authentication, authorization, and middleware.
Browser assetsShip JS/CSS through normal RCL static web assets when needed.Load those assets and install browser plugins with Heimdall.use.
Framework settingsDescribe the capabilities required by the package.Configure AddHeimdall, UseHeimdall, antiforgery, request headers, and runtime defaults.

No additional server plugin interface, plugin ID, or plugin API version is required to discover attributed methods. Browser plugin API v1 has its own explicit identity and compatibility contract; it is independent of server assembly registration.

The APIs on this page describe the current invocation-library contract. Use a matching Server package/build that exposes AddHeimdallInvocationAssembly. Installing a package whose DLL is referenced does not by itself guarantee that its methods are part of Heimdall's discovery set.

Register a library from the consuming application

The generic overload uses a marker type to identify the assembly to scan. Its name makes the whole-assembly scope explicit.

C#
using Heimdall.Server;

builder.Services.AddHeimdall();
builder.Services.AddHeimdallInvocationAssembly<CompanyEditorMarker>();

// Equivalent when an Assembly value is already available:
builder.Services.AddHeimdallInvocationAssembly(
    typeof(CompanyEditorMarker).Assembly);

The marker must be declared in the assembly that contains the invocation methods. The generic call discovers all eligible attributed methods in that assembly, including unrelated components in the same library. It does not register only the marker, one type, one component, or one invocation.

  • Use a non-static marker type for the generic overload; C# does not allow a static class as a generic type argument.
  • The Assembly overload can use typeof(SomeStaticInvocations).Assembly when a public static type is the convenient assembly reference.
  • A marker in a different assembly will scan that different assembly. Transitive package references are not recursively scanned.
  • Registration is additive and deduplicated by Assembly. Repeating the same marker assembly does not discover it twice.
  • Call before or after AddHeimdall, before building the service provider. Packages should not depend on a particular relative registration order.
  • Register each assembly when the package intentionally splits content across several assemblies.

Let the package author own its Add method

A package can wrap its marker, dependencies, and options in one descriptive extension. The consuming application then calls the package's entry point and configures Heimdall once.

C#
// Company.Editor/EditorRegistration.cs
using Heimdall.Server;
using Microsoft.Extensions.DependencyInjection;

namespace Company.Editor;

public sealed class CompanyEditorMarker;

public sealed class EditorOptions
{
    public string SavedLabel { get; set; } = "Saved";
}

public static class EditorRegistration
{
    public static IServiceCollection AddCompanyEditor(
        this IServiceCollection services,
        Action<EditorOptions>? configure = null)
    {
        services.AddHeimdallInvocationAssembly<CompanyEditorMarker>();
        services.AddScoped<EditorPanel.Invocations>();
        services.AddOptions<EditorOptions>();
        if (configure is not null) services.Configure(configure);
        return services;
    }
}
C#
// Host startup. Either relative order is supported.
builder.Services.AddCompanyEditor(options =>
    options.SavedLabel = "Document saved");
builder.Services.AddHeimdall(options =>
    options.EnableDetailedErrors = builder.Environment.IsDevelopment());

AddCompanyEditor is package-owned application code. It contributes discovery and its own normal services; it does not need to call AddHeimdall again or replace the application's framework-wide settings. A package can keep its marker internal when consumers only need its public registration extension.

Use standard options registration and validation when configuration requires it. Keep package service-registration semantics deliberate: assembly registration is idempotent, while your AddScoped/AddOptions/configure calls retain their ordinary ASP.NET Core behavior.

Mix static methods and instance services

The same discovered assembly can contain static helpers and DI-backed invocation classes. Attribute discovery does not impose an instance-only interface.

C#
using Heimdall.Server;
using Heimdall.Server.Rendering;
using Microsoft.AspNetCore.Html;

public static class EditorStaticInvocations
{
    [ContentInvocation("company.editor.reset")]
    public static IHtmlContent Reset()
        => HeimdallHtml.PluginCommand(
            "company.editor.set-text",
            new { Text = "" }, "#company-editor");
}

public sealed class EditorInvocations(IEditorRepository editors)
{
    [ContentInvocation("company.editor.save")]
    public async Task<IHtmlContent> Save(
        EditorDocument document,
        CancellationToken ct)
    {
        var model = await editors.SaveAsync(document, ct);
        return EditorStatus.Render(model);
    }
}

The repository/model/rendering types in this focused example belong to the application. The frontend plugin guide includes the complete EditorPanel and EditorRegistration source with both static and instance invocations.

Static methods may receive request services as method parameters, with implicit service binding or [FromServices] when you want to make the binding explicit. Instance classes are convenient when multiple methods share constructor dependencies. Both use the normal one-payload rule, framework parameters, authorization metadata, request timeouts, and rate-limiter metadata.

Discovery does not choose every service lifetime

Heimdall stores invocation descriptors in a singleton registry. The descriptor points to a method and activation plan; it is not a singleton instance of every library class.

Invocation targetActivation
Static methodNo target instance is created. Service parameters still resolve from the request service provider.
Declaring type registered scopedResolve that type from the current request scope; it can share scoped dependencies within the request.
Declaring type registered transientResolve a new target according to the container's transient semantics.
Declaring type registered singletonReuse the application's explicitly registered singleton; the application is responsible for its concurrency and dependency choices.
Declaring type unregisteredCreate through ActivatorUtilities using request services. Register explicitly when container-owned disposal or a specific lifetime matters.
C#
builder.Services.AddScoped<EditorRepository>();
builder.Services.AddScoped<EditorPanel.Invocations>();
builder.Services.AddHeimdallInvocationAssembly<CompanyEditorMarker>();

Adding an assembly is discovery, not bulk registration of every class in that assembly. The package still registers its own repositories and collaborators with appropriate lifetimes. Constructor parameters and service parameters use request services; they do not become root-provider singletons because the descriptor registry is a singleton.

Avoid keeping per-request user state in static fields or a shared singleton invocation target. A disposable invocation class created outside the container's registrations should not be assumed to receive container-owned lifetime management.

Discovery sets, invocation IDs, and duplicates

Use additive library registration for reusable content. The existing AddHeimdall assembly overload remains useful when the host intentionally chooses the initial discovery set.

C#
// Explicit initial host discovery set:
builder.Services.AddHeimdall(
    typeof(Program).Assembly,
    typeof(SharedApplicationMarker).Assembly);

// Additional contributions remain included:
builder.Services.AddHeimdallInvocationAssembly<CompanyEditorMarker>();

Passing an explicit non-empty assembly list to AddHeimdall replaces the normal entry/calling-assembly selection for that initial set. AddHeimdallInvocationAssembly contributions are merged with whichever initial set the host chose.

RuleResult
[ContentInvocation("company.editor.save")]An explicit globally resolved invocation ID.
[ContentInvocationPrefix("company.editor")] plus [ContentInvocation("save")]Resolves to company.editor.save.
[ContentInvocation] without an explicit method valueUses the method name; without a prefix it also uses the declaring type name.
Duplicate resolved IDsRegistry construction fails; IDs must be globally unique across discovered assemblies.
Case differencesIDs resolve case-insensitively, including duplicate detection.
Additional methods in the marker assemblyAll eligible attributed methods participate, not just the type chosen as the marker.

Choose package-qualified identifiers from a bounded application-defined set. Renaming a declaring type or method can change an inferred ID, so explicit IDs are preferable for a published library contract. Browser capability names and server invocation IDs can look similar while identifying different things.

Options and page routes stay with their owners

A package can define options for its own server behavior. The host supplies values and decides how that feature fits its application.

C#
builder.Services.AddCompanyEditor(options =>
    options.SavedLabel = "Saved to company documents");

// The host chooses page URL, layout, and endpoint authorization.
app.MapHeimdallPage("/workspace/document-editor", context =>
    MainLayout.Render(EditorPanel.Render(), "Document editor"))
    .RequireAuthorization("CompanyEditors");

MainLayout and CompanyEditors are application-owned examples; configure the policy through normal ASP.NET Core authorization. A package that owns a set of pages can expose its own MapCompanyEditorPages extension with route-prefix/options input. That extension is a normal endpoint mapping API and is not a side effect of invocation assembly scanning.

ConfigurationOwner
EditorOptions / IOptions<EditorOptions>Library's server behavior, registered through its service extension.
HeimdallServiceSettingsHost framework setup, security defaults, detailed errors, and Bifrost configuration.
Heimdall.use(editorPlugin, { commitKey: "Enter" })Browser plugin options, supplied by the host's registration module.
Page route patterns and endpoint policiesThe consuming application or an explicitly called package mapping extension.

Keep those boundaries explicit so a library can be reused in an existing Razor Pages or MVC application as well as a native Heimdall host.

RCL assets and browser installation

An RCL is a natural distribution boundary for rendering helpers, invocation methods, JavaScript, and CSS. Browser loading remains explicit in the consuming application.

Text
// RCL files:
wwwroot/js/editor-plugin.js
wwwroot/css/editor.css

// Public URLs for PackageId Company.Editor:
/_content/Company.Editor/js/editor-plugin.js
/_content/Company.Editor/css/editor.css
JavaScript
// Application: wwwroot/js/install-plugins.js
import editorPlugin from "/_content/Company.Editor/js/editor-plugin.js";
Heimdall.use(editorPlugin, { commitKey: "Enter" });

The host includes CSS, loads the core runtime before the registration module, and uses ordinary MapStaticAssets/UseStaticFiles setup as appropriate to its ASP.NET Core app. A framework package reference is distinct from asset loading and runtime installation.

The library module uses the host runtime rather than shipping another Heimdall copy. Keep application asset URLs aware of the deployment's path base. Static-site output must copy required web-root/RCL assets explicitly when using SSG.

Library project and consuming host

A Razor Class Library can reference Server when it contributes invocations. A rendering-only library can reference Rendering without registering Heimdall's execution pipeline.

XML
<Project Sdk="Microsoft.NET.Sdk.Razor">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <PackageId>Company.Editor</PackageId>
  </PropertyGroup>
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="HeimdallFramework.Server"
                      Version="$(HeimdallFrameworkVersion)" />
  </ItemGroup>
</Project>

Set HeimdallFrameworkVersion through your solution's shared build properties or an explicit build property to a version/build containing this contract. The property is an example of solution-owned version management, not a Heimdall registration option. Server supplies Rendering transitively; the authoring namespace stays Heimdall.Server.Rendering.

The consuming host references the library package or project, calls AddCompanyEditor and AddHeimdall, configures normal middleware/security/static assets, calls UseHeimdall, and maps its pages. The complete host in the frontend plugin guide demonstrates that sequence.

If a library supplies only ordinary render helpers, no invocation scan is needed until it actually contains [ContentInvocation] methods. If it supplies browser behavior, install that browser plugin separately even when there is no server-side invocation assembly.

Compatibility and terminology

Generate new registration code with the invocation names so the server and browser APIs use the same vocabulary.

Current API or protocolCompatibility note
AddHeimdallInvocationAssembly<TMarker>()AddHeimdallAssembly<TMarker>() remains an obsolete alias.
AddHeimdallInvocationAssembly(Assembly)AddHeimdallAssembly(Assembly) remains an obsolete alias.
Heimdall.invoke(invocationId, payload, options)The supported programmatic browser invocation method.
/__heimdall/v1/content/invocationsThe old /__heimdall/v1/content/actions transport remains a compatibility route.
X-Heimdall-Content-InvocationThe server accepts the legacy X-Heimdall-Content-Action header as a fallback.

Heimdall.config.endpoints.contentInvocations is the canonical browser endpoint property. The old contentActions property remains an alias to the same value. New request/provider/event integrations use invocationId and the content-invocation request kind.

An ASP.NET Core MVC controller still has MVC actions and uses [NonAction] for controller-local Heimdall methods. Those native API names stay unchanged. The AI skill identifier heimdall-content-actions also remains stable for installed skill consumers; its guidance uses content-invocation terminology.

Verify the package boundary

Test a library through a separate consuming application rather than relying only on methods compiled into the host assembly.

  • Register the package before and after AddHeimdall and verify the same additive discovery behavior.
  • Discover multiple attributed methods and both static and instance classes from the contributed assembly.
  • Verify scoped collaborators differ across requests and follow the intended DI registration within one request.
  • Supply host options and verify the package uses the configured values.
  • Check duplicate invocation IDs fail visibly rather than silently replacing another library's content.
  • Exercise real HTTP binding, antiforgery, authorization, cancellation, metadata, and returned HTML.
  • Load RCL JavaScript/CSS through public _content URLs and install the plugin through the public browser API when the package includes one.
  • Test the packaged output as part of your own release process, in addition to a project-reference development host.

Heimdall.E2E.TestPlugin is the framework's dedicated standalone RCL fixture. Heimdall.E2E consumes its service extension, options, rendering API, invocations, and assets through the same public boundaries. Its workbench and hosted tests cover real HTTP/SSE behavior with readable and minified runtimes.

Shell
# In a matching framework checkout:
cd Heimdall.E2E
npm run test:plugins

Combine ordinary package design with the focused Heimdall runtime contracts.