Microsoft.Extensions.AI is the set of .NET libraries that gives every AI provider the same programming model: one IChatClient interface for chat models, one IEmbeddingGenerator interface for embeddings, and a middleware pipeline for caching, telemetry and tool calling. This guide is for C# developers who want to use it well in production, not just call a model once. You will learn the core types, streaming, tool calling, structured output, dependency injection, provider adapters, custom middleware and how to test code that depends on a language model.

What Is Microsoft.Extensions.AI?#

Microsoft.Extensions.AI (MEAI) plays the same role for generative AI that ILogger plays for logging: a small, stable abstraction that libraries and applications agree on, with provider-specific implementations plugged in underneath. It was built by the .NET team together with the Semantic Kernel team, and its core interfaces were extracted from Semantic Kernel so that the whole ecosystem could share them. Today Microsoft Agent Framework, the MCP C# SDK, the AI project templates and the vector data libraries all build on these types.

The functionality ships in two main packages:

  • Microsoft.Extensions.AI.Abstractions contains the exchange types: IChatClient, IEmbeddingGenerator<TInput, TEmbedding>, ChatMessage, ChatOptions, ChatResponse, content types and tool types. Libraries that implement a provider usually reference only this package.
  • Microsoft.Extensions.AI adds the higher-level utilities: ChatClientBuilder, middleware such as FunctionInvokingChatClient and OpenTelemetryChatClient, dependency injection helpers and structured output extensions. Applications normally reference this package plus one or more provider adapters.

The libraries became stable in May 2025 with version 9.5.0, moved to the 10.x line with .NET 10 in November 2025, and ship roughly monthly; 10.10.0 was released in September 2026. They target .NET Standard 2.0 as well as modern .NET, so they work on .NET 8, 9 and 10 and even in .NET Framework applications. Newer areas are marked experimental and raise the MEAI001 diagnostic until they stabilize: image generation (IImageGenerator), speech to text, text to speech, real-time clients, chat history reduction and chat routing with failover.

How Microsoft.Extensions.AI Works#

The core contract is deliberately small. IChatClient has two request methods and a service-lookup method:

  • GetResponseAsync sends a list of messages and returns a complete ChatResponse.
  • GetStreamingResponseAsync sends the same input and returns an IAsyncEnumerable<ChatResponseUpdate> that yields partial results as the model produces them.
  • GetService lets callers retrieve metadata or the underlying provider object, such as ChatClientMetadata or the raw OpenAI client.

Everything else is data. The following table summarizes the types you will use daily.

TypePurposeKey members
ChatMessageOne message in a conversationRole, Contents, Text, AuthorName
ChatRoleWho produced the messageSystem, User, Assistant, Tool
AIContent subclassesMultimodal message partsTextContent, DataContent, UriContent, FunctionCallContent, FunctionResultContent, TextReasoningContent, UsageContent
ChatOptionsPer-request settingsInstructions, ModelId, Temperature, MaxOutputTokens, Tools, ToolMode, ResponseFormat, Reasoning, ConversationId
ChatResponseComplete resultMessages, Text, Usage, FinishReason, ModelId, ConversationId
ChatResponseUpdateOne streamed chunkText, Contents, FinishReason
AIFunctionA tool the model may callcreated with AIFunctionFactory.Create

Two design choices are worth understanding early. First, the interface supports both stateless and stateful services. With a stateless service you send the full history on every call. With a stateful service, such as the OpenAI Responses API, the service keeps history and returns a ConversationId that you pass back through ChatOptions.ConversationId, so you only send new messages. Second, any parameter a provider supports but the abstraction does not model can still reach the provider through ChatOptions.AdditionalProperties or RawRepresentationFactory, so the abstraction never blocks you from provider features.

Getting Started#

Install the package and a provider adapter. The OpenAI adapter works with OpenAI, with Azure OpenAI through the v1 endpoint, and with other OpenAI-compatible servers:

Bash
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI

A console chat loop needs only an IChatClient, a history list and the AddMessages helper, which appends every message from the response, including tool calls, back to the history:

C#
using Microsoft.Extensions.AI;

// Fully qualify OpenAI.Chat.ChatClient: that namespace also defines a ChatMessage type.
IChatClient client = new OpenAI.Chat.ChatClient("gpt-5-mini",
    Environment.GetEnvironmentVariable("OPENAI_API_KEY")!).AsIChatClient();

List<ChatMessage> history =
[
    new(ChatRole.System, "You are a concise assistant for .NET developers."),
];

