holonsoft.AutoPoco 6.1.0

dotnet add package holonsoft.AutoPoco --version 6.1.0
                    
NuGet\Install-Package holonsoft.AutoPoco -Version 6.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="holonsoft.AutoPoco" Version="6.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="holonsoft.AutoPoco" Version="6.1.0" />
                    
Directory.Packages.props
<PackageReference Include="holonsoft.AutoPoco" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add holonsoft.AutoPoco --version 6.1.0
                    
#r "nuget: holonsoft.AutoPoco, 6.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package holonsoft.AutoPoco@6.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=holonsoft.AutoPoco&version=6.1.0
                    
Install as a Cake Addin
#tool nuget:?package=holonsoft.AutoPoco&version=6.1.0
                    
Install as a Cake Tool

holonsoft / AutoPoco

CI NuGet

AutoPoco is a highly configurable framework for the purpose of fluently building readable (test) data. holonsoft ported this famous lib to newest version of dotnet

New in 6.1.0

Trade identifiers

New data sources in holonsoft.AutoPoco.DataSources.Identifiers for the numbers that circulate in real trade data: article numbers, the serial numbers of devices and logistic units, and the identifiers of books, periodicals and securities. Every number gets a correct check digit, so it passes the validation of an importer or a database constraint instead of being rejected as a typo.

Source Produces
GtinSource A GTIN with a valid GS1 check digit, a GtinFormat.Gtin13 unless another format is asked for. Gtin8 is the EAN-8, Gtin12 the UPC-A, Gtin13 the EAN-13 and Gtin14 the ITF-14 of a carton.
Ean13Source The GTIN-13 under its everyday name, the barcode number of a retail article.
SsccSource An SSCC, the eighteen digit number of a single logistic unit such as a pallet or a parcel, with the same GS1 check digit as a GTIN.
IsbnSource An ISBN, an IsbnFormat.Isbn13 unless Isbn10 is asked for. The ISBN-13 starts with a registration group that is actually handed out (978-0 to 978-5, 978-7, 979-8, 979-10, 979-11, 979-12), the ISBN-10 carries a mod 11 check digit that can be an X.
IssnSource An ISSN, the eight character number of a periodical, without the hyphen. The check character uses the same mod 11 arithmetic as an ISBN-10 and can be an X.
IsinSource An ISIN, the twelve character number of a security: a two letter country prefix (drawn from countries that really have a numbering agency, or a given one), nine alphanumeric characters and a Luhn check digit over the expanded letters.
ImeiSource An IMEI, the fifteen digit serial number of a mobile device with a valid Luhn check digit. Give the eight digit Type Allocation Code as a prefix when the numbers have to look like one device model.
AsinSource An ASIN, the ten character article number Amazon assigns.

Each of them has a Nullable... variant that returns null every now and then, like the other reference type sources.

x.Include<Article>()
  .Setup(a => a.Ean).Use<Ean13Source>()
  .Setup(a => a.CartonGtin).Use<GtinSource>(GtinFormat.Gtin14)
  .Setup(a => a.Isbn).Use<IsbnSource>(IsbnFormat.Isbn10)
  .Setup(a => a.Asin).Use<AsinSource>();

A prefix fixes the leading digits of a GTIN, an SSCC or an IMEI, so a whole catalogue can share one GS1 company prefix and every article still gets its own number. Only the check digit is calculated, everything between the prefix and the check digit stays random.

  .Setup(a => a.Ean).Use<Ean13Source>("40063")

The numbers are syntactically valid. They are not registered with GS1, ISBN International, Amazon or any other registry and do not identify a real article, device or security, so please keep them inside your test data.

IsbnSource stays inside the registration groups that ISBN International has handed out, so a validator that knows those groups accepts the number. The range 979-0 is left out on purpose, it belongs to the ISMN of sheet music and never carries an ISBN. A GTIN has no such restriction: without a prefix the leading digits are drawn freely, so a number can land in a range GS1 reserves for a special purpose, e.g. 2 for goods weighed in the shop or 977 for periodicals. Give a prefix when that matters.

An ISBN comes without hyphens on purpose: where the groups of an ISBN start depends on the registrant ranges that ISBN International publishes, and a wrongly grouped ISBN is worse test data than an ungrouped one.

Credit cards

CreditCardSource had two correctness problems up to 6.0. Its Luhn check digit was calculated with the doubling shifted by one position, so the numbers failed a real Luhn validation. And only the first digit of the scheme was fixed, so an "American Express" could start with 36 and be detected as a Diners Club, and a "MasterCard" could start with 56, a range no scheme issues. Both are fixed: the check digit is correct and every number now starts inside a range its scheme really issues in (34/37 for American Express, 51-55 and the 2 series 2221-2720 for Mastercard, 6011, 644-649 and 65 for Discover, 4 for Visa). The generated sequences changed once for that.

New in holonsoft.AutoPoco.DataSources.Business: TestBinCreditCardSource (and its Nullable... variant) builds every number on one of the first-six-digit test BINs the payment processors publish for their test environments, e.g. 411111 or 555555. A number from CreditCardSource can land in a range a real issuer uses and can therefore coincide with a card that exists; a number on a published test BIN is recognisable as test data and very unlikely to belong to anybody. Use it when the generated data leaves your machine, e.g. in a shared developer database. The BINs the source can draw are published as TestBinCreditCardSourceBase.PublishedTestBins.

  .Setup(c => c.CardNumber).Use<TestBinCreditCardSource>(CreditCardType.Visa)

IBANs and VAT IDs

Two more sources in holonsoft.AutoPoco.DataSources.Identifiers, for the numbers on an invoice.

