File: ApplicationModel\ResourceCommandService.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// A service to execute resource commands.
/// </summary>
public class ResourceCommandService
{
    private readonly ResourceNotificationService _resourceNotificationService;
    private readonly ResourceLoggerService _resourceLoggerService;
    private readonly IServiceProvider _serviceProvider;
 
    // Constructor is pureposefully internal so adding new dependencies in the future isn't a public API change.
    internal ResourceCommandService(ResourceNotificationService resourceNotificationService, ResourceLoggerService resourceLoggerService, IServiceProvider serviceProvider)
    {
        _resourceNotificationService = resourceNotificationService;
        _resourceLoggerService = resourceLoggerService;
        _serviceProvider = serviceProvider;
    }
 
    /// <summary>
    /// Execute a command for the specified resource.
    /// </summary>
    /// <param name="resourceId">The id of the resource.</param>
    /// <param name="commandName">The command name.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The <see cref="ExecuteCommandResult" /> indicates command success or failure.</returns>
    public async Task<ExecuteCommandResult> ExecuteCommandAsync(string resourceId, string commandName, CancellationToken cancellationToken = default)
    {
        if (!_resourceNotificationService.TryGetCurrentState(resourceId, out var resourceEvent))
        {
            return new ExecuteCommandResult { Success = false, ErrorMessage = $"Resource '{resourceId}' not found." };
        }
 
        return await ExecuteCommandCoreAsync(resourceEvent.ResourceId, resourceEvent.Resource, commandName, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Execute a command for the specified resource.
    /// </summary>
    /// <param name="resource">The resource. If the resource has multiple instances, such as replicas, then the command will be executed for each instance.</param>
    /// <param name="commandName">The command name.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The <see cref="ExecuteCommandResult" /> indicates command success or failure.</returns>
    public async Task<ExecuteCommandResult> ExecuteCommandAsync(IResource resource, string commandName, CancellationToken cancellationToken = default)
    {
        var names = resource.GetResolvedResourceNames();
        // Single resource for IResource. Return its result directly.
        if (names.Length == 1)
        {
            return await ExecuteCommandCoreAsync(names[0], resource, commandName, cancellationToken).ConfigureAwait(false);
        }
 
        // Run commands for multiple resources in parallel.
        var tasks = new List<Task<ExecuteCommandResult>>();
        foreach (var name in names)
        {
            tasks.Add(ExecuteCommandCoreAsync(name, resource, commandName, cancellationToken));
        }
 
        // Check for failures.
        var results = await Task.WhenAll(tasks).ConfigureAwait(false);
        var failures = new List<(string resourceId, ExecuteCommandResult result)>();
        for (var i = 0; i < results.Length; i++)
        {
            if (!results[i].Success)
            {
                failures.Add((names[i], results[i]));
            }
        }
 
        if (failures.Count == 0)
        {
            return new ExecuteCommandResult { Success = true };
        }
        else
        {
            // Aggregate error results together.
            var errorMessage = $"{failures.Count} command executions failed.";
            errorMessage += Environment.NewLine + string.Join(Environment.NewLine, failures.Select(f => $"Resource '{f.resourceId}' failed with error message: {f.result.ErrorMessage}"));
 
            return new ExecuteCommandResult
            {
                Success = false,
                ErrorMessage = errorMessage
            };
        }
    }
 
    internal async Task<ExecuteCommandResult> ExecuteCommandCoreAsync(string resourceId, IResource resource, string commandName, CancellationToken cancellationToken)
    {
        var logger = _resourceLoggerService.GetLogger(resourceId);
 
        logger.LogInformation("Executing command '{CommandName}'.", commandName);
 
        var annotation = resource.Annotations.OfType<ResourceCommandAnnotation>().SingleOrDefault(a => a.Name == commandName);
        if (annotation != null)
        {
            try
            {
                var context = new ExecuteCommandContext
                {
                    ResourceName = resourceId,
                    ServiceProvider = _serviceProvider,
                    CancellationToken = cancellationToken
                };
 
                var result = await annotation.ExecuteCommand(context).ConfigureAwait(false);
                if (result.Success)
                {
                    logger.LogInformation("Successfully executed command '{CommandName}'.", commandName);
                    return result;
                }
                else
                {
                    logger.LogInformation("Failure executing command '{CommandName}'. Error message: {ErrorMessage}", commandName, result.ErrorMessage);
                    return result;
                }
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error executing command '{CommandName}'.", commandName);
                return new ExecuteCommandResult { Success = false, ErrorMessage = "Unhandled exception thrown." };
            }
        }
 
        logger.LogInformation("Command '{CommandName}' not available.", commandName);
        return new ExecuteCommandResult { Success = false, ErrorMessage = $"Command '{commandName}' not available for resource '{resourceId}'." };
    }
}