Enhancing the .NET Aspire Dashboard with Custom HTTP Commands
The .NET Aspire Dashboard is a centralized view into distributed application insights, environment variables, logs, traces, and monitoring capabilities. The .NET Aspire Dashboard isn't just a traditional insights dashboard; it also allows developers to manage applications to some extent. For example, resources can be started and stopped directly from the Dashboard. The latest version of .NET Aspire (9.2) introduced interesting capabilities to extend application management in the Dashboard.
During local development and testing, developers often need to clear the database, rerun migrations, or seed test data. These actions usually occur at application startup by performing database operations in the code or manually running SQL scripts. It's cumbersome to clean and seed data multiple times in the same session while the application is running.
This blog post introduces Custom Commands, particularly Custom HTTP Commands, to enhance the capabilities of the .NET Aspire Dashboard. The post explains how to create custom actions for adding and removing test data online during debugging or testing. There are at least a couple of ways to do this, but as said, this post focuses on Custom HTTP Commands.
Actions in .NET Aspire Dashboard
By default, each resource in .NET Aspire Dashboard has multiple actions, such as restart, show logs, etc. Actions are shown in the right column in the Resource view.

You can create your own action commands using Custom Resource Commands or Custom HTTP Commands. These command types are resource-specific, meaning that the action performs an operation which is using the resource somehow. For example, seeding test data to a SQL database. Custom HTTP Command sends an HTTP request to a specific endpoint defined in the resource. You can specify the path of the endpoint, but it needs to be the same resource where the custom action was connected to.
Why to use HTTP Commands?
Typically, in a .NET Aspire solution, there are multiple separate and independent microservices. In this sample, we have a microservice called "Weather". The Weather Service includes its layers, API, and particularly the infrastructure layer, that communicates with the database (Database Context).
Custom HTTP command approach enables the use of existing API and database infrastructure capabilities of the microservice, making the extension work pretty straightforward. Basically, the biggest work is to create and secure new API endpoints for seeding and deleting data. The beauty of this approach is to keep the solution simple and dependencies clean.
There are alternative solutions, such as using Executables or Worker Projects for data seeding and deletion. You can easily initiate the process from the .NET Aspire Dashboard without needing to create custom commands, as these are separate resources. With this option, you can use this capability also in Azure, not only locally like with custom commands. If you have multiple needs to execute different kinds of operations, then the creation of multiple separate Executables or Worker Projects can be cumbersome, and then custom HTTP commands can be a better choice.

