SaddamHossain.Toolkit.FakeData
1.0.0
dotnet add package SaddamHossain.Toolkit.FakeData --version 1.0.0
NuGet\Install-Package SaddamHossain.Toolkit.FakeData -Version 1.0.0
<PackageReference Include="SaddamHossain.Toolkit.FakeData" Version="1.0.0" />
<PackageVersion Include="SaddamHossain.Toolkit.FakeData" Version="1.0.0" />
<PackageReference Include="SaddamHossain.Toolkit.FakeData" />
paket add SaddamHossain.Toolkit.FakeData --version 1.0.0
#r "nuget: SaddamHossain.Toolkit.FakeData, 1.0.0"
#:package SaddamHossain.Toolkit.FakeData@1.0.0
#addin nuget:?package=SaddamHossain.Toolkit.FakeData&version=1.0.0
#tool nuget:?package=SaddamHossain.Toolkit.FakeData&version=1.0.0
SaddamHossain.Toolkit.FakeData
A modern, dependency-free, strongly-typed fake data generator for .NET — with deterministic seeding and first-class country providers.
Version 1.0.0 — the initial release. 197 public members across 19 types, 378 tests, zero dependencies.
Contents
Introduction · Features · Installation · Quick start · Architecture · API · Seed support · Country providers · Object generation · Validation · Performance · Requirements · Roadmap · Versioning · Contributing · Security · License
Introduction
Every project needs realistic test data, and every project ends up generating it badly: a static
array of five names, a Random that reshuffles on every run, an address whose city and country have
never met, a phone number that turns out to belong to somebody.
SaddamHossain.Toolkit.FakeData does that job properly. One entry point, three shapes for every
generator, data that is internally consistent, and a seed that means the same thing on every machine
you will ever run it on.
using SaddamHossain.Toolkit.FakeData;
// A single value
string name = Fake.Person.FullName();
// A collection
IReadOnlyList<string> names = Fake.Person.FullNames(100);
// A strongly-typed object
PersonModel person = Fake.Person.Generate();
// Many objects
IReadOnlyList<PersonModel> people = Fake.Person.Generate(100);
// Country-specific
IReadOnlyList<BangladeshPersonModel> bangladeshis = Fake.Country.Bangladesh.Generate(100);
// Reproducible
Fake.UseSeed(42);
IReadOnlyList<PersonModel> repeatable = Fake.Person.Generate(100);
Features
- Zero dependencies. The package references no other NuGet package. Nothing enters your dependency graph but this assembly.
- Multi-targeted for
net8.0,net9.0andnet10.0. - Trim- and AOT-safe. Marked
IsAotCompatible, so Native AOT and trimmed publishes stay warning-free. Nounsafe, nodynamic, no regular expressions, and no reflection beyond the embedded-resource lookup that loads the datasets. - Genuinely deterministic.
Fake.UseSeed(42)reproduces byte-identical data on any OS, any CPU architecture and any target framework — because the generator ships inside the package rather than delegating toRandom, whose seeded algorithm may change between .NET versions. - Thread-safe with no locks. Seed state is per-thread, so parallel tests cannot consume each other's draws and no generator ever contends.
- Internally consistent objects. A generated person's email is built from their name, their given name matches their gender, and a company's website and contact address share a domain derived from its own name.
- Country providers that are actually correct.
Fake.Country.Bangladeshproduces addresses whose upazila really is inside its district, which really is inside its division. - Safe by construction. Phone numbers use the NANP fictional range, IPv4 addresses come from the
RFC 5737 documentation blocks, IPv6 from RFC 3849, MAC addresses are locally administered, and
SafeEmail()stays inside RFC 2606 reserved domains. - Datasets in JSON, not in C#. 21 embedded JSON files parsed with a hand-written
Utf8JsonReaderwalker — no hardcoded arrays, noJsonSerializer, nothing for the trimmer to break. - Fully documented. Every public member ships XML documentation covering its parameters, return
value and every exception it throws deliberately — enforced at build time, since
CS1591is an error here. Members whose usage is not obvious from the summary alone carry a worked<example>. - Source Link + symbols. Step straight into the source from your debugger.
Installation
dotnet add package SaddamHossain.Toolkit.FakeData
Or via the Package Manager Console:
Install-Package SaddamHossain.Toolkit.FakeData
Quick start
Everything lives in a single namespace, so one using makes the whole library discoverable through
IntelliSense by typing Fake.:
using SaddamHossain.Toolkit.FakeData;
That is deliberate. The library is organised into folders internally, but splitting the namespace
by feature would force you to import six namespaces to read one generated person — the same
reasoning behind System.Linq and Microsoft.Extensions.*.
Architecture
Fake the single entry point
├── UseSeed / ResetSeed / CurrentSeed
├── Person ─┐
├── Internet │
├── Company ├─ generic generators, locale-neutral
├── Address │
├── Commerce ─┘
└── Country
└── Bangladesh ICountryProvider<BangladeshPersonModel>
Models PersonModel · CompanyModel · AddressModel · InternetModel
CommerceProductModel · BangladeshPersonModel
Enums Gender · CountryCode
Internally, six helpers do the work and none of them is public: SeedManager owns the per-thread
generator, RandomProvider is the drawing surface every generator goes through, SelectionHelper
turns a dataset plus a draw into one value or a batch, JsonDatasetReader parses the embedded JSON,
DatasetLoader reads it out of the assembly, and CountryResolver maps a CountryCode onto its
provider.
Every generator follows the same three shapes. That is the whole ergonomic argument:
| Shape | Example | Returns |
|---|---|---|
| Single value | Fake.Person.FullName() |
string |
| Collection | Fake.Person.FullNames(100) |
IReadOnlyList<string> |
| Object | Fake.Person.Generate() |
PersonModel |
| Objects | Fake.Person.Generate(100) |
IReadOnlyList<PersonModel> |
A test asserts this pairing across the whole API, so it cannot drift.
API
Fake.Person
| Method | Returns | Summary |
|---|---|---|
FirstName() / (Gender) |
string |
Given name |
LastName() |
string |
Family name |
FullName() / (Gender) |
string |
Given + family |
Gender() |
Gender |
Which name list a person draws from |
Email() |
string |
Derived from a generated name |
Phone() |
string |
NANP fictional range |
DateOfBirth() / (int, int) |
DateOnly |
18–80 by default |
Generate() |
PersonModel |
Complete, internally consistent |
Plural forms: FirstNames, LastNames, FullNames, Genders, Emails, Phones,
DatesOfBirth, Generate(int).
Fake.Internet
| Method | Returns | Summary |
|---|---|---|
UserName() |
string |
ada.lovelace |
Email() / (string, string) |
string |
Fictional brand domain |
SafeEmail() |
string |
RFC 2606 reserved domain |
Domain() |
string |
northwind.dev |
Url() |
string |
Absolute HTTPS |
IPv4() |
string |
RFC 5737 documentation block |
IPv6() |
string |
RFC 3849 2001:db8::/32 |
MacAddress() |
string |
Locally administered |
Generate() |
InternetModel |
All of the above, consistent |
Plural forms: UserNames, Emails, SafeEmails, Domains, Urls, IPv4Addresses,
IPv6Addresses, MacAddresses, Generate(int).
Fake.Company
| Method | Returns | Summary |
|---|---|---|
Name() |
string |
Northwind Technologies |
Industry() |
string |
Renewable Energy |
Department() |
string |
Quality Assurance |
JobTitle() |
string |
Senior Data Engineer |
Generate() |
CompanyModel |
Website and email share the name's domain |
Plural forms: Names, Industries, Departments, JobTitles, Generate(int).
Fake.Address
| Method | Returns | Summary |
|---|---|---|
Street() |
string |
482 Maple Avenue |
City() |
string |
City or town |
State() |
string |
State, province or region |
PostCode() |
string |
Five digits, no leading zero |
Country() / CountryCode() |
string |
Name / ISO 3166-1 alpha-2 |
FullAddress() |
string |
One comma-separated line |
Generate() |
AddressModel |
Country name and code always agree |
Plural forms: Streets, Cities, States, PostCodes, Countries, CountryCodes,
FullAddresses, Generate(int).
Generic addresses are locale-neutral, not country-accurate. A generated record may pair a city, a region and a country that have no relationship, and the post code follows no national scheme. This is a deliberate trade and it is worth stating plainly: one generic generator can be shallow everywhere or accurate nowhere. When the address must be genuinely correct, use a country provider.
Fake.Commerce
| Method | Returns | Summary |
|---|---|---|
Product() |
string |
Ergonomic Steel Chair |
Category() |
string |
Home Improvement |
Sku() |
string |
SKU-4827-XQ |
Price() / (decimal, decimal) |
decimal |
Exact to two places |
Currency() |
string |
ISO 4217 code |
Generate() |
CommerceProductModel |
Code and symbol always agree |
Plural forms: Products, Categories, Skus, Prices, Currencies, Generate(int).
Fake.Country
| Member | Returns | Summary |
|---|---|---|
Bangladesh |
BangladeshProvider |
The implemented provider |
Supported |
IReadOnlyList<ICountryProvider> |
What ships today |
Get(CountryCode) |
ICountryProvider |
Throws if not implemented |
TryGet(CountryCode, out …) |
bool |
Non-throwing, flow-analysed |
IsSupported(CountryCode) |
bool |
Feature check |
Extension methods
Applying the seeded generator to your own collections:
| Method | Receiver | Returns |
|---|---|---|
PickRandom() |
IReadOnlyList<T> |
T |
PickRandom(int) |
IReadOnlyList<T> |
IReadOnlyList<T> |
Shuffle() |
IReadOnlyList<T> |
IReadOnlyList<T> |
These bind to
IReadOnlyList<T>rather thanIEnumerable<T>on purpose. .NET 10 addedEnumerable.ShuffleonIEnumerable<T>; declaring ours there too would make every call site with both namespaces in scope fail withCS0121: ambiguous call— on net10.0 only.
Seed support
Fake.UseSeed(seed) makes every subsequent generation on the calling thread reproducible.
Fake.UseSeed(12345);
string first = Fake.Person.FullName();
Fake.UseSeed(12345);
string second = Fake.Person.FullName();
// first == second — on Windows, Linux, macOS, x64, Arm64, net8.0, net9.0 and net10.0.
This guarantee is underwritten by a xoshiro256** generator implemented inside the package,
seeded through SplitMix64. Random cannot underwrite it: the algorithm behind the seeded
Random(int) constructor is explicitly documented as an implementation detail that may change
between .NET versions, and a test fixture that silently reshuffles itself when the host upgrades its
runtime is worse than no determinism at all.
The seed applies to the thread that set it. This is the one rule worth internalising:
Fake.UseSeed(42);
await Task.Run(() =>
{
// This thread was never seeded — it generates unseeded data.
Fake.Person.FullName();
});
Test runners execute collections in parallel, and a single shared generator would let two seeded tests consume each other's draws — producing a determinism feature that fails intermittently, which is the worst possible failure mode because it looks like flakiness somewhere else. Per-thread state makes a seed mean something under parallelism, and removes all locking from the hot path.
Fake.ResetSeed() returns the thread to unseeded generation; Fake.CurrentSeed reports the seed in
force, or null.
Not a source of secrets. The generator is chosen for reproducibility, which is the exact opposite of unpredictability. Nothing produced by this library may be used as a password, token, key, or any identifier that needs to be unguessable.
Country providers
Fake.Country.Bangladesh is the first implemented provider.
Fake.Country.Bangladesh.Name(); // "Tanvir Rahman"
Fake.Country.Bangladesh.Mobile(); // "01712345678"
Fake.Country.Bangladesh.NID(); // "4829173065"
Fake.Country.Bangladesh.Division(); // "Khulna"
Fake.Country.Bangladesh.District(); // "Cox's Bazar"
Fake.Country.Bangladesh.Upazila(); // "Kaliakair"
Fake.Country.Bangladesh.PostCode(); // "4000"
Fake.Country.Bangladesh.Company(); // "Meghna Group Industries"
Fake.Country.Bangladesh.Address(); // AddressModel
The geography nests correctly. An address names a real upazila inside the real district that contains it, inside the real division that contains that:
Holding 199, Sarak 8, Gopibagh, Barhatta, Netrokona, Mymensingh 4310, Bangladesh
House 171, Sarak 32, Sonadanga, Debiganj, Panchagarh, Rangpur 3700, Bangladesh
House 212, Lane 19, Kalabagan, Tazumuddin, Bhola, Barishal 5100, Bangladesh
Three independent draws from three flat lists would produce "Teknaf, Rangpur, Sylhet", which is nonsense to anyone who lives there — and is exactly the kind of detail that makes a demo lose credibility. The dataset is stored as a nested hierarchy for this reason, and tests assert all 8 divisions, all 64 districts, and that every upazila maps to a real district.
Mobile numbers use real operator prefixes (013–019) so they pass the format validation most Bangladeshi systems apply. National IDs are structurally shaped like a Smart NID but carry no checksum and encode nothing — treat them as opaque test identifiers.
Adding countries without breaking changes
CountryCode names more countries than are implemented, on purpose:
Fake.Country.IsSupported(CountryCode.Bangladesh); // true
Fake.Country.IsSupported(CountryCode.Japan); // false — on the roadmap
if (Fake.Country.TryGet(CountryCode.Japan, out ICountryProvider? provider))
{
Console.WriteLine(provider.Address()); // No null warning here.
}
That split lets you write code today that keeps compiling when a provider lands, and makes a feature
check a method call rather than a version comparison. ICountryProvider holds only what is
genuinely universal; anything country-specific lives on the concrete provider, which is what makes
each new country purely additive.
// Write once, work for any country the library supports.
static void Seed(ICountryProvider country, int rows)
{
foreach (AddressModel address in country.Addresses(rows))
{
Console.WriteLine($"{country.IsoCode}: {address}");
}
}
Seed(Fake.Country.Bangladesh, 100);
Object generation
Generated objects are internally consistent — the record reads like one person, not eight unrelated draws:
PersonModel person = Fake.Person.Generate();
person.FullName; // "Ada Lovelace"
person.Gender; // Female — and "Ada" came from the matching name list
person.UserName; // "ada.lovelace"
person.Email; // "ada.lovelace@fabrikam.com" — the user name, on a generated domain
person.Address; // AddressModel
person.Age; // computed from DateOfBirth
person.AgeOn(new DateOnly(2030, 1, 1));
Every model is a sealed record, so value equality makes a determinism assertion a single line:
Fake.UseSeed(7);
IReadOnlyList<PersonModel> first = Fake.Person.Generate(50);
Fake.UseSeed(7);
IReadOnlyList<PersonModel> second = Fake.Person.Generate(50);
Assert.Equal(first, second); // compares every field of all fifty
ToString() is overridden on every model to render something you would actually want in a log or a
CSV, rather than the record default:
Fake.Person.Generate().ToString(); // "Ada Lovelace <ada.lovelace@fabrikam.com>"
Fake.Address.Generate().ToString(); // "482 Maple Avenue, Fairhaven, Northern Province 48203, Portugal"
Fake.Commerce.Generate().ToString(); // "Ergonomic Steel Chair — £249.99"
Age and leap days
Age is computed from DateOfBirth each time it is read, so it moves with the calendar — pin
DateOfBirth in tests, or use AgeOn(date).
A 29 February birth has its anniversary on 1 March in a common year. DateOnly.AddYears would
clamp it to 28 February, which would make someone born on 29 February 2000 turn one a day before the
month they were born in had ended:
// born 2000-02-29
person.AgeOn(new DateOnly(2001, 2, 28)); // 0
person.AgeOn(new DateOnly(2001, 3, 1)); // 1
person.AgeOn(new DateOnly(2004, 2, 29)); // 4
person.AgeOn(new DateOnly(2100, 2, 28)); // 99 — 2100 is not a leap year
Validation
Every generator validates its arguments:
Fake.Person.FirstNames(0); // ArgumentOutOfRangeException — count must be positive
Fake.Person.FirstNames(-1); // ArgumentOutOfRangeException
Fake.Person.DateOfBirth(50, 20); // ArgumentOutOfRangeException — inverted range
Fake.Commerce.Price(100m, 10m); // ArgumentOutOfRangeException — inverted range
Fake.Internet.Email(null!, "Smith"); // ArgumentNullException
Fake.Country.Get(CountryCode.Japan); // NotSupportedException, naming what IS supported
Zero is rejected rather than returning an empty list: "any positive count" is the documented contract, and a silent empty result is a harder bug to find than an exception.
Collections draw with replacement, so count may exceed the dataset size and duplicates are
possible. That is what makes Fake.Person.FirstNames(1000) meaningful against a few hundred names.
Performance
Run the benchmarks yourself:
cd benchmarks/SaddamHossain.Toolkit.FakeData.Benchmarks
dotnet run -c Release -f net10.0
Five suites are included: single generation, bulk generation (100 and 1000), seeded generation,
dataset loading, and country providers. All carry [MemoryDiagnoser], because the claims worth
checking are about allocation:
- A draw from a dataset allocates nothing — the string already exists in the cached array.
- Bulk generators allocate their array once at exactly the requested size, so cost scales linearly.
- Seeded and unseeded generation run the same code; determinism costs nothing measurable.
- Dataset parsing is a once per process cost, paid lazily, and only for datasets you touch.
Requirements
| Runtime | .NET 8.0, .NET 9.0 or .NET 10.0 |
| SDK (to build) | .NET 10.0 SDK or later |
| Language | C# 12 or later |
| OS | Any platform supported by .NET — no OS-specific code |
Roadmap
Public API changes follow Semantic Versioning — no breaking change without a major version bump.
| Candidate | Notes |
|---|---|
| More country providers | CountryCode already names United States, Canada, United Kingdom, India, Japan and Australia. Each needs real names, identifiers, address hierarchy and phone formats — a provider ships when its data is genuinely right, not merely plausible. |
Fake.Lorem |
Sentences and paragraphs. Straightforward, but it needs a corpus decision first. |
Fake.Finance |
IBAN, credit-card numbers with valid Luhn checksums, account numbers. |
Fake.Date |
Ranges, business days, recent/soon helpers. |
| Custom datasets | Supplying your own JSON in place of the embedded set. Wanted, but the API has to stay AOT-safe. |
Requests and use cases are welcome on the issue tracker — a generator that solves a real problem you have is a much better argument than one that rounds out a table.
Versioning
This package follows Semantic Versioning 2.0.0.
| Change | Version bump | Example |
|---|---|---|
| Breaking change to the public API | Major — 2.0.0 |
A method removed, renamed, or its signature or documented behaviour changed |
| New API, fully backward compatible | Minor — 1.1.0 |
A new generator, a new overload, a new country provider |
| Bug fix with no API change | Patch — 1.0.1 |
A correctness fix, a performance improvement |
Three guarantees come with that:
AssemblyVersionmoves only on a major release. Code compiled against1.0.0keeps loading1.4.2with no binding redirect. A unit test enforces this, so it cannot drift by accident.- The public API surface is pinned by a test. Any addition, removal or signature change fails the build with a readable diff, which makes every contract change a deliberate, reviewed decision.
- Seeded output is part of the contract. Pinned-value tests assert exact strings for fixed seeds, so a change to the generator, the draw order, or any dataset fails the build rather than silently invalidating every fixture you have recorded. Such a change is treated as breaking.
Pre-releases use the standard suffix form — 1.1.0-preview.1 — and are never promoted to stable
without a version bump.
Contributing
Contributions are welcome, and adding a country provider is the most valuable one available — the architecture exists for it. CONTRIBUTING.md covers building, the coding and testing standards, what counts as a breaking change, and a step-by-step guide to adding a country.
The short version:
dotnet build -c Release # must produce zero warnings — warnings are errors here
dotnet test -c Release # 378 tests × 3 target frameworks
dotnet format --verify-no-changes
Open an issue before writing code: design discussion happens before implementation, not during review. Everyone taking part is expected to follow the Code of Conduct.
Security
Report vulnerabilities privately through GitHub Security Advisories, never as a public issue. SECURITY.md sets out what is in scope, the response times to expect, and the hardening a reviewer can verify before adopting the package.
One point bears repeating here: nothing this library produces may be used as a secret. The
generator is chosen for reproducibility, which is the exact opposite of unpredictability. For
passwords, tokens and keys, use System.Security.Cryptography.RandomNumberGenerator.
Links
| 📦 NuGet | nuget.org/packages/SaddamHossain.Toolkit.FakeData |
| 💻 Source | github.com/saddamhossain/SaddamHossain.Toolkit.FakeData |
| 🐛 Issues | Report a bug or request a feature |
| 💬 Discussions | Ask a question |
| 📋 Changelog | CHANGELOG.md |
| 🤝 Contributing | CONTRIBUTING.md |
| 🔒 Security | SECURITY.md |
| 🌐 Website | saddamhossain.net |
License
Licensed under the MIT License — free for commercial and personal use, with no attribution required beyond retaining the notice. A copy of the licence ships inside the NuGet package itself.
Copyright © 2026 Md. Saddam Hossain
Built by Md. Saddam Hossain. If this package saves you time, a ⭐ on GitHub is appreciated.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 60 | 8/7/2026 |