Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

4 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

MapWithSourceGenerator

An incremental source generator for .NET that automatically generates mappers between classes with support for granular transformations.

🌟 Key Features

  • 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 IEnumerable collections

πŸ“¦ Installation

Option 1: Project Reference

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>

πŸš€ Quick Start

1. Mark Your Classes

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; }
    }
}

2. Use the Generated Mappers

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();

πŸ“– Attributes Reference

[Mapped(Type targetType)]

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; }
}

[Transform(Type targetType, Type handlerType)]

Applies a custom transformation to a property during mapping.

Parameters:

  • targetType: The destination type for this transformation
  • handlerType: The handler type implementing ITransformationHandler<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; }

[Transform(Type targetType, string targetPropertyName, Type handlerType)]

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 transformation
  • targetPropertyName: The name of the property in the target class (can be null to use the same name)
  • handlerType: The handler type implementing ITransformationHandler<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;
}

πŸ”§ Transformation Handlers

Interface

public interface ITransformationHandler<TInput, TOutput>
{
    TOutput Handle(TInput input);
}

Built-in Handlers

StringToUpperTransformationHandler

public class StringToUpperTransformationHandler : ITransformationHandler<string, string>
{
    public string Handle(string input) => input?.ToUpper();
}

Custom Handler Examples

Email Formatter

public class EmailFormatterHandler : ITransformationHandler<string, string>
{
    public string Handle(string input)
    {
        if (string.IsNullOrWhiteSpace(input))
            return input;
            
        return input.Trim().ToLowerInvariant();
    }
}

Date to String

public class DateToStringHandler : ITransformationHandler<DateTime, string>
{
    public string Handle(DateTime input)
    {
        return input.ToString("yyyy-MM-dd");
    }
}

Phone Number Formatter

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;
    }
}

String to Length (Different Property Name)

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; }

πŸ’‘ Usage Examples

Example 1: Simple Mapping

[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();

Example 2: Multiple Targets with Different Transformations

[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

Example 3: Mapping to Different Property Names

[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")

Example 4: Collection Mapping

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();

Example 5: Nested Objects

[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();

πŸ” Generated Code Example

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);
    }
}

βš™οΈ Configuration & Debugging

View Generated Files

Enable in your .csproj:

<PropertyGroup>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Files will be in: obj\{Configuration}\{TargetFramework}\generated\

Include Generated Files in Solution (Optional)

<ItemGroup>
    <Compile Include="$(CompilerGeneratedFilesOutputPath)\**\*.cs" 
             Visible="false" />
</ItemGroup>

πŸ“‹ Rules & Conventions

What Gets Mapped

βœ… 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)

Transformation Rules

  • βœ… 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

πŸ› οΈ Troubleshooting

Mapper Not Generated

Problem: Mapper methods don't appear after adding [Mapped].

Solutions:

  1. Clean and rebuild the solution
  2. Verify the target type is accessible
  3. Check that the Core project is referenced as an Analyzer
  4. Restart your IDE

"Generated namespace not found" Error

Problem: Can't find the .Generated namespace.

Solutions:

  1. Rebuild the project
  2. Check the analyzer reference configuration
  3. Verify the source generator is loaded (check Analyzers node in Solution Explorer)

Transformations Not Applied

Problem: [Transform] attribute has no effect.

Solutions:

  1. Verify the target type matches exactly
  2. Check the handler implements ITransformationHandler<TInput, TOutput>
  3. Ensure the handler is accessible from the generated code
  4. Verify the property type matches the handler's input type

Property Not Mapped

Problem: A property doesn't appear in the generated mapper.

Solutions:

  1. Check the property is public
  2. Verify the property has both getter and setter
  3. Ensure the target class has a property with the same name (or use Transform with target property name)
  4. Check the target property is not read-only

πŸ“ Requirements

  • .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+

🎯 Best Practices

  1. Keep transformation handlers simple: They run at mapping time
  2. Use meaningful names: Generated methods are based on class names
  3. Document custom handlers: Especially for complex transformations
  4. Test transformations: Transformations are only applied in one direction
  5. Consider performance: Transformations are executed every time you map
  6. Use target property names wisely: Only when source and target have different property names
  7. Null handling: Always handle null in custom transformation handlers

πŸ“„ License

This project is open source. Feel free to use and modify as needed.

🀝 Contributing

Contributions are welcome! Feel free to:

  • Report bugs
  • Suggest new features
  • Submit pull requests

πŸ“š Additional Resources

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages