Site Admin
PowerPortalsPro ships a Site Admin page that lets a SystemUser-backed admin clear server-side caches without restarting the host. The framework provides the cache-clear API (IPowerPortalsProService.ClearAllCachesAsync / ClearCacheAsync); your portal supplies the page itself, the role gate, and the nav link.
Wiring it up
Three things to wire in: the role-claim transformer, the page itself, and a nav entry. The portal templates (powerportalspro-csharp with the Auto, WebAssembly, or Server interactivity options) ship all three out of the box — the snippets below mirror what the templates emit so you can lift them into a portal that wasn't generated from a template.
1. Configure which Dataverse roles grant admin
SystemAdminClaimsTransformer stamps a synthetic SystemAdmin role claim onto any principal holding either the built-in System Administrator Dataverse role or a custom role configured via PortalIdentityOptions.SystemAdminRoleName. The built-in role is always treated as admin; the configured role is optional and layers on top. UI authorization ([Authorize(Roles = "SystemAdmin")] and <AuthorizeView Roles="SystemAdmin">) checks the synthetic claim, so a custom Dataverse role can be granted admin access without touching razor code.
// appsettings.json — optional. Holders of the built-in "System Administrator"
// Dataverse role are always treated as portal admins; SystemAdminRoleName layers
// an additional Dataverse role on top.
{
"PortalIdentity": {
"SystemAdminRoleName": "Portal Administrator"
}
}
// Program.cs
builder.Services.Configure<PortalIdentityOptions>(
builder.Configuration.GetSection("PortalIdentity"));
builder.Services.AddSingleton<IClaimsTransformation, SystemAdminClaimsTransformer>();
2. Add the Site Admin page
The [Authorize(Roles = "SystemAdmin")] attribute is honored by AuthorizeRouteView; the inner <AuthorizeView Roles="SystemAdmin"> is defense-in-depth in case a host swaps the router. The full page (with the Clear All button, the per-cache list with localized descriptions, and the toast feedback) lives in the templates' SiteAdmin.razor.
@attribute [Route("admin/site-admin")]
@attribute [Authorize(Roles = "SystemAdmin")]
@inject IPowerPortalsProService _powerPortalsProService
@inject IToastService _toastService
<AuthorizeView Roles="SystemAdmin">
<Authorized>
</Authorized>
</AuthorizeView>
3. Surface it in the nav
Wrap the Admin nav group in an <AuthorizeView Roles="SystemAdmin"> so non-admins don't see a link they can't follow. The link target is whichever route the page registers itself at.
<AuthorizeView Roles="SystemAdmin">
<Authorized>
<FluentNavGroup Title="Admin" Icon="@(new Size20.WrenchSettings())">
<FluentNavLink Href="admin/site-admin"
Icon="@(new Size20.LauncherSettings())">
Site Admin
</FluentNavLink>
</FluentNavGroup>
</Authorized>
</AuthorizeView>
What gets cached
The framework registers nine caches as IClearableCache; the Site Admin page renders them sorted alphabetically with their localized descriptions next to each one. The descriptions ship in the framework's ppp-server.defaults.en.json under app.cache-descriptions.
- TableMetadata — Per-table metadata (columns, types, choice options, relationships). Refresh after schema changes.
- ViewMetadata — Cached Dataverse views (FetchXML, columns, sort). Refresh after editing a view in Dataverse.
- EntityMetadata — Raw Dataverse entity metadata used by lower-level framework code. Same source as TableMetadata in a different shape — typically reset together.
- StringLocalizer — All localized strings — Dataverse table/column labels, web resources, and folder JSON files. Refresh after editing any localization source.
- UserPrivileges — Per-user resolved security privileges (which roles grant which permissions). Refresh after changing a user's role assignments.
- TablePermissions — Per-user, per-table CRUD permissions resolved through the table-permission handler chain. Mostly derived from UserPrivileges for SystemUsers.
- PrivilegeMetadata — Org-wide privilege definitions from the Dataverse privilege table. Almost never changes — only Dataverse platform updates introduce new entries.
- EnvironmentFileSettings — Org-level file-upload settings (max size, allowed extensions). Refresh after changing these in Dataverse.
- EmailSender — Resolved sender record for outbound emails (lookup of the configured sender address to its Dataverse entity reference).
StringLocalizer rebuild is the slow one
Clearing the StringLocalizer cache walks every Dataverse table's metadata + every web resource + every folder file and rebuilds the in-memory dictionary. Expect 20–30 seconds against a real environment. The atomic-rebuild guarantees readers continue to see the previous values throughout the rebuild — there's no period where the cache is empty.
Adding your own cache
Implement IClearableCache and register it as both your concrete service AND as IClearableCache. The Site Admin page picks it up automatically and renders it alphabetically with the rest. The Name property is the public identifier — it's what shows on the per-cache button.
public class MyCustomCache : IClearableCache
{
public string Name => "MyCustomCache";
public Task ClearAsync(CancellationToken cancellationToken)
{
// Drop your in-memory state, repopulate, etc.
return Task.CompletedTask;
}
}
// Program.cs
builder.Services.AddSingleton<MyCustomCache>();
builder.Services.AddSingleton<IClearableCache>(sp => sp.GetRequiredService<MyCustomCache>());
Localized description
The Site Admin page looks up app.cache-descriptions.<Name> in the localizer to render the description text next to the button. Add an entry under that namespace in your portal's app.en.json (and any other language files); pages without a registered description gracefully render just the button.
{
"app": {
"cache-descriptions": {
"MyCustomCache": "What this cache holds and when it should be reset."
}
}
}
Clear All vs. per-cache
Clear All runs every registered cache's ClearAsync in parallel and reports per-cache results (succeeded / failed / elapsed ms) in the response. The per-cache buttons fire the same ClearAsync for one cache only — useful when you only need to drop one (for example, after editing a single Dataverse view).
API reference
The cache-clear API lives on IPowerPortalsProService; the underlying orchestration that runs each cache's ClearAsync in parallel lives on ICacheManager. The interfaces are summarized below.
IClearableCache Interface
Properties
Name | Type | Default | Description |
|---|---|---|---|
Name | string | A short, stable name for telemetry / UI feedback (e.g. |
NameMethods
Name | Parameters | Type | Description |
|---|---|---|---|
ClearAsync | CancellationToken cancellationToken | Task | Clears the cache's contents. For caches that maintain pre-loaded data (e.g. |
ClearAsyncICacheManager Interface
Methods
Name | Parameters | Type | Description |
|---|---|---|---|
ClearAllAsync | CancellationToken cancellationToken | Task<IReadOnlyList<CacheClearResult>> | Clears every registered Services.IClearableCache in parallel and returns a per-cache report. A failed cache doesn't block the others — its entry in the report carries the exception message instead of |
ClearAsync | string name CancellationToken cancellationToken | Task<CacheClearResult> | Clears one named cache. Returns null when no registered cache matches name (case-insensitive). Failures are wrapped in the result rather than thrown, matching Threading.CancellationToken). |
GetCacheNames | IReadOnlyList<string> | Returns the names of every registered Services.IClearableCache, suitable for an admin UI that wants to render per-cache clear buttons. |
ClearAllAsyncServices.IClearableCache in parallel and returns a per-cache report. A failed cache doesn't block the others — its entry in the report carries the exception message instead of ClearAsyncCancellationToken cancellationToken
null when no registered cache matches name (case-insensitive). Failures are wrapped in the result rather than thrown, matching Threading.CancellationToken).GetCacheNamesServices.IClearableCache, suitable for an admin UI that wants to render per-cache clear buttons.