AuditBlazor ServerAugust 2026 · 6 min read

Audit Trail Implementation in a Blazor Server Admin Dashboard

TL;DR

An audit trail is only useful when the admin can read it. In Blazor Server, a paged MudBlazor MudTable with server-side filters and a detail dialog turns raw audit rows into a searchable timeline, guarded by an Auditor role.

Audit Trail Implementation is only half the job. Once every change is captured in the database, someone has to actually read it — and in most enterprise apps, that someone is an administrator in a Blazor Server admin dashboard. The final piece of a complete audit trail is the UI that turns raw events into a searchable, filterable timeline of who changed what and when. This article builds that UI the way Indotalent does.

You will see a query service that pages audit events efficiently, a MudBlazor MudTable that renders them with server-side sorting and filtering, a detail dialog that shows the before and after values, and a role check that keeps the trail read-only and protected.

Audit Trail Implementation in Blazor Server: What the Admin Should See

An audit dashboard is a read-only surface. Admins need to scan a timeline, filter by entity or user, and drill into one event to see the exact values that changed. Everything else is noise. Start from these requirements:

  • Timeline list — recent events first, showing action, entity, user, and time
  • Filters — by entity type, action, user, and date range
  • Detail view — before and after values for the changed properties
  • Read-only rules — the page never exposes edit or delete buttons

Blazor Server is the right fit here. The UI holds an open SignalR circuit to the backend, so paging and filtering happen server-side with no REST round trips, and live updates are possible when new audit events arrive.

Audit Trail Implementation: The Query Service Behind the Table

Never load every audit row into the browser. Audit tables grow without bound, so paging, sorting, and filtering must be pushed down to the database. A small query service keeps the razor page clean:

public sealed class AuditQueryService
{
    private readonly AppDbContext _db;

    public AuditQueryService(AppDbContext db) => _db = db;

    public async Task<PagedResult<AuditLogDto>> SearchAsync(
        AuditFilter filter, CancellationToken ct = default)
    {
        var query = _db.AuditLogs.AsNoTracking();

        if (!string.IsNullOrWhiteSpace(filter.EntityName))
            query = query.Where(a => a.EntityName == filter.EntityName);
        if (!string.IsNullOrWhiteSpace(filter.UserId))
            query = query.Where(a => a.UserId == filter.UserId);
        if (filter.From.HasValue)
            query = query.Where(a => a.ChangedAt >= filter.From.Value);
        if (filter.To.HasValue)
            query = query.Where(a => a.ChangedAt <= filter.To.Value);

        var total = await query.CountAsync(ct);

        var items = await query
            .OrderByDescending(a => a.ChangedAt)
            .Skip((filter.Page - 1) * filter.PageSize)
            .Take(filter.PageSize)
            .Select(a => new AuditLogDto(
                a.Id, a.EntityName, a.EntityId, a.Action,
                a.UserName, a.ChangedAt, a.Reason,
                a.OldValuesJson, a.NewValuesJson))
            .ToListAsync(ct);

        return new PagedResult<AuditLogDto>(items, total);
    }
}

The filter object carries the user's selections, the page state carries size and index, and the result returns one page plus the total count for the pager. EF Core translates the whole thing into a single parameterized SQL query.

Render the Trail with a MudBlazor MudTable

MudTable is the workhorse for this page. Its ServerData delegate is called whenever the admin pages, sorts, or changes the page size, which maps directly to the query service:

<MudTable T="AuditLogDto" ServerData="LoadData"
          Striped="true" Hover="true" Dense="true">
    <HeaderContent>
        <MudTh>User</MudTh>
        <MudTh>Entity</MudTh>
        <MudTh>Action</MudTh>
        <MudTh>Changed At</MudTh>
        <MudTh></MudTh>
    </HeaderContent>
    <RowTemplate>
        <MudTd>@context.UserName</MudTd>
        <MudTd>@context.EntityName</MudTd>
        <MudTd>@context.Action</MudTd>
        <MudTd>@context.ChangedAt.ToLocalTime()</MudTd>
        <MudTd><MudButton Variant="Variant.Text"
                     OnClick="() => OpenDetails(context)">
            View</MudButton></MudTd>
    </RowTemplate>
    <PagerContent>
        <MudTablePager PageSizeOptions="[10, 25, 50]" />
    </PagerContent>
</MudTable>

The matching handler takes the MudBlazor table state and translates it into the filter:

private async Task<TableData<AuditLogDto>> LoadData(TableState state)
{
    var filter = new AuditFilter
    {
        Page = state.Page + 1,
        PageSize = state.PageSize,
        EntityName = _selectedEntity,
        UserId = _selectedUser,
        From = _fromDate,
        To = _toDate
    };

    var result = await _service.SearchAsync(filter);
    return new TableData<AuditLogDto>
    {
        Items = result.Items,
        TotalItems = result.Total
    };
}

Because the query runs in EF Core, the table stays fast no matter how many rows accumulate. The pager reports honest totals, and admins never wait on an unbounded query.

Filters, Detail View, and Permissions

Add filter controls above the table — MudSelect for entity and user, MudDatePicker for the date range — and re-trigger LoadData when they change. For the detail view, open a MudDialog that deserializes the before and after JSON and renders a small diff: property name, old value, and new value, one row per changed property. A color-coded chip for the action (Create, Update, Delete) makes the timeline scannable at a glance.

Finally, guard the page. Wrap it with an authorization check that requires an Auditor or Admin role, keep it read-only by construction — no edit or delete buttons anywhere — and project audit data through the service so no tenant or user can reach another's raw rows. In a Blazor Server app the security boundary is the circuit, so enforcing the role at the component and the service level closes both entry points.

FAQ

Is Blazor Server the right choice for an audit dashboard?

Yes. Audit pages benefit from server-side paging, sorting, and filtering, and Blazor Server's SignalR circuit avoids the REST round trips of a classic SPA. Only a small page loads into the browser.

Should I load all audit rows into the browser?

No. Load one page at a time through ServerData. Audit tables grow without bound, and a full load freezes both the circuit and the browser.

Can I export the audit trail to CSV or PDF?

Yes. Run the same filter through an export endpoint that streams rows to CSV or PDF, respecting the same Auditor role and the same tenant scoping rules as the table.

What about live updates when new audit events arrive?

Blazor Server has SignalR built in. Push a message to connected circuits when the audit store appends an event, and refresh the current page so the timeline stays current.

Key Takeaways

  • An audit trail is only useful when the admin UI makes it searchable
  • Page, sort, and filter audit queries server-side in the database
  • Use MudTable with ServerData and a PagedResult query service
  • Guard the audit page with an Auditor or Admin role and keep it read-only
  • Indotalent Blazor Server products include audit dashboards — $21 each

Ready to see audit history in a real Blazor admin dashboard?

Every Indotalent product ships a Blazor Server admin dashboard with audit history in its complete .NET 10 source code. Complete source code — $21 each.

Explore Products