Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
version:
required: false
type: string
default: '1.1.0-dev'
default: '1.2.0-dev'
workflow_dispatch:
inputs:
deploy:
Expand All @@ -33,7 +33,7 @@ on:
version:
required: false
type: string
default: '1.1.0-dev'
default: '1.2.0-dev'
description: The version of DomainMapper to be referenced from the documentation
pull_request:
branches:
Expand Down Expand Up @@ -99,7 +99,7 @@ jobs:
# we never want to deploy with no version set
# stop the deployment here
- name: Version not set
if: ${{ inputs.version == '' || inputs.version == '1.1.0-dev' }}
if: ${{ inputs.version == '' || inputs.version == '1.2.0-dev' }}
run: exit 1
- uses: actions/deploy-pages@v5
id: deployment
46 changes: 39 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
- run: sudo apt update && sudo apt -y install zipmerge # zipmerge is used to merge the multi target nupkg
- uses: actions/setup-dotnet@v5
- run: dotnet tool restore
- run: RELEASE_VERSION="1.1.0-dev.$GITHUB_RUN_ID" ./build/package.sh
- run: RELEASE_VERSION="1.2.0-dev.$GITHUB_RUN_ID" ./build/package.sh
- uses: actions/upload-artifact@v7
with:
name: domainmapper-nupkg
Expand All @@ -85,9 +85,11 @@ jobs:
name: domainmapper-nupkg
path: artifacts
# Rule 52 anonymously probes repository and project URLs, which cannot succeed while the repository is private.
- run: >-
dotnet meziantou.validate-nuget-package --github-token "$GITHUB_TOKEN"
--excluded-rule-ids 52 ./artifacts/*.nupkg
- run: |
for package in ./artifacts/*.nupkg; do
dotnet meziantou.validate-nuget-package --github-token "$GITHUB_TOKEN" \
--excluded-rule-ids 52 "$package"
done
env:
GITHUB_TOKEN: ${{ github.token }}
integration-test:
Expand Down Expand Up @@ -126,13 +128,20 @@ jobs:
dotnet build
-f net${{ matrix.dotnet }}
-p:DomainMapperPackageSmokeTargetFramework=net${{ matrix.dotnet }}
-p:DomainMapperNugetPackageVersion="1.1.0-dev.$GITHUB_RUN_ID"
-p:DomainMapperNugetPackageVersion="1.2.0-dev.$GITHUB_RUN_ID"
working-directory: ./samples/DomainMapper.PackageSmoke
- run: >-
dotnet run --no-build
-f net${{ matrix.dotnet }}
-p:DomainMapperPackageSmokeTargetFramework=net${{ matrix.dotnet }}
working-directory: ./samples/DomainMapper.PackageSmoke
- run: >-
dotnet run
-f net${{ matrix.dotnet }}
-p:DomainMapperPackageSmokeTargetFramework=net${{ matrix.dotnet }}
-p:DomainMapperProjectionSmoke=true
-p:DomainMapperNugetPackageVersion="1.2.0-dev.$GITHUB_RUN_ID"
working-directory: ./samples/DomainMapper.PackageSmoke
integration-test-net-framework:
needs: package
runs-on: windows-latest
Expand All @@ -151,13 +160,20 @@ jobs:
dotnet build
-f net48
-p:DomainMapperPackageSmokeTargetFramework=net48
-p:DomainMapperNugetPackageVersion=1.1.0-dev.$env:GITHUB_RUN_ID
-p:DomainMapperNugetPackageVersion=1.2.0-dev.$env:GITHUB_RUN_ID
working-directory: ./samples/DomainMapper.PackageSmoke
- run: >-
dotnet run --no-build
-f net48
-p:DomainMapperPackageSmokeTargetFramework=net48
working-directory: ./samples/DomainMapper.PackageSmoke
- run: >-
dotnet run
-f net48
-p:DomainMapperPackageSmokeTargetFramework=net48
-p:DomainMapperProjectionSmoke=true
-p:DomainMapperNugetPackageVersion=1.2.0-dev.$env:GITHUB_RUN_ID
working-directory: ./samples/DomainMapper.PackageSmoke
sample:
runs-on: ubuntu-latest
needs: package
Expand All @@ -175,7 +191,23 @@ jobs:
# use nupkg artifact instead of project references
- run: dotnet nuget add source "$(pwd)/artifacts"
- run: dotnet clean
- run: dotnet build -p:DomainMapperNugetPackageVersion="1.1.0-dev.$GITHUB_RUN_ID"
- run: dotnet build -p:DomainMapperNugetPackageVersion="1.2.0-dev.$GITHUB_RUN_ID"
working-directory: ./samples/DomainMapper.Sample
- run: dotnet run --no-build
working-directory: ./samples/DomainMapper.Sample
native-aot:
needs: package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v5
- uses: actions/download-artifact@v8
with:
name: domainmapper-nupkg
path: artifacts
- run: dotnet nuget add source "$(pwd)/artifacts"
- run: >-
dotnet publish samples/DomainMapper.AotSmoke/DomainMapper.AotSmoke.csproj
--configuration Release -p:HUSKY=0 -p:TreatWarningsAsErrors=true
-p:DomainMapperNugetPackageVersion="1.2.0-dev.$GITHUB_RUN_ID"
- run: ./artifacts/publish/DomainMapper.AotSmoke/release/DomainMapper.AotSmoke
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ All notable changes to DomainMapper are recorded here. The project follows seman

## [Unreleased]

## [1.2.0] - 2026-08-15

### Added

- Explicit `Replace`, `ClearAndFill`, and `Append` existing-target collection policies.
- Invocation-local reference preservation for shared and cyclic mutable object graphs.
- Closed-world runtime dispatch through generated `TryMapRuntime` and `MapRuntime` methods.
- Cached provider-neutral expression projections through the optional `DomainMapper.Projections` package.
- Diagnostics `DMPR105` through `DMPR107` for unsupported reference tracking, projection eligibility, and registry declarations.
- Incremental invalidation, concurrency, trimming, and native AOT validation fixtures.

### Changed

- Incremental generator inputs are fingerprinted per mapper and reachable source contract so isolated changes keep unrelated mapper outputs cached.
- The 1.2 packages validate public API compatibility against the 1.1 release.

### Fixed

- Reference tracking distinguishes target contracts when one source instance participates in heterogeneous target shapes.
- Projection generation rejects failed mappings, custom delegates, and user-defined conversion calls while retaining pure lifted conversions.
- Runtime registries reject open-world interface ambiguity and value-type derived dispatch, handle nullable annotations, and exclude mappings whose deferred helpers fail.
- Incremental invalidation now includes containing partial types and inherited mapper declarations that affect emitted source.

## [1.1.0] - 2026-08-15

### Added
Expand All @@ -30,6 +53,7 @@ All notable changes to DomainMapper are recorded here. The project follows seman
- Compile-time convention mapping for mutable and immutable targets.
- Target-owned and mapper-owned factories, collection and dictionary mapping, and existing-target updates.

[Unreleased]: https://github.com/skuirrels/DomainMapper/compare/v1.1.0...HEAD
[Unreleased]: https://github.com/skuirrels/DomainMapper/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/skuirrels/DomainMapper/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/skuirrels/DomainMapper/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/skuirrels/DomainMapper/releases/tag/v1.0.0
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>1.1.0-dev</Version>
<Version>1.2.0-dev</Version>
<Authors>DomainMapper Contributors</Authors>
<!-- release environment constants -->
<DefineConstants Condition="'$(DOMAINMAPPER_ENVIRONMENT)' == 'next'">$(DefineConstants);ENV_NEXT</DefineConstants>
Expand Down
2 changes: 2 additions & 0 deletions DomainMapper.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
<Project Path="benchmarks/DomainMapper.Benchmarks/DomainMapper.Benchmarks.csproj" />
</Folder>
<Folder Name="/samples/">
<Project Path="samples/DomainMapper.AotSmoke/DomainMapper.AotSmoke.csproj" />
<Project Path="samples/DomainMapper.PackageSmoke/DomainMapper.PackageSmoke.csproj" />
<Project Path="samples/DomainMapper.Sample/DomainMapper.Sample.csproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/DomainMapper.Abstractions/DomainMapper.Abstractions.csproj" />
<Project Path="src/DomainMapper.Projections/DomainMapper.Projections.csproj" />
<Project Path="src/DomainMapper/DomainMapper.csproj" />
</Folder>
<Folder Name="/test/">
Expand Down
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

**Map data. Preserve invariants.**

DomainMapper is a small compile-time mapper for .NET with a domain-driven design bias. Its source generator emits direct C# and does not use runtime reflection. Version `1.1.0` adds explicit mapping contracts while retaining domain-owned constructors and factories.
DomainMapper is a small compile-time mapper for .NET with a domain-driven design bias. Its source generator emits direct C# and does not use runtime reflection. Version `1.2.0` adds opt-in collection policies, reference preservation, runtime dispatch, and query projections while retaining domain-owned constructors and factories.

## Version 1.1.0
## Version 1.2.0

Install DomainMapper from NuGet with:

```bash
dotnet add package DomainMapper --version 1.1.0
dotnet add package DomainMapper --version 1.2.0
```

## Domain-first mapping
Expand All @@ -35,7 +35,7 @@ public static partial class OrderMapper

## Supported mappings

DomainMapper 1.1.0 supports:
DomainMapper 1.2.0 supports:

- mutable targets with accessible parameterless constructors;
- immutable targets and records with accessible constructors;
Expand All @@ -48,14 +48,18 @@ DomainMapper 1.1.0 supports:
- target/source completeness, typed ignores, and allow-listed partial updates;
- conditional and null-aware assignments with constant substitution;
- typed completion hooks, mapping composition, and bounded recursion;
- `Replace`, `ClearAndFill`, and `Append` policies for existing-target collections;
- invocation-local reference preservation for mutable cyclic graphs;
- closed-world generated runtime dispatch with explicit derived-source opt-in;
- cached provider-neutral expression projections through the separate `DomainMapper.Projections` contract package;
- nested and generic mapper types and generic mapping methods;
- direct generated code that enumerates general sequences and preallocates only when the source exposes a count.

Fields participate when they are named by an explicit mapping contract; convention mapping remains property-only for compatibility. See the [authoritative capabilities and limitations](docs/docs/configuration/capabilities.md) and [versioned changelog](CHANGELOG.md).

Construction is fail-closed: every accessible writable target member must be mapped, and source-matched target state that is not writable from the generated mapper is rejected with `DMPR101`.

Query projections, a generated runtime registry, private-member mutation, reference preservation, and derived-type dispatch remain unsupported.
Unsupported projection or tracking shapes fail at build time. Private-member mutation remains unsupported, and no feature scans assemblies, infers persistence semantics, or introduces mutable runtime mapping configuration.

## Build and test

Expand Down Expand Up @@ -84,6 +88,7 @@ See [the benchmark methodology](docs/benchmarks.md).
## Project layout

- `src/DomainMapper.Abstractions` — public, compile-time mapping attributes and policy types.
- `src/DomainMapper.Projections` — optional provider-neutral projection declaration contract.
- `src/DomainMapper/Engine` — contract discovery, semantic planning, conversion policy, and C# emission.
- `test/DomainMapper.Tests` — engine contract and performance-gate tests.
- `benchmarks/DomainMapper.Benchmarks` — balanced DomainMapper-versus-Mapperly evidence.
Expand Down
133 changes: 133 additions & 0 deletions benchmarks/DomainMapper.Benchmarks/AdvancedFeatureBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.Linq.Expressions;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;

namespace DomainMapper.Benchmarks;

[MemoryDiagnoser]
[CategoriesColumn]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[Config(typeof(BalancedComparisonConfig))]
public class AdvancedFeatureBenchmarks
{
private static readonly Expression<Func<BenchmarkRenamedSource, BenchmarkRenamedTarget>> HandWrittenProjection =
source => new BenchmarkRenamedTarget(source.ID, source.DateCreated, source.Warehouse.Description);

private readonly BenchmarkFlatSource _flat = new()
{
Id = 42,
Name = "Ada",
Amount = 12.5m,
CreatedAt = DateTimeOffset.UnixEpoch,
};
private readonly BenchmarkCollectionSource _collection = new([1, 2, 3, 4, 5, 6, 7, 8]);
private readonly BenchmarkCollectionTarget _domainCollection = new();
private readonly BenchmarkCollectionTarget _handCollection = new();
private readonly BenchmarkGraphSource _graph;

public AdvancedFeatureBenchmarks()
{
_graph = new BenchmarkGraphSource { Value = 42 };
_graph.Next = _graph;
}

[Benchmark(Baseline = true)]
[BenchmarkCategory("RegistryDispatch")]
public object HandWrittenRegistry() => HandWrittenMap(_flat, typeof(BenchmarkFlatTarget));

[Benchmark]
[BenchmarkCategory("RegistryDispatch")]
public object DomainMapperRegistry() => DomainMapperBenchmarkMapper.MapRuntime(_flat, typeof(BenchmarkFlatTarget))!;

[Benchmark(Baseline = true)]
[BenchmarkCategory("ReferenceTracking")]
public BenchmarkGraphTarget HandWrittenReferenceTracking() => HandWrittenMapGraph(_graph);

[Benchmark]
[BenchmarkCategory("ReferenceTracking")]
public BenchmarkGraphTarget DomainMapperReferenceTracking() => DomainMapperBenchmarkMapper.MapGraph(_graph);

[Benchmark(Baseline = true)]
[BenchmarkCategory("CollectionClearAndFill")]
public BenchmarkCollectionTarget HandWrittenCollectionMutation()
{
_handCollection.Items.Clear();
foreach (var item in _collection.Items)
_handCollection.Items.Add(item);
return _handCollection;
}

[Benchmark]
[BenchmarkCategory("CollectionClearAndFill")]
public BenchmarkCollectionTarget DomainMapperCollectionMutation()
{
DomainMapperBenchmarkMapper.UpdateCollection(_collection, _domainCollection);
return _domainCollection;
}

[Benchmark(Baseline = true)]
[BenchmarkCategory("ProjectionRetrieval")]
public Expression<Func<BenchmarkRenamedSource, BenchmarkRenamedTarget>> HandWrittenProjectionRetrieval() => HandWrittenProjection;

[Benchmark]
[BenchmarkCategory("ProjectionRetrieval")]
public Expression<Func<BenchmarkRenamedSource, BenchmarkRenamedTarget>> DomainMapperProjectionRetrieval() =>
DomainMapperBenchmarkMapper.ProjectRenamed();

private static object HandWrittenMap(object source, Type targetType)
{
if (source.GetType() == typeof(BenchmarkFlatSource) && targetType == typeof(BenchmarkFlatTarget))
{
var typed = (BenchmarkFlatSource)source;
return new BenchmarkFlatTarget
{
Id = typed.Id,
Name = typed.Name,
Amount = typed.Amount,
CreatedAt = typed.CreatedAt,
};
}
throw new InvalidOperationException();
}

private static BenchmarkGraphTarget HandWrittenMapGraph(BenchmarkGraphSource source)
{
var references = new Dictionary<ReferenceKey, object>();
return MapNode(source, references);
}

private static BenchmarkGraphTarget MapNode(BenchmarkGraphSource source, Dictionary<ReferenceKey, object> references)
{
var referenceKey = new ReferenceKey(source, typeof(BenchmarkGraphTarget));
if (references.TryGetValue(referenceKey, out var existing))
return (BenchmarkGraphTarget)existing;
var target = new BenchmarkGraphTarget { Value = source.Value };
references.Add(referenceKey, target);
target.Next = source.Next is null ? null : MapNode(source.Next, references);
return target;
}

private readonly struct ReferenceKey : IEquatable<ReferenceKey>
{
private readonly object _source;
private readonly Type _targetType;

public ReferenceKey(object source, Type targetType)
{
_source = source;
_targetType = targetType;
}

public bool Equals(ReferenceKey other) => ReferenceEquals(_source, other._source) && _targetType == other._targetType;

public override bool Equals(object? value) => value is ReferenceKey other && Equals(other);

public override int GetHashCode()
{
unchecked
{
return (System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(_source) * 397) ^ _targetType.GetHashCode();
}
}
}
}
Loading
Loading