Introduction
In the previous article, we discussed a practical roadmap for building AI applications with .NET. We covered how ASP.NET Core, Azure OpenAI, Semantic Kernel, Microsoft.Extensions.AI, Azure AI Search, RAG, agents, and Responsible AI fit into enterprise AI application development.
In this article, we will move from roadmap to implementation.
We will build a secure AI-ready chat API using ASP.NET Core.
The key point is this:
Do not call AI services directly from the frontend. Keep the AI service behind a secure ASP.NET Core backend API.
However, there is one practical challenge many developers face while learning Azure OpenAI:
Some Azure free subscriptions may not have available quota to deploy Azure OpenAI models.
This is not a blocker. We can still build the application using clean architecture.
In this article, we will create an interface-based AI service layer. First, we will use a local MockAiChatService so the project can run without Azure OpenAI quota. Later, the same interface can be replaced with AzureOpenAiChatService when Azure OpenAI model quota is available.
Why This Approach?
In real enterprise applications, we should avoid tightly coupling the controller directly to one AI provider.
Bad approach:
ChatController -> Azure OpenAI SDK directly
Better approach:
ChatController -> IAiChatService -> MockAiChatService / AzureOpenAiChatService
This gives us flexibility.
This is a useful enterprise pattern because the controller does not need to know which AI provider is being used.
What We Will Build
We will build a simple ASP.NET Core Web API with this endpoint:
POST /api/chat
The API will accept a user message and return a structured response.
Sample request:
{
"userMessage": "Explain what deductible means in health insurance."
}
Sample response:
{
"success": true,
"answer": "A deductible is the amount you usually pay for covered services before your insurance plan starts paying.",
"model": "mock-ai-service-local-demo",
"errorMessage": null
}
Why Not Call Azure OpenAI Directly from Frontend?
In enterprise applications, the frontend should not directly call Azure OpenAI or any AI model provider.
Avoid this:
Browser / Mobile App -> Azure OpenAI
Use this:
Browser / Mobile App -> ASP.NET Core API -> AI Service -> Azure OpenAI
The backend API helps with:
API key protection
Authentication
Authorization
Input validation
Rate limiting
Logging
Cost control
Prompt governance
Responsible AI checks
Safe error handling
This is why ASP.NET Core is a strong foundation for enterprise AI applications.
High-Level Architecture
Project Setup
Project Setup
Create a new ASP.NET Core Web API project.
dotnet new webapi -n EnterpriseAiChat.Api
cd EnterpriseAiChat.Api
Create the following folders:
Controllers
Models
Options
Services
Recommended project structure:
EnterpriseAiChat.Api
│
├── Controllers
│ └── ChatController.cs
│
├── Models
│ ├── ChatRequest.cs
│ └── ChatResponse.cs
│
├── Options
│ └── AzureOpenAiOptions.cs
│
├── Services
│ ├── IAiChatService.cs
│ ├── MockAiChatService.cs
│ ├── AzureOpenAiChatService.cs
│ ├── IChatRequestValidator.cs
│ └── ChatRequestValidator.cs
│
├── appsettings.json
└── Program.cs
Create Request Model
Create a file:
Models/ChatRequest.cs
namespace EnterpriseAiChat.Api.Models;
public class ChatRequest
{
public string UserMessage { get; set; } = string.Empty;
}
This model represents the user input sent to the API.
Create Response Model
Create a file:
Models/ChatResponse.cs
namespace EnterpriseAiChat.Api.Models;
public class ChatResponse
{
public bool Success { get; set; }
public string Answer { get; set; } = string.Empty;
public string? Model { get; set; }
public string? ErrorMessage { get; set; }
}
This response model gives us a consistent structure whether the response comes from a mock service or Azure OpenAI.
Create AI Chat Service Interface
Create a file:
Services/IAiChatService.cs
using EnterpriseAiChat.Api.Models;
namespace EnterpriseAiChat.Api.Services;
public interface IAiChatService
{
Task<ChatResponse> GetResponseAsync( ChatRequest request, CancellationToken cancellationToken);
}
This interface is the most important part of the design.
The controller will depend on IAiChatService, not on a specific implementation.
That means we can easily switch between:
MockAiChatService
AzureOpenAiChatService
Other provider implementation
Create Mock AI Chat Service
Create a file:
Services/MockAiChatService.cs
using EnterpriseAiChat.Models;
namespace EnterpriseAiChat.Services
{
public class MockAiChatService : IAiChatService
{
public Task<ChatResponse> GetResponseAsync(ChatRequest request, CancellationToken cancellationToken)
{
string userMessage = request.UserMessage.ToLower(); string answer;
if (userMessage.Contains("deductible"))
{
answer = "A deductible is the amount you usually pay for covered services before your insurance plan starts paying. " + "For example, if your deductible is $2,000, you may need to pay covered healthcare costs until that amount is met.";
}
else if (userMessage.Contains("rag"))
{
answer = "RAG stands for Retrieval-Augmented Generation. It retrieves relevant information from documents or databases first, " + "then sends that context to an AI model to generate a grounded answer.";
}
else if (userMessage.Contains(".net") || userMessage.Contains("dotnet"))
{
answer = ".NET is useful for AI applications because it provides secure APIs, dependency injection, authentication, logging, " + "database integration, and cloud deployment capabilities.";
}
else
{
answer = "This is a mock AI response for local development. Replace MockAiChatService with AzureOpenAiChatService when Azure OpenAI quota is available.";
}
ChatResponse response = new() { Success = true, Answer = answer, Model = "mock-ai-service-local-demo", ErrorMessage = null };
return Task.FromResult(response);
}
}
}
This allows us to test the full API flow without needing Azure OpenAI quota.
Create Request Validator
AI APIs should not blindly accept any user input.
Create a file:
Services/IChatRequestValidator.cs
using EnterpriseAiChat.Models;
namespace EnterpriseAiChat.Services;
public interface IChatRequestValidator
{
string? Validate(ChatRequest request);
}
Create another file:
Services/ChatRequestValidator.cs
using EnterpriseAiChat.Models;
using EnterpriseAiChat.Services;
namespace EnterpriseAiChat.Services;
public class ChatRequestValidator : IChatRequestValidator
{
private const int MaxInputLength = 2000;
public string? Validate(ChatRequest request)
{
if (request == null)
{
return "Request body is required.";
}
if (string.IsNullOrWhiteSpace(request.UserMessage))
{
return "User message is required.";
}
if (request.UserMessage.Length > MaxInputLength)
{
return $"User message should not exceed {MaxInputLength} characters.";
}
return null;
}
}
This simple validator protects the API from empty input and very large prompts.
Create Chat Controller
Create a file:
Controllers/ChatController.cs
using EnterpriseAiChat.Models;
using EnterpriseAiChat.Services;
using Microsoft.AspNetCore.Mvc;
namespace EnterpriseAiChat.Controllers;
[ApiController]
[Route("api/chat")]
public class ChatController : ControllerBase
{
private readonly IAiChatService _aiChatService;
private readonly IChatRequestValidator _validator;
public ChatController(
IAiChatService aiChatService,
IChatRequestValidator validator)
{
_aiChatService = aiChatService;
_validator = validator;
}
[HttpPost]
public async Task<IActionResult> Chat(
[FromBody] ChatRequest request,
CancellationToken cancellationToken)
{
string? validationError = _validator.Validate(request);
if (validationError != null)
{
return BadRequest(new ChatResponse
{
Success = false,
ErrorMessage = validationError
});
}
ChatResponse response =
await _aiChatService.GetResponseAsync(
request,
cancellationToken);
if (!response.Success)
{
return StatusCode(
StatusCodes.Status500InternalServerError,
response);
}
return Ok(response);
}
}
The controller is simple because the logic is delegated to services.
This is a clean API design.
Update Program.cs
Replace Program.cs with the following:
using EnterpriseAiChat.Options;
using EnterpriseAiChat.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.Configure<AzureOpenAiOptions>(
builder.Configuration.GetSection("AzureOpenAI"));
builder.Services.AddScoped<IAiChatService, AzureOpenAiChatService>();
builder.Services.AddScoped<IChatRequestValidator, ChatRequestValidator>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
Test the API
Run the application:
dotnet run
Open Swagger:
https://localhost:/swagger
Test:
POST /api/chat
Request:
{
"userMessage": "Explain what deductible means in health insurance."
}
Expected response:
_{
"success": true,
"answer": "A deductible is the amount you usually pay for covered services before your insurance plan starts paying. For example, if your deductible is $2,000, you may need to pay covered healthcare costs until that amount is met.",
"model": "mock-ai-service-local-demo",
"errorMessage": null
}
_
Try another request:
{
"userMessage": "What is RAG in AI applications?"
}
Expected response:
{
"success": true,
"answer": "RAG stands for Retrieval-Augmented Generation. It retrieves relevant information from documents or databases first, then sends that context to an AI model to generate a grounded answer.",
"model": "mock-ai-service-local-demo",
"errorMessage": null
}
Azure OpenAI Ready Configuration
Even though this article uses a mock service for local testing, we can keep the project ready for Azure OpenAI.
Create a file:
Options/AzureOpenAiOptions.cs
namespace EnterpriseAiChat.Options;
public class AzureOpenAiOptions
{
public string Endpoint { get; set; } = string.Empty;
public string DeploymentName { get; set; } = string.Empty;
public string ApiKey { get; set; } = string.Empty;
}
Update appsettings.json:
{
"AzureOpenAI": {
"Endpoint": "https://your-resource-name.openai.azure.com/",
"DeploymentName": "your-deployment-name"
}
}
Do not store the API key in appsettings.json.
For local development, use User Secrets:
dotnet user-secrets init
dotnet user-secrets set "AzureOpenAI:ApiKey" "YOUR_API_KEY"
For production, use Azure Key Vault or Managed Identity.
AzureOpenAiChatService for Future Use
When Azure OpenAI quota is available, add the Azure OpenAI implementation.
Install packages:
dotnet add package Azure.AI.OpenAI
dotnet add package Azure.Identity
Create a file:
Services/AzureOpenAiChatService.cs
using Azure;
using Azure.AI.OpenAI;
using EnterpriseAiChat.Models;
using EnterpriseAiChat.Options;
using EnterpriseAiChat.Services;
using Microsoft.Extensions.Options;
using OpenAI.Chat;
namespace EnterpriseAiChat.Services;
public class AzureOpenAiChatService : IAiChatService
{
private readonly AzureOpenAIClient _client;
private readonly AzureOpenAiOptions _options;
private readonly ILogger<AzureOpenAiChatService> _logger;
public AzureOpenAiChatService(
IOptions<AzureOpenAiOptions> options,
ILogger<AzureOpenAiChatService> logger)
{
_options = options.Value;
_logger = logger;
_client = new AzureOpenAIClient(
new Uri(_options.Endpoint),
new AzureKeyCredential(_options.ApiKey));
}
public async Task<ChatResponse> GetResponseAsync(
ChatRequest request,
CancellationToken cancellationToken)
{
try
{
ChatClient chatClient =
_client.GetChatClient(_options.DeploymentName);
List<ChatMessage> messages = new()
{
new SystemChatMessage(
"You are a helpful enterprise AI assistant. " +
"Give clear, concise, and professional answers. " +
"If you are unsure, say you are unsure."),
new UserChatMessage(request.UserMessage)
};
ChatCompletion completion =
await chatClient.CompleteChatAsync(
messages,
cancellationToken: cancellationToken);
string answer = completion.Content.Count > 0
? completion.Content[0].Text
: string.Empty;
return new ChatResponse
{
Success = true,
Answer = answer,
Model = _options.DeploymentName
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Azure OpenAI request failed.");
return new ChatResponse
{
Success = false,
ErrorMessage = "Unable to generate AI response at this time."
};
}
}
}
Azure OpenAI Deployment Flow
The normal deployment flow looks like this:
If you are using a free subscription and no models are available, you may see quota or region-related issues.
Conclusion
In this article, we built a secure AI-ready chat API using ASP.NET Core.
Because Azure OpenAI quota may not be available in some free subscriptions, we used a local MockAiChatService first. This allowed us to test the full API flow without waiting for cloud model deployment.
The most important design pattern is:
Controller -> Interface -> Implementation
This allows us to run locally with:
MockAiChatService
and later switch to:
AzureOpenAiChatService
without changing the controller.
This is a practical and enterprise-friendly approach for .NET developers who want to start building AI applications today and connect to Azure OpenAI when quota is available.
GitHub URL: https://github.com/indirakumar710/EnterpriseAiChat





Top comments (0)