Using Automapper Profiles to Perform Mapping Between Different Objects

in #utopian-io8 years ago (edited)

What Will I Learn?

How to use AutoMapper to perform the mapping between view models and domain objects and vice versa.

Requirements

Automapper https://github.com/AutoMapper/AutoMapper

Difficulty

Intermediate

Using Automapper Profiles to Perform Mapping Between Different Objects

AutoMapper works brilliantly and I would recommend it to anyone who needs to perform mapping between different objects.

The approach I've always taken when using AutoMapper is to create a mapper class which is responsible for mapping one type to another type. So for example given the two entities below

public class Address
    {
        public string FirstLine { get; set; } 
        public string Country { get; set; } 
        public string PostCode { get; set; }
    }

And

    public class AddressViewModel
    {
        [DisplayName("Address Line One")]
        public string PersonAddressLineOne { get; set; } 
        [DisplayName("Country Of Residence")]
        public string PersonCountryOfResidence { get; set; } 
        [DisplayName("Post Code")]
        public string PostCode { get; set; }
    }

If I wanted to map from an Address entity to an AddressViewModel entity I would create an AddressToAddressViewModelMapper class which is responsible for performing the mapping (shown below)

    public class AddressToAddressViewModelMapper : IMapper<Address, AddressViewModel>
    {
        public AddressToAddressViewModelMapper()
        {
            AutoMapper.Mapper.CreateMap<Address, AddressViewModel>()
                .ForMember(x => x.PersonAddressLineOne, opt => opt.MapFrom(source => source.FirstLine))
                .ForMember(x => x.PersonCountryOfResidence, opt => opt.MapFrom(source => source.Country));
        } 
        public AddressViewModel Map(Address source)
        {
            var destination = AutoMapper.Mapper.Map<Address, AddressViewModel>(source);
            return destination;
        }
    }

The issue I have with this approach is that even for a small number of entities you have to create a large number of mapper classes and this bloats your solution and further to this if you are using dependency injection you end up injecting a large number of mappers into each controller increasing the complexity of both the code and any unit tests that you have.

To illustrate how complex such mappers can get, I will show an example of mapping from a simple domain model to an equally simple view model

Shown below are the domain objects

    public class Person
    {
        public int Id { get; set; } 
        public string Firstname { get; set; } 
        public string Surname { get; set; } 
        public Address Address { get; set; } 
        public List<Note> Notes { get; set; }
    }
    public class Address
    {
        public string FirstLine { get; set; } 
        public string Country { get; set; } 
        public string PostCode { get; set; }
    } 
    public class Note
    {
        public int Id { get; set; } 
        public string Text { get; set; }
    }

Which need to be mapped to the following view model objects

    public class PersonViewModel
    {
        [DisplayName("Person Id")]
        public int Id { get; set; } 
        [DisplayName("Person Firstname")]
        public string Firstname { get; set; } 
        [DisplayName("Person surname")]
        public string Surname { get; set; } 
        public AddressViewModel Address { get; set; } 
        public List<NoteViewModel> Notes { get; set; }
    } 
    public class AddressViewModel
    {
        [DisplayName("Address Line One")]
        public string PersonAddressLineOne { get; set; } 
        [DisplayName("Country Of Residence")]
        public string PersonCountryOfResidence { get; set; } 
        [DisplayName("Post Code")]
        public string PostCode { get; set; }
    } 
    public class NoteViewModel
    {
        [DisplayName("Note Id")]
        public int Id { get; set; } 
        [DisplayName("Note Text")]
        public string Text { get; set; }
    }

To perform the mapping we need to create a mapper class for each mapping that is performed, in this example mappers that perform the following mappings are required

  • Person to PersonViewModel
  • Address to AddressViewModel
  • Note to NoteViewModel

The mapping classes are shown below, note that when performing a mapping from a Person domain object the mapper responsible for performing the mapping will need to be aware of the mappers for the other complex types that the Person domain object contains (Address and Note)

    public class PersonToPersonViewModelMapper : IMapper<Person, PersonViewModel>
    {
        private NoteToNoteViewModelMapper _noteToNoteViewModelMapper = new NoteToNoteViewModelMapper();
        private AddressToAddressViewModelMapper _addressToAddressViewModelMapper = new AddressToAddressViewModelMapper(); 
        public PersonToPersonViewModelMapper()
        {
            AutoMapper.Mapper.CreateMap<Person, PersonViewModel>();
        } 
        public PersonViewModel Map(Person source)
        {
            var destination = AutoMapper.Mapper.Map<Person, PersonViewModel>(source); 
            destination.Address = source.Address.MapUsing(_addressToAddressViewModelMapper); 
            destination.Notes = source.Notes.MapAllUsing(_noteToNoteViewModelMapper); 
            return destination;
        }
    }
    public class AddressToAddressViewModelMapper : IMapper<Address, AddressViewModel>
    {
        public AddressToAddressViewModelMapper()
        {
            AutoMapper.Mapper.CreateMap<Address, AddressViewModel>()
                .ForMember(x => x.PersonAddressLineOne, opt => opt.MapFrom(source => source.FirstLine))
                .ForMember(x => x.PersonCountryOfResidence, opt => opt.MapFrom(source => source.Country));
        } 
        public AddressViewModel Map(Address source)
        {
            var destination = AutoMapper.Mapper.Map<Address, AddressViewModel>(source);
            return destination;
        }
    }
    public class NoteToNoteViewModelMapper : IMapper<Note, NoteViewModel>
    {
        public NoteToNoteViewModelMapper()
        {
            AutoMapper.Mapper.CreateMap<Note, NoteViewModel>();
        } 
        public NoteViewModel Map(Note source)
        {
            var destination = AutoMapper.Mapper.Map<Note, NoteViewModel>(source);
            return destination;
        }
    }

