diff --git a/.gitattributes b/.gitattributes
index 1ff0c42..5b65514 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,6 +2,7 @@
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
+*.yml text eol=lf
###############################################################################
# Set default behavior for command prompt diff.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..e936c5b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,27 @@
+name: CI
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
+ with:
+ dotnet-version: '8.x'
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore -c Release
+
+ - name: Test
+ run: dotnet test --no-build -c Release --logger "trx;LogFileName=results.trx"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..886a9d1
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,40 @@
+name: Publish
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
+ with:
+ dotnet-version: '8.x'
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore -c Release
+
+ - name: Test
+ run: dotnet test --no-build -c Release
+
+ - name: Pack
+ run: dotnet pack --no-build -c Release -o ./nupkg
+
+ - name: Login to NuGet
+ uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1
+ with:
+ usernameVar: NUGET_USER
+ passwordVar: NUGET_TOKEN
+
+ - name: Push to NuGet
+ run: dotnet nuget push ./nupkg/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ env.NUGET_TOKEN }} --skip-duplicate
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
new file mode 100644
index 0000000..6b244fb
--- /dev/null
+++ b/.github/workflows/sonar.yml
@@ -0,0 +1,65 @@
+name: SonarCloud
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ sonar:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5
+ with:
+ dotnet-version: '8.x'
+
+ - name: Setup Java (Sonar scanner requirement)
+ uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
+ with:
+ distribution: temurin
+ java-version: '17'
+
+ - name: Cache SonarCloud packages
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
+ with:
+ path: ~/.sonar/cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+
+ - name: Install sonar scanner and coverage tool
+ run: |
+ dotnet tool install --global dotnet-sonarscanner
+ dotnet tool install --global dotnet-coverage
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Begin Sonar analysis
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: |
+ dotnet-sonarscanner begin \
+ /k:"PFalkowski_StrongRandom" \
+ /o:"pfalkowski" \
+ /d:sonar.token="$SONAR_TOKEN" \
+ /d:sonar.host.url="https://sonarcloud.io" \
+ /d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml \
+ /d:sonar.exclusions="**/*.Test/**,**/*Test.cs"
+
+ - name: Build
+ run: dotnet build --no-restore -c Release
+
+ - name: Collect coverage
+ run: dotnet-coverage collect "dotnet test --no-build -c Release" -f xml -o coverage.xml
+
+ - name: End Sonar analysis
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN"
diff --git a/Extensions.Standard.Randomization.Test/BufferedRadnomProviderTest.cs b/Extensions.Standard.Randomization.Test/BufferedRandomProviderTest.cs
similarity index 98%
rename from Extensions.Standard.Randomization.Test/BufferedRadnomProviderTest.cs
rename to Extensions.Standard.Randomization.Test/BufferedRandomProviderTest.cs
index 9a8a9e7..6a9fb82 100644
--- a/Extensions.Standard.Randomization.Test/BufferedRadnomProviderTest.cs
+++ b/Extensions.Standard.Randomization.Test/BufferedRandomProviderTest.cs
@@ -4,7 +4,7 @@
namespace Extensions.Standard.Randomization.Test
{
- public class BufferedRadnomProviderTest
+ public class BufferedRandomProviderTest
{
[Theory]
[InlineData(1)]
diff --git a/Extensions.Standard.Randomization.Test/Extensions.Standard.Randomization.Test.csproj b/Extensions.Standard.Randomization.Test/Extensions.Standard.Randomization.Test.csproj
index 1d93b08..27284e1 100644
--- a/Extensions.Standard.Randomization.Test/Extensions.Standard.Randomization.Test.csproj
+++ b/Extensions.Standard.Randomization.Test/Extensions.Standard.Randomization.Test.csproj
@@ -1,22 +1,27 @@
-
- netcoreapp2.0
+
+ net8.0
+ enable
+ false
-
-
-
-
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
-
-
-
-
diff --git a/Extensions.Standard.Randomization.Test/StrongRandomTest.cs b/Extensions.Standard.Randomization.Test/StrongRandomTest.cs
index f7c53d5..000a5ae 100644
--- a/Extensions.Standard.Randomization.Test/StrongRandomTest.cs
+++ b/Extensions.Standard.Randomization.Test/StrongRandomTest.cs
@@ -221,8 +221,8 @@ public void NextBytesThrowsArgumentExceptionWhenNullArrayPassedIn()
{
var providerMock = new RandomProviderMock(0);
var tested = new StrongRandom(providerMock);
- byte[] nullBytesArray = null;
- Assert.Throws(() => tested.NextBytes(nullBytesArray));
+ byte[]? nullBytesArray = null;
+ Assert.Throws(() => tested.NextBytes(nullBytesArray!));
}
[Fact]
public void NextThrowsArgumentOutOfRangeExceptionWhenNegativeIntPassedIn()
diff --git a/Extensions.Standard.Randomization.Test/UtilitiesTest.cs b/Extensions.Standard.Randomization.Test/UtilitiesTest.cs
index f9912c3..4f1be05 100644
--- a/Extensions.Standard.Randomization.Test/UtilitiesTest.cs
+++ b/Extensions.Standard.Randomization.Test/UtilitiesTest.cs
@@ -117,10 +117,8 @@ public void NextLetterRetrunsValidResult()
}
}
- [Theory]
- [InlineData("test")]
- [InlineData(".NETStandard")]
- public void NextAlphanumericRetrunsValidResult(string toChooseFrom)
+ [Fact]
+ public void NextAlphanumericRetrunsValidResult()
{
var randomSubstitute = Substitute.For();
for (int i = '0'; i < '9' + 1; ++i)
diff --git a/Extensions.Standard.Randomization/BufferedRadnomProvider.cs b/Extensions.Standard.Randomization/BufferedRadnomProvider.cs
index 65cbe5f..672ab36 100644
--- a/Extensions.Standard.Randomization/BufferedRadnomProvider.cs
+++ b/Extensions.Standard.Randomization/BufferedRadnomProvider.cs
@@ -1,56 +1,12 @@
-using System;
-using System.Security.Cryptography;
+using System;
namespace Extensions.Standard.Randomization
{
- ///
- /// Internally uses RNGCryptoServiceProvider. Buffered call to RNGCryptoServiceProvider up to bufferSize
- ///
- public class BufferedRandomProvider : IRandomProvider
+ /// Preserved for binary compatibility. Use instead (fixed spelling).
+ [Obsolete("Use BufferedRandomProvider instead (typo in original name fixed). This class will be removed in a future major version.")]
+ public sealed class BufferedRadnomProvider : BufferedRandomProvider
{
- public BufferedRandomProvider(int bufferSize)
- {
- _bufer = new Lazy(() => new byte[bufferSize], true);
- }
-
- private readonly Lazy _bufer;
- private int _currentIndex;
-
- private void GetBytesInternal(byte[] buffer)
- {
- using (var csprng = RandomNumberGenerator.Create())
- {
- csprng.GetBytes(buffer);
- }
- }
-
- private void RefreshBuffer()
- {
- GetBytesInternal(_bufer.Value);
- _currentIndex = 0;
- }
-
- public void GetBytes(byte[] input)
- {
- if (!_bufer.IsValueCreated)
- {
- RefreshBuffer();
- }
- if (input.Length > _bufer.Value.Length)
- {
- GetBytesInternal(input);
- return;
- }
- else if (input.Length + _currentIndex > _bufer.Value.Length)
- {
- RefreshBuffer();
- }
-
- for (var i = 0; i < input.Length; ++i)
- {
- input[i] = _bufer.Value[i + _currentIndex];
- }
- _currentIndex += input.Length;
- }
+ ///
+ public BufferedRadnomProvider(int bufferSize) : base(bufferSize) { }
}
}
diff --git a/Extensions.Standard.Randomization/BufferedRandomProvider.cs b/Extensions.Standard.Randomization/BufferedRandomProvider.cs
new file mode 100644
index 0000000..321ffec
--- /dev/null
+++ b/Extensions.Standard.Randomization/BufferedRandomProvider.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Security.Cryptography;
+
+namespace Extensions.Standard.Randomization
+{
+ ///
+ /// Buffered wrapper around that amortises expensive CSPRNG calls
+ /// by filling an internal byte buffer in one shot and dispensing bytes from it on subsequent requests.
+ ///
+ public class BufferedRandomProvider : IRandomProvider
+ {
+ private readonly Lazy _buffer;
+ private int _currentIndex;
+
+ /// Number of random bytes to pre-fetch per CSPRNG call.
+ public BufferedRandomProvider(int bufferSize)
+ {
+ _buffer = new Lazy(() => new byte[bufferSize], true);
+ }
+
+ private static void GetBytesInternal(byte[] buffer)
+ {
+ using var csprng = RandomNumberGenerator.Create();
+ csprng.GetBytes(buffer);
+ }
+
+ private void RefreshBuffer()
+ {
+ GetBytesInternal(_buffer.Value);
+ _currentIndex = 0;
+ }
+
+ /// Fills with cryptographically-strong random bytes.
+ public void GetBytes(byte[] input)
+ {
+ if (!_buffer.IsValueCreated)
+ {
+ RefreshBuffer();
+ }
+
+ if (input.Length > _buffer.Value.Length)
+ {
+ GetBytesInternal(input);
+ return;
+ }
+
+ if (input.Length + _currentIndex > _buffer.Value.Length)
+ {
+ RefreshBuffer();
+ }
+
+ for (var i = 0; i < input.Length; ++i)
+ {
+ input[i] = _buffer.Value[i + _currentIndex];
+ }
+ _currentIndex += input.Length;
+ }
+ }
+}
diff --git a/Extensions.Standard.Randomization/Extensions.Standard.Randomization.csproj b/Extensions.Standard.Randomization/Extensions.Standard.Randomization.csproj
index 44a07ae..6329ff9 100644
--- a/Extensions.Standard.Randomization/Extensions.Standard.Randomization.csproj
+++ b/Extensions.Standard.Randomization/Extensions.Standard.Randomization.csproj
@@ -5,34 +5,30 @@
Extensions.Standard.Randomization
Extensions.Standard.Randomization
Piotr Falkowski
-
+ Piotr Falkowski
StrongRandom
- Random interface implemented with Cryptographically-Secure Pseudo-Random Number Generator. Everywhere you use System.Random, StrongRandom can be used now. Additionally missing boilerplate extensions for any Random: NextBool, NextChar, NextString etc. added.
- Piotr Falkowski © 2018
- https://opensource.org/licenses/MIT
+ System.Random drop-in backed by a Cryptographically-Secure Pseudo-Random Number Generator. Everywhere you use System.Random, StrongRandom can be used. Extension methods for any Random: NextBool, NextChar, NextByte, NextNormal and more.
+ Piotr Falkowski © 2026
+ MIT
git
- Random, CSPRNG, Extension-methods
- True
+ Random, CSPRNG, Extension-methods, security, cryptography
https://github.com/PFalkowski/StrongRandom
https://github.com/PFalkowski/StrongRandom
StrongRandom
- 2.0.0
- Update .NET Standard sdk to 2.0
+ 2.1.0
+ latest
+ enable
+ true
+ $(NoWarn);CS1591
+ README.md
-
- True
- True
- Resources.resx
-
+
-
- ResXFileCodeGenerator
- Resources.Designer.cs
-
+
\ No newline at end of file
diff --git a/Extensions.Standard.Randomization/StrongRandom.cs b/Extensions.Standard.Randomization/StrongRandom.cs
index e2be6b4..7ae15ea 100644
--- a/Extensions.Standard.Randomization/StrongRandom.cs
+++ b/Extensions.Standard.Randomization/StrongRandom.cs
@@ -5,7 +5,7 @@ namespace Extensions.Standard.Randomization
public class StrongRandom : Random
{
private readonly IRandomProvider _provider;
- public StrongRandom(IRandomProvider provider = null)
+ public StrongRandom(IRandomProvider? provider = null)
{
_provider = provider ?? new BufferedRandomProvider(44);
}
diff --git a/README.md b/README.md
index 641e422..b88409f 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,116 @@
# StrongRandom
-[](https://www.nuget.org/packages/StrongRandom/)
-[](https://choosealicense.com/licenses/mit/)
+[](https://github.com/PFalkowski/StrongRandom/actions/workflows/ci.yml)
+[](https://sonarcloud.io/summary/new_code?id=PFalkowski_StrongRandom)
+[](https://sonarcloud.io/summary/new_code?id=PFalkowski_StrongRandom)
+[](https://www.nuget.org/packages/StrongRandom/)
+[](https://www.nuget.org/packages/StrongRandom/)
+[](LICENSE)
+[](https://www.buymeacoffee.com/piotrfalkowski)
-```System.Random``` interface implemented with Cryptographically-Secure Pseudo-Random Number Generator.
-Everywhere you use System.Random, StrongRandom can be used.
+`System.Random` drop-in backed by a **Cryptographically-Secure Pseudo-Random Number Generator (CSPRNG)**. Everywhere you use `System.Random`, `StrongRandom` can be used instead. Also ships extension methods that add `NextBool`, `NextByte`, `NextChar`, `NextFloat`, `NextNormal` and more to _any_ `Random` instance.
-**Important!
-Use with caution in security critical scenarios, as the distribution can be somewhat skewed in integer returning method calls and NextDouble(). GetBytes() is direct call to underlying RNG provider and is safe.**
+> **Security note:** `NextBytes()` is a direct call to the underlying CSPRNG and is safe. Integer-returning methods (`Next`, `Next(max)`, `Next(min, max)`) and `NextDouble()` involve a transformation step that introduces a slight bias — do **not** rely on these for security-critical decisions such as key generation. Use `NextBytes()` for raw entropy.
-Extensions for any Random: NextBool, NextChar, NextString etc. added.
+## Installation
+
+```
+dotnet add package StrongRandom
+```
+
+## Quick start
+
+```csharp
+using Extensions.Standard.Randomization;
+
+// Drop-in: assign to a System.Random variable
+Random rng = new StrongRandom();
+
+int dice = rng.Next(1, 7); // 1–6
+double d = rng.NextDouble(); // [0, 1)
+byte[] key = new byte[32];
+rng.NextBytes(key); // CSPRNG-safe raw bytes
+```
+
+## Extension methods (`Utilities`)
+
+All extensions target `System.Random`, so they work with `StrongRandom`, plain `new Random()`, or a mock.
+
+| Method | Signature | Description |
+|--------|-----------|-------------|
+| `NextByte` | `(short upperLimit = 256)` | Random byte in `[0, upperLimit)` |
+| `NextBool` | `()` | `true` or `false` with equal probability |
+| `NextChar` | `(char lower = ' ', char upper = '\x7F')` | Random character in printable ASCII range |
+| `NextChar` | `(string chooseFrom)` | Random character from a specific set |
+| `NextLowercaseLetter` | `()` | Random letter `a`–`z` |
+| `NextUppercaseLetter` | `()` | Random letter `A`–`Z` |
+| `NextLetter` | `()` | Random letter `a`–`z` or `A`–`Z` |
+| `NextAlphanumeric` | `()` | Random digit, uppercase, or lowercase letter |
+| `NextFloat` | `()` | Random `float` in `[0, 1)` |
+| `NextFloat` | `(float min, float max)` | Random `float` in `[min, max)` |
+| `NextDouble` | `(double max)` | Random `double` in `[0, max)` |
+| `NextDouble` | `(double min, double max)` | Random `double` in `[min, max)` |
+| `NextNormal` | `(double mean, double sd = 1)` | Normally distributed value (Box-Muller) |
+
+```csharp
+var rng = new StrongRandom();
+
+bool coinFlip = rng.NextBool();
+byte b = rng.NextByte();
+char letter = rng.NextLetter();
+char alphanumeric = rng.NextAlphanumeric();
+float f = rng.NextFloat(0f, 1f);
+double normal = rng.NextNormal(mean: 0, sd: 1);
+char fromSet = rng.NextChar("AEIOU");
+```
+
+## Customising the provider
+
+`StrongRandom` depends on `IRandomProvider` for its byte source. The default is `BufferedRandomProvider(44)`, which pre-fetches 44 bytes per CSPRNG call to reduce overhead.
+
+```csharp
+// Larger buffer — fewer round-trips to the CSPRNG
+var rng = new StrongRandom(new BufferedRandomProvider(256));
+
+// Custom provider (e.g. for testing)
+public class MyProvider : IRandomProvider
+{
+ public void GetBytes(byte[] input) { /* ... */ }
+}
+var testRng = new StrongRandom(new MyProvider());
+```
+
+## API
+
+### `StrongRandom`
+
+Inherits from `System.Random`. All `Random` members work as expected; randomness comes from the CSPRNG provider.
+
+| Member | Notes |
+|--------|-------|
+| `StrongRandom(IRandomProvider? provider = null)` | `null` uses `BufferedRandomProvider(44)` |
+| `Next()` | CSPRNG-backed |
+| `Next(int maxValue)` | CSPRNG-backed |
+| `Next(int minValue, int maxValue)` | CSPRNG-backed |
+| `NextDouble()` | CSPRNG-backed |
+| `NextBytes(byte[] buffer)` | Direct CSPRNG fill — unbiased |
+
+### `BufferedRandomProvider`
+
+| Member | Notes |
+|--------|-------|
+| `BufferedRandomProvider(int bufferSize)` | Pre-fetches `bufferSize` bytes per CSPRNG call |
+| `GetBytes(byte[] input)` | Fills `input` from the buffer, refreshing as needed |
+
+### `IRandomProvider`
+
+```csharp
+public interface IRandomProvider
+{
+ void GetBytes(byte[] input);
+}
+```
+
+## License
+
+MIT — see [LICENSE](LICENSE).