ConventionMapper.Testing 1.10.0

dotnet add package ConventionMapper.Testing --version 1.10.0
                    
NuGet\Install-Package ConventionMapper.Testing -Version 1.10.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="ConventionMapper.Testing" Version="1.10.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ConventionMapper.Testing" Version="1.10.0" />
                    
Directory.Packages.props
<PackageReference Include="ConventionMapper.Testing" />
                    
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 ConventionMapper.Testing --version 1.10.0
                    
#r "nuget: ConventionMapper.Testing, 1.10.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 ConventionMapper.Testing@1.10.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=ConventionMapper.Testing&version=1.10.0
                    
Install as a Cake Addin
#tool nuget:?package=ConventionMapper.Testing&version=1.10.0
                    
Install as a Cake Tool

ConventionMapper.Testing

Test helpers for mappings written with ConventionMapper or ConventionMapper.SourceGenerator. No dependency on a test framework: failures throw MapperAssertException, which xUnit, NUnit and MSTest all report.

dotnet add package ConventionMapper.Testing

Which members does a map write?

MapperAssert.WritesExactly fills a source object (distinct values, nested objects, one-element collections), runs the map and compares the destination members that got a value with the list you expect:

MapperAssert.WritesExactly((LocationData s) => s.ToImportData("GL1", 10m),
    "LocationNumber", "Street1", "City", "GlClassCode", "Exposure");

With the source generator, each mapper class has WriteSets: the members each generated method writes. Compare the real behavior with it, without listing members by hand:

MapperAssert.WritesExactly((LocationData s) => s.ToImportData(), Mappings.WriteSets["ToImportData(LocationData)"]);

Keys are "Name(ParameterTypes)" as in the data-flow report, plus "Name" for methods without overloads. To survive renames and signature changes, look the write set up by name and parameter types instead: Mappings.WriteSetOf(nameof(Mappings.ToImportData), typeof(LocationData)), or MapperAssert.WriteSetOf(typeof(Mappings), nameof(Mappings.ToImportData), typeof(LocationData)). Update methods use MapperAssert.UpdatesExactly((Dto s, Entity t) => s.Update(t), expected).

Do the values come from the right members?

WritesExactly checks which members are written, not where their values come from: a swapped pair (SubmitUserId filled from CompleteUser.Id, CompleteUserId from SubmitUser.Id) passes it. MapperAssert.WritesFrom checks each destination member against the source member path it should be copied from. Every sample value is distinct, so a value that comes from the wrong member is found, and the failure names the member it really came from:

MapperAssert.WritesFrom((Claim c) => c.ToDto(), ("SubmitUserId", "SubmitUser.Id"), ("CompleteUserId", "CompleteUser.Id"));
// CompleteUserId: expected the value of CompleteUser.Id (12), but it holds 9, the value of SubmitUser.Id

The expected pairs must come from you (or from a spec), not from the code under test. With the source generator you can pass the generated lineage instead, MapperAssert.LineageOf(typeof(Mappings), nameof(Mappings.ToDto), typeof(Claim)), but that records the mapping configuration, so a configuration mistake such as a [MapMember] that swaps two members is in the lineage too and passes. It only proves the generated code does what the configuration says. Pair it with the snapshot-tested data-flow report, where a reviewer sees the swap, or state the pairs that matter explicitly.

Nested objects mapped by another method are left to that method's own check. Built-in conversions (numbers, ToString, enum names) are followed; a conversion it can't follow is only reported when the value clearly came from elsewhere.

A baseline for every map

[Fact]
public void Every_map_matches_its_write_set() =>
    MapperAssert.AllMappersMatchWriteSets(typeof(Mappings));

Runs every generated create and update method with sample data (extra parameters filled too) and compares it with the generator's WriteSets and Lineage: it must not throw, must not write members outside its write set, must write the members it copies from a filled source, and each value must come from the member the generator recorded. A new map gets this coverage before anyone writes a dedicated test.

