Your First AI Application in .NET: A Step-by-Step Guide

,

Welcome to this comprehensive guide on integrating Artificial Intelligence (AI) into .NET applications! If you’re new to AI development, this article explains everything step by step, including how to work with both traditional AI services and modern MCP (Model Context Protocol) servers. We’ll start with simple ways to call AI services and build up to creating your own MCP servers that AI agents can connect to for enhanced functionality.
AI integration means adding smart features to your apps, like answering questions or generating text, using tools from companies like OpenAI. .NET is a framework for building apps in C#, and we’ll use it here to connect to AI. Don’t worry if terms seem complex—we’ll break them down with simple analogies and examples.
We’ll use practical examples like asking “What’s the capital of India?” or “What’s the capital of Kerala?” to demonstrate concepts.
All examples use C# in .NET Core console apps (create with dotnet new console) and assume .NET 8 or later. A console app is like a simple program that runs in a command window, perfect for testing without a full UI.

Prerequisites

Before diving in, ensure you have the following set up to avoid common pitfalls:

  • .NET SDK: Install .NET 8 or later from dotnet.microsoft.com. Verify with dotnet --version in your terminal.
    API Keys and Accounts:
  • For OpenAI: Sign up at openai.com and generate an API key from your dashboard. This key authenticates your requests and incurs usage-based costs—start with a small budget.
  • For Azure OpenAI: Create an Azure account at portal.azure.com, deploy a model (e.g., gpt-4o-mini) in the Azure AI Studio, and note your endpoint, key, and deployment name. Azure offers enterprise features like private endpoints for security.
  • Development Environment: Use Visual Studio, VS Code, or Rider. For VS Code MCP integration, install extensions like GitHub Copilot or Claude for AI agent support.
  • Cost Awareness: AI APIs charge per token (roughly words processed). Test with free tiers or low-volume queries to learn without surprise bills.
  • Error Handling Tip: Always wrap async calls in try-catch blocks in production (e.g., try { … } catch (Exception ex) { Console.WriteLine($”Error: {ex.Message}”); }) to handle network issues or invalid keys gracefully.

What You’ll Learn

  1. Getting Started with AI in .NET
    * Using the OpenAI NuGet Package
    * Using the Azure OpenAI Package
  2. Building with Semantic Kernel
    * AI with Semantic Kernel
    * Functions and Plugins in Semantic Kernel
    * Creating Prompt Functions with CreateFunctionFromPrompt
  3. Working with Knowledge & Search
    * Implementing Vector Search with Semantic Kernel
    * Enhancing results using the Text Search Wrapper
    * Applying RAG (Retrieval-Augmented Generation) with Vector Search
  4. Next-Level AI Integration
    * Building an AI Agent with an MCP Server
  5. Local LLM
    * Run LLMs like Ollama or LM Studio locally for privacy, low latency, and no token costs

1. Traditional AI Integration

Traditional integration is like directly texting an AI friend for answers. It’s simple and doesn’t require fancy setups, making it great for your first AI project.

Using OpenAI NuGet Package

This is the simplest way for beginners to start with AI. You directly call OpenAI’s API to get responses.
What it does: Sends a prompt to OpenAI and prints the response. Great for quick tests. An API is like a menu of services you can order from.
Setup: Run dotnet add package OpenAI. NuGet is like an app store for code libraries.
Code:

using OpenAI.Chat;

var client = new ChatClient("gpt-4o-mini", "<your-api-key>");  // ChatClient: Connects directly to OpenAI for AI tasks. Replace <your-api-key> with your actual OpenAI key (get one from openai.com).

var messages = new ChatMessage[]
{
    new SystemChatMessage("You are a helpful assistant."),  // SystemChatMessage: Defines AI's role and behavior, like telling it to be friendly.
    new UserChatMessage("What's the capital of India?")     // UserChatMessage: User's input or question—this is what you're asking.
};

var response = await client.CompleteChatAsync(messages);  // CompleteChatAsync: Sends chat and gets AI response. 'Await' handles waiting for the reply.
Console.WriteLine(response.Value.Content[0].Text);  // Prints the AI's answer to the console.


Expected Output:
The capital of India is New Delhi
.


This code creates a chat with the AI. The system message sets the tone, and the user message is your question. The AI thinks and responds based on its training. Useful explanation: Prompts like this can be chained for conversations—add more UserChatMessage/SystemChatMessage pairs to build context, mimicking a real dialogue.

Using Azure OpenAI Package

For enterprise scenarios, Azure OpenAI provides better security and control. Azure is Microsoft’s cloud service, like a secure online storage for AI.

Setup: Run dotnet add package Azure.AI.OpenAI.

Code:

using Azure.AI.OpenAI;

var client = new OpenAIClient(  // OpenAIClient: Connects your .NET app to Azure OpenAI services for AI tasks
    new Uri("https://<your-endpoint>.openai.azure.com/"),  // URI is like the web address for your Azure AI service.
    new Azure.AzureKeyCredential("<your-key>")  // Credential is like a password to access the service.
);

var chatCompletionsOptions = new ChatCompletionsOptions  // ChatCompletionsOptions: Configuration object for Azure OpenAI chat requests—like a settings menu.
{
    DeploymentName = "<your-deployment>",  // Deployment is your specific AI model setup in Azure.
    Messages =
    {
        new ChatRequestSystemMessage("You are a helpful assistant."),  // ChatRequestSystemMessage: Defines AI's role or behavior
        new ChatRequestUserMessage("What's the capital of India?")     // ChatRequestUserMessage: User's question or input
    }
};

var response = await client.GetChatCompletionsAsync(chatCompletionsOptions);  // GetChatCompletionsAsync: Sends chat request and returns AI response
Console.WriteLine(response.Value.Choices[0].Message.Content);  // Prints the first choice from the AI's response.
Screenshot

This is similar to the previous example but uses Azure for more control, like in big companies. You need an Azure account to get the endpoint and key. Useful explanation: Azure supports fine-tuning models for domain-specific tasks (e.g., legal or medical) and integrates seamlessly with other Azure services like Cosmos DB for storing chat histories.