Source Produces
IbanSource An IBAN of any SEPA country in the electronic format without spaces, with correct mod 97 check digits. Where a country keeps its own check digits inside the account part they are calculated as well: the Belgian mod 97 tail, the Spanish DC pair, the French and Monegasque RIB key, the Italian and Sammarinese CIN letter, the Norwegian, Estonian, Finnish, Hungarian, Polish and Icelandic schemes and the Portuguese and Slovenian ISO 7064 pairs. new IbanSource("DE") pins the country.
TestBlzIbanSource A German IBAN whose bank code starts with a nine. The Bundesbank hands out clearing areas one to eight only, and in the current official bank code file not a single one of the 13760 codes starts with a nine, so the number is recognisable as test data and can name nobody's account.
VatIdSource A VAT identification number with a valid check digit, e.g. DE249051813 or ATU13585627, for AT, BE, DE, DK, EE, FI, GB, HR, HU, IE, IT, LT, LU, NL, PL, PT, SE, SI, SK and XI (Northern Ireland). Only countries with a published, calibrated check digit rule are drawn, new VatIdSource("DE") pins the country. EL, ES, FR, CY, CZ, LV, MT, BG and RO have not agreed to a publication of their construction rules, so they stay out on purpose: a number that only looks right is worse test data than a missing country.

Each has a Nullable... variant. The structures come from the SWIFT IBAN registry and the official construction rules of the tax authorities; the tests are calibrated against the registry example of every country and against registered VAT IDs of real companies before they judge the generator.

One warning, stronger than for the credit cards: an IBAN needs no CVV and no second factor for a SEPA direct debit, so a generated IBAN that coincidentally names a real account is a sharper tool than a generated card number. IbanSource draws its bank codes, so use TestBlzIbanSource whenever the data leaves your test systems. A generated VAT ID is harmless in comparison, VIES simply reports it as not registered.

Devices, codes and coordinates

Source Produces
VinSource A seventeen character vehicle identification number with the valid North American check digit at position nine, I, O and Q never appear, and the model year position follows 49 CFR 565. A prefix pins the world manufacturer identifier, e.g. new VinSource("WVW").
MacAddressSource A unicast MAC address as upper case hex, colons, hyphens or bare via MacAddressFormat. The plain source draws universally administered addresses; TestMacAddressSource sets the locally administered bit, the space IEEE 802 keeps free of manufacturer assignments, so those addresses provably name no vendor hardware.
GuidV7Source An RFC 9562 version 7 UUID whose 48 timestamp bits are drawn from a seeded date range instead of the clock, so the same seed gives the same UUIDs on every run. Version and variant bits are correct and the encoded instant stays inside the range.
PatternSource A string from a pattern: # digit, @ capital letter, * both, backslash escapes, everything else literal. "@@@-#####" gives KDX-83741. The building block for every code that follows no public standard.
SkuSource An SKU, @@@-##### unless another pattern is given. An SKU is merchant convention, so the pattern is the contract; for standardised article numbers use GtinSource and friends.
SerialNumberSource A serial number, four blocks of four characters unless another pattern is given.
LotNumberSource A lot or batch number with the widespread date code of two digit year plus day of the year, e.g. L26265-0387, the date drawn from a seeded range, never from the clock.
LatitudeSource, LongitudeSource Decimal degrees, the whole world unless a bounding box is given, e.g. new LatitudeSource(47, 55) with new LongitudeSource(5, 15). The two members are drawn independently; where the pair has to name one meaningful place, build it with Impose.
EoriSource An EORI customs number (Annex 12-01 UCC IA) for BE, DE, DK, FR, GB, HR, IT, NL, PL and XI. The format itself has no check digit, but the national part is built from the underlying national identifier with its real checks: the Belgian enterprise number, the Danish CVR, a doubly Luhn-valid French SIRET, the Croatian OIB, the Italian partita IVA, the Polish NIP plus 00000, the UK VAT registration number plus 000. Austria publishes no structure and the Portuguese third country check digit is unpublished, so both stay out.

Each has a Nullable... variant.

holonsoft.AutoPoco.Faker, realistic values from Bogus

A separate, optional package. The core package keeps its promise of having no dependency, and who wants believable names and addresses installs one more:

dotnet add package holonsoft.AutoPoco.Faker

The split of work is the whole idea. Bogus is good at a single believable value, AutoPoco is good at everything around it: building the object graph, keeping members apart, following relationships and repeating the whole thing for a seed. So Bogus never builds an object here, it only answers "give me a city name".

Source Produces
FakerFirstNameSource, FakerLastNameSource, FakerFullNameSource Names in the language of the locale
FakerEmailAddressSource An email address on one of the example domains
FakerPhoneNumberSource A phone number in the format of the locale
FakerStreetAddressSource, FakerCitySource, FakerPostalCodeSource, FakerCountrySource The parts of an address
FakerCompanyNameSource A company name including its legal form, e.g. "Müller GmbH"
FakerProductNameSource A product name that reads like a catalogue entry
FakerPriceSource A decimal amount between two bounds, 1 to 1000 with 2 decimals by default
FakerCurrencyCodeSource An ISO 4217 code such as EUR
FakerDateTimeSource A point in time between two bounds, 2000-01-01 to 2035-12-31 by default

Every one of them has a Nullable... variant, like the sources of the core package.

var factory = AutoPocoContainer.Configure(x => {
   x.UseSeed(4711);

   x.Include<Customer>()
     .Setup(c => c.FirstName).Use<FakerFirstNameSource>("de")
     .Setup(c => c.LastName).Use<FakerLastNameSource>("de")
     .Setup(c => c.EmailAddress).Use<FakerEmailAddressSource>("de")
     .Setup(c => c.City).Use<FakerCitySource>("de");
});