while (Console.ReadLine() is { Length: > 0 } input)
{
    history.Add(new ChatMessage(ChatRole.User, input));

    ChatResponse response = await client.GetResponseAsync(history);
    Console.WriteLine(response.Text);

    history.AddMessages(response);
}

ChatResponse.Text concatenates the text content of all response messages, which is what you want for display. When you need details, inspect response.Messages, response.Usage for token counts or response.FinishReason to detect truncation.

Streaming Responses with GetStreamingResponseAsync#

Streaming is the default user experience for chat, because it shows the first tokens after a fraction of a second instead of making the user wait for the entire answer. GetStreamingResponseAsync accepts exactly the same inputs as GetResponseAsync, and C# await foreach makes consumption natural:

C#
List<ChatResponseUpdate> updates = [];

await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync(
    history, new ChatOptions { MaxOutputTokens = 800 }, cancellationToken))
{
    Console.Write(update.Text);
    updates.Add(update);
}

// Rebuild a complete response from the chunks for history, logging and usage.
ChatResponse full = updates.ToChatResponse();
history.AddMessages(full);
Console.WriteLine($"\nFinish reason: {full.FinishReason}, tokens: {full.Usage?.TotalTokenCount}");

Collecting the updates matters. Tool calls, usage information and the finish reason arrive as content inside the updates, and ToChatResponse merges them into one coherent ChatResponse. Always pass a CancellationToken from the HTTP request or UI, so that an abandoned request stops consuming tokens.

Controlling Requests with ChatOptions and Multimodal Content#

ChatOptions carries the common parameters as strongly typed properties. Instructions adds system-level guidance without polluting your stored history, Reasoning controls effort on reasoning models, and ModelId overrides the default model for a single call. Messages can mix text with images, audio or documents through DataContent (bytes plus media type) and UriContent (a URL):

C#
byte[] screenshot = await File.ReadAllBytesAsync("error-dialog.png", cancellationToken);

ChatMessage question = new(ChatRole.User,
[
    new TextContent("What is the likely root cause of this error? Answer in two sentences."),
    new DataContent(screenshot, "image/png"),
]);

ChatOptions options = new()
{
    Instructions = "You are a senior .NET support engineer.",
    Temperature = 0.2f,
    MaxOutputTokens = 300,
    Reasoning = new ReasoningOptions { Effort = ReasoningEffort.Low },
};

ChatResponse answer = await client.GetResponseAsync([question], options, cancellationToken);

Not every provider or model supports every option. Adapters ignore or translate settings they cannot honor, so test with the models you actually deploy, especially for temperature, which some reasoning models reject.

Tool Calling with AIFunctionFactory and UseFunctionInvocation#

Tool calling lets the model request that your code run a function, then use the result to finish its answer. MEAI splits the job in two. AIFunctionFactory.Create turns a .NET method or lambda into an AIFunction with a JSON schema derived from its parameters and [Description] attributes. FunctionInvokingChatClient, added with UseFunctionInvocation, runs the loop: it detects function call requests, invokes your functions, sends the results back and repeats until the model produces a final answer.

C#
using System.ComponentModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

// appServices is the application's root IServiceProvider, for example app.Services.
IChatClient toolClient = client.AsBuilder()
    .UseFunctionInvocation(configure: f =>
    {
        f.MaximumIterationsPerRequest = 5;  // cap runaway tool loops
        f.AllowConcurrentInvocation = true; // run independent calls in parallel
        f.IncludeDetailedErrors = false;    // do not leak exception text to the model
    })
    .Build(appServices);

ChatOptions options = new()
{
    Tools = [AIFunctionFactory.Create(GetInvoiceTotalAsync)],
};

ChatResponse result = await toolClient.GetResponseAsync(
    "How much is outstanding on invoice INV-2026-0042?", options);

[Description("Gets the outstanding amount, in euros, for an invoice number.")]
static async Task<decimal> GetInvoiceTotalAsync(
    [Description("Invoice number such as INV-2026-0042.")] string invoiceNumber,
    IServiceProvider services,              // bound at invocation, hidden from the model
    CancellationToken cancellationToken)    // bound to the request token
{
    await using AsyncServiceScope scope = services.CreateAsyncScope();
    var invoices = scope.ServiceProvider.GetRequiredService<IInvoiceRepository>();
    return await invoices.GetOutstandingAmountAsync(invoiceNumber, cancellationToken);
}

