Auto-mapping MVC ViewModels
I recently came across a blog post on LosTechies detailing how AutoMapper could be used to map from models to ViewModels in MVC controllers utilising custom ActionFilters.
I found this to be a fantastic way of keeping the code in our controller actions concise, avoiding ‘fat controllers’. In essence it allows you to decorate an MVC action with the [AutoMap(typeof(Person),typeof(PersonViewModel))]
and then instead of having to write any mapping code at all within the action it will automatically take the model supplied within the return View();
statement and map it to the defined ViewModel.
One limitation I found was that if I wanted to create a View that was strongly typed to a List<MyViewModel>
however, this action filter wouldn’t work and I’d have to result to manually writing the code to map the objects in the controller action.
Based on the code written by Jimmy Bogard in the above post I have extended it to map lists of view models as well as single instances of a view model.
A custom ActionFilter was defined (inheriting from ActionFilterAttribute
) and within the constructor the source and destinations types are passed in.
public class AutoMapFilter : ActionFilterAttribute
public Type SourceType { get; }
public Type DestType { get; }
public AutoMapFilter(Type sourceType, Type destType)
{
SourceType = sourceType;
DestType = destType;
}
The OnActionExecuted
method is then overridden. This method is called after executing the code in the controller but before the ViewEngine begins generating the HTML code.
[AutoMapFilter(typeof(Person), typeof(PersonViewModel))]
public ActionResult Index()
{
using (var personRepo = new PersonRepository())
{
var persons = personRepo.Get();
//OnActionExecuted will be called here just before the view is generated.
return View(persons);
}
}
This is where the fun begins, the ViewModel that was passed within the return statement is available from within the ActionExecutedContext
passed into the method and can be accessed like this:
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var model = filterContext.Controller.ViewData.Model;
I then have the check if this model is a simple model object or whether it is a List of Models. As this need to remain generic I won’t know at compile time what List<>
type it will be. Therefore this is how I need to check if it’s a list:
if (model is IList && model.GetType().IsGenericType)
The mapping for a simple Model is simple and handled within the else
statement with automapper by calling Mapper.DynamicMap(model,SourceType,DestType)
.
If however I’m dealing with a list of models I will first need to create a List of my desired type, which again we won’t know at compile time. This can be achieved by utilising (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(DestType))
.
I am then able to iterate over the original List of Objects and map each individually adding them to my new List of ViewModels.
The final step is to update the ViewModel within the context to the new List of ViewModels. This is what will be used by the ViewEngine.
The full code for the custom ActionFilter can be seen below and an example solution is available on my GitHub account here.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Web.Mvc;
using AutoMapper;
namespace Learning.AutoMapper.Controllers
{
public class AutoMapFilter : ActionFilterAttribute
{
public Type SourceType { get; }
public Type DestType { get; }
public AutoMapFilter(Type sourceType, Type destType)
{
SourceType = sourceType;
DestType = destType;
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var model = filterContext.Controller.ViewData.Model;
object vm;
if (model is IList && model.GetType().IsGenericType)
{
vm = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(DestType));
foreach (var o in (IList)model)
{
var mappedObj = Mapper.DynamicMap(o, SourceType, DestType);
((IList)vm).Add(mappedObj);
}
}
else
{
vm = Mapper.DynamicMap(model, SourceType, DestType);
}
filterContext.Controller.ViewData.Model = vm;
}
}
}