Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cbe954a
Optional detailed body:
ycherkes Aug 31, 2026
c1956d4
Refactor generator pipeline around mapper-scoped value candidates
ycherkes Aug 31, 2026
21f09b0
Improve attribute discovery, embedding, and incremental coverage
ycherkes Aug 31, 2026
3a5ddbb
Move mapper generation to value-only incremental outputs
ycherkes Aug 31, 2026
20ea129
Remove obsolete mapper lookup and diagnostic reporting paths
ycherkes Aug 31, 2026
58e84b1
bump version to 0.7.0
ycherkes Aug 31, 2026
b21dc7b
Implemented conditional formatting.
ycherkes Aug 31, 2026
9d0faa8
• Fixed the generator and generated-attribute warnings.
ycherkes Aug 31, 2026
a49ef48
fix: preserve nullability in rewritten null-conditional expressions
ycherkes Sep 1, 2026
40e887c
feat: support generic projectable and updatable mappings
ycherkes Sep 1, 2026
eafc167
ci: add NuGet package build workflow
ycherkes Sep 1, 2026
a2b569e
build: harden generator validation and NuGet CI
ycherkes Sep 1, 2026
da7fd92
fix: eliminate nullable warnings across generated mappings
ycherkes Sep 1, 2026
5ccde31
fix: fully qualify type references in generated code
ycherkes Sep 1, 2026
e72d344
fix(generator): centralize nullable policy and preserve source context
ycherkes Sep 1, 2026
a113426
Fixed all actionable findings from PR Review Findings.md.
ycherkes Sep 1, 2026
4e6eeeb
• ## Changes made
ycherkes Sep 1, 2026
573fc11
Improve source generator diagnostic incrementality
ycherkes Sep 1, 2026
07f1771
extract MapperSourceResult to its own file, code cleanup
ycherkes Sep 1, 2026
f6de3af
fix misssng using
ycherkes Sep 1, 2026
00b63d1
fix error message
ycherkes Sep 1, 2026
76046ec
fix build
ycherkes Sep 1, 2026
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
104 changes: 104 additions & 0 deletions .github/workflows/build-nuget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: Build NuGet package

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.400

- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'global.json') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Restore dependencies
run: dotnet restore AlephMapper.slnx

- name: Build solution
run: dotnet build AlephMapper.slnx --configuration Release --no-restore

- name: Run tests
run: dotnet test AlephMapper.slnx --configuration Release --no-build --no-restore -- --report-trx --results-directory artifacts/test-results

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: artifacts/test-results
if-no-files-found: warn

- name: Pack NuGet package
run: dotnet pack source/AlephMapper.csproj --configuration Release --no-restore -p:BuildNumber=${{ github.run_number }} --output artifacts/nuget

- name: Verify package consumption
shell: bash
run: |
package_file=$(find artifacts/nuget -name 'AlephMapper.*.nupkg' -print -quit)
package_version=$(basename "$package_file" | sed -E 's/^AlephMapper\.(.*)\.nupkg$/\1/')
mkdir -p artifacts/package-smoke

cat > artifacts/package-smoke/PackageSmoke.csproj <<EOF
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AlephMapper" Version="$package_version" PrivateAssets="all" />
</ItemGroup>
</Project>
EOF

cat > artifacts/package-smoke/Mapper.cs <<'EOF'
using AlephMapper;

namespace PackageSmoke;

public sealed class Source
{
public string Name { get; set; } = string.Empty;
}

public sealed class Destination
{
public string Name { get; set; } = string.Empty;
}

public static partial class Mapper
{
[Projectable]
public static Destination Map(Source source) => new() { Name = source.Name };
}
EOF

dotnet restore artifacts/package-smoke/PackageSmoke.csproj --source artifacts/nuget
dotnet build artifacts/package-smoke/PackageSmoke.csproj --configuration Release --no-restore

