Custom resource commands in .NET Aspire

Each resource in the .NET Aspire app model is represented as an IResource and when added to the distributed application builder, it's the generic-type parameter of the IResourceBuilder<T> interface. You use the resource builder API to chain calls, configuring the underlying resource, and in some situations, you might want to add custom commands to the resource. Some common scenario for creating a custom command might be running database migrations or seeding/resetting a database. In this article, you learn how to add a custom command to a Redis resource that clears the cache.

Important

These .NET Aspire dashboard commands are only available when running the dashboard locally. They're not available when running the dashboard in Azure Container Apps.

Add custom commands to a resource

Start by creating a new .NET Aspire Starter App from the available templates. To create the solution from this template, follow the Quickstart: Build your first .NET Aspire solution. After creating this solution, add a new class named RedisResourceBuilderExtensions.cs to the app host project. Replace the contents of the file with the following code:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;

namespace Aspire.Hosting;

internal static class RedisResourceBuilderExtensions
{
    public static IResourceBuilder<RedisResource> WithClearCommand(
        this IResourceBuilder<RedisResource> builder)
    {
        builder.WithCommand(
            name: "clear-cache",
            displayName: "Clear Cache",
            executeCommand: context => OnRunClearCacheCommandAsync(builder, context),
            updateState: OnUpdateResourceState,
            iconName: "AnimalRabbitOff",
            iconVariant: IconVariant.Filled);

        return builder;
    }

    private static async Task<ExecuteCommandResult> OnRunClearCacheCommandAsync(
        IResourceBuilder<RedisResource> builder,
        ExecuteCommandContext context)
    {
        var connectionString = await builder.Resource.GetConnectionStringAsync() ??
            throw new InvalidOperationException(
                $"Unable to get the '{context.ResourceName}' connection string.");

        await using var connection = ConnectionMultiplexer.Connect(connectionString);

        var database = connection.GetDatabase();

        await database.ExecuteAsync("FLUSHALL");

        return CommandResults.Success();
    }

    private static ResourceCommandState OnUpdateResourceState(
        UpdateCommandStateContext context)
    {
        var logger = context.ServiceProvider.GetRequiredService<ILogger<Program>>();

        if (logger.IsEnabled(LogLevel.Information))
        {
            logger.LogInformation(
                "Updating resource state: {ResourceSnapshot}",
                context.ResourceSnapshot);
        }

        return context.ResourceSnapshot.HealthStatus is HealthStatus.Healthy
            ? ResourceCommandState.Enabled
            : ResourceCommandState.Disabled;
    }
}

The preceding code:

  • Shares the Aspire.Hosting namespace so that it's visible to the app host project.
  • Is a static class so that it can contain extension methods.
  • It defines a single extension method named WithClearCommand, extending the IResourceBuilder<RedisResource> interface.
  • The WithClearCommand method registers a command named clear-cache that clears the cache of the Redis resource.
  • The WithClearCommand method returns the IResourceBuilder<RedisResource> instance to allow chaining.

The WithCommand API adds the appropriate annotations to the resource, which are consumed in the .NET Aspire dashboard. The dashboard uses these annotations to render the command in the UI. Before getting too far into those details, let's ensure that you first understand the parameters of the WithCommand method:

  • name: The name of the command to invoke.
  • displayName: The name of the command to display in the dashboard.
  • executeCommand: The Func<ExecuteCommandContext, Task<ExecuteCommandResult>> to run when the command is invoked, which is where the command logic is implemented.
  • updateState: The Func<UpdateCommandStateContext, ResourceCommandState> callback is invoked to determine the "enabled" state of the command, which is used to enable or disable the command in the dashboard.
  • iconName: The name of the icon to display in the dashboard. The icon is optional, but when you do provide it, it should be a valid Fluent UI Blazor icon name.
  • iconVariant: The variant of the icon to display in the dashboard, valid options are Regular (default) or Filled.

Execute command logic

