AdminForge 0.6.0

dotnet add package AdminForge --version 0.6.0
                    
NuGet\Install-Package AdminForge -Version 0.6.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AdminForge" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AdminForge" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="AdminForge" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AdminForge --version 0.6.0
                    
#r "nuget: AdminForge, 0.6.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AdminForge@0.6.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AdminForge&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=AdminForge&version=0.6.0
                    
Install as a Cake Tool

AdminForge

AdminForge auto-generates an admin panel for ASP.NET Core apps. Point it at your DbContext, register a few dashboards or forms if you want them, mount it — done.

Quick start

builder.Services.AddAdminForge<AppDbContext>(forge => forge
    .RequireAuthorizationPolicy("Admins") // or .AllowAnonymousAccess() for an open panel
    /* ... */);
app.MapAdminForge(); // mounts at /admin

Install:

dotnet add package AdminForge

That's it. No JS toolchain, no separate admin host — Blazor Server components shipped inside the package render against MudBlazor.

What you get

  • Auto-generated CRUD pages for every EF Core entity (list, view, create, edit, delete) — with filter, sort, pagination, and validation. Text filters match substrings, case-insensitively.
  • Provider-backed tables — AddTable<T> on any keyed class describes it from its properties and serves it through the IAdminDataProvider<T> you register; ReadOnly() drops the create and edit surface, and a column offers a sort or filter control only once Sortable() / Filterable() says the provider honours it, as the table offers a search box only once Searchable() does. A host with no DbContext at all calls AddAdminForge(forge => ...) and registers a provider per table.
  • Every table has a provider at boot — MapAdminForge() resolves one per registered table and names the ones it cannot serve, rather than failing on the first page load.
  • One DI scope per operation — every list, find, save, action and widget resolves its provider and handler in a fresh scope, so a scoped DbContext or service lives for one call, not for the hours a Blazor circuit stays open. The scope's IUserAccessor names the user the circuit was opened for.
  • Dashboards composed in C# from stat cards, line charts, and table widgets, arranged in a row-based grid layout. UseHomeDashboard("ops") renders one on the home page and drops its sidebar entry.
  • Host pages — AddPage<StatusPage>(n => n.Group("System")) lists a Razor component of your own in the sidebar; it declares its route under /admin/ and renders inside the shell with the theme and the circuit's user.
  • Generic forms with 9 field types (text, number, float, bool, date, datetime, markdown, file upload, select) and a typed submit handler. A select's choices are a fixed list or a delegate over the host's services, resolved when the form opens and checked again on submit; Multiple() hands the handler a list. The handler's ctx.ShowResult(markdown) renders a Markdown document below the form — tables, lists, code blocks; from an entity action it opens in a dialog.
  • Per-entity custom actions surfaced as buttons on the entity view, with an optional confirmation. An action that needs input declares fields with AddField on the same builder as a form, opens them as a dialog, and its handler receives a FormSubmission; the input is validated like a form and lands in the audit event.
  • Related tables auto-generated from collection navigations; cross-entity links are configurable. Inline() renders the related table on the detail page — its own sort, paging and filters, pinned to the parent row, with its filter bar tucked behind one button — and Columns(...) picks which columns it shows.
  • Collection columns — a list on a provider-backed row (IReadOnlyList<string>, IReadOnlyList<SomeRecord>) renders on the detail page without a table, a provider or a key of its own: chips for scalars, a small table for records. Never listed, never edited. A Resolved computed column that returns a list renders the same way.
  • Content columns — Content(Content.Markdown) on a string column, or Text, Json, Html. The cell shows the first line and an open button; the dialog renders the whole value by kind: Markdown through the renderer, JSON re-indented, HTML in a sandboxed frame that runs no scripts and cannot reach the panel.
  • Cell links — LinksTo<TTarget>() turns a column carrying another table's id into a link to that row, for read models that have no navigation to follow.
  • Computed columns, in two kinds. From(expr) projects server-side, so it sorts, filters and pages with everything else; Resolve((sp, row, ct) => …) computes in-process — another service, another database, an expensive call — and is therefore detail-only until ShownInList() says one call per row is acceptable. Both render on the list and the detail page.
  • Per-surface visibility — HiddenInList(), HiddenInView() and HiddenInEdit() each hide a column from one surface; HideColumn(...) hides it from all three.
  • Columns render in Column order, and one value formatter serves the list and the detail page; Format("yyyy-MM-dd") overrides the pattern per column.
  • Audit log hook — a single delegate receives every create/update/delete/custom-action event.
  • Per-action authorization policies — AdminForge:{Entity}:{Action} policies are materialised on demand by a provider that wraps the host's own, so the host's policies keep resolving. IAdminAuthorizationPolicy is asked before every read and write the bridge performs, with the row when there is one, and the panel asks it again when rendering so a refused create, edit, delete or action button is not shown.
  • Authorization required at mount — MapAdminForge() throws at startup unless the host set an umbrella policy or registered its own IAdminAuthorizationPolicy. An open panel has to say so: AllowAnonymousAccess(). The umbrella policy goes on the panel's endpoints, so the host's authentication scheme handles a rejected request — a cookie scheme redirects to its login page. The panel's scripts and styles are served anonymously.
  • Sign-out button — WithSignOut("/admin/logout") puts a button in the app bar that posts to a host-owned endpoint; the signed-in user's name shows beside it.
  • Live updates for single-entity views (polling) and dashboard line charts (polling or IAsyncEnumerable streaming) — multiple browser tabs share one upstream stream.
  • Global search — one box in the app bar searches every searchable table at once and opens the row picked.
  • Environment badge — WithEnvironment("staging", "#ef6c00") colours the app bar and labels it, so nobody edits production thinking it is staging.
  • Theming hook — set a logo and primary / secondary palette colour via WithTheme(...); defaults render MudBlazor's stock palette.

