Generated data
GenAuto.Of<T>() builds a value of any type from its shape (constructor, properties, collections, enums, nullability) and mixes
plausible data with edge cases by default. You do not write the adversarial generator: it is already in.
GenAuto.Of<Order>()
GenAuto.Of<Order>(AutoMode.Plausible)
GenAuto.Of<Order>(AutoMode.Adversarial)
GenAuto.Of<Order>(AutoMode.Mixed, o => o.Use(Gen.Const(OrderStatus.Pending)).AddStringEdges("N/A"))
- Mixed (default): mostly plausible, about one leaf in four an edge value
- words, small positive numbers, dates near 2026: what a demo would show
- edge values only, including the hostile group
What the edge values are (each list is ordered simplest first, so a failure shrinks to the simplest hostile value that breaks the rule):
| Type | Edge values |
|---|---|
string |
"", " ", tabs, newlines, \r\n, leading/trailing/internal spaces, NBSP, ideographic and thin spaces, U+2028/2029, zero-width space/joiner, BOM, soft hyphen, bidi overrides (trojan source), C0/C1 control characters, DEL, private-use, noncharacters; emoji, ZWJ sequences, skin tones, flags, combining marks and "Zalgo", NFC vs NFD ("é" vs "e\u0301"), CJK, Arabic, Hebrew; Turkish İ/ı, ß, ligatures; homoglyphs and fullwidth forms; "null", "NaN", "0x10", "1e5", "00123", ISO dates and GUIDs as text; SQL, HTML, format-string, template, JNDI, path-traversal, CSV-formula and URL-encoding payloads; email- and URL-shaped fragments ("a@", "a@@b.c", "http://"); lengths 50/100/255/256/1000/4000/4001/10,000; null for nullable. Adversarial only: NUL and lone surrogates |
| integers (every width) | 0, 1, -1, min, max, min+1, max-1, powers of two and one either side (127/128, 255/256, 32767/32768, 65535/65536, 231, 253+1), 9/10/11, 99/100/101, 999/1000/1001 |
double, float |
NaN (several payloads), +/-Infinity, -0.0, epsilon, subnormals, Max/Min, 2^53 and 2^53+1, 0.1, 0.2, 0.1+0.2, 1e-7, 1e21 |
decimal |
0, +/-1, +/-0.01, 0.005/0.015 (rounding), Max/Min, 1e-28, the same number at different scales (1, 1.0, 1.00, 1.000000000), -0.0 |
| dates and times | default, MinValue, MaxValue, one tick either side, Unix epoch, Y2038, leap days, 1900 (not a leap year), DST gaps and overlaps, offsets -12:00, +14:00, +05:45; TimeSpan zero, negative, 24h, 24h + 1 tick, Max/Min |
Guid, Uri |
Guid.Empty, all-F, v1/v7-shaped; relative, scheme-only, userinfo, IDN, port 0 and 65535, 2,000+ characters |
| enums | every defined value and undefined ones ((E)999, (E)-1, (E)int.MaxValue) |
| collections, dictionaries | empty, one item, duplicates, the same instance twice, keys differing only by case or whitespace |
| relations between fields | Start == End, Start > End, End == Start + 1 unit for pairs like Start/End, From/To, Min/Max, Open/Close; duplicate identities across the items of a list (Id, Sku, Email: same, different case, padded) |
Plausible strings come from a corpus built once from Bogus (words, names, emails, urls, cities, ibans, EAN13) and picked by index,
matched to the member name (Email gets emails, City cities). That keeps data realistic without Bogus's global lock on the hot path
(see below), and a smaller index is a shorter string, so it shrinks.
A constructor that throws on a hostile value is treated as correct behaviour: the generator redraws (20 tries) instead of failing. A
constraint that throws on hostile data is reported as a violation ("TotalMatchesLines threw OverflowException"), not an abort:
a rule that crashes instead of reporting is a finding. Values whose ToString throws are printed safely in failure messages.
| Where | What is generated | Override with |
|---|---|---|
Spec() with no start |
Plausible, valid entities from fixed seeds, covering every enum/bool value at least once, up to twelve | Spec(start), .From(a, b), .Data(mode) |
| An action taking an argument | Two plausible values plus the edge values that matter most for the type ("" and " ", 0 and -1, NaN, the earliest and latest date); all values of an enum or bool |
.Domain<T>(...), .DomainFrom<S, T>(...), .Data(mode) |
ForAll, Preserves |
Mixed entities, kept only if valid |
gen: / start: / data: |
Probe, RejectsInvalid |
Half Mixed, half Adversarial, unfiltered, so violations occur |
gen: / raw: |
Boundaries() |
Every edge value, one property at a time, on a valid baseline | baseline: |
OrderConstraints.All.Boundaries(writeLine: Console.WriteLine);
takes a valid value, swaps in each edge value for one property at a time, and reports which rules reject it and, more usefully, which hostile values nothing rejects. It reads like the documentation the constraints never wrote:
Boundaries from BankAccount { Iban = NL00TEST, Balance = 31.2, OverdraftLimit = 40.5, Status = Active }:
Iban: 0 of 173 edge values rejected, 62 hostile values accepted
GAP, nothing rejects: "", " ", "\u0009", "\u000A", "\u000D\u000A", "\u000D", "\u000C", "\u000B", and 54 more
Balance: 1 of 25 edge values rejected, 6 hostile values accepted
rejected by BalanceAboveLimit: -79228162514264337593543950335
GAP, nothing rejects: -1, -0.01, -0.005, -0.0000000000000000000000000001, 79228162514264337593543950335
(OverdraftLimit: the same shape as Balance)
Use report.Gaps to assert, report.RulesFor("Balance") to see which rules guard a property.
Know the limits:
- A random string argument is almost always a no-op.
RemoveLine(sku)with a corpus word never matches a line that exists. Use.DomainFrom<OrderLine, string>(l => l.Sku)so the argument comes from the state. - Hostile data finds hostile bugs. Zero-config now tries
Reserve(-1), an undefined enum,decimal.MaxValue. Expect findings in code that never saw them (an overflow in a computedTotal, an unguardedDeposit(-5)). Usedata: AutoMode.Plausible/.Data(AutoMode.Plausible)for a domain you have not hardened yet, and treat the rest as a to-do list. - Tight constraints still defeat zero-config. A realistic email, a balanced journal entry, plausible vitals: the default reaches
some by luck (
"[email protected]") but not reliably. The error names the constraints that rejected everything; give the test a start (Spec(start)),Use<T>(gen), or a Bogus rule (f.Internet.Email()). - Abstract types and interfaces are built as one of their concrete subtypes: the
[JsonDerivedType]list when there is one, otherwise every concrete, non-generic subtype in the type's own assembly (nested records included). Framework types are never searched, and an interface nothing implements still asks forAutoOptions.Use<T>(gen). - Internal construction (a domain that keeps production code from building or editing its records): a type with no public constructor but an
internalone is built through it, and a property with aninternal(orinternal init) setter is filled like a public one. The generator reaches them by reflection, which does not needInternalsVisibleTo; the test code that then writeswith { ... }does. Aprivateorprotectedsetter is still left alone. - Not built by AutoGen: classes with neither a public or internal constructor nor a static
From/Create/Of/New/Parsefactory. Everything else is: immutable, frozen and read-only collections,System.Tuple,KeyValuePair, value tuples,IPAddress,Version,Int128,UInt128,BigInteger,HalfandRune`. - Value objects and typed ids are built the way their author intended:
[JsonConstructor]picks the constructor, and a type with a private constructor and a static factory (DocsetPath.From(string),OrderId.Create(Guid)) goes through it, redrawing when its guard refuses a value. A strongly-typed id that wraps aGuid,string,intorlongtakes part in keys and foreign keys like the primitive does. - Unions and
[Union]types (C#union Shape(Circle, Square), or the polyfill of it) get every case, not the one a "most parameters" rule would pick. - DataAnnotations are respected:
[Range],[StringLength],[MaxLength],[MinLength],[Required]and[RegularExpression]hold inPlausibledata;MixedandAdversarialdraw each rule's boundary (the smallest and largest valid value, and the one just outside) as an edge. - At the nesting limit a non-nullable object is its smallest valid value (collections empty, nested objects built from their own smallest parts), not
null; only a type that contains itself non-nullably ends innull. Elements of aList<string?>are sometimesnull; elements of aList<string>never are. - Foreign keys are generated, not only valid ones:
Among the items of one collection: an
Id(or<Type>Id) and other*Idmembers of the same type (Person.ManagerId) give a valid forest inPlausible; inMixed/Adversariala share of collections have a reference to nothing, to itself, or round a cycle of two or three.Lists of keys (
DirectReportIds,IncludedIds): a valid graph, then a dangling include, a self-include, or two items including each other.Between two collections of one object:
Order.CustomerIdinto a siblingCustomerslist (a member named<ElementType>Id): always valid inPlausible, sometimes dangling otherwise.Declared relations, for the references the names do not give away (a list of ids on one object into a collection on another, a URI built from a repository name, a dictionary keyed by its entity's own name, a path that must be unique across every level of a tree). They are declared with expressions, so the member names are checked by the compiler:
GenAuto.Of<Site>(AutoMode.Mixed, o => o .References<Docset, Product, string>(d => d.Products, p => p.Id) .Reference<Mount, Repo, string, Uri>(m => m.Source, r => r.Name, name => new Uri($"{name}://x")) .KeyedBy<Assembly, string, Repo>(a => a.Repositories, r => r.Name) .Identity<TocNode, string>(n => n.Path));- list of ids -> products anywhere in the value
- a value built from the key
- dictionary key = entity key
- unique across every level of a tree
Plausible data always refers to something that exists, whichever side is generated first (a reference that comes first promises a key, and the target takes it);
MixedandAdversarialsometimes refer to nothing, let a dictionary key drift, or repeat a path at two levels. Types in a declared relation get four more levels before the nesting limit cuts them to a constant.Relations declared once, as rules. A relation can also be declared as the rule itself, so the rule and the generator cannot drift apart.
Relation.Reference,Relation.ReferenceListandRelation.Uniquebuild the rule, ando.From(rules)(applied for you byAutoTest.For(rules)) reads what each one relates and declares it for the generator:public static readonly Constraint<DocsetInCodex> CodexGroupKnown = Relation .Reference((DocsetInCodex p) => new[] { p.Docset }, d => d.CodexGroup, p => p.Codex.Groups, g => g.Id) .Rule(message: "codex.group names a group the codex site does not define."); DomainGen.Of<DocsetInCodex>(o => o.From(CodexConstraints.All));- no .Reference<...>() to keep in step
The rule runs the delegates you pass; nothing is compiled, so this is safe in a native AOT build. The member names the generator needs are read once from the text of the
fromandkeylambdas, so they must be plain member accesses (d => d.CodexGroup). Anything else (d => d.Name.ToLower(), a conditional) cannot be read andFromthrows saying which, rather than quietly drawing data that ignores the relation. A rule whose projections are not members (a scheme read from a URI, the keys of a dictionary) stays aConstraints<T>.Referencesrule with a hand-declared relation. Where the target type also appears outsidetargets, a generated reference can point at one of those, so scope the relation to a type that only appears where the rule looks.Derived invariants: a member computed from others (
PrivateRepositoriesis the private subset ofAvailableRepositories) cannot be guessed.o.Normalize<AssemblyConfiguration>(Derive)applies a function you give it, typically the loader's own derivation: always to plausible data, to about half of mixed data, never to adversarial data, so the invariant holds where it should and is still broken where a rule needs testing.- Size: collections get
MaxCollectionscaled across the distribution (it now has an effect above 4) and narrow by one at each level of nesting, so a tree stays a few dozen nodes however deep it goes.o.Large()is the preset for realistic trees (MaxDepth6, up to eight per collection); the defaults keep property tests quick.MaxDepthcounts a collection and then its element as two levels, so a tree of N levels of nodes needs aMaxDepthof about 2N: a rule about a chain of six (Probereports it as never violated) needsMaxDepth = 16, not 6. Every count from none toMaxCollectioncan occur, so a failure at exactly four items is as reachable atMaxCollection12 as at 4, and it shrinks to four. Sizes put structure first (items before magnitudes), which is what shrinks a failure to its fewest items. A collection that plans at leastEarlyExitFrom(32) items stops drawing while CsCheck is shrinking once it can no longer beat the smallest failure so far: automatic for big values, a shrink of a 400-item value takes about a millisecond instead of 25, and smaller values are generated exactly as without it (0applies it everywhere,int.MaxValuenever).
- Size: collections get
Probesays how big the values were.ProbeReport.Reach(LargestCollection,Depth) and a line under the table: "The generated values went up to N items in a collection and D levels deep", with a reminder to raiseMaxCollectionorMaxDepthwhen a rule is never violated. A rule about more than that cannot fail on values that never had it, which is a limit of the generator and not a tautology.Not generated: references to something outside the generated value (a key into a database, a repository elsewhere). Duplicate ids and
Start/Endpairs are generated too.
An AutoBogus-backed generator (plausible data only, drawn under Bogus's global lock) lives in the separate, optional
Nullean.Entities.CsCheck.AutoBogus package (GenAutoBogus.Of<T>(configure)), for teams that already
configured AutoFaker. The measured case for the split is in docs/autobogus-decision.md.
Your own Bogus rules still work and still compose:
Gen<OrderLine> lines = GenFakerExtensions.Faker<OrderLine>(faker => faker
.CustomInstantiator(f => new OrderLine(f.Commerce.Ean13(), f.Random.Int(1, 100), f.Finance.Amount(0.01m, 500m))));
- One
Faker<T>/AutoFakerper thread, created on first use and reused. Bogus takes a process-wide lock on every draw (Randomizer.Locker, aninternal staticthat cannot be turned off), so a Bogus-backed generator stops scaling at about two threads: measured 71kOrderdraws/second on one thread and 97k on eight, against 617k and 1.6M forGenAuto.Of<Order>(). This is whyGenAuto.Of<T>does not draw from Bogus at runtime. configuremust be pure: it runs once per thread, not once per sample.- Only draws that represent real domain data count toward
Size; a flag, or a value that is always the same, is not recorded. - Routing
Randomthrough the PCG is not the whole story.Faker.IndexFaker/IndexVariableare separate counters Bogus increments on everyGenerate(), independent ofRandom. Since the same faker is reused for every draw on a thread, a rule readingf.IndexFakerwould drift across unrelated draws and silently break reproducibility. Both are reset to zero before every draw, at the cost ofIndexFakerno longer being a useful sequence (it could never survive shrink-and-replay anyway).f.IndexGlobal,f.UniqueIndexandFaker.GlobalUniqueIndexare process-wide statics shared with every otherFaker, so resetting them would corrupt unrelated code: avoid them inconfigureif you need reproducibility and shrinking.
| You see | It means | Do |
|---|---|---|
Set seed: "..." or -e CsCheck_Seed=... |
A real counterexample, already shrunk | Pass seed: (or set CsCheck_Seed) to replay it exactly. Iterations come from iter: or CsCheck_Iter. |
Requirement: X - does not hold in the state reached with a trace |
An action sequence reaches an invalid state (shortest one, for Exhaustive) |
Fix the entity. If Apply is meant to stop it, add .Enforced(). |
VacuousPropertyException: ... never exercised: A:b->c |
A guard or edge was never taken: the constraint held but proved nothing there | Add an action or domain value that reaches it, or requireCoverage: false if it is a known gap. |
VacuousPropertyException: No action changed the entity |
Nothing was explored (often every action rejected, or the start already fills the limit) | Check .Domain(...), .Limit(...), and the start. |
The state space did not close |
Too many states, or states that never compare equal | Add .Key(...), .Boundary(...), lower .Limit(...), or use .Sample(). |
AutoBogus produced no valid X ... Most rejecting constraints: A, B |
The generator cannot satisfy those constraints | Give a start (Spec(start)), a gen:, or a Bogus rule. |
AutoBogus could not generate a Y / No values to try for Y |
An action argument type AutoBogus cannot build | Declare .Domain<Y>(...). |