What it can't catch: it compares the generated code with the generator's own record of the configuration, so a mistake in the configuration itself passes. If [MapMember("CompleteUser.Id", "SubmitUserId")] swaps two members, the code and the record agree, and the check passes. It finds problems the configuration doesn't show: exceptions on real values (a parse, a [MapRequired] member, a conversion method), and generator bugs. To check the configuration is right, state the expected sources yourself with WritesFrom (above) or the expected members with WritesExactly.

When sample values aren't valid input (a string that is parsed, say), pass options: AllMappersMatchWriteSets(typeof(Mappings), new SampleDataOptions().For<Row, string>(r => r.Code, n => n.ToString())). Projections and collection merges are skipped.

Round trips

For a map and its reverse, back(to(x)) must equal x, deeply (nested objects member by member, collections item by item):

MapperAssert.RoundTrips<Person, PersonDto>(Mappings.ToDto, Mappings.ToPerson);

// 100 randomized inputs: nullable members sometimes null, empty, blank, very long and non-ASCII strings,
// zero and extreme numbers, empty and multi-item collections.
MapperAssert.RoundTrips<Person, PersonDto>(Mappings.ToDto, Mappings.ToPerson, runs: 100);

// Members the reverse map can't restore on purpose:
MapperAssert.RoundTrips<Person, PersonLite>(Mappings.ToLite, Mappings.FromLite, ignore: new[] { "Nickname" });

A failure lists the member paths that changed, or the exception, with the seed of that run. Replay it with options: new SampleDataOptions().Randomized(seed).

Snapshots of the generated code

A library upgrade or an attribute change can alter what the generator writes for a mapper. Snapshot one method's generated code in its own file, so the change fails a test that names that mapper:

[Fact]
public void ToDto_generated_code_is_reviewed() =>
    MapperAssert.GeneratedCodeMatches(typeof(Mappings), nameof(Mappings.ToDto), new[] { typeof(Order) });
    // generated-code/Mappings.ToDto(Order).cs, next to this test file

[Fact]
public void Generated_code_is_reviewed() =>
    MapperAssert.GeneratedCodeMatchesDirectory(typeof(Mappings));   // generated-code/Mappings/<method>.cs, one per method

The project that contains the mapper needs <ConventionMapperEmbedGeneratedCode>true</ConventionMapperEmbedGeneratedCode>. A missing file is created and the test fails once so it gets reviewed and committed; a change fails with a line diff; run the tests with CONVENTIONMAPPER_UPDATE_SNAPSHOTS=1 to accept changes. In directory mode, a file for a method that no longer exists fails as stale.

Is every mapping method tested?

[Fact]
public void Every_mapping_method_has_a_test() =>
    MapperAssert.AllMethodsCovered(typeof(Mappings), typeof(MappingTests).Assembly);

Fails with the list of generated mapping methods that no code in the test assembly calls (directly, as a method group or inside an expression tree).

A call alone doesn't check anything, so there is a stricter version: every create and update method must run inside a WritesExactly or UpdatesExactly check (projections must at least be called).

[Fact]
public void Every_mapping_method_is_verified() =>
    MapperAssert.AllMethodsVerified(typeof(Mappings), typeof(MappingTests).Assembly);

Is every mapping method used?

[Fact]
public void No_unused_maps() =>
    MapperAssert.AllMethodsUsed(typeof(Mappings), typeof(OrderService).Assembly);