Configuration sketch

builder.Services.AddAdminForge<AppDbContext>(forge => forge
    .WithTitle("My App Admin")
    .WithWelcomeMessage("Pick a table from the sidebar.")
    .RequireAuthorizationPolicy("Admins")
    .WithSignOut("/admin/logout")
    .WithAuditLog((evt, ct) => audit.RecordAsync(evt, ct))
    .WithTheme(t => { t.PrimaryColor = "#00897b"; t.LogoUrl = "/logo.svg"; })

    .AddTable<User>(e => e
        .Nav(n => n.Group("People"))
        .DisplayMember(u => u.DisplayName)
        .AddAction("Reset password", async (sp, user, ctx) =>
        {
            if (!await ctx.ConfirmAsync($"Reset {user.Email}?")) return;
            await sp.GetRequiredService<IUserService>().ResetAsync(user.Id);
            ctx.ShowSuccess("Password reset email sent.");
        }))

    .AddTable<Order>()

    // Not on the DbContext: served by services.AddAdminForgeDataProvider<AuditEntry, AuditProvider>()
    .AddTable<AuditEntry>(e => e
        .ReadOnly()
        .Column(a => a.At, c => c.Sortable())
        .Column(a => a.Action)
        .Column<int>("Retries", c => c
            .Resolve((sp, entry, ct) => sp.GetRequiredService<IRetryLog>().CountAsync(entry.Id, ct))))

    .AddDashboard("ops", d => d
        .WithTitle("Operations")
        .AddStatCard("Open orders", async (sp, ct) =>
            await sp.GetRequiredService<AppDbContext>().Orders.CountAsync(ct))
        .AddLineChart<Snapshot>("Throughput",
            xAxis: p => p.At, yAxis: p => p.Count,
            configure: c => c.WithStreaming(metricsStream)))

    .AddForm("notify", form => form
        .WithTitle("Send Notification")
        .AddField(f => f.Text("Title").Required())
        .AddField(f => f.Markdown("Body"))
        .OnSubmit((sp, values, ctx) => SendAsync(values))));

Examples

examples/TodoApp — EF Core + SQLite, the DbContext path:

task example:todo:seed   # one-shot DB seed
task example:todo        # run the host on http://localhost:5xxx/admin

examples/CrmApp — no DbContext at all: four flat read models, one provider each, with the nested tables, cell links and per-table search that path needs:

task example:crm

Status

Preview. APIs may shift between minor versions. The shape is settled, but expect renames and additions as the library hardens.

Releasing

Push a v* tag; CI packs and publishes it to nuget.org. Steps in docs/releasing.md.

Limitations / non-goals

  • Route prefix is locked to /admin — the Blazor @page routes are compile-time. A runtime route-rewriter is on the roadmap.
  • File uploads are in-memory in this release — a streaming IFileStorageHandler is planned.
  • No multi-tenancy, no custom page builder, no multi-step forms, no i18n in v1.
  • Blazor Server only for now — the architecture is renderer-agnostic (Core produces view models + IAdminUIBridge), but only the Blazor UI is shipped today.

License

MIT.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.6.0 47 10/1/2026
0.5.0 87 9/15/2026
0.4.0 102 9/8/2026
0.3.0 109 9/6/2026
0.2.0 110 9/5/2026
0.1.0 112 8/26/2026