Migrating from the OpenAI SDK to Microsoft.Extensions.AI in .NET: A Step-by-Step Guide

Most .NET teams started their AI work the same way: install the official OpenAI package, call it directly from a service, ship it. That is exactly the right first move. The problem shows up six months later, when you want to add a cheaper model for classification, run a local model for the regulated tenant, cache responses, or write a unit test that does not hit a network. At that point you discover your provider SDK is threaded through your business logic, and the decision to migrate from the OpenAI SDK to Microsoft.Extensions.AI stops being architectural taste and becomes a prerequisite for everything else.
I've done this migration on services that had gone well past the point where it was comfortable, and the good news is that it is smaller than it looks. The abstraction is deliberately thin, the provider packages do most of the adapting, and you can do it endpoint by endpoint. The full migrated project, including the caching and telemetry middleware and the fake client used in tests, is on Patreon if you want the assembled version.
The whole point of the abstraction is that swapping providers becomes a registration change rather than a rewrite. Chapter 3 of AI-Powered .NET APIs builds a first AI endpoint on IChatClient and then swaps between Ollama, GitHub Models, OpenAI, and Azure OpenAI without touching the endpoint code, which is the clearest way to see what you are actually buying here.
Why Migrate?
Microsoft.Extensions.AI is to AI providers what ILogger is to logging sinks: one abstraction, many implementations, and a middleware pipeline in between. Concretely, that buys you four things.
Provider portability. OpenAI, Azure OpenAI, Ollama, GitHub Models, and others all reduce to
IChatClient. Your business logic never names a vendor.A middleware pipeline. Function invocation, logging, distributed caching, and OpenTelemetry instrumentation are composable decorators rather than code you write inside every call site.
Testability. A fake
IChatClientis trivial. Faking a concrete SDK client is not.A common vocabulary across the .NET AI stack. Microsoft Agent Framework, the evaluation libraries, and the vector data abstractions all speak these types. Staying on the raw SDK means converting at every boundary.
What it does not buy you is access to provider-specific features that have no cross-provider equivalent. That is the central trade-off, and it is covered below. If you are still choosing between the layers, our comparison of Microsoft.Extensions.AI vs Semantic Kernel vs Agent Framework is the better starting point.
What Actually Changes in Your Code
Less than you would guess. The provider client you already have becomes the transport underneath an IChatClient.
| Concept | OpenAI SDK | Microsoft.Extensions.AI |
|---|---|---|
| Client type | ChatClient |
IChatClient |
| Send a message | CompleteChat(...) |
GetResponseAsync(...) |
| Stream | CompleteChatStreaming(...) |
GetStreamingResponseAsync(...) |
| Message | UserChatMessage and friends |
ChatMessage(ChatRole.User, text) |
| Result | ChatCompletion |
ChatResponse |
| Streamed chunk | StreamingChatCompletionUpdate |
ChatResponseUpdate |
| Token usage | Provider usage object | response.Usage (UsageDetails) |
| Tools | Provider tool definitions | AIFunctionFactory.Create(...) |
One naming caution: the abstraction went through a rename before it stabilised, and older samples still show CompleteAsync and ChatCompletion. If you copy one of those you will get a compile error that looks like a missing package. We wrote up that exact confusion in Microsoft.Extensions.AI CompleteAsync not found.
The Step-by-Step Migration Path
Step 1 - add the provider adapter package. Install Microsoft.Extensions.AI plus the adapter for your provider, for example Microsoft.Extensions.AI.OpenAI. Keep the OpenAI package: the adapter builds on it rather than replacing it.
Step 2 - register IChatClient in DI. This is the seam. Everything downstream depends on the interface from here on.
// Microsoft.Extensions.AI 10.x, .NET 10
builder.Services.AddChatClient(sp =>
new OpenAIClient(builder.Configuration["OpenAI:ApiKey"])
.GetChatClient("gpt-4.1-mini")
.AsIChatClient());
Azure OpenAI is the same shape with AzureOpenAIClient and a deployment name; Ollama plugs in through a client that implements IChatClient directly. That symmetry is the payoff.
Step 3 - convert one call site. Pick the least critical endpoint and change only it. The typical before-and-after is a handful of lines:
ChatResponse response = await _chat.GetResponseAsync(
[new ChatMessage(ChatRole.System, systemPrompt),
new ChatMessage(ChatRole.User, question)],
new ChatOptions { Temperature = 0.2f, MaxOutputTokens = 500 },
ct);
Step 4 - move cross-cutting concerns into the pipeline. This is where the migration starts paying for itself. Retry logic, logging, and caching you hand-rolled around the SDK become builder calls instead:
builder.Services.AddChatClient(/* inner client */)
.UseFunctionInvocation() // automatic tool calling
.UseDistributedCache() // exact-match response cache
.UseOpenTelemetry(); // GenAI traces, token counts, latency
Delete the hand-written equivalents as you go. Leaving both in place means paying twice and debugging interleaved retries.
Step 5 - convert tools last. Function calling has the largest surface area of provider-specific behaviour, so move it once the simple paths are stable. AIFunctionFactory.Create(...) turns an ordinary method into a tool, and UseFunctionInvocation() handles the call loop. Microsoft's function calling quickstart shows the minimal shape.
Step 6 - swap the provider once, in a test. The migration is only genuinely finished when you can point the same code at a different model without touching anything outside Program.cs. Prove it before you declare victory.
Common Pitfalls
Leaving the concrete SDK type in your service signatures. If a handler takes
ChatClientrather thanIChatClient, you have added a package and gained nothing. Search for provider type names in constructor parameters after the migration.Assuming feature parity for provider extras. Reasoning-effort settings, provider-specific response formats, and preview features may not have first-class abstraction properties.
ChatOptions.AdditionalPropertiesandRawRepresentationare the documented escape hatches. Use them consciously and comment why, because each one is a portability leak.Double-handling streaming.
GetStreamingResponseAsyncyieldsChatResponseUpdatevalues that include tool-call and usage updates, not only text. Filtering only text works until you enable tools. Our walkthrough of streaming LLM responses with IChatClient covers the server-sent-events side of that properly.Forgetting that usage is now on the response. If your cost telemetry read the SDK's usage object directly, repoint it at
response.Usageor your dashboards will silently flatline.Registering the chat client as the wrong lifetime. Register the client as a singleton and let the pipeline decorators handle per-request concerns. Creating a client per request throws away connection reuse.
Migrating embeddings and chat in the same change.
IEmbeddingGeneratoris a separate abstraction with the same benefits. Do it as a second, separate pass so a regression has one obvious cause.
Verification Checklist
No provider SDK type appears in any service constructor or method signature
The same code runs against a second provider with only a registration change
Retry, caching, and telemetry exist once, in the pipeline, not also inside call sites
Token usage and cost telemetry read from
response.Usageand still populate dashboardsTool-calling endpoints have integration tests that exercise a real function invocation
A fake
IChatClientbacks the unit tests, and no test reaches the network
FAQ
Does Microsoft.Extensions.AI replace the OpenAI SDK?
No, it sits on top of it. Microsoft.Extensions.AI.OpenAI adapts the official OpenAI client to IChatClient, so you keep the SDK as the transport and program against the abstraction. Provider packages stay in your project; they just stop appearing in your application code.
Will I lose OpenAI-specific features by migrating to IChatClient?
Not entirely, but you will have to reach for them explicitly. Settings without a cross-provider equivalent go through ChatOptions.AdditionalProperties, and the underlying provider response stays reachable via RawRepresentation. Treat every use of either as a deliberate portability trade-off worth a comment, because it pins that code path to one provider.
How do I unit test code that uses IChatClient?
Implement the interface with a fake that returns canned ChatResponse values, or use the test helpers in the ecosystem. This is the single largest practical win of the migration: prompts, tool wiring, and response handling all become testable without a network call or an API key in CI.
Can I migrate to Microsoft.Extensions.AI incrementally?
Yes, and you should. Register IChatClient alongside your existing client, convert one endpoint, and let the two coexist. Because the adapter wraps the same underlying SDK client, both paths talk to the same provider with the same credentials during the transition.
What is the difference between Microsoft.Extensions.AI and Semantic Kernel?
Microsoft.Extensions.AI is the low-level abstraction layer over model providers - clients, messages, embeddings, tools. Semantic Kernel and Microsoft Agent Framework are higher-level orchestration frameworks that build on top of those abstractions. Migrating to Microsoft.Extensions.AI does not commit you to either, and it makes adopting one later much cheaper.
Do I need to change my prompts when migrating to Microsoft.Extensions.AI?
No. Prompts are strings and roles, and both map directly onto ChatMessage with a ChatRole. What can change subtly is how system messages are combined and how default options such as temperature are applied, so keep a small set of golden-output tests to confirm behaviour did not drift while the plumbing changed underneath.
About the Author
I'm Celin Daniel, Co-founder of Coding Droplets. I've been building .NET and ASP.NET Core systems in production for 13+ years - APIs, distributed backends, enterprise platforms. Everything I write here comes from real shipping experience: patterns that held up, trade-offs that bit us, and lessons learned the hard way.
GitHub: codingdroplets
YouTube: Coding Droplets
Website: codingdroplets.com