The first argument is the Bogus locale. Locales are written lower case with an underscore before the region, en, en_US, de, de_AT, de_CH and so on, and every locale of the pinned Bogus version works with every source. Leave it out for the default en. Where a locale is missing a piece of data Bogus falls back to English rather than failing, so an unusual locale can give you an English city.

Seeds

The AutoPoco session seed drives Bogus. Every source instance owns one Bogus faker, and that faker draws from AutoPoco's own StableRandom, so the same seed gives the same data, each member keeps its own stream, and the static Randomizer.Seed of Bogus is never read or written.

// the same customers, every run, in every process
var customers = factory.CreateSession().Collection<Customer>(1000);

One limit worth knowing. AutoPoco guarantees a sequence across .NET versions because it brings its own generator. Bogus takes its values from locale data that ships inside its package, so a different Bogus version can give different names for the same seed. That is why this package depends on exactly Bogus 35.6.5 and not on "35.6.5 or newer". A project that already references another Bogus version gets a NuGet version conflict instead of a silent change of its test data, which is the trade this package makes on purpose. Pin this package too when your tests compare against stored data.

What stays in the core package

Numbers with a check digit. Bogus has a Commerce.Ean of its own and its check digit is not guaranteed to be valid, so use Ean13Source, GtinSource, IsbnSource and AsinSource from the core package for article numbers and let Bogus do the names.

The sources are also independent of each other. A FakerCitySource and a FakerPostalCodeSource on the same object give a correct city and a correct postal code, but not a postal code that belongs to that city, and an email address does not contain the name of the customer it sits on. Build the member from the finished object where the parts have to match:

var customers = factory.CreateSession().Collection<Customer>(1000, c =>
   c.Impose(x => x.EmailAddress, (_, x) => $"{x.FirstName}.{x.LastName}@example.com".ToLowerInvariant()));

The two packages together

x.Include<Product>()
  .Setup(p => p.Name).Use<FakerProductNameSource>("de")     // Bogus, believable
  .Setup(p => p.Ean).Use<Ean13Source>("40063")              // core, check digit is correct
  .Setup(p => p.Price).Use<FakerPriceSource>(5m, 500m, 2, "de");

x.Include<Order>()
  .Setup(o => o.Customer).Use<AutoSource<Customer>>()       // AutoPoco, the graph
  .Setup(o => o.Lines).Collection(1, 4);

Bogus creates believable single values, AutoPoco creates the consistent, repeatable object graph around them. That combination is what fills a shared developer database.

New in 6.0.0

Breaking changes at a glance

  • Every generated sequence changed once, mainly because every member has its own random stream now. Tests that pin generated values need new expectations. This is the last time, see "Why the sequences changed" below.
  • IntegerSource, NullableIntegerSource, LongSource, NullableLongSource, RandomNumberSource and NullableRandomNumberSource treat max as inclusive now, it used to be exclusive. new IntegerSource(1, 3) produces 3 as well. This matches NumberSource<T> and the date sources: the maximum of every integer and date source is inclusive now.
  • Int128Source and NullableInt128Source pick uniformly from their range instead of generating a value and clamping it to the bounds. The old implementation never produced a negative value and returned the upper bound almost always on a restricted range.
  • RandomStringSource and UrlSource never used the last character of the allowed character set and the last top level domain of their list. ColorSource, RandomTextSource and RandomUtfTextSource treat their maximum as inclusive now, so new ColorSource(0, 255) reaches 255 and a text source asked for 3 to 6 paragraphs can produce 6.
  • ColorSource applies the alpha given to SetColorRange(alpha, ...), it used to be silently dropped and every color came back fully opaque.
  • CreditCardSource with CreditCardType.Random draws every card type. Discover was the last value of the enum and could never come out, the picker stopped at AmericanExpress.
  • TimeSpanSource and NullableTimeSpanSource pick uniformly from the inclusive tick range now. The old implementation returned TimeSpan.MinValue on every draw of the default range, produced values outside the range that was asked for (1 to 2 hours gave 22 to 58 minutes) and looped forever when the maximum lay before the minimum.
  • DoubleSource, FloatSource and DecimalSource interpolate between their bounds without overflowing. new DoubleSource() returned infinity on every draw, and DecimalSource lost about half of its significant digits on the way through double.
  • UrlSource follows the session seed with its host part as well. The nested string source kept the default seed, so every session produced the same host names no matter what UseSeed said. ExtendedEmailAddressSource gives its first and last name source separate streams, they used to share one and repeated the same name pairs.
  • AutoPocoGlobalSettings is gone, the defaults are read-only constants in AutoPocoDefaults.
  • The never implemented Ctor(...) stub on the type builder is gone.
  • The lambdas receiving the generation context get a non-nullable IGenerationContext, no more ctx!.
  • Recompile needed, source code unchanged: four engine constructors gained optional parameters, LongSourceBase.SetMinMax takes long instead of int (it could not reach the range of its own source before) and Int128IdSource.SetStartValue takes Int128 instead of long.

Features and fixes

  • Lambda data sources: compute a member value inline instead of writing a data source class
var counter = 0;
var factory = AutoPocoContainer.Configure(x => {
   x.Conventions(c => c.UseDefaultConventions());
   x.Include<SimpleUser>()
      // runs once per generated object
      .Setup(c => c.Id).From(() => ++counter)
      .Setup(c => c.EmailAddress).From(() => $"user{counter}@example.org")
      // the context variant can build related objects
      .Setup(c => c.Role).From(ctx => ctx.Single<SimpleUserRole>().Impose(r => r.Name, "Guest").Get());
});

// the same at generation time, overriding the configuration for this call only
var users = session.List<SimpleUser>(10)
   .Source(u => u.LastName, () => "Smith")
   .Get();

