Adapter Pattern for ViewModels in Asp.net core MVC

Viewed 706

Currently I am converting ViewModels into model objects and also the other way round. I do this in the controller and sometimes it can be quite a big object with many properties. I was wondering if this is bad practice. Should I create adapter classes to convert them and then just use the adapter in the controller.

Below is an example of how I convert the ViewModel object to a Model object in the controller:

    public async Task<IActionResult> Register(RegisterViewModel registerModel)
    {
        if (ModelState.IsValid)
        {
            ApplicationUser user = new ApplicationUser
            {
                UserName = registerModel.Email,
                Email = registerModel.Email,
                FirstName = registerModel.FirstName,
                LastName = registerModel.LastName,
                AddressLine1 = registerModel.AddressLine1,
                AddressLine2 = registerModel.AddressLine2,
                City = registerModel.City,
                PostCode = registerModel.PostCode,
                PhoneNumber = registerModel.PhoneNumber,
                NotificationPreference = Enum.GetName(typeof(NotificationPreference), NotificationPreference.None)
            };
            await userManager.CreateAsync(user, registerModel.Password);
        }
        return View(registerModel);
    }

So should I create the ApplicationUser here or should I create it in an adapter class.

2 Answers

Short answer: It depends on number of things and certain personal preferences.

Long Answer:

We need to take into account a number of factors in order to answer this question:

  1. Whether some of the projects in our solution already use converters/adapters
  2. How big the solution is at the moment
  3. How big the solution will be in the future
  4. How many models/viewmodels there are (there will be)
  5. How precisely do we want to follow certain design patterns such as SOLID, separation of concerns etc
  6. Our personal preferences and experience

One simple option would be to add a ctor or overload the implicit casting operator for each ViewModel:

public class ApplicationUser
{
    // Properties

    public ApplicationUser(RegisterModel registerModel)
    {
         UserName = registerModel.Email;
         Email = registerModel.Email;
         FirstName = registerModel.FirstName;
         // etc
    }

    //    OR:
    //    public static implicit operator ApplicationUser(RegisterModel) => 
              new ApplicationUser { UserName = registerModel.Email, ... };
}

For bigger/enterprise projects we could utilize AutoMapper or even create our own converters for each pair model -> viewmodel however I'd think twice before doing so.

Use AutoMapper, it's not that complicated.

Startup.cs ConfigureServices:

    var mappingConfig = new MapperConfiguration(mc =>
    {
        mc.AddProfile(new RegisterViewModelProfile());

    });

    IMapper mapper = mappingConfig.CreateMapper();
    services.AddSingleton(mapper);

Create the RegisterViewModelProfile class. (I usually put these in the corresponding viewmodel class file, in your case its the RegisterViewModel.cs file. Others create one MapperProfile.cs and put all Profile classes in that file, but ofc you can create separate files for each.)

public class RegisterViewModelProfile : Profile
    {
        public RegisterViewModelProfile()
        {
            CreateMap<RegisterViewModel, ApplicationUser>()
                .ForMember(dest=>dest.UserName, opt=>opt.MapFrom(src=>src.Email))
                .ForMember(dest=>dest.NotificationPreference , opt=>opt.MapFrom(src=> Enum.GetName(typeof(NotificationPreference), NotificationPreference.None) ));
                //you dont need to map the other attributes because they have the same name and type in VM and in Model so AutoMapper does it automagically

            //you can map the other way around too if you need to the same way, and you can even do conditional mapping and/or overwrite data etc
            CreateMap<ApplicationUser, RegisterViewModel>()
                .ForMember(dest => d.Email, opt => opt.MapFrom(src => "Masked because of GDPR"));

        }
    }

In your controller inject the mapper, and do the mapping when you need to do:

 public class JobsteplogsController : Controller
    {
        private readonly IMapper _mapper;

        public UserController(JobManagerContextCustom context, IMapper mapper)
        {
            _mapper = mapper;
        }
        public async Task<IActionResult> Register(RegisterViewModel registerModel)
        {
            if (ModelState.IsValid)
            {
                ApplicationUser user = _mapper.Map<ApplicationUser>(registerModel);

                await userManager.CreateAsync(user, registerModel.Password);
            }
            return View(registerModel);
        }

    }
Related