Mapperly Documentation

repository·main·Indexed 25 days ago

https://github.com/riok/mapperly

Mapperly is a .NET source generator that creates object mapping code at build time to ensure minimal runtime overhead and verifiable code. The documentation covers installation via the Riok.Mapperly NuGet package, usage of the [Mapper] attribute, configuration of additional mapping parameters, and detailed migration guides for versions 3.0, 4.0, and 5.0.

Tokens
27.9K
Snippets
89
Records
130
Agent score
87%

What's inside Mapperly

  1. Overview of Mapperly

    main

    Mapperly is a .NET source generator designed to automate object-to-object mappings. Instead of writing tedious boilerplate code by hand, you only need to define the method signature for the mapping, and Mapperly generates the implementation at build time.

    Key characteristics:

    • Build-time generation: Code is generated during compilation, resulting in minimal runtime overhead.
    • No Reflection: Because it avoids reflection, it is fully trimming-safe and AoT (Ahead-of-Time) friendly.
    • Readable & Debuggable: The generated code is standard C# that you can inspect and debug.
    • Zero Runtime Dependency: Mapperly does not require a runtime library to function.
    • Pluggable: You can implement specific mappings manually, and Mapperly will pick them up and use them during the generation process.
  2. Choose a Mapperly release channel

    main

    Mapperly is distributed through two channels:

    • Stable Channel: Production-ready releases subject to semantic versioning. Use this for production environments.
    • Next Channel: Preview releases containing upcoming features. These may contain breaking changes and are not subject to semantic versioning. Use this for testing and early access.
  3. Use IQueryable projections for ORM optimization

    main

    Mapperly supports IQueryable<T> projections, which is highly effective for optimizing ORM performance (e.g., with Entity Framework). Only the fields present in the target class are retrieved from the database.

    To implement this, define a partial method in your mapper that extends IQueryable<TSource> and returns IQueryable<TTarget>.

    [Mapper]
    public static partial class CarMapper
    {
        public static partial IQueryable<CarDto> ProjectToDto(this IQueryable<Car> q);
    }
    
    // Usage
    var dtos = await DbContext.Cars
        .Where(...)
        .ProjectToDto()
        .ToListAsync();
  4. Map private and protected members

    main

    Mapperly can map inaccessible members like private or protected properties using the UnsafeAccessorAttribute. This approach is AOT-safe and has zero performance overhead compared to ordinary property access.

    By default, IncludedMembers and IncludedConstructors are set to MemberVisibility.AllAccessible, which only maps members that are ordinarily visible to external types. To enable mapping of inaccessible members, you must set these properties to MemberVisibility.All in the [Mapper] attribute.

    [Mapper(
        IncludedMembers = MemberVisibility.All,
        IncludedConstructors = MemberVisibility.All)]
    public partial class FruitMapper
    {
        public partial FruitDto ToDto(Fruit source);
    }
    
    public class Fruit
    {
      private bool _isSeeded;
      public string Name { get; set; }
      private int Sweetness { get; set; }
    }
    
    public class FruitDto
    {
      private FruitDto() {}
      private bool _isSeeded;
      public string Name { get; set; }
      private int Sweetness { get; set; }
    }
  5. Manually map properties to constructor parameters

    main

    When source property names do not match target constructor parameter names, use the [MapProperty] attribute to define the mapping.

    • If the target is a record, you can use nameof(TargetType.PropertyName) to refer to the parameter.
    • If the target is a class with a constructor where the parameter name differs from the property name, use a string literal to specify the exact parameter name.
    public class Car
    {
        public string ModelName { get; set; }
    }
    
    public record CarDto(string Model);
    
    [Mapper]
    public partial class CarMapper
    {
        [MapProperty(nameof(Car.ModelName), nameof(CarDto.Model))]
        public partial CarDto ToDto(Car car);
    }
    
    // Using string literal for class constructors with non-matching parameter names
    public class CarDto
    {
        public CarDto(string model) // parameter name is 'model'
        {
            ModelName = model;
        }
    
        public string ModelName { get; }
    }
    
    [Mapper]
    public partial class CarMapper
    {
        [MapProperty(nameof(Car.ModelName), "model")]
        public partial CarDto ToDto(Car car);
    }
  6. Run custom logic before or after a mapping

    main

    To execute custom code during a mapping process, wrap the generated mapping method inside a manual method. Use the [UserMapping(Default = true)] attribute on your manual method to ensure Mapperly uses this wrapper instead of calling the generated partial method directly when a conversion is required.

    [Mapper]
    public partial class CarMapper
    {
        private partial CarDto CarToCarDto(Car car);
    
        // Default ensures Mapperly uses this mapping whenever a conversion
        // from Car to CarDto is needed instead of the `CarToCarDto` method.
        [UserMapping(Default = true)]
        public CarDto MapCarToCarDto(Car car)
        {
            // custom before map code...
            var dto = CarToCarDto(car);
            // custom after map code...
            return dto;
        }
    }
  7. Create your first mapper with [Mapper]

    main

    To define a mapper, create a partial class and decorate it with the [Mapper] attribute from Riok.Mapperly.Abstractions. Define your mapping logic as partial methods. Mapperly will generate the implementation for these methods at build time.

    // Mapper declaration
    [Mapper]
    public partial class CarMapper
    {
        public partial CarDto CarToCarDto(Car car);
    }
    
    // Mapper usage
    var mapper = new CarMapper();
    var car = new Car { NumberOfSeats = 10, ... };
    var dto = mapper.CarToCarDto(car);
    dto.NumberOfSeats.ShouldBe(10);
  8. Use runtime target type parameters

    main

    If the target type of a mapping is unknown at compile time, you can define a mapping method that accepts a Type parameter. Mapperly will implement this by using the user-defined mappings available in the mapper.

    [Mapper]
    public static partial class ModelMapper
    {
        public static partial object Map(object source, Type targetType);
    
        private static partial BananaDto MapBanana(Banana source);
        private static partial AppleDto MapApple(Apple source);
    }
    
    class Banana {}
    class Apple {}
    
    class BananaDto {}
    class AppleDto {}
  9. Configure strict mappings and error handling

    main
    To ensure mapping issues are caught at build time rather than runtime, keep strict mappings enabled (this is the default in Mapperly). For Release builds, it is recommended to treat Mapperly warnings as errors. You can achieve this by using C# compiler options or by configuring individual Mapperly analyzer diagnostics.
  10. Use separate mappers for requests and responses

    main

    When mapping between API models and domain objects, use separate mappers for different directions to correctly apply RequiredMappingStrategy:

    • API Requests → Domain Objects: Use RequiredMappingStrategy.Source to ensure all fields in the request are consumed.
    • Domain Objects → API Responses: Use RequiredMappingStrategy.Target to ensure all fields in the response are populated.
    [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Source)]
    public partial class RequestMapper
    {
        public partial CreateOrderCommand MapRequest(CreateOrderRequest source);
    }
    
    [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
    public partial class ResponseMapper
    {
        public partial GetOrderResponse MapResponse(Order source);
    }