Organizing AutoMapper's Map Configurations in MVC
- Using Global.asax - a file containing all application event-handling for ASP.NET. It also comes pre-wired in ASP.NET projects (including MVC) created with the project wizard.
- Creating an HttpModule – a slightly more advanced methodology leveraging a callback for module initialization (called at the start of an application).
“Typically, the configuration bootstrapper class is in its own class, and this bootstrapper class is called from the startup method.”
This approach helps in clearing up some of the clutter, by putting the map creation code into a separate static class.
However, since we’re using MVC and the MVVM pattern, the solution likely consists of multiple domain models mapping to multiple view models, the resulting bootstrap class disagrees with the Single Responsibility Principle.
Although this blog entry has a lengthier process, it has much shorter solution. If you're short on time, come back for the full journey after work. In the meantime feel free to jump directly to the solution.
Attempt 1: Decompose Controller Mappings by Using Profiles
AutoMapper provides the concept of a "Mapping Profile" in the AutoMapper.Profile class. In order to address the violation of SRP, we may leverage this concept to divide the different controller mapping configurations. For example, if we had an Address controller and a User controller in our solution, we could use the classes shown in the following class diagram:
UserMappingProfile.cs:
public class UserMappingProfile : AutoMapper.Profile
{
protected override void Configure()
{
base.Configure();
// Do your Mapper.CreateMap() here...
}
}
In order to load each of the Profile instances into the static Mapper instance, we’ll need to add code to the Global.asax. The example below shows the code directly in Global.asax, for brevity, but could be moved to a configuration static class.
Global.asax.cs: (initial)
protected void Application_Start()
{
AutoMapper.Mapper.Initialize(a =>
{
a.AddProfile();
a.AddProfile();
});
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
Unfortunately, this leads to a violation of the Open-Closed Principle (OCP) - Global.asax would be modified every time a new ViewModel or Controller action is added, because the additional Profile would be added to the Mapper.Initialize call. So, we could use reflection to add all of the Profiles to the Mapper.
Global.asax.cs: (refactor)
protected void Application_Start()
{
var profileType = typeof (Profile);
// Get an instance of each Profile in the executing assembly.
var profiles = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => profileType.IsAssignableFrom(t)
&& t.GetConstructor(Type.EmptyTypes) != null)
.Select(Activator.CreateInstance)
.Cast();
// Initialize AutoMapper with each instance of the profiles found.
Mapper.Initialize(a => profiles.ForEach(a.AddProfile));
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
IEnumerableExtensions.cs: (For code-completeness)
public static class IEnumerableExtensions
{
public static void ForEach(this IEnumerable enumerable,
Action action)
{
foreach(T item in enumerable) {action(item);}
}
}
Something still isn't right here - it looks like this solution still violates OCP. What if we want to ignore one of the profiles at application start?
Attempt 2: Address Open-Closed Principle Violation by Using an Attribute
If we had an attribute that could be added to our controller classes for AutoMapperStart methods, that should resolve our OCP violation. Something like this should work:
AddressController.cs:
[AutoMapperMapCreator(MethodName = "AutoMapperStart")]
public class AddressController : Controller
{
public static void AutoMapperStart()
{
// TODO: Do your Mapper.CreateMap() here..
}
}
AutoMapperMapCreatorAttribute.cs:
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public class AutoMapperMapCreatorAttribute : Attribute
{
public AutoMapperMapCreatorAttribute() {
Enabled = true;
}
public string MethodName { get; set; }
public bool Enabled { get; set; }
public void ExecuteFor(Type type)
{
if (!Enabled) { return; }
// Get the static, empty parameter method.
MethodInfo methodInfo = type.GetMethod(
MethodName, BindingFlags.Public | BindingFlags.Static,
null, new Type[0], null);
if (methodInfo == null)
{
throw new ArgumentException(string.Format(
"Cannot find method {0} for type {1} - method must be public, static, and have empty parameters.",
MethodName, type.Name));
}
// Invoke it.
methodInfo.Invoke(null, new object[0]);
}
}
A little reflection magic added to Global.asax, and the attributes will be evaluated during application start! Before all you nay-reflection-sayers jump all over me - this reflection only occurs at application start. It's not going to impact request processing, sheesh!
Global.asax.cs:
protected void Application_Start()
{
// Get all of the types with AutoMapperMapCreatorAttribute.
var types = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.GetCustomAttributes(typeof(AutoMapperMapCreatorAttribute), true).Length > 0);
// Cycle through each type..
foreach (var type in types)
{
var attributes = type
.GetCustomAttributes(typeof (AutoMapperMapCreatorAttribute), true)
.Cast();
// Ignore any types where enabled=false.
if (attributes.Any(a => !a.Enabled)) { continue; }
// Execute each of the methods defined by the attributes.
foreach (var attribute in attributes)
{
attribute.ExecuteFor(type);
}
}
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
Ok, great, it works! But, I don't like the clutter in Global.asax. We could potentially push the attribute processing into a separate class. That would help. Or, even better, we could use that HttpModule trick that I mentioned earlier, eliminating the need to have this in Global.asax! You know... this attribute seems pretty general purpose. Hasn't someone already done this?
Solution: WebActivator (aka, the punchline)
Heck yeah, that'd work! David Ebbo even released it as a NuGet Package.
If you weren't aware, WebActivator offers a simple method for bootstrapping your web projects. As of writing this, WebActivator exposes callbacks for the following events:
- Pre-start - WebActivator.PreApplicationStartMethodAttribute
- Post-start - WebActivator.PostApplicationStartMethodAttribute
- Shutdown - WebActivator.ApplicationShutdownMethodAttribute
To our original controller, we'd only need to add:
- the WebActivator attribute, and
- the static method responsible for map creation
AddressController.cs:
[assembly: WebActivator.PreApplicationStartMethod(typeof(AddressController), "AutoMapperStart")]
namespace BlogExample.Controllers
{
public class AddressController : Controller
{
public static void AutoMapperStart()
{
// TODO: Do your Mapper.CreateMap<tsource,>() here..
}
}
}</tsource,>
Epilogue
So, you're probably asking yourself:
"So, Mike..."
"Please, call me Fraser."
"What?"
"Yeah, I work with three Mikes. So, I go by Fraser."
"Ok..."
"And people always think of Kelsey Grammer's show, so they mistakenly think I'm smart."
"Um, yeah. So, Fraser, why all of process just to get to a one-liner?"
I'm glad you asked. You see, I tend to re-invent the wheel. I like to problem solve more than I like searching. However, my process gives me a deeper understanding to a problem. I'm one of those annoying kids who always asked "Why". When I learn why an methodology is better, I better understand when to reuse that solution. Moreover, since the answer is so simple, you might miss how many issues it addresses:
- Single Responsibility Principle
- Open-Closed Principle
- Ease of maintenance
- A reusable tool for good framework design
Besides, isn't it amusing to see how my little brain operates?