The lambda is in charge of the value, including null. FuncSource<T> is the class behind it and can be used directly wherever an IDataSource is expected.

  • Nullability-aware null generation, opt-in: x.RespectNullableAnnotations(threshold) turns every property or field declared as nullable (string?, int?, DateTime?, ...) into null with the given probability in percent (default 15), whatever data source is configured for it. Members without a nullable annotation, members of types compiled without nullable annotations and values set by Impose are never touched.
var factory = AutoPocoContainer.Configure(x => {
   x.RespectNullableAnnotations(20);   // 20 percent of the nullable members become null
   x.Conventions(c => c.UseDefaultConventions());
   x.Include<Customer>()
      .Setup(c => c.City).Use<CitySource>();   // City is string?, so it is null now and then
});

Every nullable member gets its own deterministic null pattern, so the nullable members of one object do not all become null at once, and two sessions of the same factory produce the same pattern. Sources that produce nulls on their own (the Nullable* sources) keep doing so on top of it. Generation time overrides via Source(...) are covered as well.

  • NumberSource<T> and NullableNumberSource<T>: one generic source for every built-in numeric type (byte, short, int, long, Int128, their unsigned variants, nint, float, double, Half, decimal, even char), with min and max. Integer types are drawn uniformly from the inclusive range, both bounds can be produced, the whole range of the type is the default. Floating point types and decimal are interpolated between the bounds without overflowing, even for the whole range of the type. A maximum below the minimum, NaN or an infinite bound throws an ArgumentOutOfRangeException. The existing IntegerSource, DecimalSource and friends stay as they are.
x.Include<Order>()
   .Setup(c => c.Rating).Use<NumberSource<byte>>((byte) 1, (byte) 5)
   .Setup(c => c.Amount).Use<NumberSource<decimal>>(1m, 100m)
   .Setup(c => c.ExternalId).Use<NumberSource<UInt128>>(s => s.SetMin(1UL))
   .Setup(c => c.Discount).Use<NullableNumberSource<decimal>>(0m, 30m, 50);   // null in 50 percent of the cases
  • Records and immutable types are constructed through their constructor, no parameterless constructor needed anymore. Constructor parameters are fed from the registered members with the same name (case-insensitive) and a compatible type, so Setup(c => c.FirstName).Use<FirstNameSource>() on a positional record property ends up as the FirstName argument, and a get-only property of a class ends up as the matching argument. Members taken by the constructor are not set again afterwards. Impose and Source(...) at generation time keep working on positional record properties.
public record Customer(string FirstName, string LastName, string? Nickname, DateOnly Birthday, Address Address);

public class Money {
   public Money(decimal amount, string currency) { Amount = amount; Currency = currency; }
   public decimal Amount { get; }
   public string Currency { get; }
}

var factory = AutoPocoContainer.Configure(x => {
   x.Conventions(c => c.UseDefaultConventions());
   x.Include<Customer>()
      .Setup(c => c.FirstName).Use<FirstNameSource>()
      .Setup(c => c.LastName).Use<LastNameSource>()
      .Setup(c => c.Birthday).Use<DateOnlySource>(new DateOnly(1950, 1, 1), new DateOnly(2000, 12, 31));
   x.Include<Money>()
      .Setup(c => c.Amount).Use<NumberSource<decimal>>(1m, 100m)
      .Setup(c => c.Currency).From(() => "CHF");
});

The rules: the constructor that takes the most members which cannot be set otherwise wins, then the one with the fewest parameters, so ordinary classes keep using their parameterless constructor and their setters. With the default conventions get-only properties that match a constructor parameter are registered too, so an unconfigured Money gets the default member values through its constructor. A parameter without a matching member gets its declared default value, the default of its value type, or for other reference types an object generated by the session. A struct without any configured member stays at its default, so an unconfigured DateOnly parameter no longer throws. A get-only property with a data source but no matching constructor parameter throws an InvalidOperationException that names the property. CtorSource<T> is the class behind it; new CtorSource<T>(constructorInfo) pins a constructor and still takes the matching members. Constructor arguments honor RespectNullableAnnotations and the recursion limit, so a self referencing record such as record Node(string Name, Node? Child) terminates.

  • Seeds: x.UseSeed(seed) sets the seed of a factory, factory.CreateSession(recursionLimit, seed) gives a single session another seed. Every data source in a session gets its own random stream derived from the session seed and the member it feeds (Type.Member), so two members with the same source type no longer produce the same values, the nullable sources of one object no longer become null together, and the values of a member do not depend on which other members are configured. A seed set on a source in the configuration (Use<IntegerSource>(s => s.SetSeedToRandomValue(7))) wins over the session seed. The null patterns of RespectNullableAnnotations, the shuffle behind Random(count) and constructor arguments follow the seed as well.
var factory = AutoPocoContainer.Configure(x => {
   x.UseSeed(2024);   // default is AutoPocoDefaults.Seed (1337)
   x.Include<SimpleUser>()
      .Setup(c => c.FirstName).Use<FirstNameSource>()
      .Setup(c => c.LastName).Use<LastNameSource>();
});

var session = factory.CreateSession();          // seed 2024
var other = factory.CreateSession(5, 4711);      // seed 4711, same configuration
  • Own random number generator: every source draws from StableRandom (xoshiro256** seeded through SplitMix64), a Random subclass owned by AutoPoco, instead of System.Random. The sequence for a seed is defined by AutoPoco's code alone, so test data stays the same across .NET versions; Microsoft explicitly does not promise that for a seeded System.Random. The generator is pinned by tests. Rule from now on: catalogs (names, cities, zip codes, ...) and source algorithms are frozen within a major version, a change to either is a major version bump.

  • Breaking: AutoPocoGlobalSettings is gone, it was mutable process-wide state that leaked between test fixtures. The defaults are read-only constants in AutoPocoDefaults (Seed, NullCreationThreshold, RecursionLimit). Set a seed with UseSeed, null thresholds per source or with RespectNullableAnnotations(threshold).

  • Breaking: every generated sequence changed once with 6.0, mainly because every member has its own random stream now and to a smaller part because of the own generator. Tests that pin generated values need new expectations, this is the last time. "Why the sequences changed" at the end of this section has the full story.

  • Index-aware Impose: on a list or a selection the imposed value can depend on the position of the item (0 based), and on the item as generated so far. The position is the one in the whole list, also inside First/Next and after Random. A single generator gets Impose(member, item => value) for values that depend on other members.

