From f15428aad0b07b6b23f3f5b21de123a47d994d94 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Sun, 5 Jul 2026 16:52:44 +0200 Subject: [PATCH 1/8] Add PowerShell idiom to do the work in .NET, not cmdlet pipelines --- src/docs/Coding-Standards/PowerShell/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/docs/Coding-Standards/PowerShell/index.md b/src/docs/Coding-Standards/PowerShell/index.md index 70318df..af70df3 100644 --- a/src/docs/Coding-Standards/PowerShell/index.md +++ b/src/docs/Coding-Standards/PowerShell/index.md @@ -57,6 +57,7 @@ Beyond the basics, these language-specific habits keep PowerShell correct and fa - **Single-quote strings unless you need expansion.** Use `'literal'` by default; reserve `"...$var..."` for interpolation or escape sequences, and here-strings (`@'...'@`, `@"..."@`) for multi-line text — literal-versus-interpolated intent then stays obvious. - **Splat calls that carry many parameters.** Build a `@{}` of parameters and splat it (`Get-Thing @params`) instead of a long line of `-Param value` pairs or backtick continuations; it reads better and diffs cleanly. - **Put `$null` on the left of a comparison** — `$null -eq $x`, never `$x -eq $null`. Against a collection the right-hand form *filters* rather than tests. Use `-contains` / `-in` for membership, never `-eq`. +- **Do the work in .NET; use cmdlets to orchestrate it.** On hot paths, or where behaviour must be exact and predictable, call the .NET base class library directly instead of a cmdlet pipeline — `[System.IO.File]::ReadAllText($path)` over `Get-Content -Raw`, `[System.IO.Path]::Combine(...)` for paths, `[System.Text.StringBuilder]` for repeated concatenation, `[int]::TryParse(...)` for parsing. .NET methods are faster and their contracts are precise; keep cmdlets where their clarity is worth more than the speed. The next two rules are specific cases. - **Suppress unwanted output with `$null = ...`** (or `[void]` for method calls), not `| Out-Null` — the pipeline form is markedly slower on hot paths. - **Build collections with a typed list, not `+=` in a loop.** `$a += $x` reallocates the whole array every iteration; use `[System.Collections.Generic.List[T]]` with `.Add()`, and prefer a cmdlet's `-Filter` over piping to `Where-Object` on large sets. - **Keep secrets out of source, and never `Invoke-Expression` untrusted input.** Accept credentials as a `[PSCredential]` parameter with the `[Credential()]` attribute rather than calling `Get-Credential` inside a reusable function, so a caller can pass one they already hold, and take other sensitive values as `[securestring]`. Guard state-changing commands with `ShouldProcess` (see [Functions](Functions.md)); the wider rules live in the [Security](../Security.md) baseline. From 53f954ac12fe050047cb2eceb4afc6fcd3152189 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 6 Jul 2026 08:43:35 +0200 Subject: [PATCH 2/8] Add reuse-before-you-build hierarchy to the Functions baseline --- src/docs/Coding-Standards/Functions.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/docs/Coding-Standards/Functions.md b/src/docs/Coding-Standards/Functions.md index 374d2a7..6b6dae6 100644 --- a/src/docs/Coding-Standards/Functions.md +++ b/src/docs/Coding-Standards/Functions.md @@ -13,6 +13,15 @@ Functions are the unit of intent. A good function does one thing, says what it d - Keep it small enough to hold in your head. Whether it fits on a screen is a better test than a line count. - One level of abstraction per function — don't mix high-level orchestration with low-level detail in the same body. +## Reuse before you build + +Before writing new logic, use what already exists — and build only what does not. This is DRY and single responsibility applied across the whole codebase, not just within one function. + +- Prefer a built-in. If the language or runtime already does the job, use it instead of a hand-rolled version. +- Reuse an existing function instead of re-implementing it. If it is the weak link — too slow or imprecise on a hot path — fix it there so every caller benefits, rather than working around it. +- Take a dependency on a trusted module for a larger capability that already exists elsewhere; declare it explicitly instead of copying it in. +- Build it only when nothing fits — no built-in, no existing function, no trustworthy dependency. Size the build to the need: small logic lives inline where it is used; a larger, cohesive capability becomes its own module. + ## Signatures are contracts - Type the parameters and return value where the language allows. The signature documents intent and lets tooling catch misuse before it runs. From 2d785e5a294604aea8f445b1fdf08386e6e9ba2f Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 6 Jul 2026 08:43:35 +0200 Subject: [PATCH 3/8] Reframe the PowerShell .NET idiom under the reuse hierarchy --- src/docs/Coding-Standards/PowerShell/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/Coding-Standards/PowerShell/index.md b/src/docs/Coding-Standards/PowerShell/index.md index af70df3..95301c5 100644 --- a/src/docs/Coding-Standards/PowerShell/index.md +++ b/src/docs/Coding-Standards/PowerShell/index.md @@ -57,7 +57,7 @@ Beyond the basics, these language-specific habits keep PowerShell correct and fa - **Single-quote strings unless you need expansion.** Use `'literal'` by default; reserve `"...$var..."` for interpolation or escape sequences, and here-strings (`@'...'@`, `@"..."@`) for multi-line text — literal-versus-interpolated intent then stays obvious. - **Splat calls that carry many parameters.** Build a `@{}` of parameters and splat it (`Get-Thing @params`) instead of a long line of `-Param value` pairs or backtick continuations; it reads better and diffs cleanly. - **Put `$null` on the left of a comparison** — `$null -eq $x`, never `$x -eq $null`. Against a collection the right-hand form *filters* rather than tests. Use `-contains` / `-in` for membership, never `-eq`. -- **Do the work in .NET; use cmdlets to orchestrate it.** On hot paths, or where behaviour must be exact and predictable, call the .NET base class library directly instead of a cmdlet pipeline — `[System.IO.File]::ReadAllText($path)` over `Get-Content -Raw`, `[System.IO.Path]::Combine(...)` for paths, `[System.Text.StringBuilder]` for repeated concatenation, `[int]::TryParse(...)` for parsing. .NET methods are faster and their contracts are precise; keep cmdlets where their clarity is worth more than the speed. The next two rules are specific cases. +- **Reuse before you build, then do the work in .NET.** Work down the [reuse order](../Functions.md#reuse-before-you-build) first — a built-in cmdlet or operator, then an existing function (public or private), then a trusted module (`#Requires` / `RequiredModules`), then your own code (small logic inline, a larger capability as its own module). When you *do* implement the work — or fix an internal function that is too slow or imprecise on a hot path — call the .NET base class library directly instead of a cmdlet pipeline: `[System.IO.File]::ReadAllText($path)` over `Get-Content -Raw`, `[System.IO.Path]::Combine(...)` for paths, `[System.Text.StringBuilder]` for repeated concatenation, `[int]::TryParse(...)` for parsing. .NET methods are faster and their contracts are precise; keep cmdlets where their clarity is worth more than the speed. The next two rules are specific cases. - **Suppress unwanted output with `$null = ...`** (or `[void]` for method calls), not `| Out-Null` — the pipeline form is markedly slower on hot paths. - **Build collections with a typed list, not `+=` in a loop.** `$a += $x` reallocates the whole array every iteration; use `[System.Collections.Generic.List[T]]` with `.Add()`, and prefer a cmdlet's `-Filter` over piping to `Where-Object` on large sets. - **Keep secrets out of source, and never `Invoke-Expression` untrusted input.** Accept credentials as a `[PSCredential]` parameter with the `[Credential()]` attribute rather than calling `Get-Credential` inside a reusable function, so a caller can pass one they already hold, and take other sensitive values as `[securestring]`. Guard state-changing commands with `ShouldProcess` (see [Functions](Functions.md)); the wider rules live in the [Security](../Security.md) baseline. From ff785a8389662932b95d1945da6363463007cf8b Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 6 Jul 2026 08:47:56 +0200 Subject: [PATCH 4/8] Split the reuse and .NET guidance into two bullets --- src/docs/Coding-Standards/PowerShell/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/docs/Coding-Standards/PowerShell/index.md b/src/docs/Coding-Standards/PowerShell/index.md index 95301c5..3df1e6f 100644 --- a/src/docs/Coding-Standards/PowerShell/index.md +++ b/src/docs/Coding-Standards/PowerShell/index.md @@ -57,7 +57,8 @@ Beyond the basics, these language-specific habits keep PowerShell correct and fa - **Single-quote strings unless you need expansion.** Use `'literal'` by default; reserve `"...$var..."` for interpolation or escape sequences, and here-strings (`@'...'@`, `@"..."@`) for multi-line text — literal-versus-interpolated intent then stays obvious. - **Splat calls that carry many parameters.** Build a `@{}` of parameters and splat it (`Get-Thing @params`) instead of a long line of `-Param value` pairs or backtick continuations; it reads better and diffs cleanly. - **Put `$null` on the left of a comparison** — `$null -eq $x`, never `$x -eq $null`. Against a collection the right-hand form *filters* rather than tests. Use `-contains` / `-in` for membership, never `-eq`. -- **Reuse before you build, then do the work in .NET.** Work down the [reuse order](../Functions.md#reuse-before-you-build) first — a built-in cmdlet or operator, then an existing function (public or private), then a trusted module (`#Requires` / `RequiredModules`), then your own code (small logic inline, a larger capability as its own module). When you *do* implement the work — or fix an internal function that is too slow or imprecise on a hot path — call the .NET base class library directly instead of a cmdlet pipeline: `[System.IO.File]::ReadAllText($path)` over `Get-Content -Raw`, `[System.IO.Path]::Combine(...)` for paths, `[System.Text.StringBuilder]` for repeated concatenation, `[int]::TryParse(...)` for parsing. .NET methods are faster and their contracts are precise; keep cmdlets where their clarity is worth more than the speed. The next two rules are specific cases. +- **Reuse before you build.** Work down the [reuse order](../Functions.md#reuse-before-you-build) — a built-in cmdlet or operator, then an existing function (public or private), then a trusted module (`#Requires` / `RequiredModules`), then your own code (small logic inline, a larger capability as its own module). +- **Do the work in .NET when you implement it.** When you write the logic yourself — or fix an internal function that is too slow or imprecise on a hot path — call the .NET base class library directly instead of a cmdlet pipeline: `[System.IO.File]::ReadAllText($path)` over `Get-Content -Raw`, `[System.IO.Path]::Combine(...)` for paths, `[System.Text.StringBuilder]` for repeated concatenation, `[int]::TryParse(...)` for parsing. .NET methods are faster and their contracts are precise; keep cmdlets where their clarity is worth more than the speed. The next two rules are specific cases. - **Suppress unwanted output with `$null = ...`** (or `[void]` for method calls), not `| Out-Null` — the pipeline form is markedly slower on hot paths. - **Build collections with a typed list, not `+=` in a loop.** `$a += $x` reallocates the whole array every iteration; use `[System.Collections.Generic.List[T]]` with `.Add()`, and prefer a cmdlet's `-Filter` over piping to `Where-Object` on large sets. - **Keep secrets out of source, and never `Invoke-Expression` untrusted input.** Accept credentials as a `[PSCredential]` parameter with the `[Credential()]` attribute rather than calling `Get-Credential` inside a reusable function, so a caller can pass one they already hold, and take other sensitive values as `[securestring]`. Guard state-changing commands with `ShouldProcess` (see [Functions](Functions.md)); the wider rules live in the [Security](../Security.md) baseline. From 1e5d8ed5978b865da05e2dd2b09bfd2bf95d9f78 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 6 Jul 2026 08:50:57 +0200 Subject: [PATCH 5/8] Use the document's own responsibility term --- src/docs/Coding-Standards/Functions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/Coding-Standards/Functions.md b/src/docs/Coding-Standards/Functions.md index 6b6dae6..3c02ef8 100644 --- a/src/docs/Coding-Standards/Functions.md +++ b/src/docs/Coding-Standards/Functions.md @@ -15,7 +15,7 @@ Functions are the unit of intent. A good function does one thing, says what it d ## Reuse before you build -Before writing new logic, use what already exists — and build only what does not. This is DRY and single responsibility applied across the whole codebase, not just within one function. +Before writing new logic, use what already exists — and build only what does not. This is DRY and one responsibility applied across the whole codebase, not just within one function. - Prefer a built-in. If the language or runtime already does the job, use it instead of a hand-rolled version. - Reuse an existing function instead of re-implementing it. If it is the weak link — too slow or imprecise on a hot path — fix it there so every caller benefits, rather than working around it. From 18b380080e76cb6e00f830213df61680d6ae0e9f Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 6 Jul 2026 08:59:11 +0200 Subject: [PATCH 6/8] Use one adjective for a trusted dependency --- src/docs/Coding-Standards/Functions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/Coding-Standards/Functions.md b/src/docs/Coding-Standards/Functions.md index 3c02ef8..814ffce 100644 --- a/src/docs/Coding-Standards/Functions.md +++ b/src/docs/Coding-Standards/Functions.md @@ -20,7 +20,7 @@ Before writing new logic, use what already exists — and build only what does n - Prefer a built-in. If the language or runtime already does the job, use it instead of a hand-rolled version. - Reuse an existing function instead of re-implementing it. If it is the weak link — too slow or imprecise on a hot path — fix it there so every caller benefits, rather than working around it. - Take a dependency on a trusted module for a larger capability that already exists elsewhere; declare it explicitly instead of copying it in. -- Build it only when nothing fits — no built-in, no existing function, no trustworthy dependency. Size the build to the need: small logic lives inline where it is used; a larger, cohesive capability becomes its own module. +- Build it only when nothing fits — no built-in, no existing function, no trusted dependency. Size the build to the need: small logic lives inline where it is used; a larger, cohesive capability becomes its own module. ## Signatures are contracts From e38744e7df49e3989e7902cf6f9166caa26c2d70 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 6 Jul 2026 09:04:38 +0200 Subject: [PATCH 7/8] Name the precise #Requires -Modules directive --- src/docs/Coding-Standards/PowerShell/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/Coding-Standards/PowerShell/index.md b/src/docs/Coding-Standards/PowerShell/index.md index 3df1e6f..80dd820 100644 --- a/src/docs/Coding-Standards/PowerShell/index.md +++ b/src/docs/Coding-Standards/PowerShell/index.md @@ -57,7 +57,7 @@ Beyond the basics, these language-specific habits keep PowerShell correct and fa - **Single-quote strings unless you need expansion.** Use `'literal'` by default; reserve `"...$var..."` for interpolation or escape sequences, and here-strings (`@'...'@`, `@"..."@`) for multi-line text — literal-versus-interpolated intent then stays obvious. - **Splat calls that carry many parameters.** Build a `@{}` of parameters and splat it (`Get-Thing @params`) instead of a long line of `-Param value` pairs or backtick continuations; it reads better and diffs cleanly. - **Put `$null` on the left of a comparison** — `$null -eq $x`, never `$x -eq $null`. Against a collection the right-hand form *filters* rather than tests. Use `-contains` / `-in` for membership, never `-eq`. -- **Reuse before you build.** Work down the [reuse order](../Functions.md#reuse-before-you-build) — a built-in cmdlet or operator, then an existing function (public or private), then a trusted module (`#Requires` / `RequiredModules`), then your own code (small logic inline, a larger capability as its own module). +- **Reuse before you build.** Work down the [reuse order](../Functions.md#reuse-before-you-build) — a built-in cmdlet or operator, then an existing function (public or private), then a trusted module (`#Requires -Modules` / `RequiredModules`), then your own code (small logic inline, a larger capability as its own module). - **Do the work in .NET when you implement it.** When you write the logic yourself — or fix an internal function that is too slow or imprecise on a hot path — call the .NET base class library directly instead of a cmdlet pipeline: `[System.IO.File]::ReadAllText($path)` over `Get-Content -Raw`, `[System.IO.Path]::Combine(...)` for paths, `[System.Text.StringBuilder]` for repeated concatenation, `[int]::TryParse(...)` for parsing. .NET methods are faster and their contracts are precise; keep cmdlets where their clarity is worth more than the speed. The next two rules are specific cases. - **Suppress unwanted output with `$null = ...`** (or `[void]` for method calls), not `| Out-Null` — the pipeline form is markedly slower on hot paths. - **Build collections with a typed list, not `+=` in a loop.** `$a += $x` reallocates the whole array every iteration; use `[System.Collections.Generic.List[T]]` with `.Add()`, and prefer a cmdlet's `-Filter` over piping to `Where-Object` on large sets. From 35c61d87665037a3fdb8ba7ba50d47384d7d9130 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Mon, 6 Jul 2026 10:49:01 +0200 Subject: [PATCH 8/8] Define what .NET means in the PowerShell context --- src/docs/Coding-Standards/PowerShell/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/docs/Coding-Standards/PowerShell/index.md b/src/docs/Coding-Standards/PowerShell/index.md index 80dd820..254cf00 100644 --- a/src/docs/Coding-Standards/PowerShell/index.md +++ b/src/docs/Coding-Standards/PowerShell/index.md @@ -58,6 +58,7 @@ Beyond the basics, these language-specific habits keep PowerShell correct and fa - **Splat calls that carry many parameters.** Build a `@{}` of parameters and splat it (`Get-Thing @params`) instead of a long line of `-Param value` pairs or backtick continuations; it reads better and diffs cleanly. - **Put `$null` on the left of a comparison** — `$null -eq $x`, never `$x -eq $null`. Against a collection the right-hand form *filters* rather than tests. Use `-contains` / `-in` for membership, never `-eq`. - **Reuse before you build.** Work down the [reuse order](../Functions.md#reuse-before-you-build) — a built-in cmdlet or operator, then an existing function (public or private), then a trusted module (`#Requires -Modules` / `RequiredModules`), then your own code (small logic inline, a larger capability as its own module). +- **PowerShell already *is* .NET; work at that level rather than wrapping it.** Casts, type accelerators (`[datetime]`, `[int]`), the `-split` / `-replace` / `-match` operators, and member methods (`.Trim()`, `.Where()`) all resolve to the base class library — using .NET means reaching for BCL types and methods for the computation, not restating everything as `[Namespace.Type]::Method(...)`. Where idiomatic PowerShell already resolves to the same .NET call, leave it; reach for explicit .NET only where it is measurably faster or more precise, and keep cmdlets and the pipeline where you need them for glue or readability. - **Do the work in .NET when you implement it.** When you write the logic yourself — or fix an internal function that is too slow or imprecise on a hot path — call the .NET base class library directly instead of a cmdlet pipeline: `[System.IO.File]::ReadAllText($path)` over `Get-Content -Raw`, `[System.IO.Path]::Combine(...)` for paths, `[System.Text.StringBuilder]` for repeated concatenation, `[int]::TryParse(...)` for parsing. .NET methods are faster and their contracts are precise; keep cmdlets where their clarity is worth more than the speed. The next two rules are specific cases. - **Suppress unwanted output with `$null = ...`** (or `[void]` for method calls), not `| Out-Null` — the pipeline form is markedly slower on hot paths. - **Build collections with a typed list, not `+=` in a loop.** `$a += $x` reallocates the whole array every iteration; use `[System.Collections.Generic.List[T]]` with `.Add()`, and prefer a cmdlet's `-Filter` over piping to `Where-Object` on large sets.