Open latonz opened 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);
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)]
.
Probably this could even be implemented as a code fix, which automatically does the migration. Would be pretty advanced though.
Add a step by step guide on how to migrate from AutoMapper to Mapperly.