Building a Multi-Provider AI Chat Application with .NET 10 π€
Have you ever wanted to build an AI application but felt locked into a single provider? What if you could switch between GitHub Models, Azure OpenAI, OpenAI, or even local models with just a configuration change?
In this comprehensive tutorial, I'll show you how I built a production-ready AI chat application using .NET 10 that supports multiple AI providers through a unified interface. By the end, you'll understand:
- β How to use Microsoft.Extensions.AI for provider-agnostic code
- β Implementing Provider and Factory design patterns
- β Secure secrets management with User Secrets
- β Building interactive console applications
- β Working with 4 different AI providers
π Complete Source Code: github.com/Rahul1994jh/genai_with_dotnet
π― The Problem
When building AI applications, you typically face these challenges:
// β The problem: Different APIs for different providers
if (provider == "Azure") {
var response = await azureClient.GetChatCompletionsAsync(...);
} else if (provider == "OpenAI") {
var response = await openAiClient.CreateChatCompletionAsync(...);
} else if (provider == "GitHub") {
var response = await githubClient.Complete(...);
}
// ... more providers, more complexity
This approach leads to:
- β Hard-coded provider logic scattered everywhere
- β Difficult to test and maintain
- β Impossible to switch providers at runtime
- β Code duplication for similar operations
π‘ The Solution
Enter Microsoft.Extensions.AI - Microsoft's official abstraction layer for AI services:
// β
The solution: One interface for all providers
IChatClient client = GetClient(provider);
var response = await client.CompleteAsync(messages, options);
By using the Provider Pattern with a Factory, we can:
- β Switch providers with a configuration change
- β Add new providers without touching existing code
- β Test with mock providers easily
- β Keep code clean and maintainable
ποΈ Architecture Overview
Let me show you the architecture we'll build:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Console Application (Interactive Menus) β
ββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ
β SimpleChat.cs (Main Orchestrator) β
β β’ Provider Selection β
β β’ Chat Options Configuration β
β β’ Chat Loop β
ββββββββββ¬βββββββββββββββββ¬βββββββββββββββββ¬βββββββββββ
β β β
ββββββΌβββββ ββββββΌβββββ ββββββΌβββββ
β Config β β Factory β βIChatClientβ
β Manager β β Pattern β β(MS Ext AI)β
βββββββββββ ββββββ¬βββββ βββββββββββββ
β
βββββββββββββββΌββββββββββββββ¬ββββββββββ
β β β β
βββββΌββββ βββββΌββββ βββββΌββββ βββββΌββββ
βGitHub β βAzure β βOpenAI β βLocal β
βModels β βOpenAI β β API β βModels β
βββββββββ βββββββββ βββββββββ βββββββββ
π Design Patterns in Action
1. Provider Pattern (Strategy Pattern)
The Provider Pattern allows us to define a family of algorithms (providers) and make them interchangeable:
// All providers implement this interface
public interface IModelProvider
{
string ProviderName { get; }
string ModelName { get; }
IChatClient CreateChatClient();
}
// Each provider has its own implementation
public class GitHubModelProvider : IModelProvider
{
public string ProviderName => "GitHub Models";
public string ModelName => _settings.ModelName;
public IChatClient CreateChatClient()
{
var credential = new AzureKeyCredential(_token);
return new ChatCompletionsClient(
new Uri(_settings.EndpointUrl),
credential
).AsIChatClient(_settings.ModelName);
}
}
Why this matters:
- Adding a new provider? Just implement
IModelProvider - No changes to existing code
- Easy to test with mock implementations
2. Factory Pattern
The Factory Pattern centralizes object creation:
public static class ModelProviderFactory
{
public static IModelProvider CreateProvider(
string providerType,
ProviderSettings settings,
IConfiguration configuration)
{
var token = GetTokenIfNeeded(settings.TokenConfigKey, configuration);
return providerType.ToLower() switch
{
"github" => new GitHubModelProvider(settings, token),
"azure" => new AzureOpenAIProvider(settings, token),
"openai" => new OpenAIProvider(settings, token),
"local" => new LocalModelProvider(settings),
_ => throw new NotSupportedException(
$"Provider '{providerType}' not supported")
};
}
}
Benefits:
- One place to manage provider creation
- Easy to add new providers (just add a case)
- Encapsulates complex initialization logic
π Building the Application - Step by Step
Let's build this application from scratch!
Step 1: Create the Project
dotnet new console -n AIChat -f net10.0
cd AIChat
Step 2: Add Required Packages
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.AzureAIInference
dotnet add package Microsoft.Extensions.AI.OpenAI
dotnet add package Azure.AI.Inference
dotnet add package Azure.AI.OpenAI
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
Step 3: Define the Provider Interface
Create Providers/IModelProvider.cs:
using Microsoft.Extensions.AI;
namespace AIChat.Providers
{
public interface IModelProvider
{
string ProviderName { get; }
string ModelName { get; }
IChatClient CreateChatClient();
}
}
Step 4: Implement GitHub Models Provider
Create Providers/GitHubModelProvider.cs:
using Azure;
using Azure.AI.Inference;
using Microsoft.Extensions.AI;
namespace AIChat.Providers
{
public class GitHubModelProvider : IModelProvider
{
private readonly ProviderSettings _settings;
private readonly string _token;
public string ProviderName => "GitHub Models";
public string ModelName => _settings.ModelName;
public GitHubModelProvider(ProviderSettings settings, string token)
{
_settings = settings;
_token = token;
}
public IChatClient CreateChatClient()
{
var credential = new AzureKeyCredential(_token);
return new ChatCompletionsClient(
new Uri(_settings.EndpointUrl),
credential
).AsIChatClient(_settings.ModelName);
}
}
}
Step 5: Create Configuration Classes
Create AppSettings.cs:
namespace AIChat
{
public class AppSettings
{
public string SelectedProvider { get; set; } = "GitHub";
public Dictionary<string, ProviderSettings> Providers { get; set; } = new();
public ChatOptionsSettings ChatOptions { get; set; } = new();
public UiSettings UI { get; set; } = new();
}
public class ProviderSettings
{
public string EndpointUrl { get; set; } = string.Empty;
public string ModelName { get; set; } = string.Empty;
public string TokenConfigKey { get; set; } = string.Empty;
public string DeploymentName { get; set; } = string.Empty;
}
public class ChatOptionsSettings
{
public int MaxOutputTokens { get; set; }
public float Temperature { get; set; }
}
public class UiSettings
{
public string WelcomeTitle { get; set; } = string.Empty;
public string ExitMessage { get; set; } = string.Empty;
}
}
Step 6: Create Configuration File
Create appsettings.json:
{
"SelectedProvider": "GitHub",
"Providers": {
"GitHub": {
"EndpointUrl": "https://models.github.ai/inference",
"ModelName": "mistral-ai/Ministral-3B",
"TokenConfigKey": "GitHub:Token"
},
"Azure": {
"EndpointUrl": "https://your-resource.openai.azure.com",
"ModelName": "gpt-4",
"DeploymentName": "gpt-4",
"TokenConfigKey": "Azure:ApiKey"
},
"OpenAI": {
"ModelName": "gpt-4",
"TokenConfigKey": "OpenAI:ApiKey"
},
"Local": {
"EndpointUrl": "http://localhost:11434",
"ModelName": "llama2",
"TokenConfigKey": ""
}
},
"ChatOptions": {
"MaxOutputTokens": 300,
"Temperature": 0.2
},
"UI": {
"WelcomeTitle": "Welcome to AI Chat Assistant",
"ExitMessage": "Goodbye! π"
}
}
Don't forget to set it to copy to output directory in your .csproj:
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
Step 7: Build the Factory
Create Providers/ModelProviderFactory.cs:
using Microsoft.Extensions.Configuration;
namespace AIChat.Providers
{
public class ModelProviderFactory
{
public static IModelProvider CreateProvider(
string providerType,
ProviderSettings settings,
IConfiguration configuration)
{
// Get token from user secrets if needed
var token = string.Empty;
if (!string.IsNullOrEmpty(settings.TokenConfigKey))
{
token = configuration[settings.TokenConfigKey];
if (string.IsNullOrEmpty(token))
{
throw new InvalidOperationException(
$"Token not found for provider '{providerType}'. " +
$"Please set '{settings.TokenConfigKey}' in user secrets.");
}
}
return providerType.ToLower() switch
{
"github" => new GitHubModelProvider(settings, token),
"azure" => new AzureOpenAIProvider(settings, token),
"openai" => new OpenAIProvider(settings, token),
"local" => new LocalModelProvider(settings),
_ => throw new NotSupportedException(
$"Provider '{providerType}' is not supported.")
};
}
}
}
Step 8: Create the Main Application
Create SimpleChat.cs:
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using AIChat.Providers;
namespace AIChat
{
internal class SimpleChat
{
public static async Task RunAsync()
{
// Load configuration
var configuration = BuildConfiguration();
var settings = LoadSettings(configuration);
// Display provider selection menu
var selectedProvider = DisplayProviderSelectionMenu(settings);
// Get provider settings
if (!settings.Providers.TryGetValue(
selectedProvider, out var providerSettings))
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(
$"β Error: Provider '{selectedProvider}' not found.");
Console.ResetColor();
return;
}
// Create provider
IModelProvider provider;
try
{
provider = ModelProviderFactory.CreateProvider(
selectedProvider,
providerSettings,
configuration);
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"β Error: {ex.Message}");
Console.ResetColor();
return;
}
// Get chat client
IChatClient client = provider.CreateChatClient();
var chatOptions = new ChatOptions
{
MaxOutputTokens = settings.ChatOptions.MaxOutputTokens,
Temperature = settings.ChatOptions.Temperature
};
// Display welcome
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("ββββββββββββββββββββββββββββββββββββββββββ");
Console.WriteLine($"β {settings.UI.WelcomeTitle.PadRight(38)}β");
Console.WriteLine("ββββββββββββββββββββββββββββββββββββββββββ");
Console.ResetColor();
Console.WriteLine($"\nProvider: {provider.ProviderName}");
Console.WriteLine($"Model: {provider.ModelName}\n");
// Chat loop
while (true)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("You: ");
Console.ResetColor();
var userQuestion = Console.ReadLine();
if (string.IsNullOrWhiteSpace(userQuestion) ||
userQuestion.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"\n{settings.UI.ExitMessage}");
Console.ResetColor();
break;
}
try
{
var messages = BuildMessages(userQuestion);
Console.ForegroundColor = ConsoleColor.Blue;
Console.Write("\nAssistant: ");
Console.ResetColor();
var response = await client.CompleteAsync(messages, chatOptions);
Console.WriteLine(response.Message.Text);
Console.WriteLine();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"\nβ Error: {ex.Message}");
Console.ResetColor();
Console.WriteLine();
}
}
}
private static IConfiguration BuildConfiguration() =>
new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.AddUserSecrets<Program>()
.Build();
private static AppSettings LoadSettings(IConfiguration configuration)
{
var settings = new AppSettings();
configuration.Bind(settings);
return settings;
}
private static string DisplayProviderSelectionMenu(AppSettings settings)
{
Console.Clear();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("ββββββββββββββββββββββββββββββββββββββββββ");
Console.WriteLine("β Select AI Model Provider β");
Console.WriteLine("ββββββββββββββββββββββββββββββββββββββββββ");
Console.ResetColor();
Console.WriteLine();
var providers = settings.Providers.ToList();
for (int i = 0; i < providers.Count; i++)
{
var provider = providers[i];
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($" [{i + 1}] {provider.Key} - {provider.Value.ModelName}");
Console.ResetColor();
}
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Green;
Console.Write($"Enter choice [1-{providers.Count}] (or Enter for default): ");
Console.ResetColor();
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
return settings.SelectedProvider;
if (int.TryParse(input, out int choice) &&
choice >= 1 && choice <= providers.Count)
{
return providers[choice - 1].Key;
}
return settings.SelectedProvider;
}
private static IEnumerable<ChatMessage> BuildMessages(string userQuestion) =>
new List<ChatMessage>
{
new(ChatRole.System,
"You are a helpful, knowledgeable assistant. " +
"Provide clear and concise answers."),
new(ChatRole.User, userQuestion)
};
}
}
Step 9: Update Program.cs
using AIChat;
await SimpleChat.RunAsync();
π Setting Up User Secrets
User Secrets is a .NET feature for storing sensitive data securely during development.
Initialize User Secrets
dotnet user-secrets init
This adds a UserSecretsId to your .csproj:
<PropertyGroup>
<UserSecretsId>your-unique-id-here</UserSecretsId>
</PropertyGroup>
Store Your GitHub Token
First, get a token from GitHub Settings:
- Click "Generate new token (classic)"
- No scopes needed for GitHub Models
- Copy the token (you won't see it again!)
Then store it:
dotnet user-secrets set "GitHub:Token" "ghp_your_token_here"
Verify:
dotnet user-secrets list
π¨ Understanding Microsoft.Extensions.AI
Microsoft.Extensions.AI provides a unified abstraction layer for AI services. Here are the key concepts:
IChatClient Interface
The main interface for chat operations:
public interface IChatClient
{
Task<ChatCompletion> CompleteAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default);
}
ChatMessage
Represents a message in a conversation:
new ChatMessage(ChatRole.System, "You are a helpful assistant")
new ChatMessage(ChatRole.User, "What is AI?")
new ChatMessage(ChatRole.Assistant, "AI stands for...")
Roles explained:
- System: Instructions to the AI (sets behavior)
- User: Your questions/prompts
- Assistant: AI's responses (for conversation history)
ChatOptions
Configuration for the chat request:
new ChatOptions
{
Temperature = 0.7f, // 0.0-2.0: creativity level
MaxOutputTokens = 500, // Maximum response length
TopP = 0.9f, // Nucleus sampling
FrequencyPenalty = 0.0f, // Reduce repetition
PresencePenalty = 0.0f // Encourage new topics
}
Understanding Temperature
Temperature controls randomness in responses:
0.0 βββββββββββ 0.7 βββββββββββ 1.5 βββββββββββ 2.0
Focused Balanced Creative Chaotic
Examples:
Temperature = 0.0 (Deterministic)
Q: What is 2+2?
A: 4
A: 4 (always same)
A: 4
Temperature = 0.7 (Balanced)
Q: Describe a sunset
A: "The sky fills with warm orange and pink hues..."
A: "Golden rays paint the horizon as day fades..."
A: "Vibrant colors dance across the evening sky..."
Temperature = 1.5 (Very Creative)
Q: Describe a sunset
A: "Celestial fire dances with liquid gold..."
A: "The sun whispers farewell in a symphony of light..."
A: "Time melts into an amber ocean..."
π― Adding More Providers
Want to add Azure OpenAI? Here's the complete implementation:
AzureOpenAIProvider.cs
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.AI;
namespace AIChat.Providers
{
public class AzureOpenAIProvider : IModelProvider
{
private readonly ProviderSettings _settings;
private readonly string _token;
public string ProviderName => "Azure OpenAI";
public string ModelName => _settings.ModelName;
public AzureOpenAIProvider(ProviderSettings settings, string token)
{
_settings = settings;
_token = token;
}
public IChatClient CreateChatClient()
{
var credential = new AzureKeyCredential(_token);
var client = new AzureOpenAIClient(
new Uri(_settings.EndpointUrl),
credential);
var chatClient = client.GetChatClient(_settings.DeploymentName);
return chatClient.AsIChatClient();
}
}
}
That's it! No changes to existing code needed. Just:
- Add the provider class
- Update the factory with a new case
- Add configuration to
appsettings.json - Set the API key in user secrets
π§ͺ Testing Your Application
Run with GitHub Models
# Set token
dotnet user-secrets set "GitHub:Token" "ghp_your_token"
# Run
dotnet run
Try Different Models
Edit appsettings.json:
"ModelName": "openai/gpt-4o-mini" // Switch to GPT-4o mini
Available GitHub models:
-
mistral-ai/Ministral-3B- Fast and efficient -
meta-llama/Llama-3.2-3B-Instruct- Great for instructions -
microsoft/Phi-3-mini-4k-instruct- Excellent for code -
openai/gpt-4o-mini- Best quality
Browse all: GitHub Models Marketplace
π‘ Key Learnings
1. Provider Pattern Makes Code Flexible
By abstracting providers behind an interface:
- β Easy to add new providers
- β Easy to test (mock providers)
- β Easy to maintain (separation of concerns)
2. Factory Pattern Centralizes Creation
Having one place to create providers:
- β Consistent initialization
- β Easier to debug
- β Single source of truth
3. Configuration-Driven Development
Using appsettings.json and User Secrets:
- β No hardcoded values
- β Environment-specific configs
- β Secure secret management
4. Microsoft.Extensions.AI Abstractions
Using official abstractions:
- β Future-proof code
- β Community support
- β Consistent APIs
π Enhancing the Application
Want to take this further? Here are some ideas:
1. Add Streaming Responses
await foreach (var update in client.CompleteStreamingAsync(messages, options))
{
Console.Write(update.Text);
}
2. Add Conversation History
private static List<ChatMessage> _conversationHistory = new();
// Add to history after each exchange
_conversationHistory.Add(new ChatMessage(ChatRole.User, userInput));
_conversationHistory.Add(new ChatMessage(ChatRole.Assistant, response));
// Use history in next request
var allMessages = _conversationHistory.Concat(newMessages);
3. Add Function Calling
var tools = new List<ChatTool>
{
ChatTool.CreateFunctionTool(
"get_weather",
"Get the current weather",
// ... parameters
)
};
var options = new ChatOptions { Tools = tools };
4. Add RAG (Retrieval Augmented Generation)
// 1. Search your documents
var relevantDocs = await SearchDocuments(userQuestion);
// 2. Add context to system message
var context = string.Join("\n", relevantDocs);
var systemMessage = new ChatMessage(ChatRole.System,
$"Use this context to answer questions:\n{context}");
π What You've Learned
In this tutorial, you've learned:
- β Design Patterns - Provider and Factory patterns in action
- β Microsoft.Extensions.AI - Official .NET AI abstractions
- β Multi-Provider Support - Work with 4 different AI services
- β User Secrets - Secure credential management
- β Clean Architecture - Separation of concerns
- β Configuration Management - appsettings.json best practices
π Performance Considerations
Token Usage
Different providers have different costs:
| Provider | Input (1K tokens) | Output (1K tokens) |
|---|---|---|
| GitHub Models | FREE | FREE |
| OpenAI GPT-4 | $0.03 | $0.06 |
| OpenAI GPT-3.5 | $0.002 | $0.002 |
| Azure | Similar to OpenAI | Similar to OpenAI |
| Local (Ollama) | FREE | FREE |
Speed Comparison
From my testing:
| Provider | Average Response Time |
|---|---|
| Local Models | β‘ 1-2 seconds |
| GitHub Models | β‘β‘ 2-3 seconds |
| OpenAI GPT-3.5 | β‘β‘ 2-4 seconds |
| OpenAI GPT-4 | β‘β‘β‘ 5-10 seconds |
| Azure OpenAI | β‘β‘ 2-5 seconds |
π§ Troubleshooting
"Token not found" Error
# Check if secret is set
dotnet user-secrets list
# Set it if missing
dotnet user-secrets set "GitHub:Token" "ghp_your_token"
HTTP 401 (Unauthorized)
- Verify token format (GitHub:
ghp_..., OpenAI:sk-...) - Check token hasn't expired
- Regenerate token if needed
"Provider not found" Error
- Check
SelectedProviderspelling inappsettings.json - Ensure provider exists in
Providerssection - Provider names are case-sensitive
π Next Steps
Want to explore more? Check out:
- Build a Web UI - Convert to ASP.NET Core
- Add Authentication - Secure your application
- Deploy to Azure - Host in the cloud
- Add Monitoring - Application Insights
- Build a Mobile App - .NET MAUI frontend
π¦ Complete Source Code
The complete source code for this project is available on GitHub:
Repository: Rahul1994jh/genai_with_dotnet
β Please star the repository if you find it helpful!
Clone and run:
git clone https://github.com/Rahul1994jh/genai_with_dotnet.git
cd genai_with_dotnet/genai_with_dotnet/01_SimpleChat
dotnet restore
dotnet user-secrets set "GitHub:Token" "your_token"
dotnet run
π€ Contributing
Found a bug or want to add a feature? Contributions are welcome!
- Fork the repository
- Create a feature branch
- Submit a pull request
π¬ Let's Discuss!
What do you think about this approach? Have you used Microsoft.Extensions.AI? What AI provider do you prefer?
Drop a comment below! π
π Resources
- Microsoft.Extensions.AI Documentation
- GitHub Models Marketplace
- Azure OpenAI Service
- OpenAI API Documentation
- Ollama Documentation
- Design Patterns in C#
If you found this helpful, please:
- β Star the GitHub repository
- π Share this post
- π¬ Drop a comment
- π₯ Follow me for more .NET content
Happy coding! π
This is part of my **Building AI Apps with .NET* series. Stay tuned for more posts on advanced topics like RAG, function calling, and building production-ready AI applications!*
Top comments (0)