DEV Community

Cover image for Building a Secure AI Chat API using ASP.NET Core: Local Mock Today, Azure OpenAI Ready Tomorrow
Indirakumar R
Indirakumar R

Posted on

Building a Secure AI Chat API using ASP.NET Core: Local Mock Today, Azure OpenAI Ready Tomorrow

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.

Mock vs Live

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

HLD

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; 
}
Enter fullscreen mode Exit fullscreen mode

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; } 
}

Enter fullscreen mode Exit fullscreen mode

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); 
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
}

Request

Response

_
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;
}
Enter fullscreen mode Exit fullscreen mode

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."
            };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Azure OpenAI Deployment Flow

The normal deployment flow looks like this:

OpenAI flow

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)