# Building Generative AI Applications With GitHub Models and .NET Aspire

> Discover how to integrate AI into your .NET applications in under an hour using GitHub Models and .NET Aspire 9.4. I'll walk you through building a simple AI-powered blog analyzer that automatically categorizes your content.

Published: 2025-08-16. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/building-generative-ai-applications-with-github-models-and-dotnet-aspire

GitHub Models gives you OpenAI, Microsoft, and Meta models behind one API, and .NET Aspire 9.4 added an integration for it.
You declare the model in the AppHost with `AddGitHubModel`, reference it from your project, and call `AddAzureChatCompletionsClient` and `AddChatClient` in the consuming service.
Aspire wires the API key and connection by resource name.

I wanted to see what the simplest practical AI app I could build was, and this is what I came up with.

Every week, I publish blog posts covering different topics - architecture patterns, cloud services, programming techniques, business insights.
Sometimes I write about DevOps, other times about security.
After years of writing, I realized I had no systematic way to categorize my content.
Sure, I could manually tag each post, but where's the fun in that?

So I built a simple AI-powered blog analyzer.
It fetches any blog post, extracts the content, and uses AI to automatically categorize it.
The entire thing took less than an hour to build thanks to **.NET Aspire 9.4** and it's new **GitHub Models** integration.

What surprised me wasn't just how easy it was to build, but how the integration completely removes the typical AI service complexity.
No juggling API keys in configuration files, no manual HTTP client setup, no wrestling with different SDK patterns for different AI providers.
You declare an AI model in your AppHost just like you would a database, and Aspire handles the rest.

Here's what I learned building this simple app, and how you can use the same patterns to add AI to your applications.

## What are GitHub Models?