2. Advanced AI with Semantic Kernel

Semantic Kernel (SK) is Microsoft’s open-source framework that makes AI development more powerful and organized. Think of SK as a toolkit that helps you build smarter AI apps by combining prompts, functions, and data—like Lego blocks for AI. Install via dotnet add package Microsoft.SemanticKernel. SK abstracts AI providers, so you can switch between OpenAI and Azure easily without rewriting code.
Basic Semantic Kernel Usage
Setup: Run dotnet add package Microsoft.SemanticKernel.
Code:

using Microsoft.SemanticKernel;

var builder = Kernel.CreateBuilder();  // CreateBuilder: Starts building the Kernel with services and plugins—like assembling a car.
builder.AddOpenAIChatCompletion("gpt-4o-mini", "<your-key>");  // Adds OpenAI as the AI engine.

var kernel = builder.Build();  // Build: Finalizes and creates the Kernel object—now it's ready to use.
var result = await kernel.InvokePromptAsync("What's the capital of India?");  // InvokePromptAsync: Sends prompt to AI model and gets response—like pressing "send" on a message.
Console.WriteLine(result.GetValue<string>());  // GetValue: Extracts string value from AI response.

Expected Output:
The capital of India is New Delhi.

Here, the kernel acts as a manager that handles your AI requests. It’s more structured than direct calls, allowing you to add more features later. Useful explanation: SK supports multi-modal inputs (e.g., images) and planners for chaining tasks automatically, like “research topic → summarize → generate report.”

Functions and Plugins in Semantic Kernel

Functions let AI call your C# code automatically to get accurate data. A function is like a mini-program that does one job, and plugins group them—like apps on your phone.
Code (Plugin Class):

using Microsoft.SemanticKernel;
using System.ComponentModel;

[Description("Geography information plugin")]  // Description: Explains what this plugin does.
class GeographyPlugin
{
    [KernelFunction]  // KernelFunction: Marks C# method as callable by AI automatically—like making it AI-friendly.
    [Description("Get capital of a country or state")]
    [return: Description("Capital city name")]
    public string GetCapital(
        [Description("Country or state name")] string location)  // Parameter: Input that the AI provides.
    {
        return location.ToLower() switch  // Switch: Like a menu that picks the right answer based on input.
        {
            "india" => "New Delhi",
            "kerala" => "Thiruvananthapuram",
            "karnataka" => "Bengaluru",
            "tamil nadu" => "Chennai",
            _ => "Unknown"  // Default if no match.
        };
    }
}

Register Plugin/Usage

kernel.Plugins.AddFromType<GeographyPlugin>();  // AddFromType: Registers plugin from class type—adds it to the kernel's toolkit.
var response = await kernel.InvokePromptAsync("What's the capital of Kerala?");  // The AI can now call GetCapital automatically.
Console.WriteLine(response.GetValue<string>());

Expected Output:
The capital of Kerala is Trivandrum.


