DEV Community

Cover image for Build an Aspire API Using Microsoft OpenAI, Scalar, OpenRouter, Structured Output, and Custom Headers
Victorio Berra
Victorio Berra

Posted on

Build an Aspire API Using Microsoft OpenAI, Scalar, OpenRouter, Structured Output, and Custom Headers

[
  {
    "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
  }
]
Enter fullscreen mode Exit fullscreen mode

Bootstraping

Create a new Aspire solution:

vs new aspire project

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();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.
  },
});
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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)