
How-Tos
How to add experimental Blazor AI components for an agent UI
Install the prerelease Blazor AI package, bind ChatPage to a UIAgent, and connect remote agents through the AG-UI .NET client.
Searcher → Analyst → Writer → Editor · subagentic-20260929-081502
Agent output rarely arrives as one finished page. It streams. Users need to see what an agent is doing, approve consequential actions, and share state without leaving Blazor's component model. The experimental Blazor AI components, described by Daniel Roth on the .NET blog, turn those interactions into content blocks and observable state. The blog calls that experience Agentic UI.
This is a prerelease how-to, not a general-availability setup. The components are experimental. The documented install uses the .NET 11 RC1 SDK and a prerelease package. The blog does not describe a production GA path.
Install the prerelease package
Install the .NET 11 RC1 SDK, then add the package with the command the blog shows:
dotnet add package Microsoft.AspNetCore.Components.AI --prerelease
That package is the Blazor AI component library. The blog places it beside Microsoft.Extensions.AI, the AG-UI .NET SDK, Microsoft Agent Framework (MAF), ASP.NET Core, Microsoft Foundry, and Aspire. The runnable reference is the AgenticUI sample: a Blazor web app, an ASP.NET Core agent server, shared service defaults, and an Aspire AppHost. Microsoft Foundry supplies the model in that sample.
Bind ChatPage to a UIAgent
ChatPage is the easiest entry point. It provides a message list, input, streaming status, and retry behavior:
<ChatPage Agent="_agent" Placeholder="Ask me anything…" />
ChatPage receives a UIAgent, which manages the interaction state that the components render. UIAgent wraps an IChatClient from Microsoft.Extensions.AI, consumes its streaming ChatResponseUpdate values, and maps their contents into observable ContentBlock instances. The same abstraction applies whether the agent is remote, in process, or provider-specific.
A ContentBlock has an identity, a role, a lifecycle state, and change notifications so components rerender as streamed data arrives. The package includes blocks for conversational content, tool calls, approvals, and frontend actions. Applications can register handlers that map other model content into custom block types.
UIAgent<TState> adds strongly typed application state that Razor components can observe and edit. A state mapper can apply state received from an agent, so the user, the agent, and ordinary components collaborate through application-owned data.
Compose a custom layout
When a full chat page is the wrong shape, use the lower-level components the blog names:
AgentBoundarycreates the interaction context.MessageListrenders the current turns.MessageInputsubmits text.BlockRenderer<TBlock>selects Razor content for a particular block type.
The current preview includes a text-focused MessageInput. Images, files, audio, and other Microsoft.Extensions.AI content do not go through that preview input. Build another input and call AgentContext.SendMessageAsync(ChatMessage). Capture, transcription, and model-specific media processing stay with the application and its AI services. Cancellation while a response is streaming goes through AgentContext.CancelAsync.
Map remote agents with AG-UI
Any IChatClient is enough for basic functionality. For a richer experience, the stable AG-UI .NET SDK provides AGUIChatClient, which implements IChatClient and translates AG-UI events into Microsoft.Extensions.AI response updates.
AG-UI is an open, event-based protocol. It defines interaction events, not the UI components that render them. The Blazor AI components are informed by that model but are not directly coupled to the protocol.
In the sample, MAF builds agents and ASP.NET Core exposes them as AG-UI endpoints. AddAGUIServer and MapAGUIServer come from Microsoft.Agents.AI.Hosting.AGUI.AspNetCore. The MAF hosting layer uses AGUI.Server from the AG-UI .NET SDK to translate MAF response streams into AG-UI events carried over HTTP and Server-Sent Events:
builder.Services.AddAGUIServer();
var app = builder.Build();
var foundry = Foundry.ReadOptions(app.Configuration);
var chatClient = Foundry.CreateChatClient(foundry);
var reasoningChatClient = Foundry.CreateReasoningChatClient(foundry);
var agents = new AgentCatalog(chatClient, reasoningChatClient);
app.MapAGUIServer("/agentic_chat", agents.CreateAgenticChat());
app.MapAGUIServer("/backend_tool_rendering", agents.CreateBackendToolRendering());
app.MapAGUIServer("/human_in_the_loop", agents.CreateHumanInTheLoop());
app.MapAGUIServer("/shared_state", agents.CreateSharedState());
app.MapAGUIServer("/predictive_state", agents.CreatePredictiveState());
The Foundry helpers and AgentCatalog in that snippet belong to the sample. Each MapAGUIServer call binds one sample scenario to a path. The Aspire AppHost connects the projects, supplies the Foundry configuration, gives the Blazor project a service reference to the agent server, and coordinates startup. The blog does not show the named HTTP client registration.
The Blazor project creates an AGUIChatClient for each endpoint:
public IChatClient CreateChatClient(string endpoint)
{
HttpClient http = httpClientFactory.CreateClient("agentserver");
return new AGUIChatClient(new AGUIChatClientOptions(http, endpoint));
}
UIAgent then wraps that IChatClient, so the components use one abstraction for a remote agent and a local one.
Render tools, approvals, and shared state
A ChatPage streams a basic conversation as updates arrive. To format the received content, wrap the IChatClient in a DelegatingChatClient that accumulates the model's streamed Markdown and maps it to structured content. The sample demonstrates this with FormattedChatClient, which accumulates text by message ID and inserts a structured snapshot. RichTextContent holds a complete snapshot made from nodes for paragraphs, emphasis, inline code, lists, tables, images, and other presentation elements. The package intentionally does not choose a Markdown parser. Applications can construct the tree directly or add IChatClient middleware to convert another format into presentation nodes.
Server tool calls surface as strongly typed, source-generated FunctionInvocationContentBlock instances. The Backend Tools page asks the server agent for weather. MAF executes get_weather on the agent server, and AG-UI carries the streamed call and result to the browser. A source generator projects the function arguments and result into a typed block, and the page registers the generated handler when it creates the agent. A BlockRenderer<TBlock> inside ChatPage.MessageListContent supplies Razor for matching blocks, so the same block can show the pending call and then rerender when the typed result is present. The tool still runs on the server. The card can use dependency injection, child components, CSS, localization, and other familiar Blazor features.
Frontend tools use the same Microsoft.Extensions.AI tool model but execute through the client-side tool pipeline in AGUIChatClient. The Frontend Tools page provides set_accent_color as an executable tool in ChatOptions.Tools. When the remote agent requests the tool, AGUIChatClient invokes it in the Blazor application and returns the result so the agent can continue. Tools that need UI or user input before the interaction can continue use a different path: registering a function with RegisterUIAction produces a UIActionBlock that pauses until application UI calls InvokeAsync().
Human-in-the-loop interrupts pause an agent until the application supplies a decision. The Human in the Loop page wraps the consequential book_meeting server tool in ApprovalRequiredAIFunction. The resulting AG-UI interrupt becomes a FunctionApprovalBlock. Connect buttons to the block's approve and reject actions:
<BlockRenderer TBlock="FunctionApprovalBlock" Context="block">
@if (block.Status == ApprovalStatus.Pending)
{
<button @onclick="block.Approve">Approve</button>
<button @onclick="() => block.Reject()">Reject</button>
}
</BlockRenderer>
Approving resumes the agent and allows the tool to execute. Rejecting returns the decision without running the tool. Applications can use the same boundary to collect choices, edits, missing fields, or other structured input before work continues.
Not all agent output belongs in a message list. UIAgent<TState> exposes typed state that Razor can render anywhere, and AG-UI events can keep that state shared with the agent. The Shared State page sends the current value as RunAgentInput.State through RawRepresentationFactory on ChatOptions. When the agent calls generate_recipe, the MAF AG-UI endpoint maps the tool result to an AG-UI STATE_SNAPSHOT. An inbound state mapper deserializes that snapshot and calls SetState. The editor subscribes to state changes so an agent update schedules a component rerender, and direct edits replace State.Value so the next request includes the user's latest version.
Predictive state is the case where an agent should propose a change without immediately replacing committed state. On the Predictive State page, the frontend propose_document action's document argument is treated as proposed state. The editor shows that proposal as a diff while the previous document stays the committed baseline. A custom confirmation component adds the user's decision and invokes the action. Accepting commits the proposed document. Rejecting restores the baseline. Unresolved predictive state is also rejected when the run fails, is canceled, or completes without a decision.
Keep the experimental label
The blog says the Blazor AI components are currently experimental. API feedback, missing scenarios, and usability issues belong in the dotnet/aspnetcore repository. Stay on the .NET 11 RC1 SDK and the prerelease package. The post does not document a general-availability upgrade. The preview MessageInput is text-focused. Media, approvals, and state mapping stay in application code built on the blocks.
What to try next
Open the AgenticUI sample from the related resources on the .NET blog and walk the Agentic Chat, Backend Tools, Human in the Loop, Shared State, and Predictive State pages before you replace ChatPage with an AgentBoundary layout. Read that post beside the sample so the package boundary stays visible, and send preview feedback to the dotnet/aspnetcore repository instead of treating the API as stable.