AI-Powered Applications with .NET Aspire and Semantic Kernel

By · · AI Engineering

Building AI-powered applications used to mean calling an API endpoint and returning a response. Modern AI applications require orchestration of multiple services, observability, resilience, and the ability to scale individual components independently. This is where the combination of .NET Aspire and Semantic Kernel fits well. Together, they provide a solid developer experience for building distributed AI applications in the .NET ecosystem.

Why .NET Aspire for AI Applications?

.NET Aspire, which reached General Availability in May 2024, is a cloud-ready stack for building observable, production-ready distributed applications. While many developers think of it purely for microservices, it is an excellent fit for AI workloads that involve multiple moving parts: vector databases, LLM endpoints, caching layers, and API gateways.

Aspire gives you three things out of the box that AI applications desperately need:

Setting Up the Aspire AppHost

Let us start by creating an Aspire project that orchestrates an AI-powered API service with its dependencies.

// AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

var postgres = builder.AddPostgres("postgres")
    .AddDatabase("aidb");

var apiService = builder.AddProject<Projects.AiApi>("ai-api")
    .WithReference(cache)
    .WithReference(postgres)
    .WithExternalHttpEndpoints();

builder.Build().Run();

This AppHost definition wires up a Redis cache for storing conversation history, a PostgreSQL database for persisting user data, and our AI API service. Aspire handles service discovery, so the API service automatically knows how to connect to Redis and PostgreSQL without any connection string gymnastics.

Integrating Semantic Kernel

Semantic Kernel is Microsoft's open-source SDK for integrating AI models into applications. It provides abstractions for chat completion, embeddings, memory, and plugins. Let us wire it into our Aspire-managed service.

First, configure the service defaults in your API project:

// AiApi/Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
builder.AddRedisOutputCache("cache");

// Register Semantic Kernel
builder.Services.AddKernel()
    .AddAzureOpenAIChatCompletion(
        deploymentName: builder.Configuration["AzureOpenAI:DeploymentName"]!,
        endpoint: builder.Configuration["AzureOpenAI:Endpoint"]!,
        apiKey: builder.Configuration["AzureOpenAI:ApiKey"]!
    );

// Register custom plugins
builder.Services.AddSingleton<ProductSearchPlugin>();
builder.Services.AddSingleton<OrderPlugin>();

var app = builder.Build();
app.MapDefaultEndpoints();

The AddServiceDefaults() call is the Aspire integration point. It sets up health checks, OpenTelemetry tracing, and service discovery. Combined with Semantic Kernel's registration, you get a fully observable AI service.

Building a Plugin-Based Architecture

Semantic Kernel's plugin system allows you to expose .NET methods as functions that the AI model can call. Instead of building monolithic prompt chains, you build small, testable plugins that the kernel orchestrates.

public class ProductSearchPlugin
{
    private readonly IProductRepository _repository;

    public ProductSearchPlugin(IProductRepository repository)
    {
        _repository = repository;
    }

    [KernelFunction("search_products")]
    [Description("Search for products by name, category, or description")]
    public async Task<List<Product>> SearchProductsAsync(
        [Description("The search query")] string query,
        [Description("Maximum number of results")] int maxResults = 5)
    {
        return await _repository.SearchAsync(query, maxResults);
    }

    [KernelFunction("get_product_details")]
    [Description("Get detailed information about a specific product")]
    public async Task<Product?> GetProductDetailsAsync(
        [Description("The product ID")] int productId)
    {
        return await _repository.GetByIdAsync(productId);
    }
}

Each plugin method is annotated with KernelFunction and Description attributes. Semantic Kernel uses these to generate the function schema that gets sent to the LLM, enabling function calling capabilities.

Creating a Chat Endpoint with Auto Function Calling

Now let us build an API endpoint that uses the kernel with automatic function calling. The AI model will decide when to invoke our plugins based on the user's query.

