Map nested elements to related lists using AutoMap

2019-06-05 01:59发布

问题:

I have two sets of objects

Objects that I use in C# client application:

public class EmployeeClient
{
    public int Id { get; set; }

    public int DepartmentId { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string MiddleName { get; set; }
}

public class DepartmentClient
{
    public int Id { get; set; }

    public string Name { get; set; }
}

public class OrganizationClient
{
    public int Id { get; set; }

    public string Name { get; set; }

    public List<DepartmentClient> Departments { get; set; }

    public List<EmployeeClient> Employees { get; set; }
}

And DTOs:

public class EmployeeDto
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string MiddleName { get; set; }
}

public class DepartmentDto
{
    public int Id { get; set; }

    public string Name { get; set; }

    public List<EmployeeDto> Employees { get; set; }
}

public class OrganizationDto
{
    public int Id { get; set; }

    public string Name { get; set; }

    public List<DepartmentDto> Departments { get; set; }
}

I use AutoMapper and I need to configure mapping Client -> DTOs and DTOs -> Client. I implemented mapping DTOs->Client like this:

public class DtoToClientMappingProfile: Profile
{
    public DtoToClientMappingProfile()
    {
        CreateMap<EmployeeDto, EmployeeClient>();

        CreateMap<DepartmentDto, DepartmentClient>();

        CreateMap<OrganizationDto, OrganizationClient>()
            .ForMember(dest => dest.Employees, opt => opt.ResolveUsing(src => src.Departments.SelectMany(d => d.Employees)))
            .AfterMap(AfterMap);
    }

    private void AfterMap(OrganizationDto dto, OrganizationClient client)
    {
        foreach (var department in dto.Departments)
        {
            foreach (var employee in department.Employees)
            {
                var clientEmployee = client.Employees.First(e => e.Id == employee.Id);
                clientEmployee.DepartmentId = department.Id;
            }
        }
    }
}

It is not universal solution, but is works for me.

I've found only one option how mapping Client->DTOs could be implemented:

public class ClientToDtosMappingProfile : Profile
{
    public ClientToDtosMappingProfile()
    {
        CreateMap<EmployeeClient, EmployeeDto>();

        CreateMap<DepartmentClient, DepartmentDto>();

        CreateMap<OrganizationClient, OrganizationDto>()
            .AfterMap(AfterMap);
    }

    private void AfterMap(OrganizationClient client, OrganizationDto dto)
    {
        foreach (var employee in client.Employees)
        {
            var departmentDto = dto.Departments.First(d => d.Id == employee.DepartmentId);
            if (departmentDto.Employees == null)
            {
                departmentDto.Employees = new List<EmployeeDto>();
            }

            var configuration = (IConfigurationProvider)new MapperConfiguration(cfg =>
            {
                cfg.AddProfiles(typeof(ClientToDtosMappingProfile));
            });

            var mapper = (IMapper)new Mapper(configuration);

            var employeeDto = mapper.Map<EmployeeDto>(employee);
            departmentDto.Employees.Add(employeeDto);
        }
    }
}

It works, but I do not like this solution because I should create instance of new Mapper every time I map objects. In my real code Employee has a lot of nested elements and mapping is configured in multiple profiles.

Any ideas how it could be implemented better?

回答1:

I made my code a bit better using ResolutionContext. It allows not to create mappers in AfterMap function.

DtoToClientMappingProfile:

public class DtoToClientMappingProfile: Profile
{
    public DtoToClientMappingProfile()
    {
        CreateMap<EmployeeDto, EmployeeClient>();

        CreateMap<DepartmentDto, DepartmentClient>();

        CreateMap<OrganizationDto, OrganizationClient>()
            .ForMember(dest => dest.Employees, opt => opt.Ignore())
            .AfterMap(AfterMap);
    }

    private void AfterMap(OrganizationDto dto, OrganizationClient client, ResolutionContext resolutionContext)
    {
        if (dto.Departments == null)
        {
            return;
        }

        client.Departments = new List<DepartmentClient>();
        foreach (var department in dto.Departments)
        {
            var departmentClient = resolutionContext.Mapper.Map<DepartmentClient>(department);
            client.Departments.Add(departmentClient);
            if (department.Employees == null)
            {
                continue;
            }

            if (client.Employees == null)
            {
                client.Employees = new List<EmployeeClient>();
            }

            foreach (var employee in department.Employees)
            {
                var employeeClient = resolutionContext.Mapper.Map<EmployeeClient>(employee);
                employeeClient.DepartmentId = department.Id;
                client.Employees.Add(employeeClient);
            }
        }
    }

ClientToDtosMappingProfile:

public class ClientToDtosMappingProfile : Profile
{
    public ClientToDtosMappingProfile()
    {
        CreateMap<EmployeeClient, EmployeeDto>();

        CreateMap<DepartmentClient, DepartmentDto>();

        CreateMap<OrganizationClient, OrganizationDto>()
            .AfterMap(AfterMap);
    }

    private void AfterMap(OrganizationClient client, OrganizationDto dto, ResolutionContext resolutionContext)
    {
        if (client.Employees == null)
        {
            return;
        }

        foreach (var employee in client.Employees)
        {
            var departmentDto = dto.Departments.First(d => d.Id == employee.DepartmentId);
            if (departmentDto.Employees == null)
            {
                departmentDto.Employees = new List<EmployeeDto>();
            }

            var employeeDto = resolutionContext.Mapper.Map<EmployeeDto>(employee);
            departmentDto.Employees.Add(employeeDto);
        }
    }
}


回答2:

If you call AssertConfigurationIsValid, AM will complain about what it doesn't know how to map.

The problem seems to be that you don't have the information needed to fill the destination object in the source object.

You will need to add a resolver for each property AM complains about, like the ResolveUsing you already have, for example.

You also need to pass the extra information that's needed.

The result may not look good eventually because AM cannot rely on uniform objects to do its job, you have to tell it what to do.

Another way to go about it is to do the high level mapping in your own code and rely on AM only when the mapping is simple enough so AM can do it by itself. The more you customize AM, the less value you get from it.



标签: c# AutoMapper