The code below shows the code in the controller that actually performs the mapping

        public ActionResult Index()
        {
            var person = _personTasks.GetPerson(23); 
            var mapper = new PersonToPersonViewModelMapper();
            var personViewModel = mapper.Map(person); 
            return View(personViewModel);
        }

As you can see from the code above even for a simple example it can get complicated quite quickly. Luckily AutoMapper offers an alternative approach – Profiles.

Profiles allow you to group mapping configuration together, for example you could group mapping configuration by area or controller. The benefit of using profiles is that it removes the need for individual mapper classes, therefore reducing the complexity of your solution. In addition to this the act of creating the mapper configuration is quite a costly operation and if this logic in encapsulated in individual mapper classes this configuration is performed (unnecessarily) every time the mapper class is created but with profiles you initialize them once at application start up therefore improving the performance of your application.

For the purposes of this example I created two profiles one responsible for containing the mapping configuration when mapping from domain objects to view models and the other for mapping from view models to domain objects. The two profile classes are shown below

    public class DomainToViewModelMappingProfile : Profile
    {
        public override string ProfileName
        {
            get { return "DomainToViewModelMappings"; }
        } 
        protected override void Configure()
        {
            Mapper.CreateMap<Person, PersonViewModel>(); 
            Mapper.CreateMap<Address, AddressViewModel>()
                .ForMember(x => x.PersonAddressLineOne, opt => opt.MapFrom(source => source.FirstLine))
                .ForMember(x => x.PersonCountryOfResidence, opt => opt.MapFrom(source => source.Country)); 
            Mapper.CreateMap<Note, NoteViewModel>();
        }
    }

And

     public class ViewModelToDomainMappingProfile : Profile
    {
        public override string ProfileName
        {
            get { return "ViewModelToDomainMappings"; }
        } 
        protected override void Configure()
        {
            Mapper.CreateMap<PersonViewModel, Person>(); 
            Mapper.CreateMap<AddressViewModel, Address>()
                .ForMember(x => x.FirstLine, opt => opt.MapFrom(source => source.PersonAddressLineOne))
                .ForMember(x => x.Country, opt => opt.MapFrom(source => source.PersonCountryOfResidence)); 
            Mapper.CreateMap<NoteViewModel, Note>();
        }
    }

To register the profiles you need to initialize AutoMapper at application start up so that it knows about the profiles. To do this I’ve created a helper class (shown below) which is responsible for initializing AutoMapper

    public class AutoMapperConfiguration
    {
        public static void Configure()
        {
            Mapper.Initialize(x =>
            {
                x.AddProfile<DomainToViewModelMappingProfile>();
                x.AddProfile<ViewModelToDomainMappingProfile>();
            });
        }
    }

Then I call the Configure method from the Application_Start method of the Global.asax

        protected void Application_Start()
        {
            // Initialize automapper
            AutoMapperConfiguration.Configure(); 
            AreaRegistration.RegisterAllAreas(); 
            RegisterRoutes(RouteTable.Routes); 
            // Setup spark view engine
            var settings = new SparkSettings(); 
            settings
                .AddNamespace("System")
                .AddNamespace("System.Collections.Generic")
                .AddNamespace("System.Linq")
                .AddNamespace("System.Web.Mvc")
                .AddNamespace("System.Web.Mvc.Html"); 
            settings.AutomaticEncoding = true; 
            ViewEngines.Engines.Add(new SparkViewFactory(settings));
        }

To perform a mapping you simply to call the AutoMapper API and ask it to perform a mapping, example below

        public ActionResult Index()
        {
            var person = _personTasks.GetPerson(23); 
            var personViewModel = AutoMapper.Mapper.Map<Person, PersonViewModel>(person); 
            return View(personViewModel);
        }

Obviously to improve testability you could encapsulate AutoMapper within a custom class which you supply to the controller via dependency injection.



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @yissakhar I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Coin Marketplace

STEEM 0.13
TRX 0.34
JST 0.035
BTC 108074.45
ETH 4399.12
USDT 1.00
SBD 0.83