Parameters of type IServiceProvider, AIFunctionArguments and CancellationToken are special: MEAI binds them at invocation time and leaves them out of the schema the model sees. The provider is the one the pipeline was built with, usually the root container, so create a scope inside the tool when it needs scoped services such as a DbContext.

For tools with side effects, wrap the function in ApprovalRequiredAIFunction. The invoking client then returns a ToolApprovalRequestContent instead of running the tool, and your application resumes the conversation with the user's decision. Tools returned by an MCP client derive from AIFunction, so remote MCP tools plug into the same Tools list.

Structured Output with GetResponseAsync of T#

Many features need data, not prose. The generic GetResponseAsync<T> extension generates a JSON schema from T, asks the model to comply (using the provider's native JSON schema support where available) and deserializes the result:

C#
public enum Sentiment { Positive, Neutral, Negative }

public sealed record ReviewAnalysis(
    Sentiment Sentiment,
    string Summary,
    string[] ProductsMentioned);

ChatResponse<ReviewAnalysis> analysis = await client.GetResponseAsync<ReviewAnalysis>(
    $"Analyze this customer review:\n{reviewText}", cancellationToken: cancellationToken);

if (analysis.TryGetResult(out ReviewAnalysis? review))
{
    Console.WriteLine($"{review.Sentiment}: {review.Summary}");
}

TryGetResult returns false instead of throwing when the model's output does not deserialize, which is the right default for untrusted model output. ChatResponse<T> still exposes the raw messages and usage, so you can log failures and retry.

Building a Middleware Pipeline with ChatClientBuilder#

Middleware is where MEAI earns its place in production code. Every decorator is itself an IChatClient that wraps an inner client, so cross-cutting concerns compose without touching application code. The built-in components cover the common needs:

  • UseDistributedCache caches complete responses in any IDistributedCache implementation, keyed on the messages and options.
  • UseFunctionInvocation runs tool loops, as shown earlier.
  • UseOpenTelemetry emits traces and metrics that follow the OpenTelemetry semantic conventions for generative AI, including token usage.
  • UseLogging writes requests and responses to ILogger.
  • ConfigureOptions sets defaults, such as a model ID, on every request.

Order matters. The first Use call becomes the outermost layer, and each subsequent call wraps the ones after it around the inner provider client:

C#
IChatClient pipeline = new ChatClientBuilder(innerClient)
    .UseDistributedCache(cache)          // 1. outermost: cache hits skip everything below
    .UseFunctionInvocation()             // 2. tool loop runs inside the cache boundary
    .UseOpenTelemetry(sourceName: "Contoso.Support",
        configure: o => o.EnableSensitiveData = false)
    .UseLogging(loggerFactory)           // 4. logs every underlying model round trip
    .ConfigureOptions(o => o.ModelId ??= "gpt-5-mini")
    .Build();

With this order, a cached answer returns immediately, a cache miss runs the entire tool loop, and telemetry records each individual model call inside that loop. Move UseOpenTelemetry above UseFunctionInvocation if you prefer one span per user request instead of one per model round trip. Keep EnableSensitiveData off in production unless your telemetry backend is approved for prompt and response content.

Dependency Injection: AddChatClient and Keyed Clients#

In ASP.NET Core and generic-host apps, register clients with AddChatClient and AddEmbeddingGenerator. Both return a builder, so the middleware chain reads the same as in the standalone case, and middleware can resolve services such as IDistributedCache and ILoggerFactory from the container. Keyed registrations let different features use different models:

C#
using Microsoft.Extensions.AI;
using OpenAI;

var builder = WebApplication.CreateBuilder(args);
var openAI = new OpenAIClient(builder.Configuration["OpenAI:ApiKey"]!);

builder.Services.AddStackExchangeRedisCache(o =>
    o.Configuration = builder.Configuration.GetConnectionString("redis"));

builder.Services.AddChatClient(openAI.GetChatClient("gpt-5").AsIChatClient())
    .UseDistributedCache()
    .UseFunctionInvocation()
    .UseOpenTelemetry();

builder.Services.AddKeyedChatClient("fast", openAI.GetChatClient("gpt-5-mini").AsIChatClient())
    .UseOpenTelemetry();

builder.Services.AddEmbeddingGenerator(
        openAI.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator())
    .UseOpenTelemetry();

var app = builder.Build();

app.MapPost("/tickets/classify", async (
    TicketRequest ticket,
    [FromKeyedServices("fast")] IChatClient fastClient,
    CancellationToken ct) =>
{
    var result = await fastClient.GetResponseAsync<TicketCategory>(ticket.Body,
        cancellationToken: ct);
    return result.TryGetResult(out var category) ? Results.Ok(category) : Results.StatusCode(502);
});

app.Run();

public sealed record TicketRequest(string Body);
public sealed record TicketCategory(string Area, bool Urgent);

Registrations default to singleton lifetime, which suits provider clients because they are thread-safe and reuse HTTP connections. Both methods accept an optional ServiceLifetime if a pipeline must be scoped.

Provider Implementations: OpenAI, Azure OpenAI, Ollama and More#

An IChatClient comes either from an adapter extension method or from a provider library that implements the interface directly.

ProviderPackageHow you get an IChatClientNotes
OpenAIMicrosoft.Extensions.AI.OpenAIchatClient.AsIChatClient()Also EmbeddingClient.AsIEmbeddingGenerator(); Responses API adapter is experimental (OPENAI001)
Azure OpenAI in Microsoft FoundryMicrosoft.Extensions.AI.OpenAIOpenAI ChatClient with the /openai/v1/ endpointRecommended path for Azure, supports Entra ID
Azure AI InferenceMicrosoft.Extensions.AI.AzureAIInferenceChatCompletionsClient.AsIChatClient(model)Underlying SDK is retired; last adapter preview in November 2025
OllamaOllamaSharpnew OllamaApiClient(uri, model) implements IChatClientMicrosoft.Extensions.AI.Ollama is deprecated in its favor
ONNX Runtime GenAIMicrosoft.ML.OnnxRuntimeGenAInew OnnxRuntimeGenAIChatClient(modelPath)In-process local models

The Azure AI Inference row deserves attention because many 2024 and 2025 samples use it. Microsoft's Foundry documentation now describes the Azure AI Inference SDK as retired and recommends the OpenAI SDK against the v1 endpoint, which also serves non-OpenAI Foundry models that support the chat completions API. Switching an existing MEAI application is a one-line change at composition time:

C#
#pragma warning disable OPENAI001
using System.ClientModel.Primitives;
using Azure.Identity;
using Microsoft.Extensions.AI;
using OllamaSharp;
using OpenAI;
using OpenAI.Chat;

// Azure OpenAI in Microsoft Foundry, keyless via Entra ID.
IChatClient azure = new ChatClient(
        model: "gpt-5-mini", // deployment name
        authenticationPolicy: new BearerTokenPolicy(
            new DefaultAzureCredential(), "https://ai.azure.com/.default"),
        options: new OpenAIClientOptions
        {
            Endpoint = new Uri("https://contoso-ai.openai.azure.com/openai/v1/")
        })
    .AsIChatClient();

// Local development with Ollama.
IChatClient local = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.2");

Community and vendor packages provide implementations for other providers, including Google Gemini and Amazon Bedrock. Because they all implement the same interface, the rest of your pipeline does not change.

Embeddings with IEmbeddingGenerator#

IEmbeddingGenerator<TInput, TEmbedding> generates vectors for semantic search, clustering and retrieval-augmented generation. For text, the common shape is IEmbeddingGenerator<string, Embedding<float>>. It batches inputs, supports options such as Dimensions for models that can shorten vectors, and composes with its own middleware through EmbeddingGeneratorBuilder:

C#
IEmbeddingGenerator<string, Embedding<float>> generator =
    new EmbeddingGeneratorBuilder<string, Embedding<float>>(
            openAI.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator())
        .UseDistributedCache(cache)   // identical inputs are embedded once
        .UseOpenTelemetry()
        .Build();

GeneratedEmbeddings<Embedding<float>> batch = await generator.GenerateAsync(
    ["Reset a user's password", "Rotate an API key", "Configure SSO"],
    new EmbeddingGenerationOptions { Dimensions = 512 });

ReadOnlyMemory<float> queryVector = await generator.GenerateVectorAsync(
    "How do I change my login?", new EmbeddingGenerationOptions { Dimensions = 512 });

Use the same model and the same dimension setting for documents and queries; vectors from different models or sizes are not comparable. In most applications you pass the generator to a Microsoft.Extensions.VectorData store and let it embed records and queries for you, as described in Embeddings and Vector Databases in .NET.

Writing a Custom DelegatingChatClient#

When the built-in middleware is not enough, derive from DelegatingChatClient. It forwards every call to InnerClient, so you override only what you need. The following middleware removes email addresses from user messages before they leave your network, a common requirement when prompts may contain customer data:

C#
using System.Text.RegularExpressions;
using Microsoft.Extensions.AI;

public sealed partial class EmailRedactingChatClient(IChatClient innerClient)
    : DelegatingChatClient(innerClient)
{
    public override Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages,
        ChatOptions? options = null, CancellationToken cancellationToken = default) =>
        base.GetResponseAsync(Redact(messages), options, cancellationToken);

    public override IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages, ChatOptions? options = null,
        CancellationToken cancellationToken = default) =>
        base.GetStreamingResponseAsync(Redact(messages), options, cancellationToken);

    private static List<ChatMessage> Redact(IEnumerable<ChatMessage> messages) =>
        [.. messages.Select(message =>
        {
            if (message.Role != ChatRole.User) return message;

            ChatMessage copy = message.Clone();
            copy.Contents = [.. copy.Contents.Select(content => content is TextContent text
                ? new TextContent(EmailPattern().Replace(text.Text, "[email]"))
                : content)];
            return copy;
        })];

    [GeneratedRegex(@"[\w.+-]+@[\w-]+(\.[\w-]+)+")]
    private static partial Regex EmailPattern();
}

