Terms of use?
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 is a C# source generator for reusable manual mappings. Write one expression-bodied mapping method and generate companion methods for EF Core projections, optional update-in-place mapping, and explicitly declared structural adaptations from the same code.
Use it when you want hand-written mapping logic without maintaining a separate Expression<Func<...>> for queries or a second method for updating existing objects.
- Single-source mappings - use one mapping method for runtime mapping, query projection, and update variants.
- EF Core-friendly projections - generate
Expression<Func<TSource, TDestination>>methods for.Select(...). - Method inlining - reuse small helper methods in mappings and let AlephMapper inline them into generated code.
- Configurable null handling - choose how null-conditional access (
?.) is handled in generated expressions. - Update-in-place mapping - mutate existing destination instances, including EF Core tracked entities.
- Explicit mapping adaptation - reuse a mapping template for a specified source/destination type pair with
[Adapt].
Using the .NET CLI:
dotnet add package AlephMapperUsing PackageReference:
<PackageReference Include="AlephMapper" Version="0.5.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>With Central Package Management:
<!-- Directory.Packages.props -->
<PackageVersion Include="AlephMapper" Version="0.5.7" />
<!-- Project file -->
<PackageReference Include="AlephMapper">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>When referencing AlephMapper directly from source, add it as an analyzer-only project reference:
<ProjectReference Include="..\path\to\AlephMapper.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />PrivateAssets="all" keeps AlephMapper from flowing transitively to consumers of your library, and IncludeAssets ensures the analyzer/source-generator assets are available at build time.
- Mapping methods must be
static, expression-bodied, and declared in astatic partialclass.
Add using AlephMapper;, then mark a mapping method or mapper class with [Expressive].
using AlephMapper;
public static partial class PersonMapper
{
[Expressive]
public static PersonDto MapToPerson(Employee employee) => new()
{
Id = employee.EmployeeId,
FullName = GetFullName(employee),
Email = employee.ContactInfo.Email,
Department = employee.Department.Name
};
public static string GetFullName(Employee employee) =>
employee.FirstName + " " + employee.LastName;
}AlephMapper generates a projection companion method:
public static partial class PersonMapper
{
public static Expression<Func<Employee, PersonDto>> MapToPersonExpression() =>
employee => new PersonDto
{
Id = employee.EmployeeId,
FullName = employee.FirstName + " " + employee.LastName,
Email = employee.ContactInfo.Email,
Department = employee.Department.Name
};
}Use the generated expression in EF Core queries:
var people = await dbContext.Employees
.Select(PersonMapper.MapToPersonExpression())
.ToListAsync();Use the original method for in-memory mapping:
var employee = GetEmployee();
var dto = PersonMapper.MapToPerson(employee);C# null-conditional access (?.) is not directly supported in expression trees. AlephMapper lets you choose how to handle it.
[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)]
public static partial class PersonMapper
{
public static PersonSummary GetSummary(Person person) => new()
{
Name = person.Name,
City = person.Address?.City,
HasAddress = person.Address != null
};
}Available policies:
| Policy | Behavior |
|---|---|
None |
Reports unsupported null-conditional usage. |
Ignore |
Removes null-conditional access, for example person.Address?.City becomes person.Address.City. |
Rewrite |
Emits explicit null checks, for example person.Address != null ? person.Address.City : null. |
Ignore is the default. Use Rewrite when you want generated expressions to preserve null-safe behavior.
Mark a mapping with [Updatable] to generate an overload that writes into an existing destination instance.
using AlephMapper;
public static partial class PersonMapper
{
[Updatable]
public static Person MapToPerson(PersonUpdateDto dto) => new()
{
FirstName = dto.FirstName,
LastName = dto.LastName,
Email = dto.Email
};
}Generated usage:
var person = await db.People.FindAsync(id);
PersonMapper.MapToPerson(dto, target: person);
await db.SaveChangesAsync();This is useful with EF Core change tracking because the tracked entity instance is preserved.
Collection properties are skipped by default. To update collections, configure the policy:
[Updatable(CollectionProperties = CollectionPropertiesPolicy.Update)]
public static Person MapToPerson(PersonUpdateDto dto) => new()
{
Orders = dto.Orders.Select(OrderMapper.MapToOrder).ToList()
};Use [Adapt] when two type pairs share the mapping shape but are not the same declared types. It reuses a mapping method as a compile-time template for the explicit source and destination types supplied to the attribute; it does not scan for or infer compatible types.
using AlephMapper;
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
};
}The original MapPerson(Person) method is unchanged. AlephMapper generates an adapted map and expression companion using the specified types:
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 the flags to request a map, expression, and/or update overload. Name is required whenever expression generation is enabled; for map-only or update-only adaptations, omitting it generates an overload using the template method's name.
Before generating code, AlephMapper checks the members and conversions used by the template: the adapted source must expose the used readable member paths, the adapted destination must have compatible writable members and constructors, and generated member signatures must not conflict. Invalid adaptations report AM0005–AM0015 diagnostics.
For each [Expressive] method, AlephMapper generates a method named <OriginalMethodName>Expression() returning Expression<Func<...>>.
public static PersonDto MapToPerson(Employee employee) => ...
public static Expression<Func<Employee, PersonDto>> MapToPersonExpression() => ...For each [Updatable] method, AlephMapper generates an overload with a target parameter.
public static PersonDto MapToPerson(Employee employee) => ...
public static PersonDto MapToPerson(Employee employee, PersonDto target) => ...For each [Adapt] declaration, AlephMapper substitutes the explicitly declared source and destination types into the template body and generates the requested map, expression, and/or update members. Additional template method parameters are preserved in each generated signature.
Helper methods in the same mapper class are inlined where possible.
Methods must be:
- expression-bodied (
=>) static- declared in a
static partialclass - visible to the source generator in the current compilation
AlephMapper is best suited for object initializer mappings and small helper methods that can be inlined into generated expressions.
Check that the mapper class is static partial, the method is expression-bodied, and the method or containing class has [Expressive] or [Updatable]. For adaptation, ensure the method has an [Adapt] declaration with explicit source and destination types.
Check that the adapted source exposes every member path used by the template, the adapted destination has compatible writable members and constructors, and the generated name does not conflict with another member. Name is required when Generate includes AdaptGeneration.Expression.
The default null policy is Ignore, which removes null-conditional access in generated expressions. Use:
[Expressive(NullConditionalRewrite = NullConditionalRewrite.Rewrite)][Updatable] is intended for reference-type destinations. Value types are copied by value, so update-in-place semantics are not useful.
Generation is skipped when AlephMapper detects circular references between mapping/helper methods. Break the cycle or keep part of the logic outside the generated mapping path.
Add this to your project file:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>Generated files are emitted under the compiler-generated files output directory for your project.
| Tool | Main style | AlephMapper difference |
|---|---|---|
| AutoMapper | Runtime configuration and conventions | AlephMapper keeps mappings as ordinary C# methods. |
| Mapster | Convention/configuration mapping with code generation options | AlephMapper focuses on source-generating companions from manual mappings. |
| EntityFrameworkCore.Projectables | Projectable members for EF Core | AlephMapper targets mapping methods and update overloads. |
| Expressionify | Expression expansion | AlephMapper is mapping-oriented and also supports update-in-place generation. |
- Sample app - richer mapping examples.
[Adapt]technical guide - usage, validation rules, diagnostics, and implementation details.- Integration tests - EF Core and generated behavior coverage.
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.
This project is licensed under the MIT License. See LICENSE for details.
- Inspired by EntityFrameworkCore.Projectables and Expressionify.
- Thanks to all contributors.