[GitHub Models](https://docs.github.com/en/github-models) is a service that provides access to AI models from OpenAI, Microsoft, Meta, and others through a single API.
You get free tier access for prototyping (with rate limits), an interactive playground for testing prompts, and pay-per-use billing when you're ready for production.

The models range from cost-effective options like **GPT-4o-mini** to more advanced models.
Each model has different strengths - some excel at reasoning, others at code generation or creative writing.

When you combine GitHub Models with .NET Aspire's orchestration, you get:

- Automatic API key management via [external parameters](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/external-parameters)
- [**Service discovery**](https://milanjovanovic.tech/blog/how-dotnet-aspire-simplifies-service-discovery) between your components
- Built-in [**health checks**](https://milanjovanovic.tech/blog/health-checks-in-asp-net-core) and [**telemetry**](https://milanjovanovic.tech/blog/introduction-to-distributed-tracing-with-opentelemetry-in-dotnet)
- Consistent configuration patterns

## Setting Up the Integration

The GitHub Models integration splits into two parts: configuring models in your `AppHost` and consuming them in your services.

### AppHost Configuration

In your AppHost project, you define AI models as resources alongside your other services:

```csharp {4,8}
var builder = DistributedApplication.CreateBuilder(args);

var blogService = builder.AddExternalService("dotnet-blog", "https://www.milanjovanovic.tech/blog");
var aiModel = builder.AddGitHubModel("ai-model", "openai/gpt-4o-mini");

builder.AddProject<Projects.GitHub_Models_Demo>("github-models-demo")
    .WithReference(blogService)
    .WithReference(aiModel);
```

Notice how the AI model sits alongside the external blog service.
Both are resources that your main application depends on.
Aspire handles the connection details - you just declare what you need.

Make sure to install the [Aspire.Hosting.GitHub.Models](https://www.nuget.org/packages/Aspire.Hosting.GitHub.Models) NuGet package to enable this integration.

To call the GitHub Models inference API you need a personal access token with the `models:read` permission.
When you call `AddGitHubModel`, Aspire automatically creates a parameter named `{resourceName}-gh-apikey` (for example, `ai-model-gh-apikey`)

You can populate the parameter through user secrets for local development:

```json
{
  "Parameters": {
    "ai-model-gh-apikey": "github_pat_YOUR_PERSONAL_ACCESS_TOKEN"
  }
}
```

If you don't provide this value from configuration, Aspire will prompt you to enter it when you run the application.
You'll have an option to store the access token securely in user secrets.

### Client Setup

In your consuming service, add the Azure AI client (which works with GitHub Models):

```csharp {2}
builder
    .AddAzureChatCompletionsClient("ai-model")
    .AddChatClient();
```

That's it.
No manual HTTP client configuration, no hardcoded endpoints.
The `ai-model` name matches what you defined in the AppHost, and Aspire wires everything together.

You'll need to install the [Aspire.Azure.AI.Inference](https://www.nuget.org/packages/Aspire.Azure.AI.Inference) NuGet package to enable this integration.
It also exposes an integration with [**MEAI**](https://milanjovanovic.tech/blog/working-with-llms-in-dotnet-using-microsoft-extensions-ai) using the `AddChatClient` method.
This simplifies the process of interacting with LLMs in your applications.

## Building the Blog Analyzer

Now let's build something useful.
We'll create a service that fetches blog posts and uses AI to categorize them.

### Fetching Blog Content

First, we need to extract readable content from blog posts:

```csharp
public async Task<string> GetBlogContentAsync(string slug)
{
    var response = await httpClient.GetAsync(slug);
    response.EnsureSuccessStatusCode();
    var htmlContent = await response.Content.ReadAsStringAsync();

    return ExtractArticleContent(htmlContent);
}
```

The `ExtractArticleContent` method (not shown) uses [HtmlAgilityPack](https://www.nuget.org/packages/htmlagilitypack/) to pull out the main article text,
stripping away navigation, ads, and other page elements.

### AI-Powered Categorization

Here's where it gets interesting.
We'll use the AI model to analyze the content and assign a category:

```csharp {3-13, 15}
public async Task<string> SummarizeBlogAsync(string blogContent)
{
    var prompt =
        @"""
        You are a blog content assistant. Summarize the following blog post
        as one of the following categories: Technology, Business, Programming,
        Architecture, DevOps, Cloud, Security, General.
        Only those eight values are allowed. Be as concise as possible.
        I want a 1-word response with one of these options: Technology, Business,
        Programming, Architecture, DevOps, Cloud, Security, General.

        The blog content is: {blogContent}
        """;

    var response = await chatClient.GetResponseAsync(prompt);

    if (!response.Messages.Any())
    {
        return "General";
    }

    var category = response.Messages.First().Text switch
    {
        var s when s.Contains("Technology", StringComparison.OrdinalIgnoreCase) => "Technology",
        var s when s.Contains("Business", StringComparison.OrdinalIgnoreCase) => "Business",
        var s when s.Contains("Programming", StringComparison.OrdinalIgnoreCase) => "Programming",
        var s when s.Contains("Architecture", StringComparison.OrdinalIgnoreCase) => "Architecture",
        var s when s.Contains("DevOps", StringComparison.OrdinalIgnoreCase) => "DevOps",
        var s when s.Contains("Cloud", StringComparison.OrdinalIgnoreCase) => "Cloud",
        var s when s.Contains("Security", StringComparison.OrdinalIgnoreCase) => "Security",
        var s when s.Contains("General", StringComparison.OrdinalIgnoreCase) => "General",
        _ => "General"
    };

    return category;
}
```

A few things make this work reliably:

- The prompt is explicit about allowed categories
- We request a single-word response to avoid parsing complex output
- The switch expression handles variations in the AI's response
- There's always a fallback to "General"

### Exposing the API

Finally, we wrap everything in a simple [**API endpoint**](https://milanjovanovic.tech/blog/minimal-apis-dotnet):

```csharp {4, 7}
app.MapPost("/summarize-blog", async (
    string slug,
    BlogService blogService,
    BlogSummarizer blogSummarizer) =>
{
    var content = await blogService.GetBlogContentAsync(slug);
    var category = await blogSummarizer.SummarizeBlogAsync(content);

    return Results.Ok(new
    {
        slug,
        category,
        content = $"{content.Substring(0, 50)}..."
    });
});
```

Call this endpoint with a blog post URL slug, and you get back the category and a preview of the content.
The dependency injection handles service resolution, and Aspire manages the AI model connection behind the scenes.

Here's an example response:

```json
{
  "slug": "screaming-architecture", // https://www.milanjovanovic.tech/blog/screaming-architecture
  "category": "Architecture",
  "content": "If you were to glance at the folder structure of y..."
}
```

Here's the distributed trace in Aspire showing the request and response flow.
You can see the request to the blog service and the AI model.

![Distributed trace in Aspire showing the request and response flow using GitHub Models](https://milanjovanovic.tech/blogs/mnw_155/distributed_trace.png)

## Wrapping Up

The GitHub Models integration with .NET Aspire removes much of the complexity from adding AI to your applications.
You get:

- Simple configuration through the AppHost pattern
- Automatic service discovery and connection management
- Access to multiple AI models without vendor lock-in
- The same observability and deployment benefits as other Aspire resources

Whether you're [**adding AI to an existing system**](https://milanjovanovic.tech/blog/working-with-llms-in-dotnet-using-microsoft-extensions-ai) or building something new, this integration provides the foundation you need.
Start with simple categorization or summarization, then expand as you learn what works for your use case.

Thanks for reading.

And stay awesome!

---

## Frequently asked questions

### What are GitHub Models?

GitHub Models is a GitHub service that provides access to AI models from OpenAI, Microsoft, Meta, and others through a single API. It launched with free tier access for prototyping (with rate limits), an interactive playground for testing prompts, and pay-per-use billing for production.

### How do you use GitHub Models with .NET Aspire?

.NET Aspire 9.4 introduced a GitHub Models integration. You declare the model in the AppHost with AddGitHubModel, reference it from your project, then call AddAzureChatCompletionsClient and AddChatClient in the consuming service. Aspire wires the connection together by resource name.

### Which NuGet packages does the Aspire GitHub Models integration need?

Two packages: Aspire.Hosting.GitHub.Models in the AppHost project to enable AddGitHubModel, and Aspire.Azure.AI.Inference in the consuming service, which also exposes a Microsoft.Extensions.AI integration through the AddChatClient method.

### How does .NET Aspire manage the GitHub Models API key?

You need a GitHub personal access token with the models:read permission. AddGitHubModel automatically creates a parameter named {resourceName}-gh-apikey, which you populate through user secrets for local development. If the value is missing, Aspire prompts for it at startup and can store it securely.

### How do you get a reliable category from an LLM response?

Make the prompt explicit about the allowed categories, ask for a one-word response, match the output against each allowed value with a case-insensitive comparison, and always fall back to a default category when nothing matches.
