[
{
"date": "2025-10-04",
"temperatureC": 8,
"summary": "2025-10-04: Crisp 8°C with intermittent interdimensional drizzle — expect marmalade rain and the occasional polite wormhole. 40% chance of phosphorescent fog veins, 20% chance of localized anti-gravity gusts (hold onto your hat; it may orbit you), and a small probability of spontaneous teleporting puddles that prefer not to be stepped in. Bring an umbrella and a treaty offer — umbrellas may demand a cup of tea before cooperating.",
"temperatureF": 46
}
]
Bootstraping
Create a new Aspire solution:
Install Microsoft.Extensions.AI.OpenAI
pre-release in your ApiService project. Install non-prerelease Microsoft.Extensions.AI
as well. Things like structured output extensions are in Microsoft.Extensions.AI
.
Install Scalar.Aspire
in your AppHost project.
Scalar Aspire
Microsoft is moving away from including Swashbuckle UI stuff in their templates and docs. They now have their own OpenApi (not to be confused with OpenAi!) Swagger document generation. The code will have this already set up with a link to learn more:
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
// ...
app.MapOpenApi();
So now we want a Swagger UI, there are options, we could use the battle tested Swashbuckle, the popular ReDoc, or the FOSS Scalar https://github.com/scalar/scalar however they do have a cloud offering. If you pay you get the following:
- Write your API documentation and publish your API references (free)
- Get SSL and a super cool *.apidocumentation.com subdomain (free)
- Write free text documentation (paid)
- Collaborate with your whole team (paid)
- Use any domain (paid)
- SDK generation (paid)
To set Scalar up, do the following in your Aspire AppHost:
// Add Scalar API Reference
var scalar = builder.AddScalarApiReference(options =>
{
options.WithTheme(ScalarTheme.Laserwave);
});
// Register services with the API Reference
scalar
.WithApiReference(apiService);
One really cool thing to note, under the hood this uses their proxy which gets around CORS issues!
Scalar for Aspire includes a built-in proxy that is enabled by default to provide seamless integration with your services.
Proxy Deep Dive
Aspire code is here https://github.com/scalar/scalar/tree/main/integrations/aspire
They have a custom endpoint they register at /scalar-proxy
which uses YARP https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/getting-started?view=aspnetcore-9.0
They also have a hosted service at: https://proxy.scalar.com/. Unrelated but other services they seem to have for development: https://void.scalar.com/, https://galaxy.scalar.com/
Back at The Ranch
There are a variety of other Aspire and configuration options you can check out: https://guides.scalar.com/scalar/scalar-api-references/integrations/net-aspire
At this point, you should be able to run your app and see that the Scalar service is up and running:
Clicking the link gets you to the docs:
I am not the worlds biggest fan of the theme but I set WithTheme
to show off the capability.
AI
This tutorial uses OpenRouter. If you are not familiar with it, its essentially a gateway to thousands of other models through a single interface. You load your account with credits and they bill you depending on the provider you want. For example throw $10 in your account and you can call Grok, Gemini, Claude, and you don't need accounts with them or to manage multiple keys. LinqPad recently added built-in support for OpenRouter as well.
Head over to OpenRouter and generate a key https://openrouter.ai/settings/keys
Once you have that, right click your ApiService, click "User Secrets" and paste this:
{
"OpenAIKey": "sk-or-v1-supa-secret"
}
Add the OpenAI IChatClient. Also, for an overview of the current Microsoft .NET AI landscape this blog is awesome https://roxeem.com/2025/09/08/how-to-correctly-build-ai-features-in-dotnet/
And this is awesome too: https://learn.microsoft.com/en-us/dotnet/ai/quickstarts/build-chat-app?pivots=openai
I highlighted some of the more interesting areas of the docs. If you spent a few hours reading all of that you would be pretty ramped up on AI concepts across the board. Inch-deep, mile-wide as they say. Knowing just enough to be dangerous.
Add the chat service:
var config = new ConfigurationBuilder().AddUserSecrets<Program>().Build();
string key = config["OpenAIKey"];
builder.Services.AddTransient<CustomHeadersHandler>(); //this is a standard 'DelegatingHandler' implementation that adds HTTP headers
builder.Services.AddHttpClient("OpenAiHttpClient")
.AddHttpMessageHandler<CustomHeadersHandler>();
builder.Services.AddSingleton(services =>
{
var httpClientFactory = services.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("OpenAiHttpClient");
// Create the IChatClient
IChatClient chatClient =
new OpenAIClient(
new ApiKeyCredential(key),
new OpenAIClientOptions
{
Endpoint = new Uri("https://openrouter.ai/api/v1"),
Transport = new HttpClientPipelineTransport(httpClient)
})
.GetChatClient("openrouter/auto")
.AsIChatClient();
return chatClient;
});
CustomHeadersHandler
is entirely optional:
// Thanks https://stackoverflow.com/questions/79645038/define-custom-headers-using-net-6-openai-sdk
internal class CustomHeadersHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Optional. Site URL for rankings on openrouter.ai
//request.Headers.Add("HTTP-Referer", "MyCustomHeaderValue");
// Optional. Site Title for rankings on openrouter.ai
//request.Headers.Add("X-Title", "AnotherHeaderValue");
// Call the inner handler
return await base.SendAsync(request, cancellationToken);
}
}
I wanted to show how we could do what is easily done in NodeJS/TS
import OpenAI from 'openai';
const openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1',
apiKey: '<OPENROUTER_API_KEY>',
defaultHeaders: {
'HTTP-Referer': '<YOUR_SITE_URL>', // Optional. Site URL for rankings on openrouter.ai.
'X-Title': '<YOUR_SITE_NAME>', // Optional. Site title for rankings on openrouter.ai.
},
});
Almost to the payoff! Lets use our new Chat Client!
Replace the current WeatherForecast endpoint with the following:
app.MapGet("/weatherforecast", async ([FromServices] IChatClient chatClient) =>
{
var forecast = new List<WeatherForecast>();
for (int index = 1; index <= 1; index++)
{
var targetDate = DateOnly.FromDateTime(DateTime.Now.AddDays(index));
// Generate complete weather data using AI with structured output
var prompt = $"""
Generate realistic weather data for {targetDate:yyyy-MM-dd}.
Create temperature in Celsius (between -30 and 50) and weather conditions.
Use humor to create absurd weather conditions that may only exist on other planets or universes.
Be creative with the conditions - think interdimensional weather, alien phenomena, or impossible physics.
""";
var messages = new List<ChatMessage>
{
new ChatMessage(ChatRole.User, prompt)
};
var response = await chatClient.GetResponseAsync<WeatherData>(prompt);
var weatherData = response.Result;
forecast.Add(new WeatherForecast(
targetDate,
weatherData.TemperatureC,
weatherData.Conditions));
}
return forecast.ToArray();
})
.WithName("GetWeatherForecast");
Some cool tidbits about the above code. GetResponseAsync<WeatherData>
is special as the response is a C# object! This is thanks to the rather transparent handling of structured output from the LLM.
Finally, open your Scalar UI, scroll down to the /weatherforecast
endpoint, click "Test Request", click "Send".
Top comments (0)