riok / mapperly

A .NET source generator for generating object mappings. No runtime reflection.
https://mapperly.riok.app
Apache License 2.0
2.85k stars 145 forks source link

Docs: add automapper migration guide #397

Open latonz opened 1 year ago

latonz commented 1 year ago

Add a step by step guide on how to migrate from AutoMapper to Mapperly.

tvardero commented 1 year ago

Did a little test, it is quite easy!

Given two entities: Company and Employee. One Company has many Employees, one employee has one company.

Company and it's DTO:

public class Company
{
    public Guid Id { get; init; } = Guid.NewGuid();
    public string Name { get; set; }
    public ICollection<Employee> Employees { get; } = new List<Employee>();

    public Company(string name)
    {
        Name = name;
    }

    public Employee CreateEmployee(string name)
    {
        var employee = new Employee(Guid.NewGuid(), this, name);
        Employees.Add(employee);

        return employee;
    }
}

public record CompanyDto(Guid Id, string Name, EmployeeDto[] Employees)
{
    /// <inheritdoc />
    public virtual bool Equals(CompanyDto? other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;

        return Id.Equals(other.Id)
            && string.Equals(Name, other.Name, StringComparison.InvariantCulture)
            && Employees.SequenceEqual(other.Employees); // <-- this is the only reason to re-implement Equals method
    }

    /// <inheritdoc />
    public override int GetHashCode()
    {
        var hashCode = new HashCode();
        hashCode.Add(Id);
        hashCode.Add(Name, StringComparer.InvariantCulture);
        hashCode.Add(Employees);
        return hashCode.ToHashCode();
    }
}

Employee and it's DTO:

public class Employee
{
    public Guid Id { get; init; }
    public Guid CompanyId { get; init; }
    public Company Company { get; init; }
    public string Name { get; set; }

    public Employee(Guid id, Company company, string name)
    {
        Id = id;
        CompanyId = company.Id;
        Company = company;
        Name = name;
    }
}

public record EmployeeDto(Guid Id, Guid CompanyId, string Name);

Given Automapper profile:

public class AutomapperProfile : Profile
{
    public AutomapperProfile()
    {
        CreateMap<Company, CompanyDto>();
        CreateMap<Employee, EmployeeDto>();
    }
}

Part 1. Without registering to DI container:

Mapperly mapper:

[Mapper]
public partial class MapperlyMapper
{
    [return: NotNullIfNotNull(nameof(source))]
    public partial object? Map(object? source, Type targetType);

    public partial CompanyDto MapToDto(Company company);

    public partial EmployeeDto MapToDto(Employee employee);
}

public static class MapperlyMapperExtensions
{
    [return: NotNullIfNotNull(nameof(source))]
    public static TDestination? Map<TDestination>(this MapperlyMapper mapper, object? source) =>
        (TDestination?) mapper.Map(source, typeof(TDestination));
}

Program.cs:

using AutoMapper;
using MapperlySandbox;

var company = new Company("Company 1");

company.CreateEmployee("Employee 1");
company.CreateEmployee("Employee 2");

var automapperMapper = new AutoMapper.Mapper(new MapperConfiguration(o => o.AddProfile<AutomapperProfile>()));
var mapperlyMapper = new MapperlyMapper(); 

var companyDtoFromAutomapper = automapperMapper.Map<CompanyDto>(company);
var companyDtoFromMapperly = mapperlyMapper.Map<CompanyDto>(company);

if (companyDtoFromAutomapper != companyDtoFromMapperly) throw new Exception("Something went wrong.");

Console.WriteLine("Success!");
Console.WriteLine(companyDtoFromMapperly);

Part 2. Mapperly mapper registered to DI container as AutoMapper IMapper implementation:

Add an adapter class:

public class MapperlyMapperAsAutomapper : IMapper
{
    private MapperlyMapper _mapper = new();

    /// <inheritdoc />
    [return: NotNullIfNotNull(nameof(source))]
    public TDestination?  Map<TDestination>(object? source) => (TDestination?)_mapper.Map(source, typeof(TDestination));

    /// <inheritdoc />
    [return: NotNullIfNotNull(nameof(source))]
    public TDestination? Map<TSource, TDestination>(TSource? source) => Map<TDestination>(source); // possible boxing of value type

    /// <inheritdoc />
    [return: NotNullIfNotNull(nameof(source))]
    public object? Map(object? source, Type sourceType, Type destinationType) => _mapper.Map(source, destinationType);

    // Rest of interface is not implemented (throw new NotImplementedException)
    // Also please note that merging (void-returning mappings) is not currently supported

    // ... (omitted for brevity)

}

Change Program.cs:

using AutoMapper;
using MapperlySandbox;
using Microsoft.Extensions.DependencyInjection;

var serviceCollection = new ServiceCollection();

serviceCollection.AddTransient<IMapper, MapperlyMapperAsAutomapper>();

using var serviceProvider = serviceCollection.BuildServiceProvider();

var company = new Company("Company 1");

company.CreateEmployee("Employee 1");
company.CreateEmployee("Employee 2");

var mapper = serviceProvider.GetRequiredService<IMapper>();

var companyDto = mapper.Map<CompanyDto>(company);

Console.WriteLine("Success!");
Console.WriteLine(companyDto);
latonz commented 1 year ago

If there are no additional configurations when using CreateMap this may be true, however, the idea of this guide is to provide a document which shows how to map each AutoMapper configuration to the matching Mapperly configuration (eg. .ForMember(dst => dst.NumberOfSeats, opts => opts.MapFrom(src => src.SeatCount)) translates to [MapProperty(nameof(Car.NumberOfSeats), nameof(CarDto.SeatCount)].

latonz commented 1 year ago

Probably this could even be implemented as a code fix, which automatically does the migration. Would be pretty advanced though.