Fails for mapping methods that no code in the given production assemblies uses: maps only tests call, or nothing does, are candidates for deletion. A method reached through another used method of the mapper (a nested object's map) counts as used.

Sample data

SampleData.Create<T>() builds the filled objects used above. Strings carry the member name ("Street1-7"), so a value that lands in the wrong member is easy to spot in a failure. Constructor parameters are filled, and a private parameterless constructor (common on EF Core entities) is used when there is no public one.

SampleDataOptions customizes it; every check that fills objects takes them:

var options = new SampleDataOptions()
    .For<Address, string>(a => a.ZipCode, n => (10000 + n).ToString())   // valid values for one member
    .For<Money>(() => new Money(10m, "EUR"))                            // every value of a type
    .ForMember("CountryCode", () => "GB")                               // a member of this name in any type
    .Enum(Status.Active, Status.Closed)                                 // only these enum values
    .Construct<IShape>(() => new Square());                             // interfaces, or types without a usable constructor
options.CollectionSize = 3;           // items per collection (default 1)
options.MaxDepth = 2;                 // nesting depth (default 4)
options.FillNonPublicSetters = true;  // also private and init-only setters

var address = SampleData.Create<Address>(options);

Builders

SampleData.Builder<T>() starts from a filled object and sets only what a test cares about, so setup stays short:

var referral = SampleData.Builder<Referral>()
    .With(r => r.Status, "A")
    .With(r => r.Address.City, "Leeds")      // nested; missing objects on the way are created
    .Without(r => r.ClosedOn)                // null / default
    .Build();

var referrals = SampleData.Builder<Referral>().With(r => r.Number, i => 1000 + i).BuildMany(3);   // distinct values
var blank = SampleData.Builder<Referral>().Blank().With(r => r.Id, 7).Build();                     // start empty

Private and init-only setters and get-only auto-properties can be set, so entities that guard their state can be built too. Configure(r => ...) runs any other setup, and builders take the same SampleDataOptions.

Randomized(seed) switches to random edge cases, repeatable with the seed. Random values no longer carry member names, so WritesFrom and AllMappersMatchWriteSets refuse it; it is meant for RoundTrips and your own tests.

Product 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 was computed.  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 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on ConventionMapper.Testing:

Package Downloads
ConventionMapper.Testing.EntityFrameworkCore

Database checks for mappings into EF Core entities: DatabaseAssert.SavesRoundTrip maps, saves and reads back an entity inside a rolled-back transaction and reports every value the database changed (database defaults that replace a copied false or 0, truncated text, rounded numbers), and DatabaseAssert.ModelMatchesDatabase compares the EF Core model's columns, nullability, maximum lengths and defaults with the real schema (SQL Server and SQLite).

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.10.0 73 10/27/2026
1.9.1 118 10/27/2026
1.9.0 74 10/27/2026
1.8.0 122 10/26/2026

## 1.10.0 (unreleased)

`ReverseMap()` for enum maps, plus database-level checks, honest acknowledgements and version safety from adopting 1.9 in a large scaffolded codebase.

**Upgrading:**
- New warnings can appear on existing code: CM0034 (a copied `false` or `0` replaced by a database default) and CM0035 (a `[MapReviewed]` entry that acknowledges nothing, such as entries added for the 1.9.0 CM0028 false positive). With warnings as errors, check before upgrading.
- ConventionMapper.Testing now fails when the mapper was generated by another major or minor version of the generator. Reference the same version of both packages.

### Runtime package
- **`ReverseMap()` for enum maps:** `ConvertUsingEnumMapping(o => o.MapValue(Color.Purple, ColorDto.Violet).ReverseMap())` also creates `ColorDto` → `Color` with the same matching (by name, case-insensitive or by value) and every override inverted. `ReverseMap(fallback: Color.Red)` sets the reverse map's fallback (a forward `Fallback` can't be inverted). Two overrides onto the same value can't be inverted: validation reports it and mapping that value back throws. `ReverseMap()` on the map itself, before or after `ConvertUsingEnumMapping`, inverts the mapping too, matching AutoMapper.Extensions.EnumMapping.

