AlephMapper 0.6.1
dotnet add package AlephMapper --version 0.6.1
NuGet\Install-Package AlephMapper -Version 0.6.1
<PackageReference Include="AlephMapper" Version="0.6.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="AlephMapper" Version="0.6.1" />
<PackageReference Include="AlephMapper"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add AlephMapper --version 0.6.1
#r "nuget: AlephMapper, 0.6.1"
#:package AlephMapper@0.6.1
#addin nuget:?package=AlephMapper&version=0.6.1
#tool nuget:?package=AlephMapper&version=0.6.1
Terms of use<sup>?</sup>
By using this project or its source code, for any purpose and in any shape or form, you grant your implicit agreement to all the following statements:
- You condemn Russia and its military aggression against Ukraine
- You recognize that Russia is an occupant that unlawfully invaded a sovereign state
- You support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas
- You reject false narratives perpetuated by Russian state propaganda
To learn more about the war and how you can help, click here. Glory to Ukraine!
AlephMapper
Write a mapping once as ordinary C#. Use it in memory, as an EF Core projection, or to update an existing object.
AlephMapper is a C# source generator for explicit, reusable mappings. It generates companion methods from your expression-bodied mapping code:
Expression<Func<TSource, TDestination>>factories for LINQ providers such as EF Core;- update-in-place overloads for existing destination instances;
- explicitly requested mappings for structurally compatible type pairs.
There is no runtime mapping configuration and no second projection implementation to keep synchronized.
Why AlephMapper?
A normal C# mapping is easy to write, call, debug, and refactor:
public static PersonDto MapPerson(Person person) => new()
{
Id = person.Id,
Name = person.FirstName + " " + person.LastName
};
EF Core projections usually require the same logic in an expression tree:
public static Expression<Func<Person, PersonDto>> MapPersonExpression() =>
person => new PersonDto
{
Id = person.Id,
Name = person.FirstName + " " + person.LastName
};
Nested mappings make this duplication worse because ordinary methods cannot be called transparently inside an expression translated by EF Core. AlephMapper expands supported mapping and helper calls at compile time, generating one provider-visible expression from the original C# methods.
Use AlephMapper when you prefer:
- handwritten mappings over convention-based member discovery;
- compile-time generation over runtime mapping configuration;
- ordinary method composition over manual expression-tree composition;
- inspectable generated code and compiler diagnostics.
AlephMapper does not automatically discover mappings between arbitrary types, and the target LINQ provider still determines which generated expressions it can translate.
Installation
Using the .NET CLI:
dotnet add package AlephMapper
Using PackageReference:
<PackageReference Include="AlephMapper" Version="0.6.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
With Central Package Management:
<PackageVersion Include="AlephMapper" Version="0.6.1" />
<PackageReference Include="AlephMapper">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
When referencing the generator directly from source:
<ProjectReference Include="..\path\to\AlephMapper.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
PrivateAssets="all" prevents AlephMapper from flowing transitively to consumers of your library. IncludeAssets makes its analyzer and source-generator assets available during compilation.
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:
using AlephMapper;
public static partial class PersonMapper
{
[Expressive]
public static PersonDto MapPerson(Employee employee) => new()
{
Id = employee.EmployeeId,
FullName = GetFullName(employee),
Email = employee.ContactInfo.Email,
Department = employee.Department.Name
};
private static string GetFullName(Employee employee) =>
employee.FirstName + " " + employee.LastName;
}
AlephMapper generates a projection companion and inlines the helper:
public static partial class PersonMapper
{
public static Expression<Func<Employee, PersonDto>> MapPersonExpression() =>
employee => new PersonDto
{
Id = employee.EmployeeId,
FullName = employee.FirstName + " " + employee.LastName,
Email = employee.ContactInfo.Email,
Department = employee.Department.Name
};
}
Use the generated expression in an EF Core query:
var people = await dbContext.Employees
.Select(PersonMapper.MapPersonExpression())
.ToListAsync();
Use the original method in memory:
var person = PersonMapper.MapPerson(employee);
Composing mappings and predicates
Supported expression-bodied methods can call other mapping or helper methods. AlephMapper substitutes their arguments and expands their bodies into the generated expression:
public static partial class OrderMapper
{
[Expressive]
public static OrderDto MapOrder(Order order) => new()
{
Id = order.Id,
Customer = MapCustomer(order.Customer),
Lines = order.Lines.Select(MapLine).ToList()
};
private static CustomerDto MapCustomer(Customer customer) => new()
{
Id = customer.Id,
Name = customer.Name
};
private static OrderLineDto MapLine(OrderLine line) => new()
{
ProductId = line.ProductId,
Quantity = line.Quantity
};
}
[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:
public static partial class EmployeeConditions
{
[Expressive]
public static bool IsEligible(Employee employee) =>
IsActive(employee) &&
HasRequiredExperience(employee, 3);
private static bool IsActive(Employee employee) =>
employee.IsActive;
private static bool HasRequiredExperience(
Employee employee,
int minimumYears) =>
employee.YearsOfExperience >= minimumYears;
}
var employees = await dbContext.Employees
.Where(EmployeeConditions.IsEligibleExpression())
.ToListAsync();
This complements rather than replaces runtime predicate builders: use AlephMapper when the condition structure is known at compile time, and a dynamic expression API when runtime input determines the number or shape of conditions.
Context parameters
A mapping may accept values after its source parameter. AlephMapper moves those values to the generated expression factory while keeping a single source parameter in the returned expression:
public static partial class EmployeeMapper
{
[Expressive]
public static EmployeeDto Map(
Employee employee,
int currentYear) => new()
{
Id = employee.Id,
YearsOfExperience = currentYear - employee.StartYear
};
}
The generated signature is:
public static Expression<Func<Employee, EmployeeDto>>
MapExpression(int currentYear) =>
employee => new EmployeeDto
{
Id = employee.Id,
YearsOfExperience = currentYear - employee.StartYear
};
var employees = await dbContext.Employees
.Select(EmployeeMapper.MapExpression(DateTime.UtcNow.Year))
.ToListAsync();
Helpers may also have multiple parameters. Positional and named arguments are substituted according to the helper's declared parameters.
Extension-method inlining
AlephMapper supports expression-bodied extension methods declared with the traditional this parameter syntax:
public static class ProductExtensions
{
public static string FormatPrice(
this Product product,
string prefix) =>
prefix + product.Price;
}
public static partial class ProductMapper
{
[Expressive]
public static ProductDto Map(Product product) => new()
{
Name = product.Name,
Price = product.FormatPrice("$")
};
}
The generated expression contains "$" + product.Price, not a call to FormatPrice. Extension mapping methods can also be used as LINQ method groups:
Addresses = person.Addresses
.Select(AddressMapper.ToDto)
.ToList()
Null-safe mapping at the call site
Extension mappings work naturally with null-conditional access. This places one null check around the complete inlined mapping:
public static partial class AddressMapper
{
public static AddressDto ToDto(this Address address) => new()
{
Street = address.Street,
City = address.City,
Country = address.Country
};
}
[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)]
public static partial class PersonMapper
{
public static PersonDto ToDto(Person person) => new()
{
Name = person.Name,
Address = person.Address?.ToDto()
};
}
AlephMapper inlines ToDto() and generates an expression equivalent to:
person => new PersonDto
{
Name = person.Name,
Address = person.Address != null
? new AddressDto
{
Street = person.Address.Street,
City = person.Address.City,
Country = person.Address.Country
}
: null
}
The equivalent regular static call needs a manual condition:
Address = person.Address != null
? AddressMapper.ToDto(person.Address)
: null
person.Address?.ToDto() is both shorter and more explicit about the intended behavior: the mapping is skipped when the receiver is null. The generated expression checks the nullable boundary once instead of requiring a separate check for every mapped member.
This transformation is not applied automatically to regular method calls. MapAddress(address) always invokes MapAddress, even when address is null, whereas address?.ToDto() skips the invocation. Treating those forms as equivalent could change program behavior.
Apply ?. only at genuinely nullable boundaries. Once inside the non-null extension mapping, access the receiver normally. A receiver must be a stable member-access path. Expression generation is skipped with diagnostic AM0016 when rewriting a receiver such as GetAddress()?.ToDto() could evaluate it more than once.
Modern C# extension blocks are not currently supported.
Null handling
C# null-conditional access (?.) is not directly supported in expression trees. Configure its treatment with NullConditionalRewrite:
[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)]
public static partial class PersonMapper
{
public static PersonDto Map(Person person) => new()
{
Name = person.Name,
City = person.Address?.City
};
}
| Policy | Generated behavior |
|---|---|
None |
Skips expression generation and reports AM0017 because expression trees do not support null-conditional syntax. |
Ignore |
Removes conditional access: person.Address?.City becomes person.Address.City. |
Rewrite |
Emits an explicit check: person.Address != null ? person.Address.City : null. |
Ignore is the default. Use Rewrite when the generated expression must retain the null-safe behavior of the original method.
None does not inline through a conditional extension call because doing so would detach the inlined body from ?. and change its semantics. Expression generation is skipped with AM0017 instead of emitting uncompilable or behavior-changing code. Use Rewrite to produce an expression-compatible explicit null check.
Updating existing objects
Apply [Updatable] to generate an overload that writes mapped properties to an existing destination:
public static partial class PersonMapper
{
[Updatable]
public static Person Map(PersonUpdateDto source) => new()
{
FirstName = source.FirstName,
LastName = source.LastName,
Email = source.Email
};
}
Generated shape:
public static Person Map(PersonUpdateDto source, Person target)
{
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Email = source.Email;
return target;
}
This preserves EF Core's tracked instance:
var person = await dbContext.People.FindAsync(id);
PersonMapper.Map(request, target: person);
await dbContext.SaveChangesAsync();
Collection properties are skipped by default. Enable their update explicitly:
[Updatable(CollectionProperties = CollectionPropertiesPolicy.Update)]
public static Order Map(OrderRequest source) => new()
{
Lines = source.Lines.Select(MapLine).ToList()
};
Replacing tracked collections can affect relationships and persistence behavior, so opt in deliberately. Update generation for value-type destinations reports AM0001 because value types do not provide useful update-in-place semantics.
Adapting a mapping template
[Adapt] reuses a mapping body for one explicitly declared source/destination pair. The types need not share a base type or interface; AlephMapper validates the members, constructors, and conversions required by the template.
public static partial class PersonMapper
{
[Adapt(
typeof(Employee),
typeof(EmployeeDto),
Name = "MapEmployee",
Generate = AdaptGeneration.Map | AdaptGeneration.Expression)]
public static PersonDto MapPerson(Person source) => new()
{
Id = source.Id,
Name = source.FirstName + " " + source.LastName,
Email = source.Email
};
}
AlephMapper generates:
public static EmployeeDto MapEmployee(Employee source) => new()
{
Id = source.Id,
Name = source.FirstName + " " + source.LastName,
Email = source.Email
};
public static Expression<Func<Employee, EmployeeDto>>
MapEmployeeExpression() =>
source => new EmployeeDto
{
Id = source.Id,
Name = source.FirstName + " " + source.LastName,
Email = source.Email
};
Generate defaults to AdaptGeneration.Map | AdaptGeneration.Expression. Combine Map, Expression, and Update as needed.
Name is required when expression generation is requested. For map-only or update-only adaptations, omitting it uses the template method's name. Additional template parameters are preserved in generated signatures.
Adaptation is explicit: AlephMapper does not scan for compatible types. Invalid adaptations report diagnostics AM0005 through AM0015. See the [Adapt] technical guide for validation rules and examples.
Generated API
| Attribute | Generated member |
|---|---|
[Expressive] |
<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.
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.
Inspecting generated code
To emit generated files from a consuming project:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>
$(BaseIntermediateOutputPath)Generated
</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Inspect generated code to confirm method inlining, null handling, adapted members, and the exact expression supplied to EF Core.
Troubleshooting
A generated method is missing
Confirm that the mapper class is static partial, the method is static and expression-bodied, and the appropriate attribute is applied to the method or class. For [Adapt], check the explicit source/destination types and provide Name when generating an expression.
EF Core cannot translate a generated expression
AlephMapper generates an expression tree; EF Core and its database provider translate it. Inspect the generated expression and the provider exception for unsupported operations.
A helper or extension method was not inlined
The method must be visible in the current compilation and use a supported expression-bodied shape. Extension methods must use the traditional this parameter syntax. Circular helper calls report AM0002 or AM0003 and skip the affected generation.
?. causes a NullReferenceException
The default Ignore policy removes null-conditional access from generated expressions. Select NullConditionalRewrite.Rewrite to generate explicit null checks.
Comparison
| Tool | Primary approach | AlephMapper's focus |
|---|---|---|
| AutoMapper | Runtime configuration and conventions | Explicit C# mappings with compile-time companions |
| Mapster | Configuration/conventions with runtime and generated options | Handwritten mapping methods as the source of truth |
| Mapperly | Compile-time mapping generation from declarations and conventions | Expressions and updates derived from an existing implementation |
| EntityFrameworkCore.Projectables | Projectable members expanded for EF Core | Complete mappings, predicates, updates, and explicit adaptation |
| Expressionify | Expression expansion | Mapping-oriented companion generation |
| LINQKit | Runtime/query-time expression composition and expansion | Compile-time expansion of statically known composition |
Examples
Contributing
Contributions are welcome.
- Fork the repository.
- Create a feature branch.
- Make the change.
- Add or update tests.
- Run the test suite.
- Open a pull request.
License
AlephMapper is licensed under the MIT License.
Acknowledgments
AlephMapper was inspired by EntityFrameworkCore.Projectables and Expressionify. Thanks to all contributors.
Related projects
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.6.1 | 39 | 7/31/2026 |
| 0.6.0 | 99 | 7/23/2026 |
| 0.5.9 | 96 | 7/22/2026 |
| 0.5.8 | 91 | 7/21/2026 |
| 0.5.7 | 101 | 7/13/2026 |
| 0.5.6 | 93 | 7/12/2026 |
| 0.5.5 | 150 | 3/12/2026 |
| 0.5.4 | 120 | 3/11/2026 |
| 0.5.3 | 124 | 2/17/2026 |
| 0.5.2 | 135 | 1/4/2026 |
| 0.5.1 | 140 | 1/4/2026 |
| 0.5.0 | 262 | 12/14/2025 |
| 0.4.4 | 257 | 11/22/2025 |
| 0.4.3 | 344 | 11/21/2025 |
| 0.4.2 | 406 | 11/21/2025 |
| 0.4.1 | 446 | 11/20/2025 |
| 0.4.0 | 208 | 11/15/2025 |
| 0.3.9 | 208 | 11/15/2025 |
| 0.3.8 | 224 | 11/15/2025 |
| 0.3.7 | 227 | 11/15/2025 |