Table of Contents

Plugins

A CodeLogic plugin that also implements IWebContributor contributes web features as soon as it loads, and they disappear when it unloads. The host needs no code for it.

A web plugin

public sealed class AdminPlugin : IPlugin, IWebContributor
{
    public PluginManifest Manifest { get; } = new() { Id = "shop.admin", Name = "Shop admin", Version = "1.0.0" };
    // ... the usual IPlugin lifecycle members ...

    public Task ContributeAsync(WebContributionBuilder b, CancellationToken ct)
    {
        var orders = b.Services.GetRequiredService<OrderService>();   // host services

        b.MapTemplates(typeof(AdminPlugin).Assembly);                 // embedded theme/*.html
        b.MapForm<OrderEditForm>();

        var admin = b.MapGroup("/admin").RequireGroups("admin");
        admin.MapPage("/orders", (r, ct) => ListAsync(orders, r, ct));

        b.MapWidget("open-orders", OpenOrdersAsync, o => o.Access = new WebAccessRule { Groups = ["admin"] });
        b.PlaceWidget("sidebar", "open-orders");
        b.BridgeEvent<OrderPlacedEvent>("shop.order-placed", _ => WebRealtimeAudience.Group("admin"), e => new { e.OrderId });
        return Task.CompletedTask;
    }
}

What a plugin can contribute: routes and route groups, forms and options providers, form validators, widgets and placements, templates, page scripts and realtime bridges. Everything is tracked by a contribution handle and removed on unload; routes stop answering at once (the endpoint data source changes), and the plugin's load context can be collected.

What a plugin cannot do: add request-level security stages, replace a form or template the application registered, or turn off security for routes it does not own. Plugin routes get the same defaults as app routes (signed-in users only, CSRF, rate limits).

Templates in the plugin

<ItemGroup>
  <EmbeddedResource Include="theme\**\*.html" LogicalName="theme/%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

b.MapTemplates(assembly) registers every theme/...html resource under its path (theme/admin/index.html becomes admin/index). The site theme can override any of them by shipping a file with the same path. b.MapTemplate(path, source) adds one from a string.

Static files in the plugin

<ItemGroup>
  <EmbeddedResource Include="assets\**\*" LogicalName="assets/%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

b.MapAssets(assembly, "assets/", "admin") serves them from memory at /_weblogic/plugins/admin/... with ETags, theme.allowedAssetExtensions and the same path checks as theme assets (the name defaults to the contributor id). In templates {asset:admin:css/admin.css} writes a fingerprinted URL (cached as immutable). A theme file at assets/plugins/admin/css/admin.css overrides the plugin's file. The files go away with the plugin.

Give the plugin's pages their own look with a group layout: b.MapGroup("/admin").WithLayout("admin/layout") applies to page templates that declare no {layout:...}.

Packaging

Compile against WebLogic but use the host's copy at runtime, so the types match:

<PackageReference Include="CodeLogic" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="CodeLogic.WebLogic" ExcludeAssets="runtime" PrivateAssets="all" />
<PropertyGroup>
  <CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
</PropertyGroup>

Deploy the plugin's own assembly (and only its private dependencies) to the CodeLogic plugins folder. samples/Plugins/MiniBlog.Admin.Plugin is the complete example: admin CRUD on forms, a dashboard widget, templates shipped inside the plugin.

Application contributors

Contributors registered with web.AddContributor<T>() use the same builder; they simply live as long as the app. Give them a WebContributorDescriptor (id, name, kind) so the explorer can show who registered what.