Domain entities vs presentation model objects, part 2: mapping
This is the second half of a two-part article. Read the first half here: Domain entities vs presentation model objects.
In my last post, I wrote about the difference between domain entities and presentation model objects. Remember my two task classes — the transactional domain entity and the UI presentation object? They’re very similar, and this could lead to a lot of ugly hand-written plumbing code mapping fields on one to the other.
// Task domain entity.public class Task{ public int Id; public string Name; public DateTime? DueDate; // ...etc.}// Task presentation model object.public class TaskView{ public int Id; public string Name; public string DueDate; public bool IsUnscheduled; public bool IsOverDue; public long SortIndex;}
Instead, I’m using an open-source .NET library called AutoMapper by Jimmy Bogard — an object-object mapper (OOM) that sets values from one type to another.
Setting up a map from one type to another is dead simple — AutoMapper will automatically match fields with the same name. For other stuff we use lambda expressions, or delegate to another class. Here’s what my Task-to-TaskView mapping looks like:
// Set up a map between Task and TaskView. Note fields with the same names are// mapped automagically!Mapper.CreateMap<Task, TaskView>() .ForMember(dest => dest.DueDate, opt => opt.AddFormatter<DueDateFormatter>()) .ForMember(dest => dest.SortIndex, opt => opt.ResolveUsing<SortIndexResolver>());
That was easy! Note I’m using a custom value formatter for the DueDate:
public class DueDateFormatter : IValueFormatter{ public string FormatValue(ResolutionContext context) { DateTime? d = context.SourceValue as DateTime?; if (d.HasValue) return d.Value.ToString("dddd MMM d"); else return "Anytime"; }}
…and a custom resolver for the sort index (an integer derived from the task’s due date). Note that, while a formatter transforms one field by itself; a resolver examines the whole object to derive a value:
public class SortIndexResolver : IValueResolver{ public ResolutionResult Resolve(ResolutionResult source) { Task t = source.Value as Task; DateTime sortDate = t.DueDate.HasValue ? t.DueDate.Value : DateTime.MaxValue; long sortIndex = Convert.ToInt64(new TimeSpan(sortDate.Ticks).TotalSeconds); return new ResolutionResult(sortIndex); }}
With dedicated classes for formatting and resolving values, tests become very easy to write (although I did write my own ShouldFormatValueAs() test helper extension method):
[TestFixture]public class When_displaying_a_due_date{ [Test] public void Should_display_null_values_as_anytime() { new DueDateFormatter().ShouldFormatValueAs<DateTime?>(null, "Anytime"); } [Test] public void Should_format_date() { new DueDateFormatter().ShouldFormatValueAs<DateTime?>( new DateTime(2009, 02, 28), "Saturday Feb 28"); }}
Putting it to practice, it becomes a one-liner to create a new TaskView instance given a Task.
// Grab a Task from the repository, and map it to a new TaskView instance.Task task = this.tasks.GetById(...);TaskView taskView = Mapper.Map<Task, TaskView>(task);
Awesome!