From 3da210332158c1f8e8f9b400ad394a97bee5db2a Mon Sep 17 00:00:00 2001
From: Paul Seal
Date: Wed, 27 May 2026 13:36:09 +0100
Subject: [PATCH 1/3] Fix NuGet README rendering and harden package metadata
- Use raw.githubusercontent.com URL for the logo so it resolves on NuGet.org (the /blob/ URL served HTML, not the image) and point it at master instead of the stale dev/v2 branch
- Make the migration-guide link absolute so it works on the package page
- Enable package validation and TreatWarningsAsErrors; add Company and tidy the Copyright string
Co-Authored-By: Claude Opus 4.7 (1M context)
---
PasswordGenerator/PasswordGenerator.csproj | 8 +++++++-
Readme.md | 4 ++--
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/PasswordGenerator/PasswordGenerator.csproj b/PasswordGenerator/PasswordGenerator.csproj
index 903a0f3..27ab8a4 100644
--- a/PasswordGenerator/PasswordGenerator.csproj
+++ b/PasswordGenerator/PasswordGenerator.csproj
@@ -4,10 +4,15 @@
net8.0;net10.0
enable
latest
+ true
true
+
+ true
+
3.0.0
3.0.0.0
@@ -15,8 +20,9 @@
PasswordGenerator
Paul Seal
+ Paul Seal
A cross-platform .NET library that generates cryptographically secure random passwords, passphrases, OTPs, API keys and readable identifiers. Configurable via a fluent API, presets (OWASP/NIST) and dependency injection, with async support and entropy estimation.
- Copyright 2026
+ Copyright © 2026 Paul Seal
https://github.com/prjseal/PasswordGenerator/
https://github.com/prjseal/PasswordGenerator/
Git
diff --git a/Readme.md b/Readme.md
index f4cb17b..dd440c5 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,6 +1,6 @@
# Password Generator
-
+
A cross-platform .NET library that generates cryptographically secure random passwords, passphrases,
OTPs, API keys and readable identifiers. Configure it with a fluent API, ready-made presets
@@ -17,7 +17,7 @@ Install via NuGet: ``` Install-Package PasswordGenerator ```
It targets `net8.0` and `net10.0`, so it requires .NET 8 or later. If you need to run on .NET
Framework or other older runtimes, use the 2.x line (which targets `netstandard2.0`).
-> **Upgrading from 2.x?** See the [v2 → v3 migration guide](docs/migration-v2-to-v3.md).
+> **Upgrading from 2.x?** See the [v2 → v3 migration guide](https://github.com/prjseal/PasswordGenerator/blob/master/docs/migration-v2-to-v3.md).
> The v2 API still works; the one behavioural change is that invalid settings now **throw** (or use
> `TryNext`) instead of returning an error string as the "password".
From 221861ade19ea28923bef18484f42c4740010a44 Mon Sep 17 00:00:00 2001
From: Paul Seal
Date: Wed, 27 May 2026 13:54:37 +0100
Subject: [PATCH 2/3] Allow passphrases with no separator (char? separator)
Make the passphrase separator a nullable char where null means no
separator, so words are concatenated directly (e.g. mapleriverquartzbloom42).
- PassphraseGenerator/PassphraseOptions/presets take char? separator
- Generation appends the separator only when present (between words and
before the trailing number)
- DI: an explicitly empty Passphrase:Separator binds to null, since the
config binder otherwise skips empty values and keeps the default
- Entropy is unaffected (the separator never contributed to it)
- Tests for no-separator output, capitalize, entropy parity and config
binding; Readme and CHANGELOG updated
Co-Authored-By: Claude Opus 4.7 (1M context)
---
CHANGELOG.md | 3 +
PasswordGenerator.Tests/PassphraseTests.cs | 72 +++++++++++++++++++
PasswordGenerator/PassphraseGenerator.cs | 17 ++---
PasswordGenerator/PassphraseOptions.cs | 7 +-
PasswordGenerator/Password.cs | 8 +--
...ordGeneratorServiceCollectionExtensions.cs | 7 ++
Readme.md | 6 ++
7 files changed, 106 insertions(+), 14 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 991b5c3..1457787 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,9 @@ See the [v2 → v3 migration guide](docs/migration-v2-to-v3.md).
- **Symbol injection:** `ForPassphrase(..., includeSymbol: true)` attaches a random symbol to one
randomly chosen word, so passphrases satisfy "needs a number and a symbol" rules while staying
memorable. Entropy estimation now accounts for both the trailing number and the symbol.
+- **Optional passphrase separator:** the separator is now a `char?` — pass `separator: null` (or an
+ empty string when binding from configuration) to concatenate words with no separator. This does not
+ affect entropy.
- **`ForMemorable()` preset:** capitalized words sized to at least 80 bits of entropy.
- **Passphrases via dependency injection:** set `PasswordOptions.Passphrase` (a `PassphraseOptions`)
in code or bind a `Passphrase` section from configuration to resolve a passphrase
diff --git a/PasswordGenerator.Tests/PassphraseTests.cs b/PasswordGenerator.Tests/PassphraseTests.cs
index 5465ce3..25f1492 100644
--- a/PasswordGenerator.Tests/PassphraseTests.cs
+++ b/PasswordGenerator.Tests/PassphraseTests.cs
@@ -212,5 +212,77 @@ public void Di_BindsPassphraseFromConfiguration()
var parts = generator.Next().Split('.');
Assert.That(parts.Length, Is.EqualTo(5)); // 5 words, no trailing number
}
+
+ [Test]
+ public void Next_WithNullSeparator_ConcatenatesWordsDirectly()
+ {
+ var rng = new FixedRandomSource(0, 1, 2, 3);
+ var generator = new PassphraseGenerator(4, separator: null, capitalize: false,
+ includeNumber: false, includeSymbol: false, minimumEntropyBits: 0, randomSource: rng);
+
+ var expected = string.Concat(
+ WordList.Words[0], WordList.Words[1], WordList.Words[2], WordList.Words[3]);
+ Assert.That(generator.Next(), Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Next_WithNullSeparator_AppendsNumberWithoutSeparator()
+ {
+ // Draw order: one index per word, then the trailing number (NextInt(90) + 10).
+ var rng = new FixedRandomSource(0, 1, 5);
+ var generator = new PassphraseGenerator(2, separator: null, capitalize: false,
+ includeNumber: true, includeSymbol: false, minimumEntropyBits: 0, randomSource: rng);
+
+ var expected = WordList.Words[0] + WordList.Words[1] + "15"; // 5 % 90 + 10
+ Assert.That(generator.Next(), Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void Next_WithNullSeparator_StillCapitalizesEachWord()
+ {
+ var rng = new FixedRandomSource(0, 1);
+ var generator = new PassphraseGenerator(2, separator: null, capitalize: true,
+ includeNumber: false, includeSymbol: false, minimumEntropyBits: 0, randomSource: rng);
+
+ static string Cap(string w) => char.ToUpperInvariant(w[0]) + w.Substring(1);
+ var expected = Cap(WordList.Words[0]) + Cap(WordList.Words[1]);
+ Assert.That(generator.Next(), Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void ForPassphrase_AcceptsNullSeparator()
+ {
+ var generator = (PassphraseGenerator)Password.ForPassphrase(4, separator: null);
+ Assert.That(generator.Separator, Is.Null);
+ }
+
+ [Test]
+ public void NullSeparator_DoesNotChangeEntropy()
+ {
+ var withSeparator = Password.ForPassphrase(6, separator: '-').EstimateEntropyBits();
+ var withoutSeparator = Password.ForPassphrase(6, separator: null).EstimateEntropyBits();
+ Assert.That(withoutSeparator, Is.EqualTo(withSeparator));
+ }
+
+ [Test]
+ public void Di_BindsEmptySeparatorAsNull()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Passphrase:WordCount"] = "3",
+ ["Passphrase:Separator"] = "",
+ ["Passphrase:IncludeNumber"] = "false"
+ })
+ .Build();
+
+ var services = new ServiceCollection();
+ services.AddPasswordGenerator(configuration);
+
+ using var provider = services.BuildServiceProvider();
+ var generator = (PassphraseGenerator)provider.GetRequiredService();
+
+ Assert.That(generator.Separator, Is.Null);
+ }
}
}
diff --git a/PasswordGenerator/PassphraseGenerator.cs b/PasswordGenerator/PassphraseGenerator.cs
index 2f16631..8d9aa25 100644
--- a/PasswordGenerator/PassphraseGenerator.cs
+++ b/PasswordGenerator/PassphraseGenerator.cs
@@ -19,9 +19,10 @@ public class PassphraseGenerator : IPasswordGenerator, IDisposable
/// Creates a passphrase generator.
/// The number of words in each passphrase; must be at least one.
///
- /// The character placed between words (and before the trailing number). Note that a few EFF
- /// words contain a hyphen (e.g. "t-shirt"), so if you need to split the output back into words
- /// choose a separator that does not occur in any word, such as '.' or a space.
+ /// The character placed between words (and before the trailing number), or
+ /// for no separator at all (the words are concatenated directly). Note that a few EFF words
+ /// contain a hyphen (e.g. "t-shirt"), so if you need to split the output back into words choose a
+ /// separator that does not occur in any word, such as '.' or a space.
///
/// Whether to capitalize the first letter of each word.
/// Whether to append a random two-digit number.
@@ -39,7 +40,7 @@ public class PassphraseGenerator : IPasswordGenerator, IDisposable
///
/// is less than one.
/// The estimated entropy is below .
- public PassphraseGenerator(int wordCount = 4, char separator = '-', bool capitalize = false,
+ public PassphraseGenerator(int wordCount = 4, char? separator = '-', bool capitalize = false,
bool includeNumber = true, bool includeSymbol = false, double minimumEntropyBits = 0,
IRandomSource? randomSource = null)
{
@@ -66,8 +67,8 @@ public PassphraseGenerator(int wordCount = 4, char separator = '-', bool capital
/// The number of words in each passphrase.
public int WordCount { get; }
- /// The character placed between words.
- public char Separator { get; }
+ /// The character placed between words, or for no separator.
+ public char? Separator { get; }
/// Whether the first letter of each word is capitalized.
public bool Capitalize { get; }
@@ -104,7 +105,7 @@ public string Next()
for (var i = 0; i < WordCount; i++)
{
- if (i > 0) sb.Append(Separator);
+ if (i > 0 && Separator is char sep) sb.Append(sep);
var word = WordList.Words[_random.NextInt(WordList.Words.Length)];
if (Capitalize && word.Length > 0)
@@ -122,7 +123,7 @@ public string Next()
if (IncludeNumber)
{
- sb.Append(Separator);
+ if (Separator is char numSep) sb.Append(numSep);
sb.Append((_random.NextInt(90) + 10).ToString(CultureInfo.InvariantCulture));
}
diff --git a/PasswordGenerator/PassphraseOptions.cs b/PasswordGenerator/PassphraseOptions.cs
index c5cf6f5..5212747 100644
--- a/PasswordGenerator/PassphraseOptions.cs
+++ b/PasswordGenerator/PassphraseOptions.cs
@@ -10,8 +10,11 @@ public class PassphraseOptions
/// The number of words in each passphrase. Defaults to 4.
public int WordCount { get; set; } = 4;
- /// The character placed between words. Defaults to '-'.
- public char Separator { get; set; } = '-';
+ ///
+ /// The character placed between words. Defaults to '-'. Set to
+ /// (or an empty string in configuration) for no separator.
+ ///
+ public char? Separator { get; set; } = '-';
/// Whether the first letter of each word is capitalized.
public bool Capitalize { get; set; }
diff --git a/PasswordGenerator/Password.cs b/PasswordGenerator/Password.cs
index d01f022..327f854 100644
--- a/PasswordGenerator/Password.cs
+++ b/PasswordGenerator/Password.cs
@@ -458,7 +458,7 @@ public static IPassword ForEnvironmentName(int length = 12)
/// Diceware-style passphrase built from the EFF Large Wordlist.
/// The number of words in the passphrase.
- /// The character placed between words.
+ /// The character placed between words, or for no separator.
/// Whether to capitalize the first letter of each word.
/// Whether to append a random two-digit number.
/// Whether to attach a random symbol to one randomly chosen word.
@@ -466,7 +466,7 @@ public static IPassword ForEnvironmentName(int length = 12)
/// An optional entropy floor; when greater than zero the configuration is rejected if it
/// falls below this many bits.
///
- public static IPasswordGenerator ForPassphrase(int words = 4, char separator = '-',
+ public static IPasswordGenerator ForPassphrase(int words = 4, char? separator = '-',
bool capitalize = false, bool includeNumber = true, bool includeSymbol = false,
double minimumEntropyBits = 0)
{
@@ -479,11 +479,11 @@ public static IPasswordGenerator ForPassphrase(int words = 4, char separator = '
/// The word count is derived from the word-list size, and the same value is enforced as a floor.
///
/// The minimum entropy in bits (defaults to 80, a strong target).
- /// The character placed between words.
+ /// The character placed between words, or for no separator.
/// Whether to capitalize the first letter of each word.
/// Whether to append a random two-digit number.
/// Whether to attach a random symbol to one randomly chosen word.
- public static IPasswordGenerator ForPassphraseWithEntropy(double targetBits = 80, char separator = '-',
+ public static IPasswordGenerator ForPassphraseWithEntropy(double targetBits = 80, char? separator = '-',
bool capitalize = false, bool includeNumber = true, bool includeSymbol = false)
{
var words = PassphraseGenerator.WordCountForEntropy(targetBits, includeNumber);
diff --git a/PasswordGenerator/PasswordGeneratorServiceCollectionExtensions.cs b/PasswordGenerator/PasswordGeneratorServiceCollectionExtensions.cs
index ac67552..b298df6 100644
--- a/PasswordGenerator/PasswordGeneratorServiceCollectionExtensions.cs
+++ b/PasswordGenerator/PasswordGeneratorServiceCollectionExtensions.cs
@@ -30,6 +30,13 @@ public static IServiceCollection AddPasswordGenerator(this IServiceCollection se
var options = new PasswordOptions();
configuration.Bind(options);
+
+ // The configuration binder skips empty string values, so an explicit empty separator
+ // (the way a config file asks for "no separator") would otherwise be lost and the
+ // default '-' kept. Honor it explicitly as null.
+ if (options.Passphrase != null && configuration["Passphrase:Separator"] == string.Empty)
+ options.Passphrase.Separator = null;
+
configure?.Invoke(options);
return AddCore(services, options);
}
diff --git a/Readme.md b/Readme.md
index dd440c5..553b807 100644
--- a/Readme.md
+++ b/Readme.md
@@ -102,6 +102,12 @@ For sites that require a digit and a symbol, pass `includeSymbol: true`. A rando
to one randomly chosen word (e.g. `maple-river#-quartz-bloom-42`), so the phrase passes composition
rules while staying memorable.
+To omit the separator entirely, pass `separator: null` (or an empty string when binding from
+configuration); the words are concatenated directly, e.g.
+`Password.ForPassphrase(4, separator: null).Next()` → `"mapleriverquartzbloom42"`. This does not
+change the passphrase's entropy — the separator is a fixed character and never contributes to
+strength — it only affects readability.
+
## Quality controls
```csharp
From 3677fc844912b28fef4e049c258bc334b37237ed Mon Sep 17 00:00:00 2001
From: Paul Seal
Date: Wed, 27 May 2026 14:16:09 +0100
Subject: [PATCH 3/3] Tidy docs, XML exception tags, and rename Readme.md to
README.md
- README: fix duplicate variable in the presets example, absolutize all
repo-relative links so they resolve on NuGet.org, give the async snippet
an async context, and document CharacterClass values, DefaultBatchCount
and an appSettings.json example
- docs: note the optional/no separator and WordCountForEntropy in
api-surface; add a passphrase appSettings example in configuration-and-di
- Add tags to public throwing members that lacked them
- Rename root Readme.md to README.md to match PackageReadmeFile and the
package path (was only working via case-insensitive Windows)
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../DocumentationSnippetTests.cs | 2 +-
PasswordGenerator/IPassword.cs | 3 +
PasswordGenerator/IPasswordGenerator.cs | 2 +
PasswordGenerator/IPasswordSettings.cs | 3 +
PasswordGenerator/Password.cs | 1 +
PasswordGenerator/PasswordGenerator.csproj | 2 +-
...ordGeneratorServiceCollectionExtensions.cs | 1 +
Readme.md => README.md | 55 +++++++++++++++----
docs/api-surface.md | 5 ++
docs/configuration-and-di.md | 20 +++++++
10 files changed, 80 insertions(+), 14 deletions(-)
rename Readme.md => README.md (75%)
diff --git a/PasswordGenerator.Tests/DocumentationSnippetTests.cs b/PasswordGenerator.Tests/DocumentationSnippetTests.cs
index d9a3140..7f438a1 100644
--- a/PasswordGenerator.Tests/DocumentationSnippetTests.cs
+++ b/PasswordGenerator.Tests/DocumentationSnippetTests.cs
@@ -9,7 +9,7 @@
namespace PasswordGenerator.Tests
{
///
- /// Compile-and-run guards for the snippets in Readme.md and the v2->v3 migration guide, so the
+ /// Compile-and-run guards for the snippets in README.md and the v2->v3 migration guide, so the
/// documentation cannot drift from the public API.
///
public class DocumentationSnippetTests
diff --git a/PasswordGenerator/IPassword.cs b/PasswordGenerator/IPassword.cs
index a778230..49bb1ef 100644
--- a/PasswordGenerator/IPassword.cs
+++ b/PasswordGenerator/IPassword.cs
@@ -30,6 +30,7 @@ public interface IPassword
IPassword IncludeSpecial(string specialCharactersToInclude);
/// Replaces the pool with an explicit set of characters (no forced composition).
+ /// is .
IPassword WithCharacters(string characters);
/// Uses every printable ASCII character as the pool (no forced composition).
@@ -39,6 +40,8 @@ public interface IPassword
IPassword ExcludeAmbiguous();
/// Requires at least characters from the given class.
+ /// is negative.
+ /// A custom character pool is in use (per-class minimums cannot be combined with it).
IPassword RequireAtLeast(CharacterClass characterClass, int count);
/// Sets the required password length.
diff --git a/PasswordGenerator/IPasswordGenerator.cs b/PasswordGenerator/IPasswordGenerator.cs
index 69c17a5..8f37e51 100644
--- a/PasswordGenerator/IPasswordGenerator.cs
+++ b/PasswordGenerator/IPasswordGenerator.cs
@@ -28,12 +28,14 @@ public interface IPasswordGenerator
IReadOnlyList Generate();
/// Generates passwords.
+ /// is negative.
IReadOnlyList Generate(int count);
/// Generates the default number of passwords, observing .
ValueTask> GenerateAsync(CancellationToken cancellationToken = default);
/// Generates passwords, observing .
+ /// is negative.
ValueTask> GenerateAsync(int count, CancellationToken cancellationToken = default);
/// Estimates the strength, in bits, of the output produced by this generator.
diff --git a/PasswordGenerator/IPasswordSettings.cs b/PasswordGenerator/IPasswordSettings.cs
index ddd2b97..62b93ec 100644
--- a/PasswordGenerator/IPasswordSettings.cs
+++ b/PasswordGenerator/IPasswordSettings.cs
@@ -71,6 +71,7 @@ public interface IPasswordSettings
IPasswordSettings AddSpecial(string specialCharactersToAdd);
/// Replaces the entire pool with an explicit set of characters (no forced composition).
+ /// is .
IPasswordSettings UseCharacters(string characters);
/// Uses every printable ASCII character as the pool (no forced composition).
@@ -80,6 +81,8 @@ public interface IPasswordSettings
IPasswordSettings ExcludeAmbiguousCharacters();
/// Requires at least characters from the given class, enabling it if needed.
+ /// is negative.
+ /// A custom character pool is in use (per-class minimums cannot be combined with it).
IPasswordSettings RequireAtLeast(CharacterClass characterClass, int count);
/// The special characters used when special characters are included.
diff --git a/PasswordGenerator/Password.cs b/PasswordGenerator/Password.cs
index 327f854..64445d5 100644
--- a/PasswordGenerator/Password.cs
+++ b/PasswordGenerator/Password.cs
@@ -97,6 +97,7 @@ public Password(bool includeLowercase, bool includeUppercase, bool includeNumeri
/// Creates a password generator with an explicit random source. The caller owns the
/// supplied and is responsible for disposing it.
///
+ /// is .
public Password(IPasswordSettings settings, IRandomSource randomSource)
{
Settings = settings;
diff --git a/PasswordGenerator/PasswordGenerator.csproj b/PasswordGenerator/PasswordGenerator.csproj
index 27ab8a4..9cd289d 100644
--- a/PasswordGenerator/PasswordGenerator.csproj
+++ b/PasswordGenerator/PasswordGenerator.csproj
@@ -43,7 +43,7 @@
-
+
diff --git a/PasswordGenerator/PasswordGeneratorServiceCollectionExtensions.cs b/PasswordGenerator/PasswordGeneratorServiceCollectionExtensions.cs
index b298df6..baffb74 100644
--- a/PasswordGenerator/PasswordGeneratorServiceCollectionExtensions.cs
+++ b/PasswordGenerator/PasswordGeneratorServiceCollectionExtensions.cs
@@ -23,6 +23,7 @@ public static IServiceCollection AddPasswordGenerator(this IServiceCollection se
/// Registers the generator, binding options from configuration (e.g. appSettings.json) and then
/// applying an optional code override. Resolution order is configure (code) > configuration > default.
///
+ /// is .
public static IServiceCollection AddPasswordGenerator(this IServiceCollection services,
IConfiguration configuration, Action? configure = null)
{
diff --git a/Readme.md b/README.md
similarity index 75%
rename from Readme.md
rename to README.md
index 553b807..ecfcdfc 100644
--- a/Readme.md
+++ b/README.md
@@ -23,6 +23,9 @@ Framework or other older runtimes, use the 2.x line (which targets `netstandard2
## Basic usage
+> The examples below assume `using PasswordGenerator;` (and, for the dependency-injection section,
+> `using Microsoft.Extensions.DependencyInjection;`).
+
```csharp
// By default, all character types are available and the length is 16.
// Returns a random password with the default settings.
@@ -80,8 +83,8 @@ var password = pwd.Next();
## Presets
Ready-made starting points; later fluent calls still override them. See the
-[standards mapping](docs/migration-v2-to-v3.md#6-standards-mapping-for-the-presets) for the
-OWASP/NIST rationale.
+[standards mapping](https://github.com/prjseal/PasswordGenerator/blob/master/docs/migration-v2-to-v3.md#6-standards-mapping-for-the-presets)
+for the OWASP/NIST rationale.
```csharp
string strong = Password.ForOwasp().Next(); // full printable-ASCII pool, length 16
@@ -90,7 +93,7 @@ string otp = Password.ForOtp(6).Next(); // 6-digit one-time code
string apiKey = Password.ForApiKey(32).Next(); // URL-safe token
string envName = Password.ForEnvironmentName(12).Next();// readable id, no look-alike characters
string phrase = Password.ForPassphrase(4).Next(); // e.g. "maple-river-quartz-bloom-42"
-string strong = Password.ForPassphraseWithEntropy(80).Next(); // word count derived to clear 80 bits
+string strongPhrase = Password.ForPassphraseWithEntropy(80).Next(); // word count derived to clear 80 bits
string memorable = Password.ForMemorable().Next(); // capitalized, ~80+ bits, e.g. "Maple-River-Quartz-Bloom-Glade-Vivid-42"
```
@@ -114,7 +117,8 @@ strength — it only affects readability.
// Remove look-alike characters (I l 1 O 0 o)
var readable = new Password(20).ExcludeAmbiguous().Next();
-// Guarantee at least N characters from a class
+// Guarantee at least N characters from a class.
+// CharacterClass values: Lowercase, Uppercase, Numeric, Special.
var pwd = new Password(16).RequireAtLeast(CharacterClass.Numeric, 2).Next();
// Use a custom pool, or every printable ASCII character
@@ -138,10 +142,19 @@ if (new Password(16).TryNext(out var result))
## Async and batches
+`Generate(count)` returns a batch synchronously; the `Async` overloads return a `ValueTask` and honour
+a `CancellationToken`. The parameterless `Generate()` returns `DefaultBatchCount` passwords (1 by
+default; set the property to change it).
+
```csharp
-string password = await pwd.NextAsync(cancellationToken);
-IReadOnlyList ten = pwd.Generate(10);
-IReadOnlyList ten2 = await pwd.GenerateAsync(10, cancellationToken);
+async Task ExampleAsync(CancellationToken cancellationToken)
+{
+ var pwd = new Password(16);
+
+ string password = await pwd.NextAsync(cancellationToken);
+ IReadOnlyList ten = pwd.Generate(10);
+ IReadOnlyList ten2 = await pwd.GenerateAsync(10, cancellationToken);
+}
```
## Dependency injection
@@ -170,17 +183,35 @@ services.AddPasswordGenerator(o =>
o.Passphrase = new PassphraseOptions { WordCount = 6, Capitalize = true });
```
+You can also bind from `appSettings.json` (code configuration still takes precedence over bound
+values, which in turn take precedence over the defaults):
+
+```jsonc
+{
+ "PasswordGenerator": {
+ "Length": 20,
+ "IncludeSpecial": true,
+ "ExcludeAmbiguous": true,
+ "DefaultBatchCount": 5
+ }
+}
+```
+
+```csharp
+services.AddPasswordGenerator(config.GetSection("PasswordGenerator"));
+```
+
## Documentation
-- [v2 → v3 migration guide](docs/migration-v2-to-v3.md)
-- [Changelog](CHANGELOG.md)
-- [Design & architecture docs](docs/README.md)
+- [v2 → v3 migration guide](https://github.com/prjseal/PasswordGenerator/blob/master/docs/migration-v2-to-v3.md)
+- [Changelog](https://github.com/prjseal/PasswordGenerator/blob/master/CHANGELOG.md)
+- [Design & architecture docs](https://github.com/prjseal/PasswordGenerator/blob/master/docs/README.md)
## License & attribution
-PasswordGenerator is licensed under the [MIT License](License.md).
+PasswordGenerator is licensed under the [MIT License](https://github.com/prjseal/PasswordGenerator/blob/master/License.md).
Passphrases are generated from the **EFF Large Wordlist** (7,776 words) by the
[Electronic Frontier Foundation](https://www.eff.org/dice), used under the
[Creative Commons Attribution 3.0 US](https://creativecommons.org/licenses/by/3.0/us/)
-license. See [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) for details.
+license. See [THIRD-PARTY-NOTICES.md](https://github.com/prjseal/PasswordGenerator/blob/master/THIRD-PARTY-NOTICES.md) for details.
diff --git a/docs/api-surface.md b/docs/api-surface.md
index 8a47a15..c6ba3e2 100644
--- a/docs/api-surface.md
+++ b/docs/api-surface.md
@@ -73,6 +73,11 @@ Passphrases are built from the **EFF Large Wordlist** (7,776 words, ~12.9 bits/w
- `ForPassphraseWithEntropy(targetBits)` derives the word count needed to clear a target and enforces
it as a floor; `ForPassphrase(..., minimumEntropyBits)` enforces a floor for an explicit word count.
+ The derivation is exposed directly as the static `PassphraseGenerator.WordCountForEntropy(targetBits,
+ includeNumber)` if you want the word count without building a generator.
+- The separator is a `char?` — pass `separator: null` (or an empty string when binding from
+ configuration) to concatenate the words with no separator. This does not change entropy; the
+ separator is a fixed character and never contributes to strength.
- `includeSymbol: true` attaches a random symbol to one randomly chosen word, satisfying
"needs a number and a symbol" composition rules while staying memorable.
- `EstimateEntropyBits()` (on `IPasswordGenerator`) reports the estimated strength.
diff --git a/docs/configuration-and-di.md b/docs/configuration-and-di.md
index 31ed2fc..7842f58 100644
--- a/docs/configuration-and-di.md
+++ b/docs/configuration-and-di.md
@@ -32,6 +32,26 @@ applies. `appSettings` binding is an **opt-in, separate step** — it is never a
}
```
+## Example `appSettings.json` (passphrase)
+
+Setting a `Passphrase` section makes the registered generator produce passphrases instead of
+character passwords (the character-pool options above are then ignored). An empty `Separator`
+(`""`) means *no separator* — the configuration binder skips empty values, so the DI registration
+maps an explicit empty string to `null` for you.
+
+```jsonc
+{
+ "PasswordGenerator": {
+ "Passphrase": {
+ "WordCount": 6,
+ "Separator": "",
+ "Capitalize": true,
+ "IncludeNumber": true
+ }
+ }
+}
+```
+
## DI registration (opt-in, not auto-registered on install)
```mermaid