### Source generator
- **CM0034 (warning):** a create method writes a non-nullable `bool`, number or enum of an EF Core entity whose column has a database default other than the CLR default (`HasDefaultValueSql("((1))")`, `HasDefaultValue(1)`). EF Core leaves a property holding `false` or `0` out of the insert, so the row silently gets the default instead of the copied value. Fix by making the property nullable or configuring `HasSentinel`, or acknowledge it with `[MapReviewed]`. Strings, `DateTime`, `Guid`, nullable members, keys, members with `HasSentinel` and defaults equal to the CLR default (`((0))`) are not reported.
- **CM0035 (warning):** a `[MapReviewed]` entry on a method or the mapper class that silences nothing: no method that uses it (the method, methods inheriting it through `[MapInherit]`, or every method for the class) writes the member as a key, a database-generated value, a navigation, a column with a database default or a string that can be too long. Stale acknowledgements would hide a future problem.
- **CM0036 (info):** a create method copies a value that can be null into a nullable entity member whose column has a database default; EF Core leaves null out of the insert, so the default is stored. Often intended, so information; acknowledge it with `[MapReviewed("X", For = ReviewKind.DatabaseDefault)]`.
- **`[MapReviewed(..., For = ReviewKind.Key)]`**: an acknowledgement can name the kinds of warning it covers (`Key`, `GeneratedValue`, `Navigation`, `DatabaseDefault`, `Length`; `All` by default, as before), so acknowledging a key write no longer hides a length problem on the same member. CM0035 tracks each attribute and its kinds.
- **`[MapReviewed(..., Reason = "...")]`**: the data-flow report lists every acknowledgement with its kinds and reason in a new "Acknowledged with [MapReviewed]" section.
- The EF model reader also reads `HasDefaultValue(...)` and `HasSentinel(...)`.
- Scaffolded-model tests: code shaped exactly like `dotnet ef dbcontext scaffold` output, with every diagnostic of a typical row copy listed, so false positives on scaffolded models fail the build of the library.

### Testing
- **Version check:** `MapperAssert` methods that read generated metadata (`WriteSets`, `Lineage`, `GeneratedCode`) fail with both versions when the generator's major or minor version differs from ConventionMapper.Testing's, instead of failing in confusing ways.
- `SampleDataOptions.Clone()`: an independent copy, to add values for one run.

### ConventionMapper.Testing.EntityFrameworkCore (new package)
- **`DatabaseAssert.SavesRoundTrip(context, map, source)`**: maps, inserts inside a rolled-back transaction, reads back, and fails for every value the database changed: a default that replaced a copied `false` (CM0034 in action), cut text, rounded numbers; failed inserts carry the database's message.
- **`DatabaseAssert.AllMappersSaveRoundTrip(context, typeof(Mappings))`**: `SavesRoundTrip` for every generated create method that returns an entity of the context, with sample data and again with `false` and `0` values (the ones a database default replaces). Foreign keys aren't checked inside the rolled-back transaction, so no rows need seeding; CHECK constraints, NOT NULL and unique indexes stay on, so a value the database rejects still fails.
- **`DatabaseAssert.ModelMatchesDatabase(context)`**: compares the EF Core model's tables and columns (existence, nullability, maximum length on SQL Server, defaults) with the real schema, so a scaffolded model that drifted is found before the generator's warnings mislead. SQL Server and SQLite.
- **`DatabaseAssert.ExportSchema(context, "schema.json")`** and **`ModelMatchesSchema(context, "schema.json")`**: export the real schema once (from a copy of production), commit the file, and check the model against it in every test run, without a database.

### Build
- Running the workflow manually on `main` publishes `<next version>-preview.<run>` packages to nuget.org (unless that version is already released), so changes can be tried in a real codebase before the release tag. Previews are published on request because nuget.org keeps every version.
- A `sqlserver` CI job runs the database checks against a real SQL Server 2022 and is required before packing.
- After a release tag, the workflow opens a pull request merging `main` into every `feature/*` branch that doesn't contain it yet.
- Release checklist in [RELEASING.md](https://github.com/katragvb/Conventionmapper/blob/main/RELEASING.md), including trying the preview in a real codebase before tagging.
- Package validation baselines moved to 1.9.1; the new package validates its API across target frameworks from its first release.

Full history: CHANGELOG.md in this package, or https://github.com/katragvb/Conventionmapper/blob/main/CHANGELOG.md