var users = session.List<SimpleUser>(100)
   .Impose(u => u.Id, i => i + 1)                                            // 1..100
   .Impose(u => u.EmailAddress, (i, u) => $"{u.FirstName}.{u.LastName}{i}@example.test")
   .First(10).Impose(u => u.City, i => $"Branch {i}")
   .All()
   .Get();

var admin = session.Single<SimpleUser>()
   .Impose(u => u.LastName, "Ashton")
   .Impose(u => u.EmailAddress, u => $"{u.FirstName}.{u.LastName}@example.test")
   .Get();

The item lambda runs after the configured sources and after every Impose registered before it, so it sees those values.

  • Invoke(c => c.Method(...)) in the configuration takes lambdas and values as arguments now: Use.From(() => value) and Use.From(ctx => ...) compute an argument per object, constants and captured variables arrive as their value (constants used to arrive as the expression object, a bug), null is allowed. Any other method call in the argument list is rejected with a message that names the options.
var counter = 0;
x.Include<SimpleUser>()
   .Invoke(c => c.SetPassword(Use.From(() => $"pw-{++counter}")))
   .Invoke(c => c.SetSomething("fixed", Use.Source<string, LastNameSource>()!));
  • The lambdas receiving the generation context (From(ctx => ...), Source(member, ctx => ...), Use.From(ctx => ...)) get a non-nullable IGenerationContext, no more ctx!. A FuncSource built from such a lambda throws a clear InvalidOperationException when used outside of a session.
  • Hardening: the public configuration and generation API validates its arguments (ArgumentNullException, ArgumentOutOfRangeException for negative counts, ArgumentException for unknown members, non-source types and non-convention types) and every internal failure carries the type and member it happened at. The never implemented Ctor(...) stub on the type builder is gone. The package ships an XML documentation file.
  • IDataSource<T> is covariant now, so a source of string (e.g. CitySource) can be used for a string? member without a nullability warning.
  • Bug fix: the integer sources that existed before NumberSource<T> never produced their maximum, new IntegerSource(1, 3) gave 1 or 2 only. IntegerSource, LongSource, RandomNumberSource and their nullable variants draw uniformly from the inclusive range now, both bounds can be produced. Int128Source and NullableInt128Source were worse: they built a value from two non-negative draws and clamped it to the range, so the default source never produced a negative value and new Int128Source(1, 5) returned 5 almost every time. All of them share the pick of NumberSource<T> now. LongSourceBase.SetMinMax takes long arguments at last, and all four throw an ArgumentOutOfRangeException when the maximum lies below the minimum. The generated sequences of the long and Int128 sources changed, the ones of the integer sources did not.
  • Bug fix: TimeSpanSource did not work at all. new TimeSpanSource() returned TimeSpan.MinValue on every single draw, new TimeSpanSource(TimeSpan.FromHours(1), TimeSpan.FromHours(2)) returned values between 22 and 58 minutes, so every value was outside the range that was asked for, and a maximum before the minimum sent it into an endless loop. It picks uniformly from the inclusive tick range now, the same way DateTimeSource does, and throws an ArgumentOutOfRangeException on a reversed range.
  • Bug fix: new DoubleSource() returned infinity on every draw, because the default range is the whole range of double and the old code computed max - min. DoubleSource, FloatSource and DecimalSource interpolate as min * (1 - sample) + max * sample now, which cannot overflow. DecimalSource does it in decimal arithmetic instead of going through double, so it keeps its full precision; it used to give numbers like 27879346472004300000000000000 where it now gives 27879346472004256951386422019. All three throw an ArgumentOutOfRangeException when the maximum lies below the minimum.
  • Bug fix: CreditCardSource never produced a Discover card. CreditCardType.Random picked with Random.Next(1, 4), which stops at AmericanExpress, so the last value of the enum was unreachable; 5000 generated cards contained not a single one. The picker reads the card types from the enum now, so a type added later is drawn as well.
  • Bug fix: a handful of sources could not reach the last element of what they draw from. RandomStringSource never used the last character of its allowed character set, UrlSource never used the last top level domain of its list, and ColorSource, RandomTextSource and RandomUtfTextSource treated their maximum as exclusive, so a colour channel range of 0 to 255 never gave 255 and a text source asked for 3 to 6 paragraphs never gave 6. On top of that ColorSource dropped the alpha of SetColorRange(alpha, ...) and always returned an opaque colour.
  • Bug fix: sources that drive another source internally seed it now. UrlSource builds its host with a nested RandomStringSource that kept the default seed, so UseSeed and CreateSession(limit, seed) changed the top level domain but every session saw the same host names. ExtendedEmailAddressSource passed one seed to both its first and last name source, so the two followed the same sequence of draws and the same first name came back with the same last name. Both give their nested sources a derived stream of their own now, inside a session and standalone.
  • Bug fix: every Nullable* source ignored an explicit null creation threshold. The fixed array and dictionary based sources (names, companies, cities, capitals, countries, states, zip codes, urls) even used the threshold as their random seed. The threshold is honored now. As a consequence the null positions in the stable sequences of these sources changed, the data itself comes in the same order as before.
  • Bug fix: DateTimeSource, DateOnlySource, TimeOnlySource and DateOfBirthSource never produced the upper end of their ranges: no December, no 31st, no 23:00, no minute or second 59, and the maximum year of a date of birth was never reached. Ranges within a single year could produce values outside the range. All four sources now pick uniformly from the whole range, both bounds inclusive, and throw an ArgumentOutOfRangeException when the maximum lies before the minimum. TimeOnlySource supports ranges that wrap around midnight (22:00 to 02:00). DateTimeSource keeps the DateTimeKind of the minimum date. The generated sequences for a given seed changed.
  • Bug fix: RandomUtfTextSource could loop forever when it hit a Unicode block without any allowed character.
  • Build and packaging: GitHub Actions CI, trusted publishing to nuget.org, MinVer versioning from git tags, central package management, tests on xunit.v3 for net8/9/10
  • The library no longer drags FluentAssertions and Moq into your project as dependencies