The AI sees your function as a tool it can use. If it needs a capital, it calls this instead of guessing. Useful explanation: Plugins can be native (C# code) or imported from YAML files for non-code teams. This reduces hallucinations (AI making up facts) by grounding responses in your logic.

Advanced Prompt Functions with CreateFunctionFromPrompt

CreateFunctionFromPrompt is a powerful method that converts prompt templates into reusable functions. This is particularly useful for creating standardized AI interactions that can be called multiple times with different parameters.
What it does: Converts a text prompt template into a KernelFunction that can be invoked with parameters and execution settings. It’s like turning a recipe into a reusable cooking machine where you just change ingredients.
Code (Basic Prompt Function):

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
using Microsoft.SemanticKernel.Connectors.OpenAI; // Import for Azure OpenAI connectors


var builder = Kernel.CreateBuilder();  // CreateBuilder: Starts building the Kernel with services and plugins—like assembling a car.

// Add Azure OpenAI chat completion service instead of direct OpenAI
builder.AddAzureOpenAIChatCompletion(
    deploymentName: "gpt-4o-mini",  // Deployment name for GPT-4o-mini in your Azure AI resource
    endpoint: "AZURE_OPENAI_ENDPOINT",  // e.g., "https://your-resource.openai.azure.com/"
    apiKey: "AZURE_OPENAI_API_KEY" // Your Azure OpenAI API key
);

var kernel = builder.Build();

// CreateFunctionFromPrompt: Turns a prompt template into a reusable function that can be invoked with parameters
var geographyFunction = kernel.CreateFunctionFromPrompt(
    "What's the capital of {{$location}}? Please provide a detailed answer including some facts about the city.",  // Prompt template with parameter placeholder—{{$location}} is like a blank to fill.
    new AzureOpenAIPromptExecutionSettings  // Use Azure-specific settings (inherits from OpenAIPromptExecutionSettings)
    {
        MaxTokens = 200,  // MaxTokens: Limits response length—to keep answers short.
        Temperature = 0.1,  // Temperature: Controls randomness (0.1 = more focused, 1.0 = more creative)—low for factual answers.
        ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions  // ToolCallBehavior: Enables automatic function calling—like auto-dialing tools.
    },
    functionName: "GetGeographyInfo",  // functionName: Optional name for the function
    description: "Provides detailed information about a location's capital city"  // description: Explains what this function does
);

// Invoke the function with parameters
var result = await kernel.InvokeAsync(geographyFunction, new KernelArguments  // KernelArguments: Dictionary-like object for passing parameters—like a bag of inputs.
{
    ["location"] = "Kerala"  // Parameter value for the {{$location}} placeholder
});

Console.WriteLine(result.GetValue<string>());
Screenshot

This makes prompts reusable. Change “Kerala” to “India,” and it works without rewriting the whole prompt. Useful explanation: Settings like Temperature fine-tune creativity—use 0 for deterministic outputs (e.g., math) and higher for brainstorming. This method also supports few-shot learning by including examples in the template.

3. Vector Search

What is Vector Search? Vector search enables AI to find information based on semantic meaning rather than exact keyword matches. It converts text into numerical vectors (embeddings) that capture meaning, allowing searches like “What’s the capital of Kerala?” to find relevant information even if the stored data uses different words like “administrative center of Kerala state.”
Imagine searching for “red fruit” and finding “apple” because the AI understands meanings, not just words. Vectors are like arrows in space where similar meanings point close together. This is key for smart search in apps, like recommending products or answering questions accurately. Useful explanation: Embeddings are generated by models like text-embedding-3-small, which map text to high-dimensional spaces (e.g., 1536 numbers). Cosine similarity measures “closeness” (0-1 scale, where 1 is identical meaning).
Key Concepts of Vector Search:

  • Embeddings: Numerical representations of text that capture semantic meaning. Like turning words into a secret code of numbers.
  • Similarity Search: Finding vectors that are “close” to each other in mathematical space. Closeness means similar ideas.
  • RAG (Retrieval-Augmented Generation): Using vector search to retrieve relevant information before generating AI responses. First find facts, then let AI explain them.
  • Semantic Similarity: Text with similar meanings have similar vector representations. E.g., “car” and “automobile” are close.

Vector Class

Microsoft.Extensions.VectorData.VectorStore class defined in the Microsoft.Extensions.VectorData.Abstractions package. As the name suggests, this class abstracts vector stores and defines methods for working with vector store collections, such as GetCollection and ListCollectionNamesAsync.

The classes that implement this class include the following:

  • AzureAISearchVectorStore
  • CosmosMongoVectorStore
  • CosmosNoSqlVectorStore
  • InMemoryVectorStore
  • MongoVectorStore
  • PineconeVectorStore
  • PostgresVectorStore
  • QdrantVectorStore
  • RedisVectorStore
  • SqlServerVectorStore
  • SqliteVectorStore
  • WeaviateVectorStore

As their names indicate, implementations are provided for many major vector stores.

This time, since the aim is to avoid depending on any special vector database, let’s focus on InMemoryVectorStore and look at the basic usage of VectorStore.

Setting Up Vector Search in Semantic Kernel

Setup: Add the required packages:

dotnet add package Microsoft.SemanticKernel.Connectors.InMemory --prerelease
dotnet add package Microsoft.Extensions.VectorData.Abstractions --prerelease

Prerelease means it’s a beta version—use with caution, but great for learning new features. Useful explanation: For production, swap InMemory with persistent stores like Azure AI Search or Pinecone to handle millions of vectors without losing data on restart.

Creating a Geography Vector Store

What it does: Creates a vector store to hold geography information that can be searched semantically. A vector store is like a smart database for meanings.
Code (Geography Record Class):

using Microsoft.Extensions.VectorData;

class GeographyRecord
{
    [VectorStoreKey] public required string Id { get; set; }  // VectorStoreKey: Marks property as unique identifier for records—like a serial number.
    [VectorStoreData] public required string Info { get; set; }  // VectorStoreData: Indicates property holds searchable data—the main text.
    [VectorStoreVector(1536)] public required ReadOnlyMemory<float> Vector { get; set; }  // VectorStoreVector: Specifies vector embedding with 1536 dimensions—a long list of numbers representing meaning.
    [VectorStoreData] public required string Location { get; set; }  // Additional searchable data field.
    [VectorStoreData] public required string Type { get; set; }  // Country or State classification.
}

Vector Store Setup


Code (Vector Store Implementation):

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Embeddings;  // For ITextEmbeddingGenerationService
using Microsoft.Extensions.VectorData;  // For IVectorStore and related interfaces/attributes
using Microsoft.Extensions.DependencyInjection;

// GeographyRecord: Custom record class for storing geography data in a vector store—like a data card.
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion("<your-deployment>", "<your-endpoint>", "<your-key>");  // Registers Azure OpenAI chat completion

// AddInMemoryVectorStore: Registers an in-memory vector store in Semantic Kernel's services—stores data in RAM for speed.
builder.Services.AddInMemoryVectorStore();

// AddAzureOpenAITextEmbeddingGeneration: Adds Azure OpenAI embedding service to generate vectors from text.
builder.AddAzureOpenAITextEmbeddingGeneration("text-embedding-3-small", "<your-endpoint>", "<your-key>");

var kernel = builder.Build();

// GetRequiredService: Fetches required service instance from dependency injection—like asking for a tool from a toolbox.
VectorStore vectorStore = kernel.Services.GetRequiredService<VectorStore>();
var embeddingGeneration = kernel.GetRequiredService<ITextEmbeddingGenerationService>();  // ITextEmbeddingGenerationService: Service that converts text into vector embeddings

// GetCollection: Retrieves or creates a named collection of records—like a folder in the database.
var collection = vectorStore.GetCollection<string, GeographyRecord>("Geography");
await collection.EnsureCollectionExistsAsync();  // EnsureCollectionExistsAsync: Creates vector store collection if it doesn't exist

// Sample geography data to populate the vector store
var geographyData = new[]
{
    new { Id = "1", Info = "India's capital is New Delhi, the seat of government and administrative center.", Location = "India", Type = "Country" },
    new { Id = "2", Info = "Kerala's capital is Thiruvananthapuram, also known as Trivandrum, located in southern India.", Location = "Kerala", Type = "State" },
    new { Id = "3", Info = "Karnataka's capital is Bengaluru, formerly Bangalore, known as India's Silicon Valley.", Location = "Karnataka", Type = "State" },
    new { Id = "4", Info = "Tamil Nadu's capital is Chennai, formerly Madras, a major cultural and economic center.", Location = "Tamil Nadu", Type = "State" },
    new { Id = "5", Info = "Maharashtra's capital is Mumbai, the financial capital of India and Bollywood hub.", Location = "Maharashtra", Type = "State" },
    new { Id = "6", Info = "United States capital is Washington D.C., home to federal government institutions.", Location = "USA", Type = "Country" },
    new { Id = "7", Info = "France's capital is Paris, known for the Eiffel Tower and rich cultural heritage.", Location = "France", Type = "Country" }
};

// Populate vector store with embeddings
foreach (var data in geographyData)
{
    // GenerateAsync: Creates vector embeddings from input text asynchronously—turns text into numbers.
    var embedding = await embeddingGeneration.GenerateEmbeddingAsync(data.Info);
    
    var record = new GeographyRecord
    {
        Id = data.Id,
        Info = data.Info,
        Location = data.Location,
        Type = data.Type,
        Vector = embedding  // Vector property contains the numerical representation
    };
    
    await collection.UpsertAsync(record);  // UpsertAsync: Inserts or updates records in vector store collection—like saving or updating a file.
}

Console.WriteLine("Vector store populated with geography data!");

This code sets up a memory-like store. It takes text (like capital facts), converts them to vectors using an embedding generator, and saves them. In-memory means it’s temporary and fast for testing. Useful explanation: For larger datasets, batch embeddings (process multiple texts at once) to reduce API calls and costs—SK supports this via GenerateBatchAsync.

Performing Vector Search

Code (Basic Vector Search):

// Convert user query to vector for similarity search
string userQuery = "What's the capital of Kerala?";
var queryEmbedding = await embeddingGeneration.GenerateEmbeddingAsync(userQuery);  // Turn the query into a vector.

Console.WriteLine($"Searching for: {userQuery}");
Console.WriteLine("Results:");

// VectorSearchAsync: Performs vector-based searches and returns matching results with similarity scores—like finding nearest neighbors.
await foreach (var result in collection.SearchAsync(
                   queryEmbedding,
                   top: 3))  // Return top 3 most similar results
{
    Console.WriteLine($"Score: {result.Score:F4} | {result.Record.Info}");  // Score is how close the match is (higher = better).
}
Screenshot

The search finds “close” vectors, so even if your query doesn’t match words exactly, it finds related info. The score shows confidence 1.0 would be perfect. Useful explanation: Threshold scores (e.g., >0.7) to filter noisy results; combine with hybrid search (keywords + vectors) for precision in noisy data.

Advanced Semantic Search with Text Search Wrapper

A wrapper is also provided for VectorStore that specializes in text search. This is done using the VectorStoreTextSearch<TRecord> class. The VectorStoreTextSearch<TRecord> class implements the ITextSearch interface, allowing you to perform text searches. By using VectorStoreTextSearch<TRecord>, you can handle the data in a vector store in a way that is specialized for text search.

In order to enable text search with this class, you need to annotate the property in your RecordData class that represents the search result text with the TextSearchResultValue attribute. This attribute specifies which property should be used when retrieving the results of a text search.

For example, if you want the Info property of the GeographyRecord class to serve as the search result text, you would define it as follows:

public class GeographyRecord
{
    [VectorStoreKey] public required string Id { get; set; }  
    [TextSearchResultValue][VectorStoreData] public required string Info { get; set; }  
    [VectorStoreVector(1536)] public required ReadOnlyMemory<float> Vector { get; set; }  
}

Although we won’t use them here, you can also apply attributes such as TextSearchResultName to specify a property to use as the “name,” or TextSearchResultLink to specify a property to use as a “link.” By combining these attributes, you could, for example, specify a file namechunked text, or a URL link to the file.

In this example, since we are only working with text, we’ll only use the TextSearchResultValue attribute.

Now let’s actually perform a text search using VectorStoreTextSearch<TRecord> with our VectorStore

Code (Text Search Implementation):

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Embeddings;  
using Microsoft.Extensions.VectorData;  
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel.Data;

#pragma warning disable SKEXP0001, SKEXP0010, SKEXP0020, SKEXP0050 , CS0618 

var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion("gpt-35-turbo", "<your-endpoint>", "<your-key>");  

// AddInMemoryVectorStore: Registers an in-memory vector store in Semantic Kernel's services—stores data in RAM for speed.
builder.Services.AddInMemoryVectorStore();

// AddAzureOpenAITextEmbeddingGeneration: Adds Azure OpenAI embedding service to generate vectors from text.
builder.AddAzureOpenAITextEmbeddingGeneration("text-embedding-3-small", "<your-endpoint>", "<your-key>");

var kernel = builder.Build();

// GetRequiredService: Fetches required service instance from dependency injection—like asking for a tool from a toolbox.
var vectorStore = kernel.Services.GetRequiredService<VectorStore>();  // Use IVectorStore for type safety
var embeddingGeneration = kernel.GetRequiredService<ITextEmbeddingGenerationService>();  // ITextEmbeddingGenerationService: Service that converts text into vector embeddings

// GetCollection: Retrieves or creates a named collection of records—like a folder in the database.
var collection = vectorStore.GetCollection<string, GeographyRecord>("Geography");
await collection.EnsureCollectionExistsAsync();  // EnsureCollectionExistsAsync: Creates vector store collection if it doesn't exist (note: corrected method name from EnsureCollectionExistsAsync based on SK docs)

// Sample geography data to populate the vector store
var geographyData = new[]
{
    new { Id = "1", Info = "India's capital is New Delhi, the seat of government and administrative center.", Location = "India", Type = "Country" },
    new { Id = "2", Info = "Kerala's capital is Thiruvananthapuram, also known as Trivandrum, located in southern India.", Location = "Kerala", Type = "State" },
    new { Id = "3", Info = "Karnataka's capital is Bengaluru, formerly Bangalore, known as India's Silicon Valley.", Location = "Karnataka", Type = "State" },
    new { Id = "4", Info = "Tamil Nadu's capital is Chennai, formerly Madras, a major cultural and economic center.", Location = "Tamil Nadu", Type = "State" },
    new { Id = "5", Info = "Maharashtra's capital is Mumbai, the financial capital of India and Bollywood hub.", Location = "Maharashtra", Type = "State" },
    new { Id = "6", Info = "United States capital is Washington D.C., home to federal government institutions.", Location = "USA", Type = "Country" },
    new { Id = "7", Info = "France's capital is Paris, known for the Eiffel Tower and rich cultural heritage.", Location = "France", Type = "Country" }
};

// Populate vector store with embeddings
foreach (var data in geographyData)
{
    // GenerateAsync: Creates vector embeddings from input text asynchronously—turns text into numbers.
    var embedding = await embeddingGeneration.GenerateEmbeddingAsync(data.Info);
    
    var record = new GeographyRecord
    {
        Id = data.Id,
        Info = data.Info,
        Location = data.Location,
        Type = data.Type,
        Vector = embedding  // Vector property contains the numerical representation
    };
    
    await collection.UpsertAsync(record);  // UpsertAsync: Inserts or updates records in vector store collection—like saving or updating a file.
}

Console.WriteLine("Vector store populated with geography data!");

// Add text search functionality here
var textSearch = new VectorStoreTextSearch<GeographyRecord>(
    collection,
    embeddingGeneration);  // Use GeographyRecord as the type parameter

Console.WriteLine("");
var textQuery = "What's financial capital Bollywood?";
var  textSearchResults = await textSearch.GetTextSearchResultsAsync(textQuery);  // Perform the text search

Console.WriteLine($"Text searching for: {textQuery}");
Console.WriteLine("Text Search Results:");
await foreach (var textSearchResult in textSearchResults.Results)
{
    Console.WriteLine($"{textSearchResult.Value}");
}

Console.WriteLine("");
public class GeographyRecord
{
    [VectorStoreKey] public required string Id { get; set; } 
    [TextSearchResultValue][VectorStoreData] public required string Info { get; set; }  
    [VectorStoreVector(1536)] public required ReadOnlyMemory<float> Vector { get; set; }  
    [VectorStoreData] public required string Location { get; set; } 
    [VectorStoreData] public required string Type { get; set; } 
}
Screenshot

This wraps the vector search to make it feel like a regular text search but with semantic smarts. It’s useful for apps where users type natural questions. Useful explanation: This abstraction hides vector math, letting you query with strings. Extend with metadata filters (e.g., by date) for temporal searches.

RAG (Retrieval-Augmented Generation) with Vector Search

Up to this point, the code allows you to insert sample data and perform text searches effectively. Now, the goal is to use this functionality in a Retrieval-Augmented Generation (RAG) style.

Previously, you instantiated the VectorStoreTextSearch class manually, but it’s better to delegate this responsibility to the Dependency Injection (DI) container. When registering services to enable text search via DI, keep the following points in mind:

  • Register VectorStoreTextSearch in the DI container as the ITextSearch interface.
  • Since the constructor of VectorStoreTextSearch requires an IVectorSearchable, register the collection obtained from the VectorStore under this interface in the DI container.
  • Ensure that the IEmbeddingGenerator service is also registered in the DI container.
  • Mark the property that you want to use as the searchable text result in your data record class with the [TextSearchResultValue] attribute. For example, apply it to the Text property in your RecordData class.
  • Use the CreateWithSearch method of ITextSearch to create a plugin instance, and register this plugin with the DI container.

Now, let’s implement this approach.

#pragma warning disable SKEXP0010
#pragma warning disable SKEXP0001, CS0618
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Data;
using Microsoft.SemanticKernel.Embeddings;

// Create KernelBuilder
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion("gpt-35-turbo", "<your-endpoint>", "<your-key>");  // Registers Azure OpenAI chat completion

// AddInMemoryVectorStore: Registers an in-memory vector store in Semantic Kernel's services—stores data in RAM for speed.
builder.Services.AddInMemoryVectorStore();

// AddAzureOpenAITextEmbeddingGeneration: Adds Azure OpenAI embedding service to generate vectors from text.
builder.AddAzureOpenAITextEmbeddingGeneration("text-embedding-3-small", "<your-endpoint>", "<your-key>");

// Add InMemoryVectorStore and set up services for text search
builder.Services.AddInMemoryVectorStore();
builder.Services.AddSingleton<IVectorSearchable<GeographyRecord>>(sp => 
    sp.GetRequiredService<VectorStore>().GetCollection<string, GeographyRecord>("GeographyCollection"));
builder.Services.AddSingleton<ITextSearch, VectorStoreTextSearch<GeographyRecord>>();


// Register ITextSearch as a plugin
builder.Services.AddSingleton(sp => sp.GetRequiredService<ITextSearch>().CreateWithSearch("GeographyStore", "Data store for geography information"));

// Create Kernel
var kernel = builder.Build();

// Get VectorStore from Kernel
var vectorStore = kernel.Services.GetRequiredService<VectorStore>();

// Create collection
var collection = vectorStore.GetCollection<string, GeographyRecord>("GeographyCollection");
await collection.EnsureCollectionExistsAsync(); 

// Get embedding generation service
var embeddingGenerator = kernel.GetRequiredService<ITextEmbeddingGenerationService>();

// Register data (sample geography entries)
await collection.UpsertAsync(new[]
{
    new GeographyRecord
    {
        Id = "1",
        Info = "India's capital is New Delhi, the seat of government and administrative center.",
        Vector = (await embeddingGenerator.GenerateEmbeddingAsync(
            "India's capital is New Delhi, the seat of government and administrative center."))
    },
    new GeographyRecord
    {
        Id = "2",
        Info = "Kerala's capital is Thiruvananthapuram, also known as Trivandrum, located in southern India.",
        Vector = (await embeddingGenerator.GenerateEmbeddingAsync(
            "Kerala's capital is Thiruvananthapuram, also known as Trivandrum, located in southern India."))
    },
    new GeographyRecord
    {
        Id = "3",
        Info = "Karnataka's capital is Bengaluru, formerly Bangalore, known as India's Silicon Valley.",
        Vector = (await embeddingGenerator.GenerateEmbeddingAsync(
            "Karnataka's capital is Bengaluru, formerly Bangalore, known as India's Silicon Valley."))
    },
    new GeographyRecord
    {
        Id = "4",
        Info = "Tamil Nadu's capital is Chennai, formerly Madras, a major cultural and economic center.",
        Vector = (await embeddingGenerator.GenerateEmbeddingAsync(
            "Tamil Nadu's capital is Chennai, formerly Madras, a major cultural and economic center."))
    },
    new GeographyRecord
    {
        Id = "5",
        Info = "Maharashtra's capital is Mumbai, the financial capital of India and Bollywood hub.",
        Vector = (await embeddingGenerator.GenerateEmbeddingAsync(
            "Maharashtra's capital is Mumbai, the financial capital of India and Bollywood hub."))
    },
    new GeographyRecord
    {
        Id = "6",
        Info = "United States capital is Washington D.C., home to federal government institutions.",
        Vector = (await embeddingGenerator.GenerateEmbeddingAsync(
            "United States capital is Washington D.C., home to federal government institutions."))
    },
    new GeographyRecord
    {
        Id = "7",
        Info = "France's capital is Paris, known for the Eiffel Tower and rich cultural heritage.",
        Vector = (await embeddingGenerator.GenerateEmbeddingAsync(
            "France's capital is Paris, known for the Eiffel Tower and rich cultural heritage."))
    }
});

Console.WriteLine("");
// Perform RAG!
var response = await kernel.InvokePromptAsync(
    // Prompt for returning capitals
    """
    <message role="system">
        You are a geography expert.
        For the users query about location or capitals, provide the most accurate answer based solely on information in your data store.
        Do not use general knowledgeonly use data from the store.
    </message>
    <message role="user">
        {{$input}}
    </message>
    """,
    new(new PromptExecutionSettings
    {
        // Settings to use the data store (automatically calls the plugin)
        FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
    })
    {
        // Variable to embed in the prompt
        ["input"] = "What is known as the Silicon Valley of India?",
    });

Console.WriteLine($"Searching for: What is known as the Silicon Valley of India?");
Console.WriteLine("Results:");
Console.WriteLine(response.GetValue<string>());

Console.WriteLine("");
class GeographyRecord
{
    [VectorStoreKey]    
    public required string Id { get; set; }
    [TextSearchResultValue]
    [VectorStoreData]
    public required string Info { get; set; }
    [VectorStoreVector(1536)] public required ReadOnlyMemory<float> Vector { get; set; }
}

Screenshot

RAG makes AI smarter by giving it real data first, so the model answers based on the vector store rather than relying only on its built-in knowledge. The system prompt ensures responses stick to the data store, avoiding external knowledge.

4. Model Context Protocol (MCP) Servers

What is MCP?

MCP (Model Context Protocol) is an open standard that enables AI agents—such as Claude, GitHub Copilot, or ChatGPT—to seamlessly connect to external servers for data and functionality.
Unlike Semantic Kernel plugins, which run inside your application, MCP servers are independent applications that AI agents can interact with through a standardized protocol. This makes it easier to extend AI capabilities with external tools and services.
To read more, I’ve written a detailed article here: https://wisecodes.venuthomas.in/2025/07/22/ai-replacing-the-browser-exploring-model-context-protocol-mcp/

Creating a Geography MCP Server

This example creates an MCP server that AI agents can connect to for geography information.
Setup:

dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting
botnet add package Microsoft.Extensions.Logging.Console

This creates a new project and adds libraries. Hosting makes it run like a service. Useful explanation: Use dotnet run to start; for production, deploy to Azure App Service or Docker for always-on availability.
MCP Server Implementation (Program.cs):

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;  
using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = Host.CreateApplicationBuilder(args);

builder.Logging.AddConsole(consoleLogOptions =>  // AddConsole: Configures logging for MCP compatibility—like adding a debug log.
{
    consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;  // Ensures MCP protocol compliance—logs everything for troubleshooting.
});

builder.Services
    .AddMcpServer()  // AddMcpServer: Registers MCP server infrastructure services—the core setup.
    .WithStdioServerTransport()  // WithStdioServerTransport: Uses stdin/stdout for AI agent communication—like simple input/output.
    .WithPromptsFromAssembly();  // WithPromptsFromAssembly: Auto-discovers MCP prompt classes—finds your prompts automatically.

await builder.Build().RunAsync();  // RunAsync: Starts MCP server and keeps it running—like launching the app.

MCP Prompts with Parameters

MCP prompts are methods that AI agents can call to get specific data. Unlike Semantic Kernel functions, these return raw data that AI agents incorporate into their responses. Prompts here are like API endpoints that AIs query for facts.
Geography Prompts Class:

using ModelContextProtocol.Server;
using System.ComponentModel;

[McpServerPromptType]  // McpServerPromptType: Marks class as containing MCP prompts for AI agents—like a folder of tools.
class GeographyPrompts
{
    [McpServerPrompt(Name = "CountryCapitalPrompt")]  // McpServerPrompt: Marks method as MCP prompt with parameters
    [Description("Provides the capital city of a country")]
    public static string CountryCapitalPrompt(
        [Description("Country name")] string country)  // Parameter that AI agents can pass—like input fields.
    {
        return country.ToLower() switch  // Pattern matching for country lookup—like an if-else chain.
        {
            "india" => "New Delhi",
            "usa" => "Washington, D.C.",
            "uk" => "London",
            "france" => "Paris",
            "germany" => "Berlin",
            "japan" => "Tokyo",
            "china" => "Beijing",
            "brazil" => "Brasília",
            "canada" => "Ottawa",
            "australia" => "Canberra",
            _ => "Unknown"
        };
    }

    [McpServerPrompt(Name = "StateCapitalPrompt")]
    [Description("Provides the capital city of a state or province")]
    public static string StateCapitalPrompt(
        [Description("State or province name")] string state,
        [Description("Country (default: India)")] string country = "India")  // Default parameter value—if not specified, assumes India.
    {
        return (country.ToLower(), state.ToLower()) switch  // Tuple pattern matching—like checking two things at once.
        {
            // Indian states
            ("india", "kerala") => "Thiruvananthapuram",
            ("india", "karnataka") => "Bengaluru",
            ("india", "tamil nadu") => "Chennai",
            ("india", "maharashtra") => "Mumbai",
            ("india", "west bengal") => "Kolkata",
            ("india", "rajasthan") => "Jaipur",
            ("india", "gujarat") => "Gandhinagar",
            ("india", "punjab") => "Chandigarh",
            
            // US states
            ("usa", "california") => "Sacramento",
            ("usa", "texas") => "Austin",
            ("usa", "florida") => "Tallahassee",
            ("usa", "new york") => "Albany",
            
            _ => "Unknown"
        };
    }

    [McpServerPrompt(Name = "QuickCapitalLookup")]
    [Description("Quick capital lookup for any location")]
    public static string QuickCapitalLookup(
        [Description("Location name")] string location)
    {
        // First check countries
        var countryCapital = GetCountryCapital(location);
        if (countryCapital != "Unknown")
        {
            return $"The capital of {location} is {countryCapital}.";
        }

        // Then check states (default to India)
        var stateCapital = GetStateCapital(location, "India");
        if (stateCapital != "Unknown")
        {
            return $"The capital of {location} state is {stateCapital}.";
        }

        return $"Sorry, I don't have information about the capital of {location}.";
    }

    private static string GetCountryCapital(string country) => country.ToLower() switch
    {
        "india" => "New Delhi",
        "usa" => "Washington, D.C.",
        "uk" => "London",
        _ => "Unknown"
    };

    private static string GetStateCapital(string state, string country) => (country.ToLower(), state.ToLower()) switch
    {
        ("india", "kerala") => "Thiruvananthapuram",
        ("india", "karnataka") => "Bengaluru",
        ("india", "tamil nadu") => "Chennai",
        _ => "Unknown"
    };
}

These methods are like custom commands AIs can call. The attributes make them discoverable, and parameters let AIs customize requests. Useful explanation: Prompts can return complex JSON for structured data (e.g., { “capital”: “New Delhi”, “population”: 30000000 }); validate inputs to prevent errors.

Connecting AI Agents to MCP Servers

AI agents like Claude or GitHub Copilot in VS Code connect to MCP servers via configuration files. VS Code is a free code editor, and this setup is like plugging in a device.

VS Code Settings (.vscode/mcp.json):

{
  "inputs": [],
  "servers": {
    "GeographyMCP": {
      "type": "stdio",  // stdio: Communication method for local MCP servers—like basic chat.
      "command": "dotnet",  // command: Executable to run the MCP server
      "args": [
        "run",
        "--project",
        "C:\\YourPath\\GeographyMcpServer\\GeographyMcpServer.csproj"  // Path to your project.
      ]
    }
  }
}

Useful explanation: For remote MCP servers, use “type”: “http” with a URL; secure with API keys. Restart VS Code after editing mcp.json for changes to take effect.

How AI Agents Use MCP Servers

User interaction flow:

  • User asks: “What’s the capital of Kerala?”
  • AI agent discovers: Available MCP prompts from connected servers—like checking a menu.
  • AI agent calls: StateCapitalPrompt(state=”Kerala”, country=”India”)
  • MCP server returns: “Thiruvananthapuram
  • AI agent responds: “The capital of Kerala is Thiruvananthapuram, also known as Trivandrum. It serves as the administrative center of Kerala state in India.

The AI uses the server like a database lookup, then adds its own explanation. Useful explanation: Agents decide when to call prompts based on descriptions—keep them concise. Log calls for auditing, and handle timeouts (e.g., 5s) to avoid hanging interactions.

5. Trying Other Local LLM Providers (Ollama, LM Studio, Azure AI Foundry)

While OpenAI and Azure OpenAI are common starting points, you can also run prompts against local models (Ollama, LM Studio) or enterprise platforms (Azure AI Foundry). Semantic Kernel makes this possible with minimal changes.
Local LLMs (Large Language Models) run directly on your own hardware, such as a personal computer or server, without relying on cloud services. This provides privacy, lower latency, and no ongoing API costs, but performance depends heavily on your hardware. A powerful GPU is especially important for efficient inference.

For a deeper dive into model sizes, hardware requirements, and setup, check my other article: https://wisecodes.venuthomas.in/2025/08/24/ai-model-basics-understanding-size-hardware-and-setup/

Using Ollama

Ollama is a powerful open-source tool for running large language models (LLMs) locally on your machine. It’s ideal for development and testing without relying on cloud services. To read more: https://github.com/ollama/ollama
First, install Ollama from ollama.ai and download a model like llama3 using:

ollama pull llama3

Make sure Ollama is running in the background, then try this code:

using Microsoft.SemanticKernel;

var builder = Kernel.CreateBuilder();

// Add Ollama connector (make sure Ollama is running locally)
builder.AddOllamaChatCompletion(
    "llama3", // model you downloaded in Ollama (e.g., llama3, mistral, phi)
    "http://localhost:11434" // default Ollama endpoint
);

var kernel = builder.Build();

var result = await kernel.InvokePromptAsync("What's the capital of Kerala?");

Console.WriteLine(result.GetValue<string>());

Using LM Studio

LM Studio is a user-friendly desktop application for managing and running LLMs locally. It provides an OpenAI-compatible API endpoint, making it easy to integrate with Semantic Kernel. Getting Started with LM Studio: https://lmstudio.ai/docs/app/basics
Load a model (e.g., Llama 3) inside LM Studio, then start the local server (default port: 1234).

using Microsoft.SemanticKernel;

var builder = Kernel.CreateBuilder();

builder.AddOpenAIChatCompletion(
    "lmstudio-llama3",                  // alias for your LM Studio model
    apiKey: "not_required",             // LM Studio usually doesn’t need a key
    endpoint: "http://localhost:1234/v1" // LM Studio endpoint
);

var kernel = builder.Build();

var result = await kernel.InvokePromptAsync("What's the capital of Kerala?");

Console.WriteLine(result.GetValue<string>());

Like Ollama, LM Studio performance depends on your hardware. But you’ll never hit token limits or API charges.

Using Azure Foundry Local

Azure AI Foundry can also be deployed on-premises as a local LLM server. This allows you to run large language models on your own infrastructure with enterprise features like compliance, scaling, and monitoring — but without sending data to the cloud.

example code here: https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/building-ai-apps-with-the-foundry-local-c-sdk/4448674
to read more: https://learn.microsoft.com/en-us/azure/ai-foundry/foundry-local/

Production Tip:

  • Local tools (Ollama, LM Studio) are great for experimentation and cost savings, but don’t scale well.
  • Use Azure AI Foundry for enterprise deployments, security, or serving models to thousands of users.
  • With Semantic Kernel, switching between providers is usually just changing the builder configuration — your application code stays the same.

Conclusion

This comprehensive guide covered the complete spectrum of AI development in .NET, with extra explanations to help beginners grasp concepts like vectors (numerical meanings) and servers (independent helpers). We explored:

  • Basic AI Integration: Direct API calls to OpenAI/Azure OpenAI—easy entry point.
  • Semantic Kernel: In-process AI orchestration with plugins and prompt functions—like building blocks.
  • Vector Search: Semantic similarity search for enhanced information retrieval—key for smart apps.
  • MCP Servers: Standalone servers for multi-agent AI environments—great for sharing.
  • Local LLMs: Run models like Ollama or LM Studio on your own system for privacy, low latency, and no token costs (GPU affects performance).

Key Features Covered:

  • CreateFunctionFromPrompt: Convert prompt templates into reusable functions—like customizable recipes.
  • Vector Search: Semantic similarity search using embeddings—for meaning-based lookups.
  • RAG Implementation: Retrieval-Augmented Generation with vector databases—research then respond.
  • MCP Integration: Multi-agent server architecture—for universal AI access.

Choose based on your needs:

  • Simple AI tasks: Use direct API calls.
  • Complex single-app AI: Use Semantic Kernel with plugins and vector search.
  • Semantic search requirements: Implement vector search with RAG.
  • Multi-agent environments: Build MCP servers.
  • Local testing or private data: Use Local LLMs.

Vector search represents a fundamental shift in how applications handle information retrieval, moving from exact keyword matching to semantic understanding. Combined with RAG patterns, it enables AI applications to provide more accurate, contextual responses by retrieving relevant information before generation. For beginners, start with small datasets and experiment—these tools make AI accessible in .NET.

The examples in this guide provide practical, production-ready code that demonstrates the evolution from basic AI integration to sophisticated semantic search and multi-agent architectures. Whether you’re building internal tools or public services, these patterns will help you create robust, intelligent applications that can understand and respond to user queries with remarkable accuracy and contextual awareness.
Next Steps: Explore SK’s memory stores for long-term context, integrate with Blazor for web UIs, or experiment with Local LLMs for private, cost-free AI testing.

Key Terms Quick Reference

  • OpenAIClient: A class from the Azure.AI.OpenAI package that connects your .NET app to Azure OpenAI services for AI tasks like text generation. Think of it as a phone line to call an AI helper.
  • ChatCompletionsOptions: A configuration object that sets up details like the AI model and messages for a chat request to Azure OpenAI. It’s like setting rules for a conversation, such as who speaks first.
  • ChatRequestSystemMessage: A message type that defines the AI’s role or behavior, like “You are a helpful assistant.” This is the AI’s “personality” instruction.
  • ChatRequestUserMessage: A message type that represents the user’s question or input, like “What’s the capital of India?” This is what you, the user, say to the AI.
  • GetChatCompletionsAsync: An async method on the OpenAIClient that sends a chat request to Azure OpenAI and returns the AI’s response. Async means it can wait for the AI without freezing your app.
  • Kernel: The core class in Semantic Kernel that orchestrates AI services, plugins, and prompts like a central brain for your app. Imagine it as the conductor of an orchestra, coordinating different AI parts.
  • InvokePromptAsync: A Semantic Kernel method that asynchronously sends a prompt to an AI model and retrieves the generated response. A prompt is like a question or instruction you give to the AI.
  • KernelFunction: An attribute that marks a C# method as a callable function in Semantic Kernel, allowing AI to use it automatically. Attributes are like labels that add special behavior to code.
  • CreateFunctionFromPrompt: A method in Semantic Kernel to turn a prompt template into a reusable function that can be invoked with parameters. Templates are reusable patterns, like a fill-in-the-blank form.
  • VectorStore: An abstraction in Semantic Kernel for storing and managing vector data used in semantic searches. Vectors are like coordinates that represent meaning in data.
  • VectorStoreKey: An attribute marking a property as the unique identifier for records in a vector store. Like a unique ID card for each piece of data.
  • VectorStoreData: An attribute indicating a property holds searchable data in a vector store record. This is the actual info you want to search, like text descriptions.
  • VectorStoreVector: An attribute specifying a property as the vector embedding with a defined dimension in a vector store. Dimensions are like the number of measurements in the vector.
  • IEmbeddingGenerator: An interface for services that convert text into vector embeddings for semantic similarity searches. An interface is a contract that defines what a class can do.
  • Embedding: A class representing a vector (numeric array) that captures the semantic meaning of text for comparisons. Semantic means understanding the meaning, not just words.
  • VectorStoreTextSearch: A class wrapping vector stores to enable text-based semantic searches in Semantic Kernel.
  • UpsertAsync: An async method to insert or update records in a vector store collection. Upsert is a combo of “update” and “insert.”
  • SearchAsync: An async method that performs vector-based searches and returns matching results with similarity scores.
  • MCP Server: A standalone server application that provides tools, prompts, and data sources to AI agents through the Model Context Protocol. A server is like a helper program that runs separately and responds to requests.
  • McpServerPromptType: An attribute marking a class as containing MCP server prompts that AI agents can request.
  • McpServerPrompt: An attribute marking a method as an MCP prompt that returns data or guidance based on parameters.
  • WithStdioServerTransport: Configures an MCP server to use stdin/stdout for communication with AI agents. Stdin/stdout are like input/output pipes for programs.
  • AddMcpServer: Registers MCP server services and infrastructure in the dependency injection container. Dependency injection is a way to provide parts of your app automatically.

Thank you for reading! If you have any more questions or need further clarification, feel free to ask!

Buy me a pizza

Please follow and like us:
0

2 responses to “Your First AI Application in .NET: A Step-by-Step Guide”

Leave a Reply to Getting Started with Microsoft Agent Framework in .NET: The Next Step Beyond Semantic Kernel – </> Wisecodes Cancel reply

Your email address will not be published. Required fields are marked *