Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Created September 11, 2025 04:17
Show Gist options
  • Save davidfowl/daf74d2ac3131f92697d16e8f5137aa3 to your computer and use it in GitHub Desktop.
Save davidfowl/daf74d2ac3131f92697d16e8f5137aa3 to your computer and use it in GitHub Desktop.

Revisions

  1. davidfowl created this gist Sep 11, 2025.
    135 changes: 135 additions & 0 deletions LogCommand.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,135 @@
    #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

    using System.Text.Json;
    using System.Text.Json.Nodes;
    using Microsoft.Extensions.DependencyInjection;

    namespace Aspire.Hosting
    {
    public static class ResourceBuilderProjectExtensions
    {
    /// <summary>
    /// Registers a command handler that allows the developer to change the project's default log level
    /// in its appsettings.json at command time. This is an extension on <see cref="IResourceBuilder{T}"/>
    /// for project resources and returns the builder to allow fluent chaining.
    /// </summary>
    public static IResourceBuilder<T> WithLogConfiguration<T>(this IResourceBuilder<T> builder)
    where T : ProjectResource
    {
    if (builder is null) throw new ArgumentNullException(nameof(builder));

    // Register a named command on the builder. Many Aspire overloads accept (string name, Func<IServiceProvider, Task> handler).
    // We use a single-parameter handler to increase the likelihood of matching available overloads.
    return builder.WithCommand("with-log-configuration", "Edit Log Configuration", async (context) =>
    {
    var sp = context.ServiceProvider;
    try
    {
    var interaction = sp.GetService<IInteractionService>();
    if (interaction is null)
    {
    return CommandResults.Canceled();
    }

    var projectMetadata = builder.Resource.GetProjectMetadata();
    var projectPath = projectMetadata?.ProjectPath;
    if (string.IsNullOrWhiteSpace(projectPath))
    {
    return CommandResults.Canceled();
    }

    var projectDir = Path.GetDirectoryName(projectPath) ?? projectPath;
    var appSettingsPath = Path.Combine(projectDir, "appsettings.Development.json");

    if (!File.Exists(appSettingsPath))
    {
    return CommandResults.Canceled();
    }

    JsonNode? root;
    try
    {
    var text = await File.ReadAllTextAsync(appSettingsPath).ConfigureAwait(false);
    root = string.IsNullOrWhiteSpace(text) ? new JsonObject() : JsonNode.Parse(text);
    }
    catch
    {
    return CommandResults.Canceled();
    }

    // Build inputs for every configured log category under Logging:LogLevel and prompt as a single inputs dialog
    var logLevelObj = root?["Logging"]?["LogLevel"] as JsonObject ?? new JsonObject();

    var allowedLevels = new[] { "Trace", "Debug", "Information", "Warning", "Error", "Critical", "None" }
    .Select(l => new KeyValuePair<string, string>(l, l))
    .ToList();

    var inputs = logLevelObj.Select(kvp => new InteractionInput
    {
    Name = kvp.Key,
    Label = kvp.Key,
    InputType = InputType.Choice,
    Required = true,
    Options = allowedLevels,
    Value = kvp.Value?.GetValue<string>() ?? "Information"
    }).ToList();

    var promptTitle = "Change log levels";
    var promptMessage = $"Project: {Path.GetFileName(projectDir)}\nChoose a log level for each category:";

    var inputsResult = await interaction.PromptInputsAsync(promptTitle, promptMessage, inputs).ConfigureAwait(false);
    if (inputsResult.Canceled)
    {
    return CommandResults.Canceled();
    }

    var answers = inputsResult.Data; // InteractionInputCollection

    try
    {
    root ??= new JsonObject();
    if (root["Logging"] == null) root["Logging"] = new JsonObject();
    if (root["Logging"]!["LogLevel"] == null) root["Logging"]!["LogLevel"] = new JsonObject();

    foreach (var input in answers)
    {
    var name = input.Name;
    var value = input.Value?.Trim();
    if (string.IsNullOrWhiteSpace(value) || !IsValidLogLevel(value))
    {
    // skip invalid/empty values — the interaction choice input should prevent this, but be defensive
    continue;
    }

    root["Logging"]!["LogLevel"]![name] = value;
    }

    try { File.Copy(appSettingsPath, appSettingsPath + ".bak", overwrite: true); } catch { }

    var options = new JsonSerializerOptions { WriteIndented = true };
    var updatedText = root.ToJsonString(options);
    await File.WriteAllTextAsync(appSettingsPath, updatedText).ConfigureAwait(false);

    return CommandResults.Success();
    }
    catch
    {
    return CommandResults.Failure();
    }
    }
    catch
    {
    // swallow
    }

    return CommandResults.Success();
    });
    }

    private static bool IsValidLogLevel(string value)
    {
    var valid = new[] { "Trace", "Debug", "Information", "Warning", "Error", "Critical", "None" };
    return valid.Contains(value, StringComparer.OrdinalIgnoreCase);
    }
    }
    }