Skip to content

Core Concepts ​

Calling AddSmartAdmin and MapSmartAdmin in Program.cs buys a complete enterprise back office: auth, RBAC, multi-org data permissions, dict and config, logging, uploads, notices and announcements, a read-only demo mode, a password-expiry policy. The backend arrives as NuGet packages and the frontend as the npm package smart-admin-web — a kernel, not an application, with your business code staying in your own repository. That shape forces one constraint on everything inside it: every piece has to be replaceable.

Why not just another admin template ​

Copying an admin template gets you started fast, but as business code grows the project ends up deeply coupled to the template — and after that, upgrading base capabilities, pulling in upstream changes, or swapping out just one piece all become painful.

SmartAdmin factors these common capabilities out of business code: you can use the default implementations as-is, integrate it fairly naturally into an existing project, or replace any single piece without forking.

The replaceability model ​

It comes down to three constraints, locked in by the replaceability contract (ReplaceabilityTests):

  1. Interface registration + TryAdd — built-in services are all registered with TryAdd*, so a consumer registering the same interface before AddSmartAdmin() wins and overrides the default implementation.
  2. Template-method decomposition — long service methods are split into small virtual steps, so a consumer overrides one step via subclassing instead of copying the whole method.
  3. Business assembly mounting — a consumer's entities join CodeFirst table creation via options.ApplicationAssemblies, and their controllers get AddApplicationPart-ed automatically, extending the system without touching the kernel.

What these three look like in actual code is in Replace Built-in Services.

The frontend reaches the same outcome by the opposite mechanism: createSmartAdmin registers pages and text in the order kernel, plugins, app, and on the same key the later registration wins, so an app page with the same key as a built-in page simply takes its place. How to plug in is covered in The Frontend Template.

Package layering ​

Dependencies point downward only — this ordering is itself a load-bearing constraint:

text
SmartAdmin.Core        Pure contracts: interfaces, Options, Result<T>, ErrorCode. No SqlSugar, no ASP.NET.
   ↑
SmartAdmin.SqlSugar    Data layer: ISqlSugarClient singleton, IRepository<>, entity base classes, CodeFirst, seeding.
   ↑
SmartAdmin.Services    Domain layer: entities (Sys*), service implementations, RBAC / data scope.
   ↑
SmartAdmin.AspNetCore  Host integration: AddSmartAdmin / MapSmartAdmin, JWT, permission/session filters, built-in controllers.

SmartAdmin             Meta-package: references AspNetCore only; a consumer installs this one to pull in the whole stack.

Request pipeline ​

An authenticated request flows through, in order:

  1. Authentication — Microsoft JWT Bearer; the framework's 401 is reshaped into the standard envelope (code 40006).
  2. [RolePermission] — the permission code IS the normalized route ({METHOD}:/{route} — the GET:/api/v1/ping call from the previous page is one); there are no permission strings in code — authorization is granted by checking routes in the role-menu UI. Super admin (sadm) bypasses directly, while session validity is also checked (so a forced logout takes effect immediately).
  3. Data scope — during authorization, the current user's effective org data scope is resolved and injected into IDataScopeContext.
  4. Result envelope — controllers can return dto directly, and a filter wraps it into Result<T>; business errors are thrown as AdminException / returned as ErrorCode and turned into an envelope. Errors are numeric ErrorCodes, never localized text — i18n is handled on the frontend by translating the code.

Data layer conventions ​

  • A single SqlSugarScope singleton; global query filters automatically apply soft delete (ISoftDelete) and data scope (IOrgScoped / DataEntity filtered by the org set resolved for the current request).
  • AOP auto-fills audit fields on insert/update: snowflake Id, CreateTime, CreateUserId, CreateOrgId (the data-scope anchor), UpdateTime, UpdateUserId. Business code only needs to set business fields.
  • The snowflake WorkerId, when unset, is claimed on this machine with a file lock, so processes on one box never share a number. When scaling across machines or containers it must be set explicitly and differ per instance, or IDs generated in the same millisecond will collide.

For a more complete picture of the architecture and design rationale, see the repo's Architecture & Design Document.

Released under the Apache License 2.0