app.MapPost("/api/chat", async (
    ChatRequest request,
    Kernel kernel,
    ProductSearchPlugin productPlugin,
    OrderPlugin orderPlugin) =>
{
    kernel.Plugins.AddFromObject(productPlugin, "Products");
    kernel.Plugins.AddFromObject(orderPlugin, "Orders");

    var settings = new OpenAIPromptExecutionSettings
    {
        ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
        MaxTokens = 1024,
        Temperature = 0.7
    };

    var chatHistory = new ChatHistory();
    chatHistory.AddSystemMessage(
        "You are a helpful shopping assistant. Use the available " +
        "functions to search products and manage orders. Always " +
        "provide specific product details when available.");

    chatHistory.AddUserMessage(request.Message);

    var chatService = kernel.GetRequiredService<IChatCompletionService>();
    var response = await chatService.GetChatMessageContentAsync(
        chatHistory, settings, kernel);

    return Results.Ok(new ChatResponse(response.Content ?? ""));
});

When a user asks something like "Find me running shoes under $100," the LLM will automatically call search_products with the appropriate parameters, receive the results, and formulate a natural language response incorporating the real data.

Observability with OpenTelemetry

One of Aspire's greatest strengths is built-in observability. Since Semantic Kernel emits OpenTelemetry traces, you get end-to-end visibility into your AI operations through the Aspire dashboard.

// In ServiceDefaults/Extensions.cs
public static IHostApplicationBuilder AddServiceDefaults(
    this IHostApplicationBuilder builder)
{
    builder.ConfigureOpenTelemetry();
    builder.AddDefaultHealthChecks();
    builder.Services.AddServiceDiscovery();

    builder.Services.ConfigureHttpClientDefaults(http =>
    {
        http.AddStandardResilienceHandler();
        http.AddServiceDiscovery();
    });

    return builder;
}

private static IHostApplicationBuilder ConfigureOpenTelemetry(
    this IHostApplicationBuilder builder)
{
    builder.Logging.AddOpenTelemetry(logging =>
    {
        logging.IncludeFormattedMessage = true;
        logging.IncludeScopes = true;
    });

    builder.Services.AddOpenTelemetry()
        .WithMetrics(metrics =>
        {
            metrics.AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddRuntimeInstrumentation();
        })
        .WithTracing(tracing =>
        {
            tracing.AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation();
        });

    return builder;
}

With this setup, every AI call is traced. You can see how long the LLM took to respond, which functions were called, and how long each database query within a plugin took. This is invaluable for debugging and optimizing AI applications in production.

Adding Resilience

AI endpoints can be flaky. Models have rate limits, services go down, and latency spikes happen. Aspire's integration with Microsoft.Extensions.Http.Resilience makes it straightforward to add retry policies and circuit breakers.

builder.Services.ConfigureHttpClientDefaults(http =>
{
    http.AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.Retry.Delay = TimeSpan.FromSeconds(2);
        options.Retry.BackoffType = DelayBackoffType.Exponential;
        options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
    });
});

This ensures that transient failures from the OpenAI API do not crash your application. The circuit breaker prevents cascading failures when the AI service is truly down.

Putting It All Together

What makes combining Aspire and Semantic Kernel work well is that each tool handles what it does best. Aspire manages the infrastructure concerns: service discovery, health checks, telemetry, and resource orchestration. Semantic Kernel handles the AI concerns: prompt management, function calling, and model abstraction.

When you run dotnet run on the AppHost project, Aspire spins up Redis, PostgreSQL, and your AI API service. The dashboard shows you real-time traces, logs, and metrics. Your AI service handles chat requests with automatic function calling, backed by real data from your database.

This architecture scales naturally. Need to add a vector database for RAG? Add it as an Aspire resource. Need a background service for document processing? Add another project reference. Need to swap from Azure OpenAI to a local model? Change one line in the Semantic Kernel configuration.

If you are building AI applications in .NET, this combination is worth exploring. The developer experience is excellent, the observability is unmatched, and the architecture guides you toward production-ready patterns from day one.