He’s making a list
Checking it twice
Writing some code
To track who’s naughty and nice

Santa Claus is wri-ting… C#

tldr; With .NET 9, you can now add custom action buttons to the .NET Aspire dashboard screen

You ever wonder how Santa is able to keep track of the kids who are naughty and nice? I mean, it was probably fairly easy when an old man could send out his army of short helpers to check up on the handful of kids who fell into his purview from time to time through the year, and to keep all that recorded on paper. But today? With the world’s population exploding? How on Earth could he possibly do that now? With each passing year it’s gotten harder and harder. Never fear, Santa, I got ya covered. Let me throw together a quick proof of concept (POC) and show you the future!

.NET Aspire is the perfect tool to throw together our little POC. It will let us quickly throw together a working application that we can use to show off how easily the Naughty and Nice lists can be automated. An elf click here, an elf click there, and whammo, the lists have been automatically checked… twice.

We’ll start with a basic .NET 9 Aspire demo app. (Link to source code at the bottom). This will give us a basic application with a .NET Web API back end. That will hold a static service to start with, with two lists of names: one for the Naughty kids, and one for the Nice kids. We’ll create 4 HTTP endpoints to support it. 2 GET endpoints to retrieve the names, and 2 POST endpoints to add names to each list. For now, since this is a POC, we’ll use a library to generate random names to add to each list when the POST endpoints are called. We’ll also have a Blazor front end that retrieves the data from each list and shows them on a page. Very simple, very easy.

Ok, that’s a great start, but it needs to be more dynamic. I gotta be able to show Santa some real activity in the app. Sure, I could add a function to the web app to call the add endpoints, but that’s something I really only need in my dev environment so I can show it off to Santa and the elves. No worries. Aspire lets you do that really easily in the AppHost project. With Aspire in .NET 9, we now have the ability to add custom buttons onto the dashboard. For this demo, we’ll add two buttons that will call their respective endpoints in the API to add some test data to each of the lists!

In our API project, we’ll create a service to hold our lists, with functions to retrieve and add to them:

    public class NaughtyNiceService
    {
        public List<string> NaughtyList { get; set; } = new List<string>();
        public List<string> NiceList { get; set; } = new List<string>();

        public void AddToNaughtyList(string name)
        {
            NaughtyList.Add(name);
        }

        public void AddToNiceList(string name)
        {
            NiceList.Add(name);
        }

        public List<string> GetNaughtyList()
        {
            if (!NaughtyList.Any())
            {
                var faker = new Faker("en");
                NaughtyList.Add(faker.Name.FullName());
            }
            return NaughtyList;
        }

        public List<string> GetNiceList()
        {
            if (!NiceList.Any())
            {
                var faker = new Faker("en");
                NiceList.Add(faker.Name.FullName());
            }
            return NiceList;
        }
    }

And we defined the endpoints to be called as follows:

    public static class NaughtyNiceEndpoints
    {
        public static void AddNaughtyNiceEndpoints(this WebApplication app)
        {
            app.MapGet("/getnaughtylist", (NaughtyNiceService service) => service.GetNaughtyList())
                .WithName("GetNaughtyList");

            app.MapGet("/getnicelist", (NaughtyNiceService service) => service.GetNiceList())
                .WithName("GetNiceList");

            app.MapPost("/addnaughtylist", (NaughtyNiceService service) =>
            {
                var faker = new Faker("en");
                service.AddToNaughtyList(faker.Name.FullName());
            })
            .WithName("AddToNaughtyList");

            app.MapPost("/addnicelist", (NaughtyNiceService service) => {
                var faker = new Faker("en");
                service.AddToNiceList(faker.Name.FullName());
            })
            .WithName("AddToNiceList");
        }
    }

Pretty clear. Two endpoints to return our lists, and two for adding a random name to each list.

The rest of our important code will be put into our AppHost project. This is where the dashboard gets set up, and our custom action buttons will need to be defined here. So, the next thing we need to do is to define a resource builder extension to define what the button is and does. There’s a great example in the Aspire Samples that we’ll borrow from.

internal static class ResourceBuilderExtensions
{
    /// <summary>
    /// Adds a command to the resource that sends an HTTP request to the specified path.
    /// </summary>
    public static IResourceBuilder<TResource> WithHttpsCommand<TResource>(this IResourceBuilder<TResource> builder,
        string path,
        string displayName,
        HttpMethod? method = default,
        string? endpointName = default,
        string? iconName = default)
        where TResource : IResourceWithEndpoints
        => WithHttpCommandImpl(builder, path, displayName, endpointName ?? "https", method, "https", iconName);

    /// <summary>
    /// Adds a command to the resource that sends an HTTP request to the specified path.
    /// </summary>
    public static IResourceBuilder<TResource> WithHttpCommand<TResource>(this IResourceBuilder<TResource> builder,
        string path,
        string displayName,
        HttpMethod? method = default,
        string? endpointName = default,
        string? iconName = default)
        where TResource : IResourceWithEndpoints
        => WithHttpCommandImpl(builder, path, displayName, endpointName ?? "http", method, "http", iconName);

