Created
September 11, 2025 04:17
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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