👉 Keeps the solution clean and simple. You can use the existing API capabilities.
👉 It's easy to extend an API with new command endpoints.
👉 Needs some extra work to secure these local API endpoints.
👉 Custom commands work right now only locally.
Let's get started
.NET Aspire App Host project
To use an HTTP command with a resource, utilize the WithHttpCommand extension method found in the resource builder. You need to specify the path, display name, icon, and protection capability (e.g, API key in the header). All available icons can be checked from here.
var weatherapi = builder.AddProject<Projects.Demo_Weather_Api>("weather-api")
.WithEnvironment("ASPNETCORE_ENVIRONMENT", environment)
.WithReference(idp)
.WaitFor(idp)
.WithReference(weatherDatabase)
.WaitFor(weatherDatabase)
.WithHttpsEndpoint(port: (builder.ExecutionContext.IsPublishMode || !builder.Environment.IsDevelopment() ? httpsPort : 1443)).WithExternalHttpEndpoints();
if(builder.Environment.IsDevelopment())
{
var localApiKeyName = "local-api-key";
var localApiKey = builder.AddParameter(localApiKeyName, secret: true);
var localApiHeaderName = "X-Local-Development-API-Key";
weatherapi.WithEnvironment(localApiKeyName, localApiKey);
weatherapi.WithHttpCommand(
path: "/data/seed",
displayName: "Seed sample data",
commandOptions: new HttpCommandOptions()
{
Description = "Seed sample data to the database",
PrepareRequest = (context) =>
{
context.Request.Headers.Add(localApiHeaderName, localApiKey.Resource.Value);
return Task.CompletedTask;
},
IconName = "DatabaseArrowUp",
IsHighlighted = true
});
weatherapi.WithHttpCommand(
path: "/data/clear",
displayName: "Clear database",
commandOptions: new HttpCommandOptions()
{
Description = "Clear database",
PrepareRequest = (context) =>
{
context.Request.Headers.Add(localApiHeaderName, localApiKey.Resource.Value);
return Task.CompletedTask;
},
IconName = "DatabaseArrowDown",
IsHighlighted = true
});
}
Weather API project
Endpoint Filter
The endpoint filter verifies that the correct API key is included in the HTTP request from the .NET Aspire Dashboard.
public class LocalEndpointFilter : IEndpointFilter
{
private readonly IConfiguration _configuration;
private readonly IHostEnvironment _environment;
private const string _headerName = "X-Local-Development-API-Key";
private const string _configKeyName = "local-api-key";
public LocalEndpointFilter(IConfiguration configuration, IHostEnvironment environment)
{
_configuration = configuration;
_environment = environment;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
if(!_environment.IsDevelopment())
{
return Results.Unauthorized();
}
var headerValue = context.HttpContext.Request.Headers[_headerName].ToString();
var configValue = _configuration.GetValue<string>(_configKeyName);
if (headerValue != configValue)
{
return Results.Unauthorized();
}
return await next(context);
}
}
Endpoint Extensions
This extension introduces the API endpoints which are called from the custom HTTP commands. One endpoint handles the data seeding, and another one clears the database. Endpoint Filter is added globally to all endpoints within the endpoint group.
internal static class EndpointRouteBuilderExtensions
{
internal static IEndpointConventionBuilder MapEndpoints(
this IEndpointRouteBuilder endpoints)
{
var localEndpoints = endpoints
.MapGroup("/data")
.AddEndpointFilter<LocalEndpointFilter>();
localEndpoints.MapPost("/clear", static async (WeatherContext db) =>
{
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
return Results.Ok();
});
localEndpoints.MapPost("/seed", static async (WeatherContext db) =>
{
CancellationTokenSource tokenSource = new();
CancellationToken token = tokenSource.Token;
db.Database.EnsureCreated();
await DatabaseInitializer.SeedDataAsync(db, token);
return Results.Ok();
});
return group;
}
}
Database Initializer
Database Initializer persists the seed data to the database.
public static class DatabaseInitializer
{
public static async Task SeedDataAsync(WeatherContext dbContext, CancellationToken cancellationToken)
{
var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
foreach (var weatherForecast in SeedWeatherData.WeatherForecasts())
{
await dbContext.WeatherForecasts.AddAsync(weatherForecast);
}
await dbContext.SaveChangesAsync(cancellationToken);
});
}
}
Seed Weather Data
Seed Weather Data creates sample test data which is seeded to the database.
internal static class SeedWeatherData
{
public static List<WeatherForecast> WeatherForecasts()
{
var summaries = new[]
{
"Freezing", "Chilly", "Cool", "Mild"
};
var cities = new[]
{
"Akaa", "Alajärvi", "Alavus", "Espoo", "Forssa", "Haapajärvi", "Haapavesi", "Hamina", "Hanko", "Harjavalta", "Heinola", "Helsinki", "Huittinen", "Hyvinkää", "Hämeenlinna", "Iisalmi",
"Ikaalinen", "Imatra", "Jakobstad (Pietarsaari)", "Joensuu", "Jyväskylä", "Jämsä", "Järvenpää", "Kaarina", "Kajaani", "Kalajoki", "Kangasala", "Kankaanpää", "Kannus", "Karkkila", "Kaskinen",
"Kauhajoki", "Kauhava", "Kauniainen", "Kemi", "Kemijärvi", "Kerava", "Keuruu", "Kitee", "Kiuruvesi", "Kokemäki", "Kokkola", "Kotka", "Kouvola", "Kristiinankaupunki", "Kuhmo", "Kuopio", "Kurikka",
"Kuusamo", "Lahti", "Laitila", "Lappeenranta", "Lapua", "Lieksa", "Lohja", "Loimaa", "Loviisa", "Maarianhamina", "Mikkeli", "Mänttä-Vilppula", "Naantali", "Nivala", "Nokia", "Nurmes", "Uusikaarlepyy",
"Närpiö", "Orimattila", "Orivesi", "Oulainen", "Oulu", "Outokumpu", "Paimio", "Parainen", "Parkano", "Pieksämäki", "Pori", "Porvoo", "Pudasjärvi", "Pyhäjärvi", "Raahe", "Raasepori", "Raisio", "Rovaniemi",
"Salo", "Sastamala", "Savonlinna", "Seinäjoki", "Somero", "Tampere", "Tornio", "Turku", "Vaasa", "Valkeakoski", "Vantaa", "Varkaus", "Viitasaari", "Ylivieska", "Ylöjärvi"
};
var weatherForecasts = new List<WeatherForecast>();
foreach (var city in cities)
{
for (int i = 0; i < 10; i++)
{
weatherForecasts.Add(new WeatherForecast()
{
Id = Guid.NewGuid(),
City = city,
DateTime = DateTime.Now.AddDays(i),
TemperatureC = Random.Shared.Next(-5, 5),
Summary = summaries[Random.Shared.Next(summaries.Length)]
});
}
}
return weatherForecasts;
}
}
Summary
Custom HTTP Commands are an interesting new capability to extend the .NET Aspire Dashboard. Custom commands simplify debugging, testing, and troubleshooting by allowing easy data recreation while the application is running. The command model is user-friendly, particularly the HTTP commands, as they allow for effective reuse of technical capabilities without the need for new projects or additional dependencies.
It's interesting to follow how the capabilities of .NET Aspire Dashboard will evolve. Maybe eventually Dashboard will be the main developer portal for developers.
Comments