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.
An aspire command for project resources to enable setting log configuration at dev time
#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);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment