From da162b2471def62cb239d67a9b8acdcac23453fa Mon Sep 17 00:00:00 2001 From: Benziza Date: Thu, 17 Sep 2026 20:32:34 +0200 Subject: [PATCH 1/2] docs: add usage guide and cross-platform CI --- .github/workflows/ci.yml | 24 +++++ .gitignore | 2 + README.md | 100 +++++++++++++++++- docs/README.darija.md | 20 ++++ global.json | 6 ++ src/WhyConfig.Cli/CliApplication.cs | 10 +- src/WhyConfig.Cli/WhyConfig.Cli.csproj | 5 + .../CliApplicationTests.cs | 14 +++ 8 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/README.darija.md create mode 100644 global.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..99c62c8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + build-test-pack: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + - run: dotnet restore WhyConfig.slnx + - run: dotnet test WhyConfig.slnx -c Release --no-restore + - run: dotnet pack src/WhyConfig.Cli/WhyConfig.Cli.csproj -c Release --no-restore diff --git a/.gitignore b/.gitignore index 0ec7954..a6ec662 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ obj/ .vs/ TestResults/ *.user +artifacts/ +.tools/ diff --git a/README.md b/README.md index fca2184..2cc6d44 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,101 @@ # WhyConfig.NET -Explain which .NET configuration provider supplied a key and which earlier values it replaced. +**Why did my .NET app get this configuration value?** WhyConfig.NET shows every provider that supplied a key, in precedence order, and marks the provider that won. -Development is organized into small pull requests. See the pull request stack for the library, CLI, and documentation. +```text +Database:Host + +appsettings.json + db.production.com + overridden + +appsettings.Development.json + localhost + overridden + +Environment Variables + db.docker + WINNER + +Effective value: db.docker +``` + +The project has two entry points: + +- **WhyConfig.Core** inspects an application's actual `IConfigurationRoot`. Use it when you need the exact provider chain, including custom providers. +- **whyconfig** is a CLI for investigating a project outside the running app. It rebuilds the common .NET application configuration layers from the files and environment available to the CLI. + +## Quick start + +Requires the .NET 10 SDK. From this repository: + +```bash +dotnet run --project src/WhyConfig.Cli -- explain Database:Host --project path/to/YourApp --environment Development +``` + +The project path may be a directory or a `.csproj` file. If omitted, it defaults to the current directory. For a shell command named `whyconfig`, build and install the local tool package: + +```bash +dotnet pack src/WhyConfig.Cli -c Release -o artifacts +dotnet tool install WhyConfig.NET --tool-path .tools --add-source artifacts +``` + +Then run `.tools/whyconfig explain Database:Host --project path/to/YourApp` on macOS/Linux, or `.tools\whyconfig.exe explain Database:Host --project path\to\YourApp` on Windows. + +## CLI options + +```text +whyconfig explain [options] + +--project Project to inspect +--environment Environment to use +--secrets-id Explicit User Secrets ID +--app-arg Application argument; repeatable +--show-secrets Show values for keys that look sensitive +--json Emit machine-readable JSON +``` + +For example, if the application was launched with an argument that overrides `Api:Url`: + +```bash +whyconfig explain Api:Url --project ./MyApp --environment Development --app-arg Api:Url=http://localhost:5000 +``` + +The CLI reads `appsettings.json`, then `appsettings.{Environment}.json`, then User Secrets in `Development` when it can find a `UserSecretsId`, then its own process environment variables, then any `--app-arg` values. `--secrets-id` supplies the ID directly if it is not declared in the selected `.csproj`. If no environment is supplied, the CLI uses `DOTNET_ENVIRONMENT`, then `ASPNETCORE_ENVIRONMENT`, then `Production`. Supply `--environment` when the application's environment is different or uncertain. + +Exit code `0` means the key was found, `1` means no provider supplied it, and `2` means the command or input was invalid. + +## Inspect the running application's configuration + +Reference `src/WhyConfig.Core/WhyConfig.Core.csproj` from your application, then call the library with the actual root: + +```csharp +using WhyConfig.Core; + +var builder = WebApplication.CreateBuilder(args); +var explanation = ConfigExplainer.Explain(builder.Configuration, "Database:Host"); + +Console.WriteLine($"Winner: {explanation.Winner?.Provider}"); +Console.WriteLine($"Effective value: {explanation.EffectiveValue}"); +foreach (var source in explanation.Sources) +{ + Console.WriteLine($"{source.Provider}: {source.Value} ({(source.IsWinner ? "winner" : "overridden")})"); +} +``` + +`IConfigurationRoot.Providers` contains the registered providers in order; later providers take precedence when they contain the same key. The library calls each provider's `TryGet` and reads the effective value from the root. This follows the [Microsoft configuration documentation](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-10.0). + +## Accuracy and sensitive values + +The CLI reconstructs a **common default** configuration setup. It cannot discover custom providers, application-specific registration order, launch arguments that were not supplied through `--app-arg`, or environment variables inside a different process or container. For an exact answer about a running app, call `WhyConfig.Core` with that app's `IConfigurationRoot`. + +The CLI masks values for keys with names such as `Password`, `Token`, `Secret`, `ApiKey`, and `ConnectionString` unless `--show-secrets` is set. This is a naming heuristic; other keys may still contain sensitive data. The library returns raw values to its caller. + +## Development + +```bash +dotnet test WhyConfig.slnx -c Release +dotnet pack src/WhyConfig.Cli -c Release -o artifacts +``` + +See [the Darija introduction](docs/README.darija.md) for a short explanation of the problem and the tool. diff --git a/docs/README.darija.md b/docs/README.darija.md new file mode 100644 index 0000000..0a6e717 --- /dev/null +++ b/docs/README.darija.md @@ -0,0 +1,20 @@ +# WhyConfig.NET بالدارجة + +`WhyConfig.NET` كتعاون مطوري `.NET` يعرفو علاش واحد الإعداد خذا قيمة معيّنة. نفس المفتاح يقدر يكون فـ`appsettings.json`، و`appsettings.Development.json`، وUser Secrets، ومتغيرات البيئة. المصدر اللي كيتزاد من بعد يقدر يغلب القيمة اللي قبل منو. + +مثلاً: + +```bash +whyconfig explain Database:Host --project ./MyApp --environment Development +``` + +النتيجة كتوريك كل مصدر عطى قيمة لـ`Database:Host`، شكون تغلب عليه، وشكون ربح فالأخير. + +كاينين جوج طرق للاستعمال: + +- `whyconfig` CLI: كيقرا ملفات المشروع والبيئة اللي خدام فيها، وكيعيد بناء الطبقات المعتادة ديال .NET. +- `WhyConfig.Core`: كتستعملو داخل التطبيق مع `IConfigurationRoot` ديالو، باش تشوف حتى المصادر المخصصة اللي زادها التطبيق. + +إلا كان التطبيق خدام فـcontainer أو زاد مصادر configuration مخصصة، استعمل المكتبة داخل التطبيق باش تكون النتيجة مطابقة للقيمة اللي كيستعمل فعلاً. الـCLI كيعطيك تفسير حسب الملفات والبيئة المتاحة ليه. + +القيم اللي سميّة المفتاح ديالها كتشير لسر، بحال `Password` أو `Token`، كتظهر مخفية افتراضياً. استعمل `--show-secrets` غير إلا كنت محتاج تشوفها. diff --git a/global.json b/global.json new file mode 100644 index 0000000..512142d --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/src/WhyConfig.Cli/CliApplication.cs b/src/WhyConfig.Cli/CliApplication.cs index 2729549..48a91ac 100644 --- a/src/WhyConfig.Cli/CliApplication.cs +++ b/src/WhyConfig.Cli/CliApplication.cs @@ -92,7 +92,15 @@ private static void WriteExplanation(ConfigurationExplanation explanation, CliOp private static bool IsSensitiveKey(string key) { - var leaf = key.Split(':').Last().Replace("_", "", StringComparison.Ordinal).ToLowerInvariant(); + var segments = key.Split(':') + .Select(segment => segment.Replace("_", "", StringComparison.Ordinal).ToLowerInvariant()) + .ToArray(); + if (segments.Any(segment => segment is "connectionstrings" or "credentials" or "secrets")) + { + return true; + } + + var leaf = segments[^1]; return leaf is "key" or "credential" or "connectionstring" || leaf.Contains("password", StringComparison.Ordinal) || leaf.Contains("secret", StringComparison.Ordinal) diff --git a/src/WhyConfig.Cli/WhyConfig.Cli.csproj b/src/WhyConfig.Cli/WhyConfig.Cli.csproj index 10d130d..9ecfd03 100644 --- a/src/WhyConfig.Cli/WhyConfig.Cli.csproj +++ b/src/WhyConfig.Cli/WhyConfig.Cli.csproj @@ -20,6 +20,11 @@ whyconfig WhyConfig.NET Explain .NET configuration provider precedence for a key. + README.md + + + + diff --git a/tests/WhyConfig.Cli.Tests/CliApplicationTests.cs b/tests/WhyConfig.Cli.Tests/CliApplicationTests.cs index 0916169..e2d5565 100644 --- a/tests/WhyConfig.Cli.Tests/CliApplicationTests.cs +++ b/tests/WhyConfig.Cli.Tests/CliApplicationTests.cs @@ -52,6 +52,20 @@ public void RedactsSensitiveValuesInJsonUnlessRequested() Assert.Contains("super-secret-value", revealed); } + [Fact] + public void RedactsConnectionStringsSection() + { + using var project = new TempProject(); + project.Write("appsettings.json", """{"ConnectionStrings":{"Default":"Server=db;Password=sensitive"}}"""); + + var (code, output, _) = Run("explain", "ConnectionStrings:Default", "--project", project.Path, + "--environment", "Production"); + + Assert.Equal(0, code); + Assert.Contains("", output); + Assert.DoesNotContain("sensitive", output); + } + [Fact] public void ReportsMissingKeyWithExitCodeOne() { From 4449c7b7a55130732a6090424e96202fcaa4aa1d Mon Sep 17 00:00:00 2001 From: Benziza Date: Thu, 17 Sep 2026 20:36:50 +0200 Subject: [PATCH 2/2] docs: keep repository documentation in English --- README.md | 2 -- docs/README.darija.md | 20 -------------------- 2 files changed, 22 deletions(-) delete mode 100644 docs/README.darija.md diff --git a/README.md b/README.md index 2cc6d44..f3480d0 100644 --- a/README.md +++ b/README.md @@ -97,5 +97,3 @@ The CLI masks values for keys with names such as `Password`, `Token`, `Secret`, dotnet test WhyConfig.slnx -c Release dotnet pack src/WhyConfig.Cli -c Release -o artifacts ``` - -See [the Darija introduction](docs/README.darija.md) for a short explanation of the problem and the tool. diff --git a/docs/README.darija.md b/docs/README.darija.md deleted file mode 100644 index 0a6e717..0000000 --- a/docs/README.darija.md +++ /dev/null @@ -1,20 +0,0 @@ -# WhyConfig.NET بالدارجة - -`WhyConfig.NET` كتعاون مطوري `.NET` يعرفو علاش واحد الإعداد خذا قيمة معيّنة. نفس المفتاح يقدر يكون فـ`appsettings.json`، و`appsettings.Development.json`، وUser Secrets، ومتغيرات البيئة. المصدر اللي كيتزاد من بعد يقدر يغلب القيمة اللي قبل منو. - -مثلاً: - -```bash -whyconfig explain Database:Host --project ./MyApp --environment Development -``` - -النتيجة كتوريك كل مصدر عطى قيمة لـ`Database:Host`، شكون تغلب عليه، وشكون ربح فالأخير. - -كاينين جوج طرق للاستعمال: - -- `whyconfig` CLI: كيقرا ملفات المشروع والبيئة اللي خدام فيها، وكيعيد بناء الطبقات المعتادة ديال .NET. -- `WhyConfig.Core`: كتستعملو داخل التطبيق مع `IConfigurationRoot` ديالو، باش تشوف حتى المصادر المخصصة اللي زادها التطبيق. - -إلا كان التطبيق خدام فـcontainer أو زاد مصادر configuration مخصصة، استعمل المكتبة داخل التطبيق باش تكون النتيجة مطابقة للقيمة اللي كيستعمل فعلاً. الـCLI كيعطيك تفسير حسب الملفات والبيئة المتاحة ليه. - -القيم اللي سميّة المفتاح ديالها كتشير لسر، بحال `Password` أو `Token`، كتظهر مخفية افتراضياً. استعمل `--show-secrets` غير إلا كنت محتاج تشوفها.