- name: Upload NuGet package
uses: actions/upload-artifact@v4
with:
name: AlephMapper-nuget
path: artifacts/nuget/*.nupkg
if-no-files-found: error
46 changes: 32 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)

## Terms of use<sup>[?](https://github.com/Tyrrrz/.github/blob/master/docs/why-so-political.md)</sup>

Expand Down Expand Up @@ -72,7 +72,7 @@ dotnet add package AlephMapper
Using `PackageReference`:

```xml
<PackageReference Include="AlephMapper" Version="0.6.2">
<PackageReference Include="AlephMapper" Version="0.7.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand All @@ -82,7 +82,7 @@ With Central Package Management:

```xml
<!-- Directory.Packages.props -->
<PackageVersion Include="AlephMapper" Version="0.6.2" />
<PackageVersion Include="AlephMapper" Version="0.7.0" />

<!-- Project file -->
<PackageReference Include="AlephMapper">
Expand All @@ -101,18 +101,36 @@ When referencing the generator directly from source:

`PrivateAssets="all"` prevents AlephMapper from flowing transitively to consumers of your library. `IncludeAssets` makes its analyzer and source-generator assets available during compilation.

### Compiler compatibility

AlephMapper 0.7.0 requires a Roslyn compiler host compatible with `Microsoft.CodeAnalysis` 4.14 or later. This version uses Roslyn's embedded-marker support so its generated configuration attributes remain private to the consuming assembly, including when `InternalsVisibleTo` is used.

### Migration: `[Expressive]` to `[Projectable]`

`[Expressive]` has been renamed to `[Projectable]`. Replace every attribute use directly; no compatibility attribute is emitted.

```csharp
// Before
[Expressive]
public static PersonDto Map(Person person) => new();

// After
[Projectable]
public static PersonDto Map(Person person) => new();
```

## Quick start

Mapping methods must be `static`, expression-bodied, and declared in a `static partial` class.

Add `using AlephMapper;`, then apply `[Expressive]` to a mapping method or its containing class:
Add `using AlephMapper;`, then apply `[Projectable]` to a mapping method or its containing class:

```csharp
using AlephMapper;

public static partial class PersonMapper
{
[Expressive]
[Projectable]
public static PersonDto MapPerson(Employee employee) => new()
{
Id = employee.EmployeeId,
Expand Down Expand Up @@ -163,7 +181,7 @@ Supported expression-bodied methods can call other mapping or helper methods. Al
```csharp
public static partial class OrderMapper
{
[Expressive]
[Projectable]
public static OrderDto MapOrder(Order order) => new()
{
Id = order.Id,
Expand All @@ -185,12 +203,12 @@ public static partial class OrderMapper
}
```

`[Expressive]` is not limited to object projections. A method returning `bool` generates an `Expression<Func<TSource, bool>>`, allowing statically known conditions to be composed as ordinary methods:
`[Projectable]` is not limited to object projections. A method returning `bool` generates an `Expression<Func<TSource, bool>>`, allowing statically known conditions to be composed as ordinary methods:

```csharp
public static partial class EmployeeConditions
{
[Expressive]
[Projectable]
public static bool IsEligible(Employee employee) =>
IsActive(employee) &&
HasRequiredExperience(employee, 3);
Expand Down Expand Up @@ -220,7 +238,7 @@ A mapping may accept values after its source parameter. AlephMapper moves those
```csharp
public static partial class EmployeeMapper
{
[Expressive]
[Projectable]
public static EmployeeDto Map(
Employee employee,
int currentYear) => new()
Expand Down Expand Up @@ -266,7 +284,7 @@ public static class ProductExtensions

public static partial class ProductMapper
{
[Expressive]
[Projectable]
public static ProductDto Map(Product product) => new()
{
Name = product.Name,
Expand Down Expand Up @@ -298,7 +316,7 @@ public static partial class AddressMapper
};
}

[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)]
[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)]
public static partial class PersonMapper
{
public static PersonDto ToDto(Person person) => new()
Expand Down Expand Up @@ -347,7 +365,7 @@ Modern C# extension blocks are not currently supported.
C# null-conditional access (`?.`) is not directly supported in expression trees. Configure its treatment with `NullConditionalRewrite`:

```csharp
[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)]
[Projectable(NullConditionalRewrite = NullConditionalRewrite.Rewrite)]
public static partial class PersonMapper
{
public static PersonDto Map(Person person) => new()
Expand Down Expand Up @@ -470,11 +488,11 @@ Adaptation is explicit: AlephMapper does not scan for compatible types. Invalid

| Attribute | Generated member |
| --- | --- |
| `[Expressive]` | `<MethodName>Expression(...)` returning `Expression<Func<TSource, TDestination>>` |
| `[Projectable]` | `<MethodName>Expression(...)` returning `Expression<Func<TSource, TDestination>>` |
| `[Updatable]` | An overload with a final destination parameter named `target` |
| `[Adapt]` | The requested adapted map, expression, and/or update members |

Attributes can be applied to individual methods. `[Expressive]` and `[Updatable]` can also be applied to the containing class.
Attributes can be applied to individual methods. `[Projectable]` and `[Updatable]` can also be applied to the containing class.

AlephMapper is best suited to object initializers, predicates, constructor calls, member access, conversions, LINQ operations, and small expression-bodied methods that it can inline. Not every valid C# construct can be represented in an expression tree, and not every expression-tree operation can be translated by every query provider.

Expand Down
10 changes: 5 additions & 5 deletions docs/Adapt-Attribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Use `[Adapt]` when a mapping body should be shared by explicitly named type pair

For example, `Person` and `Employee` can both contain the fields used by a template, while `PersonDto` and `EmployeeDto` can both receive the initializer assignments. A single `Person -> PersonDto` template can therefore produce a separate `Employee -> EmployeeDto` API.

Use `[Expressive]` when the generated expression is for the original method's declared signature. Use `[Adapt]` when the generated API should use a different, explicit source and/or destination type. The two attributes can be applied to the same template method.
Use `[Projectable]` when the generated expression is for the original method's declared signature. Use `[Adapt]` when the generated API should use a different, explicit source and/or destination type. The two attributes can be applied to the same template method.

## Public API

Expand Down Expand Up @@ -55,7 +55,7 @@ public sealed class AdaptAttribute : Attribute

`SourceType` and `DestinationType` are required `typeof(...)` constructor arguments. Each `[Adapt]` is independent, so a method may be adapted to multiple pairs.

`NullConditionalRewrite` applies while helper methods are inlined for that adaptation. It uses the same policies as `[Expressive]`:
`NullConditionalRewrite` applies while helper methods are inlined for that adaptation. It uses the same policies as `[Projectable]`:

| Value | Effect |
| --- | --- |
Expand Down Expand Up @@ -217,7 +217,7 @@ The feature is implemented as part of the incremental source generator. The foll
| [`source/Generation/MapperFileEmitter.cs`](../source/Generation/MapperFileEmitter.cs) | Recreates the namespace and containing partial-type hierarchy and renders the generated file. |
| [`source/Diagnostics/DiagnosticDescriptors.cs`](../source/Diagnostics/DiagnosticDescriptors.cs) | Defines `AM0005`–`AM0015`. |

The older `[Expressive]` and `[Updatable]` behaviors are emitted by their own focused emitters. All three features share the mapping model, helper inliner, generated-file context, and output renderer.
`[Projectable]` and `[Updatable]` are emitted by their own focused emitters. All three features share the mapping model, helper inliner, generated-file context, and output renderer.

## Generation pipeline

Expand All @@ -226,8 +226,8 @@ The incremental pipeline is registered in [`source/AlephSourceGenerator.cs`](../
1. `AttributeSourceEmitter` adds `AlephMapper.Attributes.g.cs` after initialization so the consumer can use the attributes.
2. `MappingMethodCandidate` identifies method declarations contained in classes.
3. `MappingModelFactory` filters to static classes, resolves symbols, requires at least one parameter and an expression body, and collects the method's adaptation attributes.
4. `MapperSourceOutput` groups mapping models by containing mapper type. A mapper produces output when it is partial and contains an expressive, updatable, or adapted mapping.
5. For each eligible mapping, the output dispatcher runs `ExpressiveMemberEmitter`, `AdaptationMemberEmitter`, and `UpdatableMemberEmitter`.
4. `MapperSourceOutput` groups mapping models by containing mapper type. A mapper produces output when it is partial and contains a projectable, updatable, or adapted mapping.
5. For each eligible mapping, the output dispatcher runs `ProjectableMemberEmitter`, `AdaptationMemberEmitter`, and `UpdatableMemberEmitter`.
6. The adaptation emitter processes every `AdaptationModel` independently:
1. Decodes the requested flags and verifies the naming requirement.
2. Rejects duplicate source/destination pairs on the same template.
Expand Down
2 changes: 1 addition & 1 deletion examples/SampleApp/Mappers/AdaptExampleMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SampleApp.Mappers;

/// <summary>
/// Demonstrates [Adapt]: one mapping template reused for an explicitly declared
/// different source/destination pair. Unlike [Expressive], [Adapt] is not for the
/// different source/destination pair. Unlike [Projectable], [Adapt] is not for the
/// exact method signature types; it structurally substitutes the template source
/// and destination with the explicit types from the attribute.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions examples/SampleApp/Mappers/AddressMapper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using AlephMapper;
using AlephMapper;
using SampleApp.Entities;
using SampleApp.Models;

Expand All @@ -7,7 +7,7 @@ namespace SampleApp.Mappers;
public static partial class AddressMapper
{
// Entity to DTO mapping with expression-bodied syntax
[Expressive]
[Projectable]
public static AddressDto ToDto(this Address entity) => new()
{
Street = entity.StreetAddress,
Expand Down
2 changes: 1 addition & 1 deletion examples/SampleApp/Mappers/EmployeeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace SampleApp.Mappers;
/// </summary>
public static partial class EmployeeMapper
{
[Expressive]
[Projectable]
[Updatable]
public static EmployeeSummaryDto ToSummary(Employee emp, int year) => new()
{
Expand Down
10 changes: 5 additions & 5 deletions examples/SampleApp/Mappers/PersonMapper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using AlephMapper;
using AlephMapper;
using SampleApp.Entities;
using SampleApp.Models;

Expand All @@ -7,9 +7,9 @@ namespace SampleApp.Mappers;
public static partial class PersonMapper
{
// Main entity to DTO mapping with expression-bodied syntax
[Expressive]
[Projectable]
[Updatable(CollectionProperties = CollectionPropertiesPolicy.Skip)]
public static PersonDto ToDto(Person entity) => entity == null ? null : new()
public static PersonDto ToDto(Person entity) => entity == null ? null! : new()
{
Id = entity.PersonId,
FirstName = entity.FirstName,
Expand All @@ -23,7 +23,7 @@ public static partial class PersonMapper

// DTO to entity mapping with expression-bodied syntax
//[Updatable]
[Expressive]
[Projectable]
public static Person ToEntity(PersonDto dto) => new()
{
PersonId = dto.Id,
Expand Down Expand Up @@ -62,4 +62,4 @@ private static void UpdateEntity(Person entity, string firstName, string lastNam
entity.EmailAddress = email;
entity.BirthDate = birthDate;
}
}
}
7 changes: 6 additions & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"sdk": {
"version": "10.0.400",
"rollForward": "latestFeature",
"allowPrerelease": false
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
}
3 changes: 2 additions & 1 deletion source/Adaptation/AdaptationMemberPlanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public AdaptationMemberPlanner(INamedTypeSymbol mapperType)
.Where(m => m.MethodKind == MethodKind.Ordinary)
.Select(m => MethodSignature.Build(
m.Name,
m.Parameters.Select(p => TypeDisplay.ForSymbol(p.Type, p.NullableAnnotation, NullableContext.Disabled)))),
m.Parameters.Select(p => TypeDisplay.ForSymbol(p.Type, p.NullableAnnotation, NullablePolicy.Disabled)),
m.TypeParameters.Length)),
StringComparer.Ordinal);

_existingNonMethodNames = new HashSet<string>(
Expand Down
Loading
Loading