AdminForge 0.6.0
dotnet add package AdminForge --version 0.6.0
NuGet\Install-Package AdminForge -Version 0.6.0
<PackageReference Include="AdminForge" Version="0.6.0" />
<PackageVersion Include="AdminForge" Version="0.6.0" />
<PackageReference Include="AdminForge" />
paket add AdminForge --version 0.6.0
#r "nuget: AdminForge, 0.6.0"
#:package AdminForge@0.6.0
#addin nuget:?package=AdminForge&version=0.6.0
#tool nuget:?package=AdminForge&version=0.6.0
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 theIAdminDataProvider<T>you register;ReadOnly()drops the create and edit surface, and a column offers a sort or filter control only onceSortable()/Filterable()says the provider honours it, as the table offers a search box only onceSearchable()does. A host with no DbContext at all callsAddAdminForge(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
DbContextor service lives for one call, not for the hours a Blazor circuit stays open. The scope'sIUserAccessornames 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'sctx.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
AddFieldon the same builder as a form, opens them as a dialog, and its handler receives aFormSubmission; 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 — andColumns(...)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. AResolved computed column that returns a list renders the same way. - Content columns —
Content(Content.Markdown)on a string column, orText,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 untilShownInList()says one call per row is acceptable. Both render on the list and the detail page. - Per-surface visibility —
HiddenInList(),HiddenInView()andHiddenInEdit()each hide a column from one surface;HideColumn(...)hides it from all three. - Columns render in
Columnorder, 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.IAdminAuthorizationPolicyis 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 ownIAdminAuthorizationPolicy. 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
IAsyncEnumerablestreaming) — 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@pageroutes are compile-time. A runtime route-rewriter is on the roadmap. - File uploads are in-memory in this release — a streaming
IFileStorageHandleris 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 | Versions 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. |
-
net10.0
- Microsoft.AspNetCore.App.Internal.Assets (>= 10.0.0)
- Microsoft.EntityFrameworkCore (>= 10.0.8)
- MudBlazor (>= 9.5.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.