TB

MoppleIT Tech Blog

Welcome to my personal blog where I share thoughts, ideas, and experiences.

Deterministic Randomness in PowerShell: Seed Once, Reuse the Generator

Randomness is great for sampling, shuffling, test data, and demos—until you need to reproduce a result. Deterministic randomness solves this by using a fixed seed so that the same operations yield the same outputs every time. In PowerShell, the key is simple: seed once, then reuse the generator. In this post, you’ll learn practical patterns for repeatable tests, stable demos, predictable sampling, and easier debugging—without sacrificing clarity or performance.

The Core Pattern: Seed Once, Reuse the Generator

Why this matters

Deterministic randomness lets you verify behavior in CI/CD, produce stable screencasts, and debug hard-to-repro issues. If you re-seed for every call, you’ll often get the same value repeatedly, which defeats the purpose of randomness and makes tests misleading.

Correct approach: one generator, many draws

Use a single [System.Random] instance initialized with a fixed seed and reuse it for the entire operation or test run.

$rng  = [System.Random]::new(12345)
$nums1 = 1..5 | ForEach-Object { $rng.Next(1, 100) }

$rng2 = [System.Random]::new(12345)
$nums2 = 1..5 | ForEach-Object { $rng2.Next(1, 100) }

$items = @('alpha','beta','gamma','delta')
$r = [System.Random]::new(42)
$shuf = $items | Sort-Object { $r.Next() }

[pscustomobject]@{
  Repeatable = ($nums1 -join ',') -eq ($nums2 -join ',')
  Numbers    = ($nums1 -join ', ')
  Shuffle    = ($shuf -join ' ')
}

This produces the same sequence and the same shuffle every run because the seed and usage are consistent.

Anti-pattern: reseeding inside a loop

Re-seeding for each element restarts the sequence every time, often returning the same value repeatedly:

# Anti-pattern: usually returns the same value five times
1..5 | ForEach-Object { [System.Random]::new(12345).Next(1,100) }

Instead, create one generator and reuse it across iterations.

Get-Random tips

  • Avoid calling Get-Random -SetSeed in a loop; that reinitializes the generator every time.
  • Prefer one call that generates all needed values when using Get-Random deterministically.
# Anti-pattern: resets on every call
1..5 | ForEach-Object { Get-Random -SetSeed 42 -Minimum 0 -Maximum 10 }

# Better: one call that emits a deterministic sequence
Get-Random -SetSeed 42 -Minimum 0 -Maximum 10 -Count 5

Practical Recipes: Numbers, Shuffles, Sampling

Reusable, scoped generator

Keep a seeded generator at script or module scope so your functions share the same sequence. You can also surface the seed via parameter or environment variable for easy reproduction.

param([int]$Seed = $(if ($env:RNG_SEED) { [int]$env:RNG_SEED } else { 12345 }))

if (-not $script:Rng) {
  $script:Rng = [System.Random]::new($Seed)
}

function Get-DetNumber {
  param([int]$Min = 0, [int]$Max = 100)
  return $script:Rng.Next($Min, $Max)
}

# Usage
1..5 | ForEach-Object { Get-DetNumber -Min 10 -Max 50 }

Deterministic shuffles (Fisher–Yates)

Sorting by { $rng.Next() } is handy and fine for small lists, but a Fisher–Yates shuffle avoids bias and is O(n). Use the same seed to reproduce the order exactly.

function Shuffle-Deterministic {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory, ValueFromPipeline)]
    [object[]]$InputObject,
    [int]$Seed = 0
  )
  begin {
    $rng = [System.Random]::new($Seed)
    $buffer = @()
  }
  process {
    $buffer += $InputObject
  }
  end {
    for ($i = $buffer.Length - 1; $i -gt 0; $i--) {
      $j = $rng.Next(0, $i + 1)
      $tmp = $buffer[$i]
      $buffer[$i] = $buffer[$j]
      $buffer[$j] = $tmp
    }
    return $buffer
  }
}

# Example
$items = 1..10
$shuffled = $items | Shuffle-Deterministic -Seed 2024
$shuffled

