Adding AI to ASP.NET Web Applications
Adding AI to ASP.NET Web Applications
1 Overview: The Model Meets the Browser
You have an enterprise-shaped AI service — IChatService behind dependency injection, resilient, logged, its secrets secured. This tutorial gives it a face. A browser cannot and must not call Azure OpenAI directly: the key would ship to every visitor, and there would be no place to apply auth, rate limits, or logging. So the server exposes a REST API, the page calls that endpoint with fetch, and your service layer does the model work behind it. That is the whole architecture in one sentence — and the rest is doing it well.
Doing it well means four things the console app never had to face. First, latency is now visible to a human staring at a screen, so you need loading states and — better — streaming responses that show tokens as they arrive. Second, some AI work outruns a reasonable HTTP request, so you need a pattern for long-running operations that does not hold a connection open for minutes. Third, failures now reach a user, so error handling must be deliberate and typed. Fourth, all of this must sit inside a clean architecture that keeps the web framework and the AI SDK from bleeding into your domain.
2 Learning Objectives
- Expose AI features to a browser through a server-side REST API using DTOs, so the model, the key, and the SDK never leave the server.
- Stream responses to the browser token by token with IAsyncEnumerable and Server-Sent Events, and consume the stream in JavaScript.
- Handle long-running operations with an accept-and-poll pattern backed by a background service, instead of holding the request open.
- Design loading states and typed error handling with Problem Details so users always know whether the AI is working, done, or failed.
- Wire request cancellation from the browser through to the AI call, so abandoned work stops and stops billing.
- Apply clean architecture so the ASP.NET Core layer and the AI SDK stay at the edges, with domain and application logic independent of both.
3 Prerequisites
- Tutorial 13: an AI service behind IChatService, registered in DI, with options-bound configuration, structured logging, and resilience.
- ASP.NET Core basics: Program.cs, the middleware pipeline, minimal APIs or controllers, and model binding of JSON request bodies.
- Front-end fundamentals: HTML, and JavaScript's fetch API including reading a streamed response body.
- Comfort with async/await, IAsyncEnumerable, and CancellationToken from earlier tutorials in this course.
4 Key Concepts: Server in the Middle
Every AI-enabled web feature has the same three-tier shape: a browser UI, your server, and the model. The browser never sees the model. It sends a plain request — 'here is the user's question' — to your REST API; the server, holding the key and the IChatService, calls Azure OpenAI and shapes a response the browser can render. The server is where auth, rate limiting, logging, and cost control live, precisely because it is the one place the browser cannot bypass.
| Concern | Belongs to the browser | Belongs to the server |
|---|---|---|
| The API key / credential | Never | Always |
| Prompt construction & system message | No | Yes — in the service layer |
| Calling the model | No | Yes |
| Rendering the answer, loading states | Yes | Provides the data |
| Auth, rate limits, logging, cost control | No | Yes |
| Deciding sync vs streaming vs long-running | No | Yes — by response shape |
What differs between features is only the response shape, and that choice drives everything downstream. A short answer returns as one JSON body (a normal REST API call). A visible-latency answer streams token by token so the user sees progress. Work that may take minutes becomes a long-running operation: accept it, return an id, and let the client poll. The next four deep dives take these in turn, and the final one puts them inside a clean architecture so none of it contaminates your domain code.
5 Deep Dive 1: UI + AI Integration Using REST APIs
The simplest integration is a request/response REST API. The browser POSTs the user's input as JSON; a server endpoint binds it to a request DTO, calls IChatService, and returns a response DTO as JSON. DTOs matter more here than anywhere: they are the contract with the outside world, and they must not be your SDK types or even your raw domain entities. A request DTO carries exactly what the client may send (the question, maybe a conversation id) and nothing it may not (no temperature, no system prompt, no deployment name — those are server policy). A response DTO carries exactly what the client needs to render.
// Program.cs — the AI SDK is nowhere in sight here; only IChatService.
app.MapPost("/api/ask", async (
AskRequest request,
IChatService chat,
CancellationToken ct) =>
{
if (string.IsNullOrWhiteSpace(request.Question))
return Results.BadRequest(new { error = "Question is required." });
OrderAnswer answer = await chat.AskAboutOrderAsync(request.Question, ct);
return Results.Ok(new AskResponse(answer.Text, answer.Truncated));
})
.WithName("Ask");
// DTOs: the wire contract, deliberately separate from domain and SDK types.
public record AskRequest(string Question, string? ConversationId);
public record AskResponse(string Answer, bool Truncated);
async function ask(question) {
const res = await fetch('/api/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
if (!res.ok) {
const problem = await res.json(); // Problem Details (deep dive 4)
throw new Error(problem.detail ?? 'Request failed');
}
const data = await res.json();
return data.answer;
}
Notice the cancellation token parameter on the endpoint: ASP.NET Core supplies HttpContext.RequestAborted automatically, so if the user navigates away the token trips and — because tutorial 13's service passes it all the way to the SDK — the model call actually stops. When the front end lives on a different origin than the API you must also configure CORS on the server; and any input box that fires on keystrokes should debounce so a user typing a sentence does not launch ten requests. Whether you write this endpoint as a minimal API or a controller action is style; the boundary discipline is identical.
6 Deep Dive 2: Streaming Responses to the Browser
A model may take many seconds to finish a long answer, and making the user watch a spinner for all of it feels broken even when it is working. Streaming fixes the perception and often the reality: the server forwards each token as the model produces it, and the browser paints words as they arrive — the familiar 'typing' effect. The mechanism is a chain of asynchronous streams. The SDK exposes completions as an IAsyncEnumerable of update objects (CompleteChatStreamingAsync); your service layer re-exposes that as an IAsyncEnumerable<string> of text fragments; and the endpoint writes each fragment to the response as it arrives using chunked transfer, so the browser can read partial output before the whole is done.
app.MapPost("/api/ask/stream", async (
AskRequest request,
IChatService chat,
HttpContext http,
CancellationToken ct) =>
{
http.Response.Headers.ContentType = "text/event-stream";
http.Response.Headers.CacheControl = "no-cache";
// The service yields text fragments as an IAsyncEnumerable<string>.
await foreach (string fragment in chat.StreamAnswerAsync(request.Question, ct))
{
// One SSE 'data:' line per fragment, then flush so it ships immediately.
await http.Response.WriteAsync($"data: {fragment}\n\n", ct);
await http.Response.Body.FlushAsync(ct);
}
await http.Response.WriteAsync("event: done\ndata: end\n\n", ct);
});
async function askStreaming(question, onFragment) {
const res = await fetch('/api/ask/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split('\n')) {
if (line.startsWith('data: ')) onFragment(line.slice(6));
}
}
}
7 Deep Dive 3: Handling Long-Running Operations
Some AI work does not fit a request at all: a multi-step agent, a batch that summarizes fifty documents, a chain of tool calls that runs for minutes. Holding an HTTP request open that long is a mistake — it ties up a server connection, dies on any proxy or load-balancer idle timeout, and gives the browser nothing but a stalled spinner. The pattern for a long-running operation is to decouple the work from the request: accept the job, hand it to a background service, and return immediately with a job id and HTTP 202 Accepted. The client then polls a status endpoint until the job reports done, or subscribes for a push notification.
// Start: enqueue and return 202 with a job id — the request ends here.
app.MapPost("/api/jobs/summarize", (SummarizeRequest req, IJobQueue jobs) =>
{
string jobId = jobs.Enqueue(req); // hands work to a background service
return Results.Accepted($"/api/jobs/{jobId}", new { jobId });
});
// Status: the client polls this until Status is Completed or Failed.
app.MapGet("/api/jobs/{id}", (string id, IJobStore store) =>
{
JobRecord? job = store.Find(id);
if (job is null) return Results.NotFound();
return job.Status switch
{
JobStatus.Completed => Results.Ok(new { job.Status, job.Result }),
JobStatus.Failed => Results.Ok(new { job.Status, job.Error }),
_ => Results.Ok(new { job.Status }) // Pending / Running
};
});
The background service itself is a BackgroundService (a hosted IHostedService) that reads the queue, calls IChatService for each job, and writes results to a job store — in memory for a demo, but a database or distributed cache in production so a restart does not lose in-flight work. This is also where you decide on real-time push: for a nicer experience than polling, the server can notify the browser over a WebSocket-based channel when the job finishes, but polling is the simplest correct baseline and the right place to start.
8 Deep Dive 4: Loading States, Error Handling, and Clean Architecture
Because AI calls are slow and fallible, the UI must always answer two questions: is it working, and did it fail? A loading state answers the first — a disabled submit button and a spinner at minimum, streamed partial text at best, so the user never wonders whether their click registered. Pair it with a visible Stop control wired to an AbortController on the fetch, which on the server surfaces as the cancellation token that halts the model call. Silence is the one unacceptable state: an AI feature that shows nothing for fifteen seconds reads as broken even when it is perfectly healthy.
Error handling must be typed and consistent, not ad hoc. ASP.NET Core's Problem Details (RFC 9457) gives every failure a uniform JSON shape — type, title, status, detail — that the browser can handle in one code path. Map the failure classes deliberately: a 400 for bad input, a 429 the UI can turn into 'busy, try again shortly', a 503 when the circuit breaker from tutorial 13 is open, a generic 500 otherwise — and never leak a stack trace or a prompt to the client. Translate the server's status into human language in the UI; 'The assistant is busy right now' beats a raw 429.
Clean architecture keeps all of this from rotting. Picture concentric layers with dependencies pointing inward: at the center, domain models and the IChatService interface, knowing nothing of HTTP or the AI SDK; around them, application logic (the orchestration, the job workflow); at the edges, two thin adapters — the ASP.NET Core layer (endpoints, DTOs, Problem Details, SSE plumbing) and the infrastructure layer (the AI SDK implementation of IChatService, the job store). The web framework and the SDK are details at the boundary, plugged in via DI. The reward is the same as tutorial 13's, now at application scale: you can test the core without a browser or a model, swap ASP.NET Core for a worker or the SDK for another provider by rewriting an edge, and read the domain without wading through framework noise.
9 Ecosystem and Tools
| Technology | Role in an AI-enabled web app |
|---|---|
| ASP.NET Core minimal APIs / controllers | The REST API surface the browser calls; either style hosts AI endpoints |
| IAsyncEnumerable<T> + CompleteChatStreamingAsync | The async streams that carry tokens from SDK through the service layer to the response |
| Server-Sent Events (text/event-stream) | Simple one-way streaming of tokens to the browser over a single HTTP response |
| BackgroundService / IHostedService | Runs long-running AI jobs off the request thread; paired with a job store |
| ASP.NET Core Problem Details (RFC 9457) | Uniform typed error responses the front end handles in one path |
| HttpContext.RequestAborted (CancellationToken) | Propagates browser cancellation to the model call so abandoned work stops |
| SignalR (WebSockets) | Optional real-time push to replace polling for job completion notifications |
| fetch + AbortController + ReadableStream | The browser-side trio for calling, cancelling, and consuming streamed AI responses |
None of these are AI-specific — they are the standard ASP.NET Core web toolkit. That is the recurring lesson of this part of the course: an AI feature is a web feature whose backend happens to call a model, so the mature primitives you already have (streaming, background work, typed errors, cancellation) are exactly the ones you reach for. The AI SDK stays where tutorial 13 put it: behind IChatService, at the infrastructure edge.
10 Use Cases
- Support chat widget: a REST API endpoint per message, streaming the reply token by token so the assistant feels responsive on a help page.
- Inline writing assistant: a debounced call as the user pauses typing, returning suggestions rendered beside the editor.
- Document summarizer: an upload triggers a long-running operation; the user sees a progress state and gets the summary when the background job finishes.
- Search-answer box: a question posts to the server, which (in later tutorials) retrieves context and streams a grounded answer back.
- Form autofill from a description: a synchronous REST API call returns structured fields the page populates, with a loading state on the button.
- Batch classification dashboard: hundreds of items enqueued as jobs, a table polling each job's status and filling in results as they complete.
- Interactive tutor: a streamed conversation with a visible Stop button, so a learner can interrupt a long explanation and ask something else.
Read across the list and the same three response shapes recur: quick answers are synchronous REST, visible-latency answers stream, and minutes-long work becomes a background job. Choosing correctly among the three is the core design decision of every AI web feature.
11 Code Examples
These examples assume tutorial 13's IChatService is registered in DI, now extended with a streaming method. Together they show the synchronous endpoint, the streaming service method, and a typed error mapping — the three surfaces a real AI web feature needs.
// The interface gains a streaming method; SDK types stay inside the implementation.
public interface IChatService
{
Task<OrderAnswer> AskAboutOrderAsync(string question, CancellationToken ct = default);
IAsyncEnumerable<string> StreamAnswerAsync(string question, CancellationToken ct = default);
}
// Implementation: iterate the SDK's streaming updates, yield plain text fragments.
public async IAsyncEnumerable<string> StreamAnswerAsync(
string question, [EnumeratorCancellation] CancellationToken ct = default)
{
var messages = new ChatMessage[]
{
new SystemChatMessage("You are an order-support assistant."),
new UserChatMessage(question)
};
await foreach (StreamingChatCompletionUpdate update in
_chat.CompleteChatStreamingAsync(messages, cancellationToken: ct))
{
foreach (var part in update.ContentUpdate)
{
if (!string.IsNullOrEmpty(part.Text))
yield return part.Text; // one text fragment to the endpoint
}
}
}
// Program.cs — one place turns exceptions into consistent Problem Details JSON.
builder.Services.AddProblemDetails();
app.UseExceptionHandler(handler => handler.Run(async context =>
{
var feature = context.Features.Get<IExceptionHandlerFeature>();
var ex = feature?.Error;
(int status, string title) = ex switch
{
ClientResultException e when e.Status == 429
=> (StatusCodes.Status429TooManyRequests, "The assistant is busy. Please retry shortly."),
BrokenCircuitException
=> (StatusCodes.Status503ServiceUnavailable, "The assistant is temporarily unavailable."),
OperationCanceledException
=> (StatusCodes.Status499ClientClosedRequest, "Request cancelled."),
_ => (StatusCodes.Status500InternalServerError, "Something went wrong.")
};
// No stack trace, no prompt text — only a safe, typed shape for the client.
await Results.Problem(title: title, statusCode: status).ExecuteAsync(context);
}));
const btn = document.querySelector('#ask');
const out = document.querySelector('#answer');
let controller = null;
btn.addEventListener('click', async () => {
controller = new AbortController();
btn.disabled = true; // loading state
out.textContent = '';
showSpinner(true);
try {
const res = await fetch('/api/ask/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question: input.value }),
signal: controller.signal // Stop button aborts this
});
if (!res.ok) {
const problem = await res.json(); // Problem Details
throw new Error(problem.title ?? 'Request failed');
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
for (;;) {
const { value, done } = await reader.read();
if (done) break;
decoder.decode(value).split('\n').forEach(l => {
if (l.startsWith('data: ')) out.textContent += l.slice(6);
});
}
} catch (e) {
if (e.name === 'AbortError') out.textContent = '(cancelled)';
else out.textContent = 'Error: ' + e.message;
} finally {
btn.disabled = false; // back to idle
showSpinner(false);
}
});
12 Step by Step: A Streaming Assistant Page
This walkthrough puts tutorial 13's order assistant behind a web page with a streaming answer, loading and error states, cancellation, and a clean layering that keeps the SDK at the edge.
- Create the web project: 'dotnet new web -n OrderAssistantWeb' (minimal API host) and add the AI packages plus your tutorial-13 service-layer files.
- Register the stack in Program.cs exactly as before — options with ValidateOnStart, the singleton AzureOpenAIClient, AddSingleton<IChatService, ChatService> — then add builder.Services.AddProblemDetails().
- Confirm layering: put domain types and IChatService in a core project or folder with no ASP.NET Core or Azure references; keep the SDK implementation and DTOs at the edges. This is the clean architecture check.
- Extend IChatService with StreamAnswerAsync (Example 1); keep every SDK type inside the implementation, yielding only strings across the boundary.
- Add the synchronous POST /api/ask endpoint (deep dive 1) and the streaming POST /api/ask/stream endpoint (deep dive 2). Both take CancellationToken so RequestAborted flows to the model.
- Add the global Problem Details exception handler (Example 2), mapping 429, breaker-open, cancellation, and the generic case — and asserting no stack trace or prompt reaches the client.
- Build a minimal wwwroot/index.html: a text input, an Ask button, a Stop button, a spinner element, and an answer div.
- Wire the front end (Example 3): AbortController on submit, disabled button and spinner during loading, streamed fragments appended live, typed error messages, and clean return to idle in finally.
- Run 'dotnet run', open the page, ask about ORD-1042, and watch the answer stream in token by token instead of appearing all at once.
- Test the states deliberately: click Stop mid-stream and confirm the model call cancels (watch the server log); force a 429 (or fake the breaker open) and confirm the UI shows the friendly typed message, not a stack trace.
- Add a long-running path for practice: a POST /api/jobs/summarize returning 202 plus a job id, a BackgroundService that runs the work, and a GET /api/jobs/{id} the page polls — the accept-and-poll pattern from deep dive 3.
13 Limitations and Caveats
- SDK naming caveat: streaming members (CompleteChatStreamingAsync, StreamingChatCompletionUpdate, ContentUpdate) target the Azure.AI.OpenAI 2.x / OpenAI .NET line and shift between versions. If a member does not compile, check the package's current streaming sample; the async-stream pattern holds regardless of exact names. [EnumeratorCancellation] on the token parameter of an async iterator is required for cancellation to flow.
- Server-Sent Events is one-way (server to client) and its raw framing needs care: fragments containing newlines must be encoded, since a blank line terminates an SSE message. For bidirectional or higher-volume needs, consider WebSockets/SignalR.
- Once a streamed response has started (200 + body), the status code cannot change — mid-stream failures must be signalled as an in-band event, and the client must handle a stream that ends early.
- Streaming to the browser does not reduce token cost or total generation time; it improves perceived latency. A cancelled stream may still bill for tokens already produced.
- The in-memory job store shown for long-running operations loses work on restart and does not scale across instances — production needs a durable store (database/cache) and idempotent job handling.
- Polling has overhead and latency; tune the interval, add jitter, and cap total wait. Real-time push (SignalR) is nicer but adds a connection to manage.
- Proxies, load balancers, and response buffering (including some reverse proxies and output-compression middleware) can defeat streaming by buffering the body — disable buffering on streaming routes and test through your real edge.
- This tutorial's examples omit authentication and per-user rate limiting for focus; a public AI endpoint needs both, plus input size limits, or it is an open, billable proxy to your model.
14 Best Practices
- Keep the key and the model call server-side without exception; the browser calls your REST API, never Azure OpenAI.
- Shape the wire with DTOs, never expose SDK or domain types over HTTP, and never accept model parameters (temperature, system prompt) from the client — those are server policy.
- Choose the response shape by expected duration: synchronous for quick answers, streaming for visible latency, long-running jobs for minutes-long work.
- Always render a loading state and always offer a Stop control wired through AbortController to the server cancellation token — silence is a bug.
- Return errors as Problem Details, map failure classes to human messages, and never leak stack traces or prompt text to the client.
- Flow CancellationToken (HttpContext.RequestAborted) into every AI call so abandoned requests stop generating and stop billing.
- Keep ASP.NET Core and the AI SDK at the architecture's edges; if an SDK type or HttpContext reaches a domain class, stop and refactor.
- Protect public endpoints: authenticate, rate-limit per user, cap input size, and debounce client-side triggers so one user cannot storm the model.
| Common mistake | Do this instead |
|---|---|
| Calling the model endpoint directly from JavaScript | Call your server's REST API; the key and SDK live server-side |
| Returning SDK types or entities as the JSON response | Map to a response DTO shaped for the client's needs |
| A spinner that never resolves on error | Design idle/loading/success/error/cancelled as five real states |
| Holding the request open for a minutes-long job | Return 202 + job id, run it in a background service, poll for the result |
| Buffering the streamed body | Flush after each fragment and disable buffering on streaming routes |
| Leaking a 429 or stack trace to the user | Translate via Problem Details into 'the assistant is busy, try again shortly' |
20 Summary
- The browser never calls the model: it calls a server-side REST API that holds the key and the SDK and shapes requests/responses with DTOs — the one boundary where auth, rate limiting, cost control, and logging live.
- Choose the response shape by expected duration: synchronous JSON for quick answers, streaming for visible latency, and long-running accept-and-poll jobs for minutes-long work.
- Streaming is a pipeline of async streams — SDK IAsyncEnumerable to service IAsyncEnumerable<string> to Server-Sent Events over chunked transfer — improving perceived latency, with in-band error events because the status code is already committed.
- Long-running operations return 202 plus a job id, run in a background service off the request thread, and are polled (or pushed via SignalR) — never held open on the request.
- Design all five UI states — idle, loading, success, error, cancelled — with typed Problem Details errors and a Stop button whose AbortController flows a cancellation token all the way to the model call.
- Clean architecture keeps ASP.NET Core and the AI SDK at the edges with dependencies pointing inward, so the core stays testable and the framework and provider remain swappable details.
Your enterprise-shaped service now has a responsive, cancellable, gracefully-failing web face — built entirely from standard ASP.NET Core primitives, with the AI SDK still quarantined behind IChatService at the infrastructure edge. That is the recurring truth of this stretch of the course: an AI feature is a web feature whose backend calls a model, and the patterns that make it good are the ones your team already trusts. The next tutorial changes what the server does before it calls the model — retrieving real knowledge to ground the answer — while the streaming web delivery you built here carries straight over.
21 Next Steps
Next tutorial: RAG with Azure AI Search (rag-with-azure-ai-search). So far the assistant answers from the model plus whatever tools return. RAG (Retrieval-Augmented Generation) grounds it in your own documents: embed your content, index it in a search service, retrieve the most relevant passages for each question, and inject them into the prompt so the model answers from real knowledge. The retrieval step slots into the service layer behind the same IChatService, and the streaming web delivery you built in this tutorial carries it to the browser unchanged.
- Practice: build the full streaming assistant page from the walkthrough, then add a Stop button and confirm in the server log that clicking it cancels the model call mid-generation.
- Practice: implement the accept-and-poll long-running path — a BackgroundService that summarizes several documents, a 202 + job id start, and a status endpoint the page polls with a progress indicator.
- Practice: add the global Problem Details handler and deliberately trigger a 429 and a breaker-open 503, verifying the UI shows friendly typed messages with no stack traces.
- Practice: run your streaming endpoint behind a reverse proxy and fix any buffering that collapses the stream, confirming tokens still arrive incrementally through the real edge.
- Read: the official documentation for 'Minimal APIs in ASP.NET Core', 'Server-Sent Events / streaming responses', 'BackgroundService and hosted services', 'Problem Details (RFC 9457) in ASP.NET Core', and 'Handle request cancellation'.
15 Quiz: Adding AI to ASP.NET Web Applications
Pick an answer for each question, then press Check answer. (Notes are disabled in this tab.)
1. Why must a browser call a server-side REST API instead of Azure OpenAI directly?
2. What is the role of a DTO in an AI web endpoint?
3. Which client-provided value should an AI endpoint's request DTO NOT accept?
4. What does streaming a response to the browser primarily improve?
5. Which .NET type does the SDK use to expose a streaming completion?
6. What is Server-Sent Events (SSE) used for here?
7. Why can't you change the HTTP status code midway through a streamed response?
8. When should an AI operation be handled as a long-running operation rather than a normal or streaming request?
9. In the accept-and-poll pattern, what does the initial POST return?
10. Where should a long-running AI job actually execute?
11. What are the essential UI states for an AI interaction?
12. What is Problem Details used for?
13. How does clicking a Stop button in the browser actually halt the model call?
14. What does clean architecture require of the AI SDK in a web app?
15. Why should an input that triggers AI calls on keystrokes be debounced?
16 Exam: Written Questions
Try answering each question yourself before expanding the model answer.
1. Explain the three-tier shape of every AI-enabled web feature and why the server must sit in the middle.
2. Describe DTOs at an AI endpoint: what they carry, what they exclude, and why the separation matters.
3. Walk through the full path of a single token in a streaming response, from model to screen.
4. Why is error handling harder for streaming responses, and how do you handle mid-stream failures?
5. Present the accept-and-poll pattern for long-running AI operations end to end, and justify each part.
6. How do you choose among synchronous, streaming, and long-running responses for a given AI feature?
7. Describe the five UI states of an AI interaction and what each must do, including cancellation.
8. Explain how browser cancellation reaches the model and what breaks if the token chain is incomplete.
9. Lay out a clean architecture for an AI-enabled web app: the layers, the dependency direction, and what lives where.
10. What security and abuse concerns must a public AI web endpoint address that the tutorial's focused examples omit?
11. Why are the tools used in this tutorial described as 'not AI-specific', and what follows from that framing?
12. A streamed response works locally but arrives all-at-once in production behind a reverse proxy. Diagnose and fix.
13. Compare polling and real-time push (SignalR) for delivering long-running AI results. When is each appropriate?
14. Justify keeping streaming logic out of the domain: where does each piece of the streaming feature belong in the layers?
15. Design the observable behavior and server handling for a Stop button on a streaming assistant, covering both UI and server.
17 Flashcards
Click a card to reveal the back.
Why not call the model from the browser?
DTO at an AI endpoint
Streaming: what it improves
Streaming type chain
Server-Sent Events (SSE)
Streaming error handling gotcha
When to use a long-running operation
Accept-and-poll pattern
Where long jobs run
Five UI states
Problem Details (RFC 9457)
Browser → model cancellation
Clean architecture rule
Debounce (as-you-type AI)
Public AI endpoint must add
18 Interview Questions and Answers
1. How do you expose an AI feature to a web front end without leaking the key?
2. When would you stream a response, and how does it work in ASP.NET Core?
3. What's different about error handling once you're streaming?
4. A summarization job takes two minutes. How do you build that?
5. Walk me through the UI states you design for an AI call.
6. How does a Stop button actually stop the model, technically?
7. How do you keep the AI SDK from spreading through a web codebase?
8. Streaming works on your machine but not in production. What's your first hypothesis?
9. What must a public AI endpoint have that a tutorial example skips?
10. How do you decide synchronous vs streaming vs long-running for a feature?
11. Why do you insist DTOs never accept temperature or the system prompt from the client?
12. What's the role of IAsyncEnumerable in the streaming design, and the gotcha with cancellation?
13. Compare polling and SignalR for long-running job results.
14. Someone proposes returning the SDK's completion object directly as the endpoint's JSON. Why push back?
15. How does this tutorial's work build on tutorial 13 and set up tutorial 15 (RAG)?
19 Glossary
- REST API
- An HTTP endpoint over verbs and JSON that the browser calls with fetch; the server-side home of AI work, keeping the model, key, and SDK off the client.
- Minimal API
- ASP.NET Core's lightweight endpoint style (app.MapPost/MapGet) declaring routes and handlers without controllers; a compact host for AI endpoints.
- Controller
- An MVC class grouping HTTP endpoints as action methods; the controller-based alternative to minimal APIs for organizing an AI web app's server side.
- DTO
- Data Transfer Object — a small type shaping exactly the JSON on the wire, decoupling the HTTP contract from SDK and domain types. Carries intent, not model policy.
- Streaming
- Sending the response incrementally as it is produced so the browser shows tokens as they arrive; improves perceived latency, not cost or total time.
- Server-Sent Events
- A one-way HTTP streaming standard (text/event-stream) where the server pushes 'data:' messages to the browser over a single response via chunked transfer.
- IAsyncEnumerable
- A C# asynchronous stream of values; the SDK exposes streaming completions as one, and the service yields text fragments as IAsyncEnumerable<string>.
- Chunked transfer
- An HTTP mechanism for sending a response body in pieces without a known total length up front — what makes token-by-token streaming to the browser possible.
- Long-running operation
- Work that outlives a normal request; handled by accepting it, returning a job id and 202, and letting the client poll or subscribe rather than holding the connection.
- Background service
- A hosted component (BackgroundService/IHostedService) running work off the request thread — where long-running AI jobs execute, writing status to a job store.
- Polling
- The client repeatedly requesting a status endpoint (GET /jobs/{id}) until a long-running operation reports Completed or Failed.
- Loading state
- The UI condition during an in-flight AI call — spinner, streamed partial text, disabled button, Stop control — signalling the request is working.
- Cancellation token
- The signal (HttpContext.RequestAborted) flowed into the AI call so a user who navigates away or clicks Stop actually halts generation and billing.
- Problem Details
- The RFC 9457 JSON error format ASP.NET Core produces, giving the browser a uniform typed shape (type/title/status/detail) to handle failures in one path.
- Clean architecture
- Layering with dependencies pointing inward: domain and IChatService at the core, ASP.NET Core and the AI SDK as edge adapters wired by DI.
- CORS
- Cross-Origin Resource Sharing — the browser rule governing calls to another origin; configured server-side when the front end and API differ in origin.
- AbortController
- The browser API whose signal cancels a fetch; wiring a Stop button to it propagates cancellation to the server's request and the model call.
- Debounce
- Delaying an action until input pauses so as-you-type triggers collapse many keystrokes into one AI request, guarding cost and rate limits.
- Token
- A small chunk of generated text; streaming delivers the completion token by token as each is produced by the model.