The executeCommand delegate is where the command logic is implemented. This parameter is defined as a Func<ExecuteCommandContext, Task<ExecuteCommandResult>>. The ExecuteCommandContext provides the following properties:

  • ExecuteCommandContext.ServiceProvider: The IServiceProvider instance that's used to resolve services.
  • ExecuteCommandContext.ResourceName: The name of the resource instance that the command is being executed on.
  • ExecuteCommandContext.CancellationToken: The CancellationToken that's used to cancel the command execution.

In the preceding example, the executeCommand delegate is implemented as an async method that clears the cache of the Redis resource. It delegates out to a private class-scoped function named OnRunClearCacheCommandAsync to perform the actual cache clearing. Consider the following code:

private static async Task<ExecuteCommandResult> OnRunClearCacheCommandAsync(
    IResourceBuilder<RedisResource> builder,
    ExecuteCommandContext context)
{
    var connectionString = await builder.Resource.GetConnectionStringAsync() ??
        throw new InvalidOperationException(
            $"Unable to get the '{context.ResourceName}' connection string.");

    await using var connection = ConnectionMultiplexer.Connect(connectionString);

    var database = connection.GetDatabase();

    await database.ExecuteAsync("FLUSHALL");

    return CommandResults.Success();
}

The preceding code:

  • Retrieves the connection string from the Redis resource.
  • Connects to the Redis instance.
  • Gets the database instance.
  • Executes the FLUSHALL command to clear the cache.
  • Returns a CommandResults.Success() instance to indicate that the command was successful.

Update command state logic

The updateState delegate is where the command state is determined. This parameter is defined as a Func<UpdateCommandStateContext, ResourceCommandState>. The UpdateCommandStateContext provides the following properties:

  • UpdateCommandStateContext.ServiceProvider: The IServiceProvider instance that's used to resolve services.
  • UpdateCommandStateContext.ResourceSnapshot: The snapshot of the resource instance that the command is being executed on.

The immutable snapshot is an instance of CustomResourceSnapshot, which exposes all sorts of valuable details about the resource instance. Consider the following code:

private static ResourceCommandState OnUpdateResourceState(
    UpdateCommandStateContext context)
{
    var logger = context.ServiceProvider.GetRequiredService<ILogger<Program>>();

    if (logger.IsEnabled(LogLevel.Information))
    {
        logger.LogInformation(
            "Updating resource state: {ResourceSnapshot}",
            context.ResourceSnapshot);
    }

    return context.ResourceSnapshot.HealthStatus is HealthStatus.Healthy
        ? ResourceCommandState.Enabled
        : ResourceCommandState.Disabled;
}

The preceding code:

  • Retrieves the logger instance from the service provider.
  • Logs the resource snapshot details.
  • Returns ResourceCommandState.Enabled if the resource is healthy; otherwise, it returns ResourceCommandState.Disabled.

Test the custom command

To test the custom command, update your app host project's Program.cs file to include the following code:

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache")
                   .WithClearCommand();

var apiService = builder.AddProject<Projects.AspireApp_ApiService>("apiservice");

builder.AddProject<Projects.AspireApp_Web>("webfrontend")
    .WithExternalHttpEndpoints()
    .WithReference(cache)
    .WaitFor(cache)
    .WithReference(apiService)
    .WaitFor(apiService);

builder.Build().Run();

The preceding code calls the WithClearCommand extension method to add the custom command to the Redis resource. Run the app and navigate to the .NET Aspire dashboard. You should see the custom command listed under the Redis resource. On the Resources page of the dashboard, select the ellipsis button under the Actions column:

.NET Aspire dashboard: Redis cache resource with custom command displayed.

The preceding image shows the Clear cache command that was added to the Redis resource. The icon displays as a rabbit crosses out to indicate that the speed of the dependant resource is being cleared.

Select the Clear cache command to clear the cache of the Redis resource. The command should execute successfully, and the cache should be cleared:

.NET Aspire dashboard: Redis cache resource with custom command executed.

See also