Predictable sampling

To pick a deterministic sample of n items, shuffle with a seed and take the first n.

$population = 'alpha','beta','gamma','delta','epsilon','zeta','eta','theta'
$seed = 31415
$sampleSize = 3
$sample = $population | Shuffle-Deterministic -Seed $seed | Select-Object -First $sampleSize
$sample

Stable train/test splits

For data work, use a seeded shuffle and split by index. Reusing the seed reproduces the split exactly.

$data = 1..100
$seed = 123
$shuf = $data | Shuffle-Deterministic -Seed $seed
$split = [math]::Floor($shuf.Count * 0.8)
$train = $shuf[0..($split-1)]
$test  = $shuf[$split..($shuf.Count-1)]

[pscustomobject]@{ TrainCount = $train.Count; TestCount = $test.Count }

DevOps and Testing Workflow: Reproduce, Debug, Ship

Make seed a first-class citizen

  • Accept a -Seed parameter in scripts and functions.
  • Default to an environment variable (for CI), then a constant fallback.
  • Always log the seed so failing runs can be reproduced.
# Seed precedence: CLI > env var > fallback
param([int]$Seed)
if (-not $PSBoundParameters.ContainsKey('Seed')) {
  $Seed = if ($env:RNG_SEED -and $env:RNG_SEED -as [int]) { [int]$env:RNG_SEED } else { 2025 }
}

$rng = [System.Random]::new($Seed)
[pscustomobject]@{ Seed = $Seed; Example = $rng.Next(0,100) }

CI/CD runners

  • Set RNG_SEED in the pipeline to pin behavior.
  • For nightly variability, use the date as the seed and log it; re-run the same date to reproduce.
  • In test jobs, fail with the seed printed so you can reproduce locally.
# GitHub Actions example (env)
# env:
#   RNG_SEED: 8675309

# Azure DevOps YAML example
# variables:
#   RNG_SEED: 8675309

Safer demos and docs

  • Seed your samples so screenshots and blog outputs stay stable.
  • Include the seed in your documentation to help readers reproduce exactly.

Security and correctness notes

  • Not for secrets: [System.Random] is not cryptographically secure. Do not use it for keys, tokens, or anything security-sensitive.
  • Cross-version differences: Exact number sequences from [System.Random] may differ across .NET versions. For cross-runtime bit-for-bit reproducibility, pin your runtime or ship a custom PRNG with a stable algorithm. For most testing and demo uses, seeding within a consistent runtime is sufficient.
  • Concurrency: [System.Random] isn’t thread-safe. In parallel jobs or runspaces, create one generator per worker and seed them deterministically (e.g., base seed + worker index).

Quick reference and gotchas

Do this

  • Create one generator: $rng = [System.Random]::new(Seed).
  • Pass it where needed or store it in script/module scope.
  • Use it for all draws: $rng.Next(), $rng.Next(min,max), $rng.NextDouble().
  • For shuffles, prefer Fisher–Yates.
  • Log the seed; accept it via parameter or environment for reproducibility.

Avoid this

  • Creating a new Random inside loops.
  • Using Get-Random -SetSeed per iteration.
  • Expecting the same sequences across different .NET versions and runtimes.
  • Using Random for anything security-related.

Complete reproducible snippet

$seed = 12345
$rng  = [System.Random]::new($seed)

$nums = 1..5 | ForEach-Object { $rng.Next(1, 100) }

$items = @('alpha','beta','gamma','delta')
$shuffleSeed = 42
$itemsShuffled = $items | Shuffle-Deterministic -Seed $shuffleSeed

[pscustomobject]@{
  Seed            = $seed
  Numbers         = ($nums -join ', ')
  ShuffleSeed     = $shuffleSeed
  ShuffledItems   = ($itemsShuffled -join ', ')
}

What you get: repeatable tests, stable demos, predictable sampling, and easier debugging. Build confidence with deterministic techniques in PowerShell. For more advanced patterns, see the PowerShell Advanced Cookbook: https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/

← All Posts Home →