Completed
Real Estate AI Chatbot with ASP.NET Core
A production-oriented AI chatbot built with ASP.NET Core on .NET 10: Claude answers real estate questions on buying, selling, mortgages, investing and renting, streamed token by token over SignalR, with JWT authentication, per-user conversation history in SQL Server through EF Core, and clean architecture behind a swappable AI provider.
The problem
A chatbot demo is one HTTP call to a model; a chatbot people can actually use is not. Answers have to stream instead of timing out, every user's conversations have to be stored and kept private, a provider outage or refusal must not lose what the user typed or leave a half-answer, and the assistant has to stay inside its domain β here real estate, where it must show its working, admit it has no live listings, and stop short of legal, tax or financial advice.
Engineering focus
- Streaming LLM responses over SignalR
- Claude API from C# with the Anthropic SDK
- Clean architecture and a swappable AI provider
- JWT authentication and per-user data isolation
- Conversation history with EF Core and SQL Server
- Rate limiting, Problem Details and security headers
- System-prompt guardrails for a domain assistant
- Unit and integration tests with a fake AI provider
In use









How it works
- Streaming over SignalR
- The client invokes ChatHub.SendMessage. The hub pushes UserMessageSaved once the message is stored, then a ReceiveDelta per chunk, and the call completes with the final response. The API always streams from Claude, so the plain POST /api/chat endpoint shares the same code path.
- Save first, then call the model
- The user's message is written to SQL Server before the AI call, so an outage never loses it. If the client disconnects mid-answer, generation is cancelled and the partial reply is saved; same-role turns left behind by a failure are merged when the next request is built.
- Swappable AI provider
- Application code depends only on IAiChatService. AnthropicChatService implements it with the official Anthropic SDK and reports the model, token counts and stop reason; switching provider is one class and one DI line, and the tests use a fake.
- Refusal handling
- Requests opt into the API's server-side fallback, so a turn the model declines is retried on a fallback model. If it still ends with stop_reason refusal, a short notice is returned instead of a partial answer.
- Ownership by query
- Repositories only load conversations where UserId matches the signed-in user, so another user's conversation looks exactly like a 404 and IDs cannot be probed.
- Limits and errors
- One per-user chat rate limit is shared by REST and SignalR, with separate global and per-IP auth limits. Request and hub message sizes are capped, and every error is an RFC 9457 Problem Details response with a trace id. Logs carry IDs and token counts, never message content.