public static class EmailRedactionExtensions
{
    public static ChatClientBuilder UseEmailRedaction(this ChatClientBuilder builder) =>
        builder.Use(inner => new EmailRedactingChatClient(inner));
}

The redaction works on copies, so the caller's history is not modified, and it only replaces TextContent, so images and tool results pass through untouched. The Use* extension method is the convention that makes custom middleware feel native: consumers add it with one line in their builder chain. For lightweight cases, ChatClientBuilder.Use also accepts a delegate that runs around both the streaming and non-streaming paths, which avoids writing a class at all.

Testing Code That Depends on IChatClient#

Because consumers depend on an interface, unit tests do not need a network or a model. A small fake that returns canned responses and records requests covers most business logic:

C#
using System.Runtime.CompilerServices;
using Microsoft.Extensions.AI;

public sealed class FakeChatClient(params string[] replies) : IChatClient
{
    private readonly Queue<string> _replies = new(replies);

    public List<ChatMessage[]> Requests { get; } = [];

    public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages,
        ChatOptions? options = null, CancellationToken cancellationToken = default)
    {
        Requests.Add([.. messages]);
        string reply = _replies.Count > 0 ? _replies.Dequeue() : "";
        return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, reply)));
    }

    public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages, ChatOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        ChatResponse response = await GetResponseAsync(messages, options, cancellationToken);
        foreach (ChatResponseUpdate update in response.ToChatResponseUpdates())
        {
            yield return update;
        }
    }

    public object? GetService(Type serviceType, object? serviceKey = null) =>
        serviceKey is null && serviceType.IsInstanceOfType(this) ? this : null;

    public void Dispose() { }
}

