An incremental source generator for .NET that automatically generates mappers between classes with support for granular transformations.
- Automatic Mapping: Generate bidirectional mappers between classes marked with
[Mapped] - Granular Transformations: Apply property-specific transformations using
[Transform] - Custom Target Property Names: Map source properties to different target property names
- Multi-Target Support: Map one class to multiple destinations with different transformations
- Incremental Generation: Optimized performance with Incremental Source Generators
- Type Safety: All validation at compile-time, zero runtime overhead
- Collection Support: Automatic mapping of
IEnumerablecollections
Add the Core project as an analyzer in your .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<!-- Enable generated files for debugging -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="true" />
</ItemGroup>
</Project>using Core.Attributes;
namespace MyApp.Models
{
[Mapped(typeof(PersonDto))]
public class Person
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
// Transform email to uppercase when mapping to PersonDto
[Transform(typeof(PersonDto), typeof(StringToUpperTransformationHandler))]
public string Email { get; set; }
}
public class PersonDto
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
}using MyApp.Models;
using MyApp.Models.Generated;
var person = new Person
{
Id = Guid.NewGuid(),
FirstName = "John",
LastName = "Doe",
Age = 30,
Email = "john.doe@example.com"
};
// Map to DTO - email will be "JOHN.DOE@EXAMPLE.COM"
var dto = person.MapPersonToPersonDto();
// Map back to entity
var personBack = dto.MapPersonDtoToPerson();
// Map collections
var people = new List<Person> { person };
var dtos = people.MapPersonToPersonDto();Marks a class for automatic mapper generation.
Parameters:
targetType: The destination type to map to
Features:
- Repeatable: Yes (map to multiple targets)
- Generates bidirectional mapping methods
- Generates collection mapping methods
Example:
[Mapped(typeof(PersonDto))]
[Mapped(typeof(PersonViewModel))]
public class Person
{
public string Name { get; set; }
}Applies a custom transformation to a property during mapping.
Parameters:
targetType: The destination type for this transformationhandlerType: The handler type implementingITransformationHandler<TInput, TOutput>
Features:
- Repeatable: Yes (different transformations for different targets)
- Applied only in forward direction (source β target)
- Type-safe at compile time
Example:
[Transform(typeof(PersonDto), typeof(StringToUpperTransformationHandler))]
public string Email { get; set; }NEW: Applies a custom transformation to a property and maps it to a different property name in the target.
Parameters:
targetType: The destination type for this transformationtargetPropertyName: The name of the property in the target class (can benullto use the same name)handlerType: The handler type implementingITransformationHandler<TInput, TOutput>
Example:
[Mapped(typeof(PersonDto))]
public class Person
{
// Map Name property to NameLength property with transformation
[Transform(typeof(PersonDto), nameof(PersonDto.NameLength), typeof(StringLengthHandler))]
public string Name { get; set; }
}
public class PersonDto
{
public int NameLength { get; set; }
}
public class StringLengthHandler : ITransformationHandler<string, int>
{
public int Handle(string input) => input?.Length ?? 0;
}public interface ITransformationHandler<TInput, TOutput>
{
TOutput Handle(TInput input);
}public class StringToUpperTransformationHandler : ITransformationHandler<string, string>
{
public string Handle(string input) => input?.ToUpper();
}public class EmailFormatterHandler : ITransformationHandler<string, string>
{
public string Handle(string input)
{
if (string.IsNullOrWhiteSpace(input))
return input;
return input.Trim().ToLowerInvariant();
}
}public class DateToStringHandler : ITransformationHandler<DateTime, string>
{
public string Handle(DateTime input)
{
return input.ToString("yyyy-MM-dd");
}
}public class PhoneFormatterHandler : ITransformationHandler<string, string>
{
public string Handle(string input)
{
if (string.IsNullOrWhiteSpace(input))
return input;
var cleaned = Regex.Replace(input, @"[^\d]", "");
if (cleaned.Length == 10)
return $"({cleaned.Substring(0, 3)}) {cleaned.Substring(3, 3)}-{cleaned.Substring(6, 4)}";
return input;
}
}public class StringToLengthHandler : ITransformationHandler<string, int>
{
public int Handle(string input) => input?.Length ?? 0;
}
// Usage:
[Transform(typeof(PersonDto), "NameLength", typeof(StringToLengthHandler))]
public string Name { get; set; }[Mapped(typeof(PersonDto))]
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
}
public class PersonDto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
// Usage:
var dto = person.MapPersonToPersonDto();
var back = dto.MapPersonDtoToPerson();[Mapped(typeof(PersonDto))]
[Mapped(typeof(PersonViewModel))]
public class Person
{
public string Email { get; set; }
// Uppercase for DTO, lowercase for ViewModel
[Transform(typeof(PersonDto), typeof(ToUpperHandler))]
[Transform(typeof(PersonViewModel), typeof(ToLowerHandler))]
public string DisplayEmail { get; set; }
}
// Usage:
var dto = person.MapPersonToPersonDto(); // DisplayEmail in uppercase
var viewModel = person.MapPersonToPersonViewModel(); // DisplayEmail in lowercase[Mapped(typeof(PersonDto))]
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
// Transform Name to NameLength in PersonDto
[Transform(typeof(PersonDto), nameof(PersonDto.NameLength), typeof(StringToLengthHandler))]
public string FullName { get; set; }
}
public class PersonDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public int NameLength { get; set; }
}
// Usage:
var person = new Person { Name = "John", FullName = "John Doe" };
var dto = person.MapPersonToPersonDto();
// dto.NameLength will be 8 (length of "John Doe")var people = new List<Person>
{
new Person { Name = "John", Age = 30 },
new Person { Name = "Jane", Age = 25 }
};
// Map collection
var dtos = people.MapPersonToPersonDto();
// Map back
var personsBack = dtos.MapPersonDtoToPerson();[Mapped(typeof(AddressDto))]
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
[Mapped(typeof(PersonDto))]
public class Person
{
public string Name { get; set; }
public Address Address { get; set; }
}
public class PersonDto
{
public string Name { get; set; }
public AddressDto Address { get; set; }
}
// Usage - nested objects are automatically mapped
var dto = person.MapPersonToPersonDto();For this source class:
[Mapped(typeof(PersonDto))]
public class Person
{
public Guid Id { get; set; }
[Transform(typeof(PersonDto), typeof(StringToUpperTransformationHandler))]
public string Name { get; set; }
}The generator creates:
namespace MyApp.Models.Generated
{
public static class PersonMapper
{
public static PersonDto MapPersonToPersonDto(this Person obj)
{
return new()
{
Id = obj.Id,
Name = new StringToUpperTransformationHandler().Handle(obj.Name),
};
}
public static Person MapPersonDtoToPerson(this PersonDto obj)
{
return new()
{
Id = obj.Id,
Name = obj.Name,
};
}
public static IEnumerable<PersonDto> MapPersonToPersonDto(this IEnumerable<Person> obj)
=> obj.Where(o => o != null).Select(MapPersonToPersonDto);
public static IEnumerable<Person> MapPersonDtoToPerson(this IEnumerable<PersonDto> obj)
=> obj.Where(o => o != null).Select(MapPersonDtoToPerson);
}
}Enable in your .csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>Files will be in: obj\{Configuration}\{TargetFramework}\generated\
<ItemGroup>
<Compile Include="$(CompilerGeneratedFilesOutputPath)\**\*.cs"
Visible="false" />
</ItemGroup>β Mapped:
- Public properties
- Non-static properties
- Properties with getters and setters
- Properties with matching names (by default)
- Properties with transformations (can have different names)
β Not Mapped:
- Private or protected properties
- Static properties
- Read-only properties (in target class)
- Properties without matching names (unless using transformation with target property name)
- β Transformations are applied only in forward direction (source β target)
- β Reverse mapping (target β source) uses direct property copying (no transformation)
- β Multiple transformations can target different destination types
- β Transformations with custom target property names allow mapping between properties with different names
Problem: Mapper methods don't appear after adding [Mapped].
Solutions:
- Clean and rebuild the solution
- Verify the target type is accessible
- Check that the Core project is referenced as an Analyzer
- Restart your IDE
Problem: Can't find the .Generated namespace.
Solutions:
- Rebuild the project
- Check the analyzer reference configuration
- Verify the source generator is loaded (check Analyzers node in Solution Explorer)
Problem: [Transform] attribute has no effect.
Solutions:
- Verify the target type matches exactly
- Check the handler implements
ITransformationHandler<TInput, TOutput> - Ensure the handler is accessible from the generated code
- Verify the property type matches the handler's input type
Problem: A property doesn't appear in the generated mapper.
Solutions:
- Check the property is public
- Verify the property has both getter and setter
- Ensure the target class has a property with the same name (or use Transform with target property name)
- Check the target property is not read-only
- .NET Standard 2.0+ (for the Core library)
- C# 7.3+ (for the Core library)
- Any .NET version for consumer projects (.NET 6+, .NET Framework 4.7.2+, etc.)
- Microsoft.CodeAnalysis.CSharp 4.0.0+
- Keep transformation handlers simple: They run at mapping time
- Use meaningful names: Generated methods are based on class names
- Document custom handlers: Especially for complex transformations
- Test transformations: Transformations are only applied in one direction
- Consider performance: Transformations are executed every time you map
- Use target property names wisely: Only when source and target have different property names
- Null handling: Always handle null in custom transformation handlers
This project is open source. Feel free to use and modify as needed.
Contributions are welcome! Feel free to:
- Report bugs
- Suggest new features
- Submit pull requests