Why the sequences changed

A fair question when you upgrade and every pinned test value is suddenly wrong: was something broken about System.Random?

No. System.Random is not the reason, and this was never a difference between Windows and Linux. A seeded System.Random gives the same sequence on every operating system, as long as the runtime version is the same.

Two independent things came together in 6.0:

  • Per-member seeds are what actually moved the values. Until 5.x all sources of a session shared one seed. Two members with the same source type produced the same values, and the nullable members of an object became null all at once. Giving every member its own stream derived from the session seed fixes that, and it changes every sequence by definition. The bug fixes above (null thresholds, inclusive date and integer ranges) moved a few more.
  • StableRandom is insurance, not a bug fix. .NET treats the algorithm behind Random as an implementation detail and has already replaced it once: .NET 6 moved the unseeded path to xoshiro256** and kept the old algorithm for seeded instances purely for compatibility. Nothing promises that the seeded path survives the next change. "The same seed gives the same test data in five years, on whatever .NET is current then" is not something you can build on System.Random, so AutoPoco owns its generator now. Since 6.0 was going to break the sequences anyway, this was the moment to do it.

Afterwards the sequences are frozen by AutoPoco's own code for the whole major version.

For the record, this library did have a real operating system dependency once, but it was not the generator: CountrySource read the culture list from the operating system, which made test data differ between developer machines and build pipelines. That was fixed in 4.1.3 with a stable country list; the old source lives on as CountryFromCultureListSource and its tests are skipped for exactly this reason.

New in 5.1.1

  • Support for .NET 9 / .NET 10 added

New in 4.2.2

  • RandomUtfTextSource / NullableRandomUtfTextSource added

New in 4.2.1

  • Support for .net8
  • Update of several nugets (xunit, moq) to newest version

New in 4.1.3

  • CountrySource as a stable country list (status of 2023) added, support for ISO3-Codes as abbreviation
  • CountrySource (old) is now CountryFromCultureListSource. This source is not stable in terms of repeatable test data because it depends on culture list of underlying operation system. This causes tests to fail e. g. in build pipelines.

New in 4.1.2

  • small bugfix for TimeSpan (underflow / overflow) calculation

New in 4.1.1

  • support for typesafe settings via lambda chaining in USE
  • support for own factory outside of AutoPoco in USE
  • many datasources now support this notation
  • now its possible to set the random null evaluator if the standard implementation does not fit
  • now its possible to set the nullable threshold value if the standard value (15) does not fit
  • net7+ : new Int128Source / NullableInt128Source / Int128IdSource
_factoryWithComplexRule = AutoPocoContainer.Configure(x => {
         x.Include<SimpleUser>()
            .Setup(c => c.FirstName).Use<FirstNameSource>()
            .Setup(c => c.LastName).Use<LastNameSource>()
            .Setup(c => c.EmailAddress).Use<EmailAddressSource>()
            .Setup(c => c.Id).Use<Int128IdSource>(y => y.SetStartValue(100000))
            .Setup(c => c.ExternalId).Use<Int128Source>()

            // support for external factory
            .Setup(c => c.City).Use<IDataSource<string>>(new StringDataSourceFactory())

            // support for lambdas to configure a datasource
            .Setup(c => c.Birthday).Use<DateOnlySource>(
               x => x.SetMinDate(new DateOnly(1968, 1, 1))
                     .SetMaxDate(new DateOnly(2023, 10, 17))
            )
            .Invoke(c => c.SetPassword(Use.Source<string, PasswordSource>()!));
      });

New in 4.0.1

  • most of XYdatasources now have a NullableXYDataSource pendant to get random NULL values instead of normal data back. Helpful for DTOs with nullable fields
  • some new datasources, e. g. DateOnlySource, TimeOnlySource, RandomTextSource, LongSource, LongIdSource (and their Nullable pendants)
  • RandomStringSOurce now supports a configurable range of chars (e. g. 'A' .. 'z') or a char[] of allowed chars
  • some bugfixes regarding index ranges, LINQ replacements for loops
  • AutoPocoGlobalSettings introduced, set the properies before every other action / creation to change framework behavior
  • Demo project (in tests) updated
  • Please note, the CountrySourceTest may fail on your system. It depends on the culture list of your operation system.

Breaking change in 4.0.1

  • behaviour change: EmailAddressSource, ExtendedEmailAddressSource now generate data according to RFC2606 to make sure that no valid data will be generated
  • some more grouping of datasources (primitives, business, country)

New in 3.7.x

  • Ported to newest .net version (at the moment 7)
  • Unit tests changed to Xunit (was Nunit)
  • Some more unit tests, now all datasources should be covered
  • As far as possible test are now consolidated to make testing shorter and more efficient
  • Several patches and fixes applied
  • Added some more datasources
  • All datasources now have a common base class
  • Shrinked projects from 4 to 2 (implementation and tests)
  • Several improvements regarding code
  • Demo project (in tests) updated