    private static IResourceBuilder<TResource> WithHttpCommandImpl<TResource>(this IResourceBuilder<TResource> builder,
        string path,
        string displayName,
        string endpointName,
        HttpMethod? method,
        string expectedScheme,
        string? iconName = default)
        where TResource : IResourceWithEndpoints
    {
        method ??= HttpMethod.Post;

        var endpoints = builder.Resource.GetEndpoints();
        var endpoint = endpoints.FirstOrDefault(e => string.Equals(e.EndpointName, endpointName, StringComparison.OrdinalIgnoreCase))
            ?? throw new DistributedApplicationException($"Could not create HTTP command for resource '{builder.Resource.Name}' as no endpoint named '{endpointName}' was found.");

        var commandName = $"http-{method.Method.ToLowerInvariant()}-{path.ToLowerInvariant()}-request";

        builder.WithCommand(commandName, displayName, async context =>
        {
            if (!endpoint.IsAllocated)
            {
                return new ExecuteCommandResult { Success = false, ErrorMessage = "Endpoints are not yet allocated." };
            }

            if (!string.Equals(endpoint.Scheme, expectedScheme, StringComparison.OrdinalIgnoreCase))
            {
                return new ExecuteCommandResult { Success = false, ErrorMessage = $"The endpoint named '{endpointName}' on resource '{builder.Resource.Name}' does not have the expected scheme of '{expectedScheme}'." };
            }

            var uri = new UriBuilder(endpoint.Url) { Path = path }.Uri;
            var httpClient = context.ServiceProvider.GetRequiredService<IHttpClientFactory>().CreateClient();
            var request = new HttpRequestMessage(method, uri);
            try
            {
                var response = await httpClient.SendAsync(request, context.CancellationToken);
                response.EnsureSuccessStatusCode();
            }
            catch (Exception ex)
            {
                return new ExecuteCommandResult { Success = false, ErrorMessage = ex.Message };
            }
            return new ExecuteCommandResult { Success = true };
        },
        iconName: iconName,
        iconVariant: IconVariant.Regular);

        return builder;
    }
}

This code defines a couple of fluent extensions for the builder. These extensions allow us to create buttons that will call an http/https endpoint in the associated resource. The magic is the .WithCommand function. This defines a command (i.e. the action button and what it does) that gets added to a particular resource. For each button we’ll pass in the endpoint path, the text to display, and a FluentUI icon name. We can also optionally pass in the endpoint name, schema, and method, but we won’t be using those in this example.

I did make one significant change to the sample code. In the following line:

var commandName = $"http-{method.Method.ToLowerInvariant()}-{path.ToLowerInvariant()}-request";

I added the {path.ToLowerInvariant()} to the name. The default code didn’t include this and as such, the name would be duplicated for each button because the method was POST in both cases. This would cause only one command button to be created because the command name needs to be unique. Since I needed to add multiple command buttons of the POST variety, I added additional name piece so that each could have a unique name and allow all to be created. I really should have updated the path portion to remove any special characters like “/”, but since our endpoints were simple, I opted for speed.

Now that have the code to create the command buttons, we’ll pop over to our AppHost Program.cs file and update our API resource definition by adding two fluent calls to the WithHttpsCommand. The first will call our “addnaughtylist” endpoint and the second will call our “addnicelist” endpoint. This will add the buttons to the API resource commands in our Aspire dashboard.

var apiService = builder.AddProject<Projects.CSharpAdvent2024_ApiService>("apiservice")
    .WithHttpsCommand("/addnaughtylist", "Add to Naughty List", iconName: "ErrorCircle")
    .WithHttpsCommand("/addnicelist", "Add to Nice List", iconName: "CheckmarkCircle");

This will create two buttons, one labeled “Add to Naughty List”, and the other labeled “Add to Nice List”, that will call our endpoints.

Two new action buttons added

When we run our app and load the Nice and Naughty List page, we start with one name on each list.

Starting with one name each

Now let’s hit each of our action buttons a few times. Refresh the Nice and Naughty List page and… poof!

More names on each list

See how quickly I can throw together a POC for Santa! Less than an hour’s effort and we’re well on our way to revolutionizing the elf bureaucracy! It’s amazing stuff, and .NET Aspire makes it really simple to achieve. But what else can I do with these custom buttons? Pretty much anything you can write in your code. It’s all up to you. If you can code it, you can do it! Here’s a few ideas:

  • Call an endpoint
  • Reset your database
  • Generate test data
  • Run a shell script
  • Clear your Redis cache
  • Post a message to Teams
  • Generate or reset files
  • Rick Roll Please only use your commands for good…..

Really, like I said, just about anything. If you can write code to do it, you can call it from one of these action buttons. And since it’s tied to the dashboard, it’s just there for developer use on your local machine. So it’s ideal for anything you need locally but not in prod.

.NET Aspire is really off to a fantastic start and each version just gets better and better for making your developer life easier. It’s definitely gonna help me impress Santa so I can get ALL the presents this year. And, if I add a back door to make sure I stay on the Nice list… who’s gonna know, right?

Here’s a Link to the code repo