MediatRBlazor ServerAugust 2026 ยท 7 min read

CQRS with MediatR in a Blazor Server App: End-to-End Example

TL;DR

You can call commands and queries directly from Blazor Server components by injecting IMediator. This end-to-end .NET 10 example registers MediatR, loads data into a MudTable with a query handler, dispatches a create command from a dialog, and refreshes the UI through a MediatR notification.

CQRS with MediatR in a Blazor Server app gives you the same clean handler structure you would use in a Web API, but wired directly into the interactive UI. Blazor Server runs over SignalR, so each user session is a live circuit on the server. When a component injects IMediator and sends a query or command, the full pipeline โ€” validation, logging, transactions โ€” executes on the server before the rendered markup is pushed to the browser. This end-to-end example shows the entire flow in .NET 10.

The example builds a small product catalog screen: a MudTable lists products loaded through a query handler, a dialog dispatches a create command, and a MediatR notification tells the table to refresh. Every piece uses the same packages you will find in a production Indotalent application.

Why CQRS with MediatR Fits Blazor Server

Blazor Server components are stateful objects living in a server-side circuit, so business logic has to run on the server anyway. There is no client API layer to build and no HttpClient wiring to maintain. Sending a command from a component is as direct as calling a service, but you keep all the benefits of a mediator pipeline: validation before the write, a transaction around the save, and a log line for every operation.

The circuit model also means queries should be re-executed when data changes rather than cached in component state. MediatR notifications are the natural mechanism for that, because a notification handler can signal any component that cares without coupling the components together.

Registering MediatR in the Blazor Server App

Start with the package and the standard registration in Program.cs. Blazor Server apps are configured exactly like any other .NET 10 host, so the MediatR wiring is identical to an API project. FluentValidation is added the same way, so the validation behavior resolves validators from the same assembly scan.

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});

builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

With the assembly scan in place, every handler and validator in the feature slices is registered. Components never need to know which class implements an operation; they just send a request and let the pipeline do the rest.

Loading Data with a Query Handler

The catalog page starts with a query. The query record carries the search term and paging options, and the handler returns a DTO page. The page injects IMediator and calls Send from the component lifecycle.

public record SearchProductsQuery(string Term, int Page, int PageSize)
    : IRequest<PagedResult<ProductDto>>;

public class SearchProductsHandler
    : IRequestHandler<SearchProductsQuery, PagedResult<ProductDto>>
{
    private readonly AppDbContext _db;

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

    public async Task<PagedResult<ProductDto>> Handle(
        SearchProductsQuery query, CancellationToken ct)
    {
        var source = _db.Products.AsNoTracking();

        if (!string.IsNullOrWhiteSpace(query.Term))
            source = source.Where(p =>
                p.Name.Contains(query.Term));

        var total = await source.CountAsync(ct);
        var items = await source
            .OrderBy(p => p.Name)
            .Skip((query.Page - 1) * query.PageSize)
            .Take(query.PageSize)
            .Select(p => ProductDto.FromEntity(p))
            .ToListAsync(ct);

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

Note that the handler returns DTOs, not entities. Blazor Server components render whatever the query returns, so keeping entities out of the UI is a good habit that prevents accidental lazy-loading calls inside the circuit and keeps serialization predictable.

Dispatching CQRS with MediatR Commands from a Razor Component

When the user clicks Create in the dialog, the component builds a command and sends it. The Razor code below shows the essential flow: an injected mediator, a form dialog, and a send call that returns the new product id.

@inject IMediator Mediator
@inject ISnackbar Snackbar

<MudButton OnClick="OpenDialog">Create Product</MudButton>

<MudDialog @bind-Visible="_dialogOpen">
    <MudTextField @bind-Value="_name" Label="Name" />
    <MudNumericField @bind-Value="_price" Label="Price" />
    <MudButton Variant="Variant.Filled" Color="Color.Primary"
              OnClick="CreateProduct">Save</MudButton>
</MudDialog>

@code {
    private bool _dialogOpen;
    private string _name = "";
    private decimal _price;

    private async Task CreateProduct()
    {
        var command = new CreateProductCommand(_name, _price);
        var id = await Mediator.Send(command);
        Snackbar.Add($"Product {id} created", Severity.Success);
        _dialogOpen = false;
    }
}

Validation still runs, because the pipeline behavior throws ValidationException before the handler executes. In a production component you would catch that exception in the submit handler and render the messages next to the fields. The command handler itself remains identical to the one used by a REST API endpoint, so UI and API share the same business rules.

Refreshing the UI with a Notification

After a successful create, the table on the page is stale. A MediatR notification is the cleanest way to refresh it. The component publishes ProductCreatedNotification, a handler in the UI layer reloads the query, and the renderer pushes the fresh markup over SignalR.

public record ProductCreatedNotification(int ProductId)
    : INotification;

public class ProductCreatedHandler
    : INotificationHandler<ProductCreatedNotification>
{
    private readonly ProductListPage _page;

    public ProductCreatedHandler(ProductListPage page) => _page = page;

    public async Task Handle(
        ProductCreatedNotification notification, CancellationToken ct)
        => await _page.ReloadAsync();
}

This is the notify-and-refresh pattern that keeps Blazor Server UI consistent with the database. The notification handler can reload data, or it can update a scoped state object that multiple components observe. Either way, the components never talk to each other directly, and the data flow stays one-way.

Key Takeaways

  • Inject IMediator into Blazor Server components and send requests directly
  • Query handlers load data for tables and lists without exposing the database context to the UI
  • Command handlers run the full validation and transaction pipeline before the UI updates
  • MediatR notifications keep the circuit fresh after writes

FAQ

Can Blazor WebAssembly use MediatR the same way? Yes for in-process requests, but the pipeline would run on the client. Indotalent uses Blazor Server, so all logic, validation, and data access stay on the server.

Do I need a separate API if the Blazor Server app already uses MediatR? No. The command and query handlers are shared by both the Razor components and any REST API controllers, so one set of handlers serves both entry points.

Should components hold database contexts? No. Inject IMediator instead and let query and command handlers own all data access.

How do errors surface in the UI? Validation errors throw ValidationException, which the component catches and renders next to fields; infrastructure errors bubble to the circuit error handler.

Ready to study MediatR in a full Blazor Server product?

Every Indotalent product is a complete .NET 10 Blazor Server application built with CQRS and MediatR inside Vertical Slice slices. Complete source code โ€” $21 each.

Explore Products