Changes in 3.7.2

Breaking change: Reorganized datasources

  • base
  • primitives
  • business

We plan to add more datasources in future and so we want to provide a better overview over datasources

Behavior change (maybe, only some datasources)

All datasources reproduce same results within same seed - important for testing. Since 6.0 the seed of a factory is set with x.UseSeed(seed) and a single session can get another one with CreateSession(recursionLimit, seed), see "New in 6.0.0". Additionally you can use

  • DataSource.SetSeedToRandomValue()
  • DataSource.SetSeedToRandomValue(seed)

on a single source; a seed set this way wins over the session seed.

AutoPoco.NetCore

AutoPoco is a highly configurable framework for the purpose of fluently building readable test data from Plain Old CLR Objects. This is a modernized (i.e. dotnet core 2.0.0) port of the excellent AutoPoco project which hasn't been maintained since 2014. Thanks to @robashton for graciously agreeing to let me pull this fossil out of the amber and give it a light dusting.

What does this do?

AutoPoco replaces manually written object mothers/test data builders with a fluent interface and an easy way to generate a large amount of readable test data. By default, no manual set-up is required, conventions can then be written against the names/types of property or manual configuration can be used against specific objects.

The primary use cases are

  • Creating single, valid objects for unit tests in a standard manner across all tests
  • Creating large amounts of valid test data for database population

The default data sources and convention is to perform set up once on start-up, and create a 'generation session' for each test that is run, every session will generate repeatable test data so no flickering tests.

// Perform factory set up (once for entire test run)
IGenerationSessionFactory factory = AutoPocoContainer.Configure(x =>
{
    x.Conventions(c =>
    {
        c.UseDefaultConventions();
    });
    x.AddFromAssemblyContainingType<SimpleUser>();
});

// Generate one of these per test (factory will be a static variable most likely)
IGenerationSession session = factory.CreateSession();

// Get a single user
SimpleUser user = session.Single<SimpleUser>().Get();

// Get a collection of users
List<SimpleUser> users = session.List<SimpleUser>(100).Get();

// Get a collection of users, but set their role manually
mSession.List<SimpleUser>(10)
               .Impose(x => x.Role, sharedRole)
               .Get();

Head to the Documentation to get started.

Features

  • Generates the same pseudo-random data per session, so your tests don't "flicker"
  • Convention based configuration to set up defaults on un-specified properties/fields
  • Convention based configuration to match names/types on properties/fields that match a certain pattern
  • Fluent configuration to set up data sources for meaningful data such as e-mail addresses/names
  • Fluent interfaces to generate lists/single objects with all properties automatically populated
  • Ability to override conventions + configuration at object-creation time with manually specified values
  • Support for method invocation as part of an object's initialisation both automatically via configuration and at object-creation time
  • Support for the inheritance of rules from base classes/interfaces

Quick Example

Member Conventions

Define a rule for all string properties called EmailAddress

public class EmailAddressPropertyConvention : ITypePropertyConvention
{
  public void Apply(ITypePropertyConventionContext context)
  {
      context.SetSource<EmailAddressSource>();
  }

  public void SpecifyRequirements(ITypeMemberConventionRequirements requirements)
  {
      requirements.Name(x => String.Compare(x, "EmailAddress", true) == 0);
      requirements.Type(x => x == typeof(String));
  }
}       

Optionally Setting up data sources for an object

Make the properties get their data from the specified sources on creation Call the SetPassword method with a password source on creation

  x.Include<SimpleUser>()
    .Setup(c => c.EmailAddress).Use<EmailAddressSource>()
    .Setup(c => c.FirstName).Use<FirstNameSource>()
    .Setup(c => c.LastName).Use<LastNameSource>()
    .Invoke(c => c.SetPassword(
        Use.Source<String, PasswordSource>()));

Simple Object Creation

Create a user populated with defaults - all properties will be set (including other complex objects)

  SimpleUser user = mSession.Single<SimpleUser>().Get();

More Complex Object Creation

Create a role for a collection of users and ask for a list of users using that single role

           SimpleUserRole sharedRole = mSession.Single<SimpleUserRole>()
               .Impose(x=>x.Name, "Shared Role")
               .Get();

           mSession.List<SimpleUser>(10)
               .Impose(x => x.Role, sharedRole)
               .Get();

Hard Core Object Creation

Create three roles Create 100 users The first 50 of those users will be called Rob Ashton The last 50 of those users will be called Luke Smith 25 Random users will have RoleOne A different 25 random users will have RoleTwo And the other 50 users will have RoleThree And set the password on every single user to Password1

            SimpleUserRole roleOne = mSession.Single<SimpleUserRole>()
                              .Impose(x => x.Name, "RoleOne").Get();
            SimpleUserRole roleTwo = mSession.Single<SimpleUserRole>()
                              .Impose(x => x.Name, "RoleTwo").Get();
            SimpleUserRole roleThree = mSession.Single<SimpleUserRole>()
                              .Impose(x => x.Name, "RoleThree").Get();

            mSession.List<SimpleUser>(100)
                 .First(50)
                      .Impose(x => x.FirstName, "Rob")
                      .Impose(x => x.LastName, "Ashton")
                  .Next(50)
                      .Impose(x => x.FirstName, "Luke")
                      .Impose(x => x.LastName, "Smith")
                  .All().Random(25)
                      .Impose(x => x.Role,roleOne)
                  .Next(25)
                      .Impose(x => x.Role,roleTwo)
                  .Next(50)
                      .Impose(x => x.Role, roleThree)
                 .All()
                      .Invoke(x => x.SetPassword("Password1"))
                 .Get();
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on holonsoft.AutoPoco:

