Introduction
Modern .NET applications rarely rely on a single AI provider. Organizations may use Azure OpenAI for enterprise compliance, OpenAI for advanced reasoning, local models for sensitive workloads, or other providers to optimize cost and availability. Hardcoding a single provider into your application makes it difficult to adapt when requirements change or a provider becomes unavailable.
The Microsoft.Extensions.AI library provides a common abstraction for integrating AI capabilities into .NET applications. By programming against shared interfaces instead of provider-specific SDKs, you can build applications that support provider switching, failover, testing, and future extensibility with minimal code changes.
In this article, you'll learn production-ready patterns for implementing provider switching using Microsoft.Extensions.AI, along with best practices, common pitfalls, and architectural guidance.
What Is Microsoft.Extensions.AI?
Microsoft.Extensions.AI is a .NET library that provides common abstractions for AI services. Rather than coupling your application to a specific vendor, you code against interfaces that multiple providers can implement.
Some benefits include:
Provider-independent application code
Easier testing with mock implementations
Simplified migration between AI providers
Centralized dependency injection
Cleaner architecture
Instead of referencing provider SDKs throughout your codebase, only the infrastructure layer needs provider-specific implementations.
Why Provider Switching Matters
Provider switching is valuable in several production scenarios:
| Scenario | Benefit |
|---|
| Provider outage | Automatically fail over to another provider |
| Cost optimization | Route requests to lower-cost models when appropriate |
| Compliance requirements | Use approved providers for regulated workloads |
| Performance optimization | Choose providers based on latency or workload |
| Development and testing | Replace cloud models with local or mock implementations |
Without abstraction, switching providers often requires changes throughout the application.
Architecture Overview
A typical architecture looks like this:
Application
│
Business Services
│
IChatClient
│
Provider Router
│
┌────────────┬───────────────┬─────────────┐
│ │ │
Azure AI OpenAI Local Model
Business services communicate only with the abstraction, not directly with provider SDKs.
Registering AI Services
Using dependency injection keeps your application loosely coupled.
builder.Services.AddSingleton<IChatClient>(serviceProvider =>
{
return CreateChatClient();
});
The registration method can determine which provider should be used based on configuration or runtime policies.
Using Configuration for Provider Selection
Store the active provider in configuration instead of hardcoding it.
appsettings.json
{
"AI": {
"Provider": "AzureOpenAI"
}
}
Configuration-driven provider selection allows changes without recompiling the application.
Creating a Provider Factory
A factory centralizes provider creation.
public interface IChatClientFactory
{
IChatClient Create(string providerName);
}
Implementation details remain isolated from the rest of the application.
Runtime Provider Switching
Applications may need different providers for different workloads.
For example:
| Request Type | Preferred Provider |
|---|
| Customer Support | Azure OpenAI |
| Internal Assistant | Local Model |
| Code Generation | OpenAI |
| Document Analysis | Azure OpenAI |
Instead of a single provider, a routing service can evaluate request metadata and choose the appropriate implementation.
Implementing a Router
public interface IAiRouter
{
IChatClient GetClient(string workload);
}
The router becomes responsible for selecting the correct provider.
Business services remain unaware of vendor-specific logic.
Implementing Failover
Production systems should anticipate temporary provider failures.
A common strategy is:
Attempt the primary provider.
Retry transient failures.
Switch to a secondary provider if necessary.
Log the failover event.
Return the successful response.
Pseudo-code:
try
{
return await primaryClient.CompleteAsync(prompt);
}
catch (TransientException)
{
return await secondaryClient.CompleteAsync(prompt);
}
Only retry transient errors such as temporary network issues or rate limiting. Validation errors should generally not be retried.
Feature Flags for Provider Rollout
Feature flags allow gradual adoption of a new provider.
Example rollout:
10% of requests → New provider
50% of requests → New provider
100% of requests → New provider
This approach reduces deployment risk and makes rollback straightforward if issues arise.
Dependency Injection Best Practices
Prefer constructor injection over creating providers directly.
public class ChatService
{
private readonly IChatClient _client;
public ChatService(IChatClient client)
{
_client = client;
}
}
This keeps the service easy to test and avoids tight coupling.
Observability
Monitor provider behavior to support operational decisions.
Useful metrics include:
These metrics can reveal when a provider is becoming unreliable or expensive.
Comparison of Provider Selection Strategies
| Strategy | Advantages | Drawbacks |
|---|
| Configuration-based | Simple to implement | Requires configuration change to switch |
| Feature flags | Safe incremental rollout | Additional operational tooling |
| Rule-based routing | Optimized per workload | More complex routing logic |
| Health-based failover | Improves availability | Requires health monitoring |
| Cost-aware routing | Optimizes spending | Needs accurate cost data |
Common Mistakes
| Mistake | Recommended Approach |
|---|
| Using provider SDKs throughout the application | Depend on shared abstractions |
| Hardcoding provider names | Use configuration or routing services |
| Ignoring transient failures | Implement retries and failover |
| Switching providers manually | Automate routing where appropriate |
| Mixing business logic with provider logic | Keep provider logic in infrastructure |
Troubleshooting
Provider Not Switching
Verify:
Frequent Failovers
Check:
Inconsistent Responses
Different providers may vary in output style, supported features, and response formats. Validate behavior before switching production traffic.
Best Practices
Code against Microsoft.Extensions.AI abstractions rather than provider SDKs.
Centralize provider selection in a router or factory.
Use dependency injection for all AI services.
Externalize provider configuration.
Monitor latency, failures, and usage.
Test failover paths regularly.
Keep provider-specific code isolated from business logic.
Conclusion
Microsoft.Extensions.AI helps .NET developers build applications that are flexible, maintainable, and resilient. By abstracting AI providers behind common interfaces, you can adopt new models, implement failover, optimize costs, and meet compliance requirements without rewriting core business logic.
A combination of dependency injection, provider factories, routing policies, configuration-based selection, and comprehensive observability creates a robust foundation for enterprise AI applications that can evolve alongside a rapidly changing AI ecosystem.
Frequently Asked Questions
Can I use multiple AI providers in the same application?
Yes. Microsoft.Extensions.AI is designed to support provider-independent abstractions, allowing different providers to be used for different workloads or as fallback options.
Does provider switching require application redeployment?
Not necessarily. If provider selection is driven by configuration or feature flags, changes can often be made without modifying application code.
Should every request use automatic failover?
Not always. Failover is most useful for transient infrastructure issues. For requests that depend on provider-specific capabilities or output consistency, explicit routing may be more appropriate.
Is Microsoft.Extensions.AI intended to replace provider SDKs?
No. It provides common abstractions that reduce coupling. Provider-specific SDKs are still used by the infrastructure layer where needed, while application code interacts with the shared interfaces.