ModelBuilder 8.7.0-beta.7
See the version list below for details.
dotnet add package ModelBuilder --version 8.7.0-beta.7
NuGet\Install-Package ModelBuilder -Version 8.7.0-beta.7
<PackageReference Include="ModelBuilder" Version="8.7.0-beta.7" />
<PackageVersion Include="ModelBuilder" Version="8.7.0-beta.7" />
<PackageReference Include="ModelBuilder" />
paket add ModelBuilder --version 8.7.0-beta.7
#r "nuget: ModelBuilder, 8.7.0-beta.7"
#:package ModelBuilder@8.7.0-beta.7
#addin nuget:?package=ModelBuilder&version=8.7.0-beta.7&prerelease
#tool nuget:?package=ModelBuilder&version=8.7.0-beta.7&prerelease
ModelBuilder
A library for easy generation of model classes
ModelBuilder fills your classes, structs and primitives with realistic pseudo-random data so your tests can focus on behaviour instead of hand-built fixtures. A single call builds a whole object graph — nested types, constructor arguments, collections, enums and nullable values included.
var person = Model.Create<Person>();
The current version is a source-generated, reflection-free rewrite. Every type you build is discovered at compile time and gets a dedicated builder, so ModelBuilder:
- runs under Native AOT and full trimming with no IL warnings,
- reports an unbuildable type at build time (a diagnostic) instead of throwing at runtime,
- is dramatically faster with far fewer allocations than the previous reflection engine
(typically 2–14× faster and 6–50× fewer allocations — see
ModelBuilder.BenchmarkTests/BASELINE.md).
This is a new major version with a deliberately smaller surface than the v8 reflection engine.
If you are coming from v8, read the migration guide. Consumers that need runtime
reflection, dynamic/runtime-only types, or the old extensibility pipeline should stay on the v8
line.
- Creating a model
- Runnable examples
- How types are discovered
- Populating a model
- Changing the model after creation
- Logging the build process
- Handling build failures
- Configuration modules
- Tuning the build
- Built-in data
- Generating random values
- Target frameworks
- Migrating from v8
- Supporters
Creating a model
The static Model class is the entry point. It builds classes, structs and primitive types, and
walks the whole object graph reachable from the type you ask for.
var person = Model.Create<Person>();
Value-source types such as enums and primitives can be built as roots directly:
var status = Model.Create<OrderStatus>();
var id = Model.Create<Guid>();
You can also build by runtime Type when the type is only known at runtime:
object person = Model.Create(typeof(Person));
This path still depends on compile-time discovery. A builder only exists for a type the generator
saw at compile time, so the runtime Type you pass must already be buildable through one of the
discovery triggers — a Model.Create<T>(), Construct<T>(),
Populate<T>() or Model.Create(typeof(T)) call somewhere in the build, a Mapping<,>, or a
[GenerateModelBuilder] annotation. Passing a Type that the generator never saw throws a
ModelBuildException at runtime, because there is no generated builder to dispatch to:
// Throws at runtime: the concrete type came from reflection/config/a plugin, so the generator
// never saw it and produced no builder.
Type runtimeType = Type.GetType("MyApp.PluginPayload")!;
object instance = Model.Create(runtimeType);
To make such a type buildable, give the generator a compile-time anchor — most simply by annotating the type (or naming it from your test assembly):
[assembly: GenerateModelBuilder(typeof(MyApp.PluginPayload))]
Constructor arguments and parameter matching
Model.Create<T>() chooses a constructor and fills its arguments with generated values
automatically — you do not pass constructor arguments to Create. Constructor selection prefers a
parameterless constructor, then the constructor with the fewest parameters.
When a type is built through a constructor, ModelBuilder matches constructor parameters to properties of the same name (case-insensitive) and type at compile time, and the matched members are simply left out of the generated population code. A value the constructor assigned is therefore never overwritten — there is no runtime value comparison.
By default a value is generated for every parameter of the chosen constructor, including optional
ones — an optional parameter's declared default is not special-cased, so that member is still varied
like any other. Set BuildOptions.UseConstructorDefaults (see Tuning the build)
to instead use an optional parameter's declared default value. For per-argument control, use
typed construction, whose From overloads expose each optional parameter's
default.
Constructor arguments also become sibling values for the rest of the build, keyed by the
parameter name, so members derived from siblings stay consistent with them. This holds even when a
parameter is never exposed as a property — a type with Contact(string firstName, string lastName) and only an Email property still gets an Email built from those constructor arguments,
because firstName/lastName are recorded as siblings (matched case-insensitively against the
FirstName/LastName the email source looks for).
To supply specific constructor arguments yourself, use typed construction.
Typed construction
Model.Create<T>() always generates the constructor arguments for you. When you want to pass
specific constructor arguments, use Model.Construct<T>().From(...):
var person = Model.Construct<Person>().From("Fred", "Smith");
The generator emits a From overload for every public constructor of T, so the editor offers
exactly that type's constructors — with the real parameter names and the constructor's own
documentation copied across. Because the arguments are strongly typed:
- value-type arguments are not boxed and there is no
object?[]array, - a wrong argument type or count is a compile-time error, not a runtime exception,
- members assigned by the chosen constructor are not re-populated with random values (they keep
the values you passed, and sibling-derived members such as
Emailstay consistent with them).
Constructor parameters that declare a default value are emitted as optional on the generated
From overload, so you can omit them to take the default or pass an argument to override it:
// Connection(string host, int port = 8080, bool secure = true)
var fromDefaults = Model.Construct<Connection>().From("db1"); // port 8080, secure true
var overridden = Model.Construct<Connection>().From("db1", 9090); // port 9090, secure true
Every other member is still populated as usual, and the whole configuration chain composes with it:
var order = Model.Ignoring<Order>(x => x.InternalId)
.Construct<Order>()
.From(customerId);
The From overloads are generated as extension methods in the ModelBuilder namespace. Because you
already have using ModelBuilder; in scope wherever you write Model.Construct<T>(), they are
available with no extra using, and they appear in the editor as soon as Model.Construct<T>() is
written.
Ignoring members
Ignoring a member skips it during population, leaving it at whatever value the constructor or the type's own initializer set. There are three ways to declare an ignore rule, depending on whether you target one member, one member on one type, or any member matching a condition.
Model.Ignoring<T>(expression) is the fluent per-build form. It selects a member with a strongly
typed expression and returns a configuration you continue building from:
var person = Model.Ignoring<Person>(x => x.FirstName).Create<Person>();
The examples here finish with Create<T>(), but every fluent entry point on Model returns the same
configuration, and you can complete it with whichever build you need — Create<T>() for a new
instance, Populate<T>(instance) to fill one you already have, or Construct<T>().From(...) to supply
constructor arguments. The configuration also keeps composing, so you can chain further Ignoring,
Mapping, AddValueSource, UsingModule or WriteLog calls before the terminal build.
The rule applies wherever that member appears in the graph, so it also covers types nested deeper than the root:
var person = Model.Ignoring<Address>(x => x.AddressLine1).Create<Person>();
The remaining two forms are also fluent on Model (and on the configuration a module
receives), so they compose in the same chain as Ignoring<T>.
Ignoring(Type declaringType, string memberName) targets a single member on a specific type by name —
the non-generic equivalent of Ignoring<T>, useful when you only have the Type or want to name the
member as a string:
var request = Model.Ignoring(typeof(Request), nameof(Request.Data)).Create<Request>();
IgnoringAny(Func<MemberSignature, bool> predicate) ignores every member, on any type, for which the
predicate returns true. The predicate receives a MemberSignature exposing the member's
DeclaringType, Name and MemberType, so you can match by naming convention, declaring type or
member type across the whole graph:
// Ignore every member whose name ends in "Internal", regardless of which type declares it.
var account = Model.IgnoringAny(member => member.Name.EndsWith("Internal", StringComparison.Ordinal))
.Create<Account>();
// Ignore every Stream-typed member anywhere in the graph.
var report = Model.IgnoringAny(member => typeof(Stream).IsAssignableFrom(member.MemberType))
.Create<Report>();
Inside a configuration module the same two rules are declared on the
IBuildConfiguration as Ignore(Type, memberName) and IgnoreAny(predicate) (imperative names,
identical behaviour), so they can be packaged and reused.
All three forms compose — a build sees the union of every ignore rule from the fluent chain and from each applied module.
Mapping abstract and interface types
Abstract and interface members need a concrete type to build. Declare a mapping and that concrete type is used wherever the source type appears:
var payload = Model.Mapping<IShipment, GroundShipment>().Create<Parcel>();
There is no automatic assembly scan that guesses a concrete type — the mapping is explicit, which is what keeps the build deterministic and reflection-free.
Custom value sources
When the built-in data does not produce what your model needs, register a custom value source. A
value source is an IValueSource<T> that produces a T for a build target (a constructor parameter
or settable member) or a root; the simplest way to supply one is DelegateValueSource<T> wrapping a
lambda. A registered source takes precedence over the built-in sources.
Register a source for every value of a type to control all build targets of that type. The
following code generates every OrderStatus value in the graph as OrderStatus.Pending:
var order = Model.AddValueSource(new DelegateValueSource<OrderStatus>(c => OrderStatus.Pending))
.Create<Order>();
Register a source scoped to specific member names to narrow it to the constructor parameters or
settable members whose name matches one of the supplied member names (matched as a whole
PascalCase/camelCase word, the same way the built-in entity data is matched). The source still only
applies to build targets of its T, so it matches on both the member name and the type. A
member-name-scoped source takes precedence over a type-only source for a matching build target. The
following code uses this expression for every string parameter or property named AccountNumber:
var account = Model.AddValueSource(
new DelegateValueSource<string>(c => "acct-" + c.Random.NextInt32(1000, 9999)),
"AccountNumber")
.Create<Account>();
The factory receives the IBuildContext, so a custom source can use the same seams the built-in
sources use — the random source, GetSibling<T> to stay consistent with other members already set on
the instance, and GetOrAddScopedValue<T> to share one cached data item across several related
members (the mechanism the built-in location and date-of-birth sources use). In the following module
AddressRow is your own type and data set (not part of ModelBuilder); the module draws a
Street/AddressLine1 and City from a single cached AddressRow so they stay consistent on each
instance:
public sealed class AddressModule : IConfigurationModule
{
private const string AddressKey = "MyApp.Address";
public void Configure(IBuildConfiguration configuration)
{
// Both members draw from one cached address row, so they stay internally consistent.
configuration.AddValueSource(
new DelegateValueSource<string>(c => RowFor(c).Street),
"Street", "AddressLine1");
configuration.AddValueSource(
new DelegateValueSource<string>(c => RowFor(c).City),
"City");
}
// AddressRow is your own type holding a related set of address fields. ModelBuilder does not
// supply it; this example assumes an AddressRow.Random() factory you provide.
private static AddressRow RowFor(IBuildContext context)
{
return context.GetOrAddScopedValue(AddressKey, _ => AddressRow.Random());
}
}
Custom value sources can be registered through a configuration module's IBuildConfiguration exactly
as above, so they can be packaged and shared like mappings and ignore rules.
Writing a reusable value source
DelegateValueSource<T> is the quickest way to supply a source, and it has two factory shapes. The
first ignores where the value is going; the second also receives a BuildTarget describing the type
and (optional) member name being built, so one source can vary its output by member:
// Same value everywhere.
var pending = new DelegateValueSource<OrderStatus>(context => OrderStatus.Pending);
// Vary the value by the member being built.
var labelled = new DelegateValueSource<string>((context, target) =>
target.MemberName is null ? "value" : target.MemberName + "-" + context.Random.NextInt32(1, 999));
A lambda is ideal for a one-off source defined right where it is registered. When the source is worth
keeping as a reusable, testable unit, however, implement IValueSource<T> as a dedicated class.
Reach for a class when the source holds its own state or dependencies, when several tests or
configuration modules need to share the exact same logic, or simply when a
descriptive class name documents intent better than an inline lambda. A class also gives the source a
single place to unit-test.
The source's Create method receives the same IBuildContext and BuildTarget that
DelegateValueSource<T> passes to its factory:
public sealed class SkuValueSource : IValueSource<string>
{
public string Create(IBuildContext context, in BuildTarget target)
{
return "SKU-" + context.Random.NextInt32(10000, 99999);
}
}
// "Sku" is the member name to match (as in the AccountNumber example above); it is unrelated to the
// SkuValueSource class name. Drop it to apply the source to every string build target instead.
var product = Model.AddValueSource(new SkuValueSource(), "Sku").Create<Product>();
NullableValueSource<T> wraps a value-type source so it produces T?. It returns null for a share
of values set by BuildOptions.NullPercentage (a 5% default; see Tuning the build)
and otherwise delegates to the underlying source — the mechanism that makes generated nullable members
occasionally null.
The build context seams
The IBuildContext passed to every value source exposes the same seams the engine and built-in
sources use, so a custom source can participate fully in a build:
| Member | Purpose |
|---|---|
Random |
The build's IRandomSource — seedable, thread-safe, typed (see Generating random values). |
GetSibling<T>(name) / GetSibling<T>(names...) |
Read a value already set on the instance being built (the first match across the candidate names), so derived members stay consistent. Returns default when there is no match. |
GetOrAddScopedValue<T>(key, factory) |
Get-or-create a value cached for the lifetime of the current instance, so several sources can share one data item (such as an address row). A different instance elsewhere in the graph gets its own scope. |
NextCount() |
A random collection length within the configured min/max bounds (a 1–10 default; tune with SetOptions). |
Build<T>(declaringType, name) |
Recursively build a nested member value through the normal source/builder resolution, with circular-reference and depth guards applied. |
ShouldPopulate(declaringType, name, memberType) |
Whether a member passes the configured ignore rules. |
RecordSibling(name, value) / EnterSiblingScope() |
Record a value into, or open, the sibling scope that GetSibling<T> reads. |
Configuration / Log |
The active IBuildConfiguration and IBuildLog for the build. |
All numeric range methods on Random are inclusive of both bounds, and GetSibling<T> name matching
is the same whole-word, case-insensitive scheme used by the built-in entity data.
Runnable examples
A complete, runnable ModelBuilder.Examples
console project demonstrates every scenario above end to end. Its source is bundled in this NuGet
package (under ModelBuilder.Examples/, mirroring the repository), so the links below resolve both on
GitHub and when browsing the package contents. The project references the generator as an analyzer —
the wiring a consuming project needs for the source generator to run and emit the From overloads:
<ProjectReference Include="..\ModelBuilder.Generator\ModelBuilder.Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
Run the whole set with dotnet run --project ModelBuilder.Examples. Each scenario lives in its own
file under ModelBuilder.Examples/Examples, building the sample
model types in ModelBuilder.Examples/Models:
How types are discovered
Every buildable type is found at compile time by a source generator. A type gets a generated builder when it is reachable from one of these triggers:
Model.Create<T>(),Model.Populate<T>(),Model.Construct<T>()orModel.Create(typeof(T))—Tand everything reachable from it (constructor parameters and public settable properties) become buildable. This covers almost everything with no annotations.Model.Mapping<TSource, TTarget>()— makesTTargetbuildable for abstract/interface members.[GenerateModelBuilder]— names a root that is only ever built polymorphically or via a runtimeType.
Only compile-time-discoverable types are buildable. private/protected types, constructors and
members cannot be built; internal types are buildable only when exposed to the building assembly
with [InternalsVisibleTo].
Build-time diagnostics
If a type you try to build is not reachable from one of the triggers above, you get a compiler diagnostic, not a runtime exception:
| Diagnostic | Meaning |
|---|---|
MB1001 |
The requested root type cannot have a builder generated (for example it is abstract, an interface, generic or has no accessible constructor). |
MB1002 |
The root type is inaccessible (for example private) to the generated code. |
MB1005 |
A Model.Create(typeof(X)) root could not be resolved to a buildable type. |
GenerateModelBuilder
Use [GenerateModelBuilder] to opt in a root that is never named directly in a Model.Create<T>()
call — for example a type you only ever build through a base type or a runtime Type.
[GenerateModelBuilder]
internal sealed class AuditRecord
{
public Guid Id { get; set; }
public DateTimeOffset At { get; set; }
}
To opt a production type into generation without shipping the attribute in the production binary, use the assembly-level form from your test assembly:
[assembly: GenerateModelBuilder(typeof(MyApp.AuditRecord))]
Populating a model
When you already have an instance, ModelBuilder can fill it in place:
var person = new Person
{
FirstName = "Jane"
};
// Fill every other member but keep the FirstName already set.
Model.Ignoring<Person>(x => x.FirstName).Populate(person);
var customer = Model.Populate(new Person());
Changing the model after creation
Set tweaks an instance after it is built, and SetEach does the same across a sequence. Both are
plain extension methods, so they work on any object regardless of how it was created.
var person = Model.Create<Person>()
.Set(x => x.FirstName = "Joe")
.Set(x => x.Email = null);
var other = Model.Create<Person>().Set(x =>
{
x.FirstName = "Joe";
x.Email = null;
});
var organisation = Model.Create<Organisation>();
organisation.Staff.SetEach(x => x.Email = null);
SetEach has overloads for the common collection and dictionary shapes (IEnumerable<T>, IList<T>,
ICollection<T>, IReadOnlyList<T>, List<T>, Collection<T>, ReadOnlyCollection<T>, the
IDictionary/IReadOnlyDictionary/Dictionary/ReadOnlyDictionary families) and returns the same
collection type it received, so it stays chainable. A second overload passes the index alongside
each item:
var staff = Model.Create<Organisation>().Staff;
// The lambda receives the zero-based index and the item.
staff.SetEach((index, person) => person.EmployeeNumber = index + 1);
The SetEach overloads above cover the common collection and dictionary types by name. If your
variable's type is some other collection — a Queue<T>, a HashSet<T>, or a custom collection — none
of those overloads match it, so the compiler cannot pick one. SetEachExplicit is the fallback for
that case: it works on any IEnumerable<TEntry> (or IEnumerable<KeyValuePair<TKey, TValue>> for
dictionaries), runs the action over every item, and returns the collection unchanged. Because the
compiler cannot infer the type arguments from the receiver alone, you supply them explicitly — the
collection type first, then the item type — which is what gives the method its name. Index-aware
overloads are available too, matching SetEach:
var queue = new Queue<Person>(Model.Create<List<Person>>());
// <Queue<Person>, Person> = <the collection type, the item type>.
queue.SetEachExplicit<Queue<Person>, Person>((index, person) => person.Email = null);
Logging the build process
ModelBuilder can render a structured log of how a value was built. WriteLog enables logging and
sends the rendered log to your sink after the build completes. With xUnit you can route it to the
test output:
public class WriteLogTests
{
private readonly ITestOutputHelper _output;
public WriteLogTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void WriteLogRendersBuildLog()
{
var actual = Model.WriteLog(_output.WriteLine).Create<Person>();
actual.Should().NotBeNull();
}
}
The log shows the build tree — each type entered, each member created, and any member skipped by an ignore rule, a circular-reference guard or the depth guard.
Inspecting the structured log
WriteLog(Action<string>) renders the log to text, but the log is also a structured tree you can
inspect programmatically. An IBuildLog exposes Entries, a list of BuildLogEntry nodes that each
carry a Kind (CreateInstance, PopulateInstance, CreateValue or SkipMember), the TargetType,
an optional MemberName and Reason, and nested Children. This is the same tree captured on a
ModelBuildException, so you can assert on exactly what the builder did —
for example that a member was skipped for the expected reason — rather than parsing rendered text:
static bool WasSkipped(IReadOnlyList<BuildLogEntry> entries, string memberName) =>
entries.Any(e =>
(e.Kind == BuildLogEntryKind.SkipMember && e.MemberName == memberName)
|| WasSkipped(e.Children, memberName));
Handling build failures
When a build cannot complete, ModelBuilder throws a ModelBuildException. As well as the message, it
carries structured context so you can diagnose the failure without a debugger:
| Member | Description |
|---|---|
FailureKind |
A FailureKind enum categorising the cause: InaccessibleConstructor, UnmappedAbstractType, NoValueSource, NoBuilderForType, CircularReference, DepthExceeded, ValueSourceThrew or Unspecified. |
TargetType |
The type being built when the failure occurred, or null. |
TargetMember |
The member being built when the failure occurred, or null. |
BuildPath |
The BuildFrame chain from the root type down to the failing member. |
BuildLog |
The structured build log captured up to the failure. |
Branch on FailureKind rather than the message so your handling survives wording changes:
try
{
var parcel = Model.Create<Parcel>();
}
catch (ModelBuildException ex) when (ex.FailureKind == FailureKind.UnmappedAbstractType)
{
// A reachable abstract/interface member has no mapping — add a Model.Mapping<,> for it.
Console.WriteLine($"Map {ex.TargetType} before building (at {ex.TargetMember}).");
}
Configuration modules
An IConfigurationModule packages reusable build configuration so it can be shared across tests. A
module receives an IBuildConfiguration and adds type mappings, ignore rules and custom value
sources; the built-in value sources are always applied, so a module only declares the deltas it needs.
public sealed class TestModule : IConfigurationModule
{
public void Configure(IBuildConfiguration configuration)
{
// Use a concrete type wherever IShipment is needed.
configuration.AddMapping<IShipment, GroundShipment>();
// Never populate Request.Data.
configuration.Ignore(typeof(Request), nameof(Request.Data));
// Ignore any member matching a predicate, across all types.
configuration.IgnoreAny(member => member.Name.EndsWith("Internal", StringComparison.Ordinal));
}
}
Apply a module per build, and chain modules to layer configuration:
var model = Model.UsingModule<TestModule>().Create<Account>();
var layered = Model.UsingModule<UnitTestModule>()
.UsingModule<IntegrationTestModule>()
.Create<Account>();
Default configuration
A build with no configuration — Model.Create<T>() with no module, ignore rule, mapping or option
override — starts from these defaults. A module, or a fluent call on Model, only adds deltas on top:
| Category | Default |
|---|---|
| Type mappings | None. Abstract and interface members are unbuildable until you add a Mapping<,>. |
| Ignore rules | None. Every settable member and constructor parameter is populated. |
| Custom value sources | None registered, but the built-in value sources always apply (you never register them, and a custom source only overrides the built-in for its type/name). |
| Built-in typed sources | bool; every numeric type (byte/sbyte/short/ushort/int/uint/long/ulong/float/double/decimal); char; string; Guid; DateTime; DateTimeOffset; TimeSpan; Uri; Version; byte[]. Enums, nullables and collections are handled by the generator. |
| Built-in named sources | The entity-style member-name sources in Built-in data (names, email, company, location fields, age, date of birth, …). |
| Build options | MinCount 1, MaxCount 10, NullPercentage 5, MaxDepth 50, UseConstructorDefaults off — see Tuning the build. |
Tuning the build
A few build-wide settings live on BuildOptions: the collection-size range (MinCount/MaxCount),
the chance a nullable value is produced as null (NullPercentage, 0–100), and the maximum graph
depth (MaxDepth). Tune them with Model.SetOptions, which returns the same fluent configuration as
the other entry points:
// Always populate nullables, and make collections hold exactly three items.
var order = Model.SetOptions(x =>
{
x.NullPercentage = 0;
x.MinCount = 3;
x.MaxCount = 3;
})
.Create<Order>();
The same call is available on the IBuildConfiguration a configuration module
receives, so a module can set defaults that every build using it inherits:
public sealed class CompactModule : IConfigurationModule
{
public void Configure(IBuildConfiguration configuration)
{
configuration.SetOptions(x => x.MaxCount = 3);
}
}
MinCount/MaxCount are coerced when a collection length is drawn — a negative minimum becomes zero
and a maximum below the minimum is raised to it — so setting MaxCount without MinCount cannot
throw. The defaults are MinCount 1, MaxCount 10, NullPercentage 5 and MaxDepth 50.
UseConstructorDefaults (off by default) controls how the automatic Model.Create<T>() path fills
an optional constructor parameter. Off, a value is generated for it like any other member; on, the
parameter's declared default value is used instead:
// Widget(string label, int size = 7) — size uses its default of 7 instead of a generated value.
var widget = Model.SetOptions(x => x.UseConstructorDefaults = true).Create<Widget>();
This applies only to the constructor Create<T>() selects. The explicit
Construct<T>().From(...) path always gives per-argument control regardless of
this option, because its generated overloads expose each optional parameter's default.
Built-in data
ModelBuilder ships value sources for the common primitive and BCL types (bool, the numeric types,
char, string, Guid, DateTime, DateTimeOffset, TimeSpan, Uri, Version, byte[]),
plus enum, nullable and collection support that the generator wires up automatically.
Entity-style data matched by member name
Members whose name matches a known entity field are filled with embedded reference data rather than
random noise, so the values read naturally. Matching is on a whole PascalCase/camelCase word and is
case-insensitive, so ContactEmailAddress matches Email, while a short field name like PageCount
is not treated as an Age.
| Member name (with aliases) | Generated value |
|---|---|
FirstName, GivenName |
A given name |
LastName, Surname, FamilyName |
A family name |
MiddleName |
A given name |
FullName, Name, DisplayName |
First Last |
UserName, Username, Login |
first.last with a numeric suffix, lower-cased |
Email |
first.last@domain |
Domain |
A domain |
Company, Business |
A company name |
Country |
A country |
State, Province |
A state or province |
City, Suburb, Town |
A city |
PostCode, ZipCode, Postcode, Zip |
A post/zip code |
Phone, Mobile, Cell, Fax |
A phone number |
TimeZone |
A time-zone identifier |
Age |
An integer in the human-plausible range 1–100 |
DateOfBirth, DOB, BirthDate, Birthday |
A UTC date for a 1–100 year old |
A member named like one of these but typed differently from what the source produces (for example an
Age that is a string rather than an int) falls through to the ordinary type-based sources, so
the name match never produces a type mismatch.
Cross-field consistency
Related members on the same instance agree with each other instead of being sampled independently, and the agreement is order-independent — it does not matter which related member the generator builds first:
- Names flow into email, username and full name.
Emailis built from theFirstName,LastNameandDomainalready set on the object (falling back to fresh data when they are absent), andFullName/UserNameare composed from the same name fields. A model that spells the membersGivenName/Surnamestill gets a matching email. - One coherent location per instance.
Country,State,City,PostCodeandPhoneare drawn from a single built-inLocationrow, so an instance never produces an impossible combination such as London, New South Wales, India. A different instance elsewhere in the graph gets its own location. - Age follows date of birth.
DateOfBirthis the definitive value;Ageis the number of completed years between it and a fixed reference date, so a generatedAgeof 18 can never sit next to aDateOfBirththat implies 35.
Reusable reference data sets
The same embedded data the built-in sources draw from is exposed through the static ModelBuilder.Data.TestData
class, so you can use it directly in your own value sources, fixtures or assertions. Each property is a
cached IReadOnlyList<> read once from embedded resources:
TestData member |
Contents |
|---|---|
MaleNames, FemaleNames |
Given names |
LastNames |
Family names |
Companies |
Company names |
Domains |
Email/web domains |
TimeZones |
Time-zone identifiers |
Locations |
Location rows pairing City, State, Country, PostCode, Phone, StreetName and StreetSuffix so a drawn location is internally consistent |
var random = new RandomSource();
var location = TestData.Locations[random.NextInt32(0, TestData.Locations.Count - 1)];
// location.City, location.State, location.Country, location.PostCode all belong together.
Location (in the same ModelBuilder.Data namespace) is a plain mutable type. You can build your own
instances with an object initializer, or parse one from a CSV line with Location.Parse. The CSV
columns are comma-separated in this exact order:
Country,State,City,PostCode,StreetName,StreetSuffix,Phone
var custom = new Location { City = "Hobart", State = "Tasmania", Country = "Australia" };
var parsed = Location.Parse("Australia,Tasmania,Hobart,7000,Murray,Street,03 6200 0000");
These are for your own value sources and fixtures. The built-in TestData lists (including
Locations) are read-only, so there is no public API to add your own entries to the data the built-in
location source draws from — to inject custom location data into a build, register a custom value
source for the relevant members.
Generating random values
ModelBuilder's randomness comes from IRandomSource, implemented by RandomSource. It is the
Random seam on IBuildContext, but you can also use it standalone — for direct random values or to
make a build reproducible by seeding it. It is thread-safe, returns each value type directly without
boxing, and generates wide-integer and decimal ranges with their own arithmetic so there is no
precision loss through double:
// A default source is independently seeded; pass a seed to reproduce a sequence.
var random = new RandomSource(seed: 1234);
int dice = random.NextInt32(1, 6); // every range method is inclusive of both bounds
decimal price = random.NextDecimal(0m, 100m);
bool flag = random.NextBool();
var buffer = new byte[16];
random.NextBytes(buffer);
Typed methods cover the full numeric range — NextByte, NextSByte, NextInt16/NextUInt16,
NextInt32/NextUInt32, NextInt64/NextUInt64, NextSingle, NextDouble, NextDecimal — plus
NextBool and NextBytes. The integer and floating-point overloads default to the type's full range,
and the Seed property reports the seed in use so a failing test can be replayed.
Target frameworks
The library targets netstandard2.0, net8.0, net9.0 and net10.0. The source generator and the
generated code use no reflection, so a consuming application can publish with Native AOT and full
trimming with no IL2xxx/IL3xxx warnings.
Migrating from v8
The previous reflection-based extensibility pipeline (IExecuteStrategy, IConstructorResolver,
ITypeCreator, IValueGenerator, IPostBuildAction, the per-ParameterInfo/PropertyInfo
overloads and so on) has been removed in favour of the source-generated model and a small set of
supported seams (IValueSource<T>, mappings, ignore rules and [GenerateModelBuilder]). The
migration guide maps every changed and removed v8 API to its v9 equivalent.
For an automated upgrade, the package also ships an AI-agent playbook (MIGRATION-AI.md) and a machine-readable map (migration-map.json) so an agent or tool can apply the migration deterministically.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
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 |
|---|---|---|
| 8.7.0-beta.8 | 37 | 6/27/2026 |
| 8.7.0-beta.7 | 41 | 6/27/2026 |
| 8.6.0 | 27,884 | 3/17/2024 |
| 8.5.0 | 278 | 2/27/2024 |
| 8.4.0 | 12,522 | 8/9/2022 |
| 8.4.0-beta0001 | 293 | 8/9/2022 |
| 8.3.0 | 1,623 | 6/22/2022 |
| 8.2.0 | 17,469 | 8/29/2021 |
| 8.1.1-beta0001 | 388 | 8/29/2021 |
| 8.1.0 | 2,693 | 5/7/2021 |
| 8.1.0-beta0001 | 478 | 5/7/2021 |
| 8.0.0 | 877 | 4/17/2021 |
| 8.0.0-beta0087 | 391 | 4/17/2021 |
| 8.0.0-beta0057 | 412 | 4/17/2021 |
| 8.0.0-beta0028 | 409 | 4/17/2021 |
| 7.4.2-beta0001 | 443 | 4/17/2021 |
| 7.4.1 | 1,590 | 3/30/2021 |
| 7.4.1-beta0001 | 413 | 3/30/2021 |
| 7.4.0 | 1,272 | 1/2/2021 |
| 7.3.1-beta0001 | 468 | 1/2/2021 |