public sealed class TicketClassifier(IChatClient chatClient)
{
    public async Task<TicketCategory> ClassifyAsync(string body, CancellationToken ct = default)
    {
        var response = await chatClient.GetResponseAsync<TicketCategory>(
            $"Classify this support ticket:\n{body}", cancellationToken: ct);

        return response.TryGetResult(out TicketCategory? category)
            ? category
            : throw new InvalidOperationException("The model returned an unexpected format.");
    }
}

public sealed class TicketClassifierTests
{
    [Fact]
    public async Task Classifies_double_charge_as_billing()
    {
        var fake = new FakeChatClient("""{"area":"Billing","urgent":true}""");
        var classifier = new TicketClassifier(fake);

        TicketCategory category = await classifier.ClassifyAsync("I was charged twice!");

        Assert.Equal("Billing", category.Area);
        Assert.Contains("charged twice", fake.Requests[0][^1].Text);
    }
}

Fakes verify your code: prompt construction, parsing, error handling and branching. They say nothing about whether the model gives good answers. For that, add evaluation tests with the Microsoft.Extensions.AI.Evaluation libraries, which score real responses for relevance, groundedness and other metrics and can cache responses so repeated CI runs stay fast and cheap.

Microsoft.Extensions.AI vs Provider SDKs vs Agent Frameworks#

MEAI is not the only way to call a model from .NET. The table shows when each layer is the right tool.

CriterionMicrosoft.Extensions.AIProvider SDK only (for example OpenAI)Microsoft Agent Framework
Primary purposeProvider-neutral model access and middlewareFull access to one provider's APIAgents, sessions and multi-agent workflows
Provider portabilityHighNoneHigh (built on IChatClient)
Newest provider featuresUsually available, or through raw optionsImmediateThrough the underlying chat client
Middleware and DIBuilt inManualBuilt in, plus agent middleware
TestabilityEasy with fakesHarder, needs mocks of concrete clientsEasy through IChatClient and AIAgent
Best forMost app features, libraries, reusable componentsProvider-specific features such as audio or fine-tuningGoal-directed, multi-step, tool-heavy scenarios

