This tutorial was written by Markus Wildgruber.
In October 2025, Microsoft announced a new iteration of their AI Agent framework, called Microsoft Agent Framework. General Availability of the framework was announced in April 2026.
This framework is directed at C# and Python developers and seeks to consolidate the previous frameworks, Semantic Kernel, and AutoGen. If you have used Semantic Kernel before, some of the interfaces will be familiar to you, but especially the RAG part has changed a bit. This is what we will focus on throughout this article.
The sample: a Movie Recommendation Agent
In their sample_mflix-database, MongoDB provides a collection embedded_movies that already has embeddings created for a part of their movies collection. Equipped with this dataset, we can easily create a Movie Recommendation Agent that queries the collection using a vector search and provides answers grounded in this data.
To run this sample, you need the following components:
- An instance of the MongoDB
sample_mflixdatabase with theembedded_moviescollection. Please see this link for instructions on creating a free MongoDB Atlas M0 cluster. This article shows how to load the sample data into your cluster. - A vector search index named
defaulton theembedded_moviescollection. You can create this index in the MongoDB Atlas UI or have the sample code create it for you. Please note that creating the index will take a small amount of time. Use the following parameters when creating the index manually:- Vector similarity: Cosine
- Dimensions: 1536 (the embeddings have been generated with
text-embedding-ada-002that yields vectors with 1536 dimensions)
- A large language model (LLM) that is used for generating chat messages. This model must be capable of using tools. In this sample, we have used a deployment of OpenAI's gpt-4.1 model hosted on Azure OpenAI.
- An embedding model that is used to vectorize the queries that the LLM generates when retrieving data for its chat messages. As the embeddings in the collection have been created using
text-embedding-ada-002, we also need to use this model. In our case, this model was also hosted on Azure OpenAI.
The sample uses Blazor to create a basic user interface for the agent and shows two methods on how to put together the vector search:
- Using MongoDB C# Driver directly
- Using MongoDB Vector Store Connector
You can clone the repository with the sample code from Github. After downloading the sample, you can open it in Visual Studio Code or Visual Studio. To configure the secrets, you can use dotnet user-secrets and set at least the following configuration settings for the database connection string, the Azure OpenAI endpoint, and the name of the chat deployment:
dotnet user-secrets set MongoDB:ConnectionString "mongodb+srv://..."
dotnet user-secrets set Endpoint "https://..."
dotnet user-secrets set ChatDeployment "gpt-4.1"
The following two settings are optional:
dotnet user-secrets set MongoDB:Database "sample_mflix"
dotnet user-secrets set EmbeddingDeployment "text-embedding-ada-002"
A central piece is our Movie class that maps the MongoDB documents to a C# POCO:
using Microsoft.Extensions.VectorData;
using MongoDB.Bson.Serialization.Attributes;
namespace MongoDBSamples;
[BsonIgnoreExtraElements]
public record Movie(
[property: BsonRepresentation(
MongoDB.Bson.BsonType.ObjectId),
VectorStoreKey()] string Id,
[property: BsonElement("title")] string Title,
[property: BsonElement("plot")] string Plot,
[property: BsonElement("fullplot")] string FullPlot,
[property: BsonElement("cast")] List<string> Cast,
[property: BsonElement("genres")] List<string> Genres,
[property: BsonElement("plot_embedding"), VectorStoreVector(1536)]
ReadOnlyMemory<float> PlotEmbedding
);
As you can see, the definition contains both attributes for declarative MongoDB mappings and also an attribute for the Vector Store implementation.
Setting up the clients
The first step when creating an agent with Microsoft Agent Framework is to set up a ChatClient instance. In our case, we will use dependency injection to set up singleton instances of AzureOpenAIClient and ChatClient:
builder.Services.AddSingleton<AzureOpenAIClient>(prov =>
new(new Uri(cfg.Endpoint), new DefaultAzureCredential())
);
builder.Services.AddSingleton<ChatClient>(prov =>
prov.GetRequiredService<AzureOpenAIClient>().GetChatClient(cfg.ChatDeployment)
);
In this case, it is a two-step process: first, we create an instance of AzureOpenAIClient, which is initialized with our configuration settings and uses a DefaultAzureCredential for access (in a production scenario, we would use a more fine-grained approach to explicitly use the authentication method of your choosing). Your user needs to be authenticated with Azure and have access to the models in Azure OpenAI for access to work.
Afterwards, we create the ChatClient instance that is the centerpiece of our agent.
In addition, we need an EmbeddingClient to generate the vectors for our queries:
builder.Services.AddSingleton<EmbeddingClient>(prov =>
prov.GetRequiredService<AzureOpenAIClient>().GetEmbeddingClient(cfg.EmbeddingDeployment)
);
In addition, we register singleton instances of IMongoClient, IMongoDatabase and IMongoCollection<Movie> with the IoC-container. Please note that a vector search index named default is created when requesting the IMongoCollection<Movie> instance, in case it does not exist yet. This is to simplify running the sample, but the index should be created explicitly in a production scenario. Should the index be created, wait a bit for the index to be ready before hitting it with queries.
If you are working on a MongoDB Atlas M0 cluster, there is a limit on the number of search indexes you can create. If you already have three search indexes in your cluster, you need to remove one to run the sample.
Movie recommendation service implementation
In order to reuse the common parts of the Movie Recommendation Service, there is an interface IMovieRecommendationService that is implemented in the following class hierarchy:
Both approaches demonstrated in the following chapters share a lot of common code, e.g., the need to keep the chat history and the common steps for asking the agent for a recommendation. By creating a class hierarchy, the services can share a general approach and only have their specific logic for finding the relevant context via vector search.
Program.cs also contains the keyed registrations for the instances of the relevant services:
-
MongoDBVectorSearchMovieRecommendationServiceperforms a vector search directly with the tools of the MongoDB C# Driver. -
MongoDBVectorStoreMovieRecommendationServiceuses the MongoDB Vector Store Connector that has been created for Microsoft Semantic Kernel. So if you are already using this approach, the sample shows how to continue using a MongoDB-specific implementation of aVectorStore.
Through these keyed registrations, we can later on have a shared UI that uses the value of a route parameter to decide which implementation to use.
Creating recommendations
Let's have a closer look at the base class MovieRecommendationServiceBase. This class implements the general algorithm and contains the shared logic for the agents.
In its constructor, it injects the ChatClient instance and creates the agent with a common set of TextSearchOptions:
_agent = chatClient.AsAIAgent(
new ChatClientAgentOptions
{
ChatOptions = new()
{
Instructions = """
You are a conversational movie recommendation agent.
...lengthy system prompt
""",
},
AIContextProviders =
[new TextSearchProvider(RetrieveContextAsync, textSearchOptions)]
}
);
A crucial part when creating the agent is providing the instructions or system prompt that describes its capabilities:
You are a conversational and enthusiastic movie recommendation agent. Your goal is to help users find films based on their preferences.
**Core Guidelines:**
1. **Tool Usage:** You have access to the `TextSearch` tool. You must use this tool to verify plots, cast, genres, and release dates before answering. Do not rely on general training data for specific movie details.
2. **Citations:** When providing movie details, include a source link in your answer. Use ONLY the relative links found in the tool search results (e.g., [Link Text](/movies/123)). Do not invent or link to external URLs.
3. **Accuracy:** If the search tool returns no results for a specific query, do not hallucinate. State clearly that the movie or details are not found in your database.
4. **Conciseness & Tone:** Keep responses concise (maximum 2-3 sentences) but maintain an energetic, helpful tone. Use Markdown formatting (bolding, lists) to improve readability.
5. **Scope:** You specialize in movies. If a user asks about non-movie topics, politely redirect the conversation back to film recommendations.
**Response Format:**
- Use Markdown.
- Prioritize tool-verified information.
- Ensure all links are relative to the current application.
As you can see from the prompt above, it strongly encourages the agent to use context rather than rely on its own training data when providing recommendations. It also guides it to only use relative links within the current web application. This way, we can provide further details based on the data in our MongoDB database.
The AIContextProviders property is initialized with a list of context providers that - in our case - contains a TextSearchProvider instance.
The textSearchOptions are set up like this:
TextSearchProviderOptions textSearchOptions = new()
{
SearchTime =
TextSearchProviderOptions.TextSearchBehavior.OnDemandFunctionCalling,
};
This defines how the agent should retrieve the context: OnDemandFunctionCalling tells the agent to use the provider like a tool and use it as it sees fit when answering the question, while BeforeAIInvoke leads to a context search being conducted before calling the LLM - the classic RAG approach. In our case, we use OnDemandFunctionCalling so that the LLM has a chance to adjust the query that is used to retrieve the context and optimize it for the query.
The most important part of the TextSearchProvider is the provided RetrieveContextAsync method. This method calls the RetrieveContextAsync method that is responsible for finding the relevant context:
private async Task<IEnumerable<TextSearchProvider.TextSearchResult>> RetrieveContextAsync(
string query,
CancellationToken ct
)
{
_logger.LogInformation(
"Retrieving context using method {Method} with query: {Query}",
Name,
query
);
List<Movie> movies = await RetrieveMoviesAsync(query, ct);
_logger.LogInformation(
"Retrieved {Count} movies for query {Query} with method {Method}",
movies.Count,
query,
Name
);
return movies.Select(m => MapToSearchResult(m));
}
At the end, it converts the movies to TextSearchResult objects that the agent uses. This format contains a title, a link to details, and relevant information about the movie in text form:
private static TextSearchProvider.TextSearchResult MapToSearchResult(Movie m)
{
var cast = m.Cast ?? new List<string>();
var genres = m.Genres ?? new List<string>();
return new TextSearchProvider.TextSearchResult
{
SourceName = m.Title,
SourceLink = $"/movies/{m.Id}",
Text =
$@"""
Title: {m.Title}
Plot: {m.Plot}
Full plot: {m.FullPlot}
Cast: {string.Join(", ", cast)}
Genres: {string.Join(", ", genres)}
""",
};
}
The base class then defines the abstract method that the derived classes need to implement:
protected abstract Task<List<Movie>> RetrieveMoviesAsync(string query, CancellationToken ct);
The main input is the query parameter that is used to retrieve matching documents from MongoDB.
This method is used by the implementations to convert the Movie objects returned into TextSearchResults for the agent.
The user interface
As stated above, this sample uses a Blazor application to let the user chat with the implementations of the agent. This application is kept very simple and would be much more sophisticated in terms of state management, etc.
The main component MovieRecommendator.razor receives the chosen implementation as a route parameter, creates the requested instance, and uses this to chat with the agent:
@page "/movie-recommendator/{key}"
// ...
protected override void OnParametersSet()
{
base.OnParametersSet();
RecommendationService =
ServiceProvider.GetRequiredKeyedService<IMovieRecommendationService>(key);
}
private async Task SendMessage()
{
if (!string.IsNullOrWhiteSpace(userInput))
{
isLoading = true;
StateHasChanged();
try
{
AgentResponse response =
await RecommendationService.RecommendAsync(
userInput, CancellationToken.None);
userInput = string.Empty;
}
finally
{
isLoading = false;
StateHasChanged();
await inputRef.FocusAsync();
}
}
}
Approach #1: MongoDB Vector Search with MongoDB C# Driver
The first implementation uses the methods that are provided by the MongoDB C# Driver directly, after vectorizing the query:
protected override async Task<List<Movie>> RetrieveMoviesAsync(
string query,
CancellationToken ct
)
{
OpenAIEmbedding embedding =
await _embeddingClient.GenerateEmbeddingAsync(query);
QueryVector queryVector = new(embedding.ToFloats());
VectorSearchOptions<Movie>? vectorSearchOptions = new()
{
IndexName = "default",
NumberOfCandidates = 100,
};
return await _moviesCollection
.Aggregate()
.VectorSearch(
x => x.PlotEmbedding,
queryVector,
10,
vectorSearchOptions)
.ToListAsync(cancellationToken: ct);
}
We limit our results to 10 movies, which should be enough for the LLM to pick a good match for the query.
Approach #2: Using the MongoDB Vector Store Connector
This implementation makes use of the MongoDB Vector Store Connector to perform the search. In the constructor, the VectorStore instance is created:
public MongoDBVectorStoreMovieRecommendationService(
IMongoDatabase database,
ChatClient chatClient,
EmbeddingClient embeddingClient,
ILogger<MongoDBVectorStoreMovieRecommendationService> logger
)
: base(chatClient, logger)
{
VectorStore vectorStore = new MongoVectorStore(database);
_coll = vectorStore.GetCollection<string, Movie>("embedded_movies");
_embeddingClient = embeddingClient;
}
When retrieving the data, the Vector Store is used to perform the vector search:
protected override async Task<List<Movie>> RetrieveMoviesAsync(
string query,
CancellationToken ct
)
{
OpenAIEmbedding embedding =
await _embeddingClient.GenerateEmbeddingAsync(query);
return await _coll
.SearchAsync(new ReadOnlyMemory<float>(
embedding.ToFloats().ToArray()),
10)
.Select(m => m.Record)
.ToListAsync();
}
As you can see, this is very similar to the first approach, but it shortens the code for the search considerably.
Wrapping it up
These two methods show how easy it is to provide an LLM with data from your MongoDB database and create a powerful agent that runs on your data. With Microsoft Agent Framework, you can integrate these functions directly into your application. It allows you to extend a wide range of .NET applications with AI and give powerful tools to your users.



Top comments (0)