AutoMapper med ASP.NET MVC

Ett vanligt problem när man utvecklar är att man gång på gång måste skriva funktionalitet för att mappa en klass till en annan. När man arbetar med ASP.NET MVC så händer det ofta att man har en modell som är anpassad för en viss vy, men datan man tar emot kan innehålla samma rådata, men med ett helt annat upplägg. Det vi får göra då är att mappa om det mottagna objektet till det som vi sedan skall använda till vyn.

För att göra mappningen enklare så finns det ett open source-bibliotek som heter AutoMapper, vilket ni kan hitta här:

http://www.codeplex.com/AutoMapper

Fördelen med AutoMapper är att det blir mycket enklare att göra dessa mappningar, då vi får använda fluent configuration (http://martinfowler.com/bliki/FluentInterface.html) för att sätta upp mappningarna, och sedan välja vilka två objekt vi vill skicka data mellan.

Sätt upp projektet

Vi kommer att använda oss utav ett ASP.NET MVC 2-projekt. När vi skapar projektet så väljer vi att skapa ett test-projekt (jag använder Visual Studio Unit Test i exemplet, men ni kan använda MbUnit, NUnit, eller vad som nu faller er i smaken). Till detta skapar vi även ett klassbibliotek för domänmodellen.

ASP.NET MVC-projektet, samt testprojektet måste ha referenser till AutoMapper-dll:en, vilken ni kan ladda ned på CodePlex.

I ASP.NET MVC-projektet så skapar vi en ny modell, kallad CustomerDto. Utseendet på denna blir:

namespace AutoMapperMvc.Models
{
    public class CustomerDto
    {
        public string CustomerId { get; set; }
        public string FullName { get; set; }
        public string[] PhoneNumbers { get; set; }
    }
}

Vi behöver även en domänmodell som vi kommer att koppla mot CustomerDto. Den består av två klasser och en enum:

1 - Class Diagram

Customer-klassen är kunden i sig. Varje kund kan ha flera telefonnummer, och varje telefonnummer kan vara satt som antingen Work eller Home.

Customer:

using System.Collections.Generic;
 
namespace AutoMapperMvc.Domain
{
    public class Customer
    {
        public int CustomerId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public List<PhoneNumber> PhoneNumbers { get; set; }
    }
}

PhoneNumber:

namespace AutoMapperMvc.Domain
{
    public class PhoneNumber
    {
        public PhoneNumberTypeEnum Type { get; set; }
        public string Number { get; set; }
    }
}

PhoneNumberEnum:

namespace AutoMapperMvc.Domain
{
    public enum PhoneNumberTypeEnum
    {
        Work,
        Home,
    }
}

Sätt upp AutoMapper

För att kunna mappa upp objekten så behöver vi sätta upp AutoMapper. Det finns två olika sätt att göra det på: antingen direkt i controllern efter behov, eller i en statisk metod som körs när applikationen startas. Vi kommer att ha en statisk metod då det gör att hela applikationen kan använda sig utav mappningen.

I roten för vårt ASP.NET MVC-projekt skapar vi en fil som heter AutoMapperBootstrapper.cs. Den kan självklart ligga var som helst, och kommer inte att följa med vid produktionssättning då den ändå hamnar i vår assembly.

I den här filen så skapar vi en statisk metod som skapar en grundläggande mappning mellan Customer-objektet i Domain-projektet och CustomerDto-objektet i ASP.NET MVC-projektet.

using AutoMapper;
using AutoMapperMvc.Domain;
using AutoMapperMvc.Models;
 
namespace AutoMapperMvc
{
    public class AutoMapperBootstrapper
    {
        public static void Initialize()
        {
            Mapper.CreateMap<Customer, CustomerDto>();
        }
    }
}

För att metoden skall köras så lägger vi även till den här raden i Application_Start() i global.asax:

AutoMapperBootstrapper.Initialize();

Det som händer nu är att AutoMapper försöker tolka våra egenskaper så gott det kan.

Sätt upp controllern

För att använda oss utav mappningen så skapar vi en ny controller vid namn ”CustomerController”. Här har vi en ActionResult-metod vid namn ”Index”.

Det första vi gör är att se till att vi har någon data, så vi skapar upp en enkel metod direkt i controllern som ger oss den datan vi behöver för att kunna testa mappningen. Datan bör dock självklart komma från ett annat håll, som ett repository, men för att enkelt kunna visa det relevanta här så har jag valt att bara ha en metod för det direkt här.

private Customer[] GetCustomers()
{
    return new List<Customer>() {
        new Customer() {
            CustomerId = 1,
            FirstName = "Mikael",
            LastName = "Söderström",
            PhoneNumbers = new List<PhoneNumber>() {
                new PhoneNumber() {
                    Number = "555-12 34 56",
                    Type = PhoneNumberTypeEnum.Home
                },
                new PhoneNumber() {
                    Number = "555-65 43 21",
                    Type = PhoneNumberTypeEnum.Work
                }
            }
        },
        new Customer() {
            CustomerId = 2,
            FirstName = "Bill",
            LastName = "Gates",
            PhoneNumbers = new List<PhoneNumber>() {
                new PhoneNumber() {
                    Number = "555-12 34 56",
                    Type = PhoneNumberTypeEnum.Home
                },
                new PhoneNumber() {
                    Number = "555-65 43 21",
                    Type = PhoneNumberTypeEnum.Work
                }
            }
        }
    }.ToArray();
}

Det som returneras är två Customer-objekt. I vår vy vill vi dock visa upp CustomerDto, så vi väljer att mappa om objektet till det.

public ActionResult Index()
{
    Customer[] customers = GetCustomers();
 
    CustomerDto[] mappedCustomers = Mapper.Map<Customer[], CustomerDto[]>(customers);
 
    return View(mappedCustomers);
}

Mapper.Map är en generisk metod som har två typer, källtypen och måltypen. Vi vill här omvandla ett Customer-objekt till ett CustomerDto-objekt, precis som vi angav i Bootstrappern där vi skapade mappningen.

Sätt upp vyn

För att testa hur väl mappningen har gått så skapar vi en vy som är hårt typad till CustomerDto.

Vi skapar en vy där vi kan lista CustomerId, Fullname och Phone numbers.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<AutoMapperMvc.Models.CustomerDto>>" %>
<asp:Content ContentPlaceHolderID="MainContent" runat="server">
    <h2>Customers</h2>
 
    <table>
        <tr>
            <th>
                CustomerId
            </th>
            <th>
                Fullname
            </th>
            <th>
                Phone numbers
            </th>
        </tr>
    <% foreach (var item in Model) { %>
        <tr>
            <td>
                <%= Html.Encode(item.CustomerId) %>
            </td>
            <td>
                <%= Html.Encode(item.FullName) %>
            </td>
            <td>
                <ul>
                <% foreach (string number in item.PhoneNumbers) { %>
                    <li><%=number%></li>
                <% } %>
                </ul>
            </td>
        </tr>
    <% } %>
    </table>
</asp:Content>

Om vi kikar på resultatet så kan vi se att vi får det här:

2 - Mapper Result

CustomerId har mappats korrekt, Fullname är tomt och Phone numbers innehåller vilken typ det är.

Anledningen till att det har blivit så här är för att CustomerId används i både Customer och i CustomerDto. Det gör att AutoMapper enkelt kan översätta värdet. FullName kan det dock ej hitta då vi i Customer har FirstName och LastName. Det finns inget som säger att de tillsammans skall bli FullName. Till sist så har vi Phone numbers, vilket är en collection med PhoneNumber, vilket inte AutoMapper känner till.

Anpassa mappningen efter modellerna

Det finns olika sätt att lösa det här på. En väldigt intressant feature i AutoMapper är att det automatiskt känner av ifall det finns en metod som har ”Get” som prefix. Om det finns ett sådant så försöker den använda den för att sätta värdet.

Vi vill till exempel ha FirstName och LastName ihopsatt till FullName. För att lösa detta så kan vi ha en metod i Customer-objektet som vi kallar för ”GetFullName”. Där returnerar vi FirstName och LastName ihopsatt:

Nya Customer:

using System.Collections.Generic;
 
namespace AutoMapperMvc.Domain
{
    public class Customer
    {
        public int CustomerId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public List<PhoneNumber> PhoneNumbers { get; set; }
 
        public string GetFullName()
        {
            return FirstName + " " + LastName;
        }
    }
}

Om vi kikar på sidan igen så ser vi att vi nu har det här:

3 - FullName

Det som hände nu var att AutoMapper såg att vi hade en metod kallad GetFullName, och satte då värdet från den metoden till egenskapen FullName.

Skapa en resolver

Alla telefonnummer ser dock fortfarande ganska lustiga ut. I Customer så har vi en collection med typen PhoneNumber, medan vi i CustomerDto har en collection med strängar. För att kunna ange för AutoMapper vilken property som skall användas så får vi skapa en resolver.

Vi går tillbaka till AutoMapperBootstrapper som vi skapade tidigare.

Vi skapar nu en ny klass i den här filen kallad PhoneNumberResolver. Den skall ärva ValueResolver<Customer, string[]>. Precis som tidigare så är det källtypen och måltypen som anges.

Nästa steg är att köra override på metoden ResolveCore, vilket är av typen TDestination (i det här fallet string[]).

public class PhoneNumbersResolver : ValueResolver<Customer, string[]>
{
    protected override string[] ResolveCore(Customer source)
    {
        List<string> numbers = new List<string>();
 
        foreach (PhoneNumber number in source.PhoneNumbers)
        {
            numbers.Add(number.Number);
        }
 
        return numbers.ToArray();
    }
}

ResolveCore tar emot en parameter som innehåller hela det aktuella objektet. Det vi är intresserade av är telefonnummren, så vi sparar undan dessa i en collection och skickar tillbaka. Vilken typ av nummer det är bryr vi oss inte om då det ändå inte syns i vyn.

För att registrera resolvern så får vi använda oss utav fluent configuration.

Vi modifierar mappningen som vi tidigare skapade i bootstrappern så att den ser ut så här:

Mapper.CreateMap<Customer, CustomerDto>()
    .ForMember(dto => dto.PhoneNumbers, opt => opt.ResolveUsing<PhoneNumbersResolver>());

Det som händer nu är att vi för alla tillfällen där Customer mappas om till CustomerDto så skall PhoneNumberResolver användas för PhoneNumbers i CustomerDto.

För att se hur det påverkade vyn så kompilerar vi och går in på sidan igen:

4 - Phone numbers

När vi nu mappar om objekten så används egenskapen med telefonnumret istället för PhoneNumbers-objektet i sig.

Skapa en formatter

Om vi istället för att bara ange vilket fält som skall användas, vill ange hur fältet skall formateras så kan vi använda en formatter.

Precis som för resolvern så skall vi skapa en klass i bootstrapper-filen.

Istället för att bara visa siffran med CustomerId, så vill vi skriva ut ”ID: ” och sedan siffran. Det vi tar emot kommer att vara en integer, och det vi returnerar kommer att vara en sträng. Vi kommer därför att ärva ValueFormatter<int> i klassen, och köra override på FormatValueCore.

public class CustomerIdFormatter : ValueFormatter<int>
{
    protected override string FormatValueCore(int value)
    {
        return "ID: " + value.ToString();
    }
}

Det som sker här är att vi får värdet direkt som en integer, och returnerar sedan texten så som vi vill ha den. Den kommer då att få det utseendet direkt vid mappningen.

För att köra formattern på ID:t så får vi precis som tidigare använda oss utav fluent configuration, så vi lägger till en formatter:

Mapper.CreateMap<Customer, CustomerDto>()
    .ForMember(dto => dto.PhoneNumbers, opt => opt.ResolveUsing<PhoneNumbersResolver>())
    .ForMember(dto => dto.CustomerId, opt => opt.AddFormatter<CustomerIdFormatter>());

Vi anger här att CustomerId skall köras med formattern vi just skapade.

Resultatet blir:

5 - Id

Testa mappningarna

För att vara säker på att mappningarna fungerar så kan vi skapa enhetstester för det. Då det inte går att köra tester i Visual Web Developer och de enklare versionerna av Visual Studio så går jag bara igenom snabbt hur testerna kan se ut.

Börja med att skapa en ny klass i testprojektet, AutoMapperBootstrapperTest.cs.

Vi väljer här att ha två tester, ett som kollar om konfigurationen är giltig, och en som kollar om värdena har satts rätt.

AutoMapper innehåller en metod som heter Mapper.AssertConfigurationIsValid(), och som har som funktion att kolla igenom konfigurationen och se så att allt står rätt till. Vi kommer att använda den i konfigurationstestet.

Testet i sig ser ut på följande sätt:

using System.Collections.Generic;
using AutoMapper;
using AutoMapperMvc.Domain;
using AutoMapperMvc.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
 
namespace AutoMapperMvc.Tests
{
    [TestClass]
    public class AutoMapperBootstrapperTest
    {
        [TestMethod]
        public void TestAutoMapperBootstrapper()
        {
            AutoMapperBootstrapper.Initialize();
            Mapper.AssertConfigurationIsValid();
        }
 
        [TestMethod]
        public void TestAutoMapperMapping()
        {
            AutoMapperBootstrapper.Initialize();
 
            var customer = new Customer()
            {
                CustomerId = 1,
                FirstName = "Mikael",
                LastName = "Söderström",
                PhoneNumbers = new List<PhoneNumber>() {
                   new PhoneNumber() {
                    Number = "12345",
                    Type = PhoneNumberTypeEnum.Home
                   }
                }
            };
 
            var result = Mapper.Map<Customer, CustomerDto>(customer);
 
            Assert.AreEqual(result.FullName, "Mikael Söderström");
        }
    }
}

Om vi testkör det så bör båda testen vara gröna. Om vi däremot byter namn på metoden GetFullName i Customer till till exempel GetName, så kommer vi att testet inte att gå igenom, då det inte finns någon mappning mot FullName i CustomerDto.

Det här gör att vi enkelt kan se om någon mappning går fel under tiden som vi skapar upp nya objekt och mappar dem.

Det finns även väldigt många andra funktioner i AutoMapper, så jag rekommenderar att ni laddar ned det och testar. Det gör det väldigt enkelt att mappa objekt och testa mappningarna, vilket kan spara mycket tid.

2 Comments

Comments have been disabled for this content.