A common production mix is MEAI everywhere, the provider SDK for the few features the abstraction does not cover, and Agent Framework where orchestration is the real problem. For a map of the whole ecosystem, see AI in .NET: The Complete Landscape.

Best Practices#

  • Depend on IChatClient in your services. Construct provider clients only at the composition root, so switching providers or models is configuration.
  • Always propagate cancellation. Pass request tokens into every GetResponseAsync and streaming loop to stop paying for abandoned requests.
  • Cap tool loops. Set MaximumIterationsPerRequest and keep tool sets small and specific; large tool lists reduce selection accuracy and raise token costs.
  • Keep secrets and stack traces away from the model. Leave IncludeDetailedErrors off in production and return safe, descriptive error strings from tools instead.
  • Instrument and cache deliberately. Use OpenTelemetry for every client, and cache only deterministic, non-personalized prompts.
  • Prefer TryGetResult for structured output. Treat model JSON as untrusted input and validate business rules after deserialization.
  • Isolate experimental APIs. Suppress MEAI001 only in the files that use experimental features.

Common Pitfalls#

  • Forgetting to add response messages to history. Appending only the text, instead of calling AddMessages, drops tool call and result messages and confuses the next turn.
  • Mixing up ChatMessage types. The OpenAI library has its own ChatMessage; importing both namespaces causes ambiguity. Prefer MEAI types in application code and alias when necessary.
  • Wrong middleware order. Placing caching inside the tool loop, or telemetry outside it when you wanted per-call spans, produces confusing behavior. Decide what each layer should see.
  • Resending history to a stateful service. When a response returns a ConversationId, pass it back in ChatOptions and send only new messages, or you pay for the same tokens twice.
  • Embedding with mismatched models. Documents and queries embedded with different models or dimensions silently produce meaningless similarity scores.
  • Building on deprecated adapters. Microsoft.Extensions.AI.Ollama is deprecated in favor of OllamaSharp, and the Azure AI Inference SDK is retired.

Frequently Asked Questions#

Is Microsoft.Extensions.AI a replacement for Semantic Kernel?#

It replaces the lowest layer of Semantic Kernel, the model connectors, and Semantic Kernel itself now builds on it. Orchestration features such as agents and workflows live in Microsoft Agent Framework, which is the successor to Semantic Kernel and also builds on IChatClient.

Does Microsoft.Extensions.AI support the OpenAI Responses API?#

Yes. The OpenAI adapter provides AsIChatClient for the Responses client in addition to chat completions. That adapter is marked experimental with the OPENAI001 diagnostic because the underlying Responses API surface in the OpenAI .NET library is still experimental.

How do I use a different model per request?#

Set ChatOptions.ModelId on the request, or use ConfigureOptions in the pipeline to supply a default that individual calls can override. For different providers or cost tiers, register keyed clients and inject them with [FromKeyedServices].

Can I call tools without the automatic invocation loop?#

Yes. Omit UseFunctionInvocation, inspect the response for FunctionCallContent, run the functions yourself and add FunctionResultContent messages before calling the model again. This is useful when every tool call needs custom authorization, auditing or user confirmation.

How should I test prompts and answer quality?#

Use fakes for unit tests of your own logic, and use the Microsoft.Extensions.AI.Evaluation libraries for quality. The evaluators score real responses for metrics such as relevance, completeness and groundedness, and response caching keeps repeated CI runs affordable.

Summary#

  • IChatClient and IEmbeddingGenerator give every provider one programming model, and the wider .NET AI ecosystem builds on them.
  • Streaming, multimodal input, tool calling and structured output are first-class, with GetResponseAsync<T> returning typed results.
  • ChatClientBuilder middleware adds caching, tool loops, OpenTelemetry and logging, and the order of Use calls defines behavior.
  • Register clients with AddChatClient, AddKeyedChatClient and AddEmbeddingGenerator for idiomatic dependency injection.
  • For Azure, use the OpenAI adapter with the v1 endpoint; for local models, use OllamaSharp or ONNX Runtime GenAI.
  • Write custom behavior as a DelegatingChatClient with a Use* extension, and test with fakes plus evaluation.

Further Reading#