Package Downloads
holonsoft.AutoPoco.Faker

Optional Bogus integration for AutoPoco. Adds data sources for realistic names, companies, addresses, phone numbers, product names and prices, driven by the AutoPoco session seed. AutoPoco keeps building the object graph, Bogus only supplies the individual values.

GitHub repositories

This package is not used by any popular GitHub repositories.

6.1: New data sources for trade identifiers in holonsoft.AutoPoco.DataSources.Identifiers: GtinSource and Ean13Source (GTIN-8/12/13/14, i.e. EAN-8, UPC-A, EAN-13 and ITF-14, with a valid GS1 mod 10 check digit and an optional GS1 company prefix), SsccSource (the eighteen digit number of a logistic unit, same GS1 check digit, optional prefix), IsbnSource (ISBN-13 inside an allocated registration group, ISBN-10 with its mod 11 check digit including the X), IssnSource (mod 11 check character including the X), IsinSource (a real country prefix or a given one, Luhn over the expanded letters), ImeiSource (Luhn check digit, optional Type Allocation Code prefix) and AsinSource, each with a Nullable variant. IbanSource generates the IBAN of any SEPA country with correct mod 97 check digits and the national check digits inside the BBAN (BE, EE, ES, FI, FR/MC, HU, IS, IT/SM, NO, PL, PT, SI), TestBlzIbanSource a German IBAN on a bank code outside the assigned range so the number is recognisable as test data, and VatIdSource a VAT ID with a valid check digit for AT, BE, DE, DK, EE, FI, GB, HR, HU, IE, IT, LT, LU, NL, PL, PT, SE, SI, SK and XI, calibrated against the official construction rules; countries that have not published their rules are left out on purpose. More sources: VinSource (North American check digit, model year per 49 CFR 565, optional WMI prefix), MacAddressSource and TestMacAddressSource (universally vs. locally administered unicast, three formats), GuidV7Source (RFC 9562 version 7 UUIDs whose timestamp comes from a seeded range instead of the clock), PatternSource (# digit, @ letter, * both, backslash escapes) with SkuSource, SerialNumberSource and LotNumberSource (seeded year plus day-of-year date code) on top, and LatitudeSource/LongitudeSource with bounding box. EoriSource generates EORI customs numbers for BE, DE, DK, FR, GB, HR, IT, NL, PL and XI, the national part built from the underlying national identifier with its real check digits (enterprise number, CVR, SIRET, OIB, partita IVA, NIP, VAT registration number). TestBinCreditCardSource in holonsoft.AutoPoco.DataSources.Business builds card numbers on the six digit test BINs the payment processors publish, so the data is recognisable as test data. Bug fixes: CreditCardSource doubled the wrong digits when calculating the Luhn check digit, so its numbers failed a real Luhn validation; and only the first digit of the scheme was fixed, so an "American Express" could be detected as a Diners Club and a "MasterCard" could start with 56, a range no scheme issues. Every number now carries a correct check digit and starts inside a range its scheme really issues in (34/37 for American Express, 51-55 and 2221-2720 for Mastercard, 6011/644-649/65 for Discover), and the generated sequences changed once for that. 6.0: Lambda data sources (FuncSource, Setup(...).From(() => ...), Source(expr, () => ...) at generation time). Opt-in nullability-aware null generation via RespectNullableAnnotations(threshold). Generic NumberSource<T> and NullableNumberSource<T> with min/max for every built-in numeric type (integers uniform and inclusive, floating point and decimal overflow safe). Records and immutable types are constructed through their constructor, parameters are fed from the registered members with the same name, no parameterless constructor needed anymore; unconfigured struct parameters stay default instead of throwing. Seeds: UseSeed(seed) per factory, CreateSession(recursionLimit, seed) per session, every member gets its own stream derived from the seed. Own deterministic generator (StableRandom, xoshiro256**) instead of System.Random, sequences are stable across .NET versions. Breaking: AutoPocoGlobalSettings removed (AutoPocoDefaults constants instead), every generated sequence changed once, the max of IntegerSource/LongSource/RandomNumberSource and their nullable variants is inclusive now (it was exclusive), Int128Source/NullableInt128Source pick uniformly instead of clamping to the bounds (they never produced a negative value before), RandomStringSource and UrlSource reach the last character resp. the last top level domain, ColorSource/RandomTextSource/RandomUtfTextSource treat their maximum as inclusive, ColorSource applies the alpha of SetColorRange, and UrlSource and ExtendedEmailAddressSource seed their nested sources (the URL host ignored the session seed completely). CreditCardSource never produced a Discover card. TimeSpanSource was rewritten: it returned TimeSpan.MinValue on every draw of the default range, produced values outside a requested range and looped forever on a reversed one. DoubleSource returned infinity over its default range, DoubleSource/FloatSource/DecimalSource interpolate overflow safe now and DecimalSource keeps its full precision. Index-aware Impose on lists and selections (value from the position, or from position and item), Impose(member, item => value) on a single generator. Invoke arguments: Use.From(lambda), constants and variables arrive as values. Context lambdas get a non-nullable IGenerationContext. Argument validation over the public API, meaningful exception messages, XML documentation file. IDataSource<T> is covariant. Bug fixes: Nullable* sources honor an explicit null creation threshold (null positions of stable sequences changed), DateTime/DateOnly/TimeOnly/DateOfBirth sources cover their whole range including the upper bound (sequences changed), RandomUtfTextSource no longer loops forever. Build and packaging modernized: GitHub Actions CI, trusted publishing to nuget.org, MinVer tag based versioning, central package management, xunit.v3 tests on net8/9/10. FluentAssertions and Moq are no longer dependencies of the library. See README for details.