Nine MVP's Blog

30/01/2011

AutoMapper: Mapping Objects Utility

Filed under: Design Pattern — Tags: , , — Nine MVP @ 6:05 pm

Programming Level:

  • Intermediate – Advance

Computer Skills:

  1. Object Oriented Programming, Design Pattern
  2. C# 3.0, LINQ

Development Tool and Library

  1. Visual Studio 2010 SP1 or later
  2. AutoMapper 1.1 (download here)

Introduce:

ห่างหายไปนานกับการเขียนบทความออนไลน์   วันนี้มีไลบราลี่ตัวนึงมาแนะนำซึ่งมีประโยชน์มากๆ สำหรับชาว OO Programming ที่นิยมใช้ DTO (data transfer object) ซึ่งปัญหาของการใช้ DTO นั้นคือต้องมานั่งเขียนโค้ดส่งค่าทีละ property ซึ่งค่อนข้างที่จะลำบากหากวันนึงมีการเปลี่ยนแปลงชื่อของ property เราก็ต้องมานั่งแก้โค้ดให้ตรงกันกับที่แก้ไขไป  และไม่ค่อยจะยืดหยุ่นเนื่องจาก map ที่สร้างขึ้นไม่สามารถแก้ไขเปลี่ยนแปลงได้อิสระ  สิ่งต่าง ๆ เหล่านี้ได้มีผู้ผลิตไลบราลี่ออกมาแจกจ่ายให้ใช้งานกัน แต่ก็ยังไม่ตรงกับที่ต้องการ  จนกระทั่ง jimmy bogard เป็นผู้ริเริ่มโปรเจ็ก Automapper ขึ้นใน CodePlex ผมได้ทดลองเล่นตอนที่ยังเป็น Beta version เห็นว่าน่าสนใจจึงเริ่มศึกษาเพื่อใช้งานอย่างจริงจัง   หลังจากที่ได้ใช้งานในระบบงานจริง ๆ ปรากฎว่าใช้งานได้ดีและลดเวลาในการพัฒนางานไปได้ส่วนนึง จึงได้สรุปมาเป็นบทความตอนนี้

 

Basic AutoMapper – แนะนำเม็ธธอดที่ใช้บ่อยและสิ่งที่ควรรู้

1. AutoMapper.Mapper.CreateMap() เป็นการสร้างแม๊พระหว่าง type ที่ต้องการจะทำการแม๊พค่ากัน โดยมี source type และ destination type โดยมีให้ใช้ทั้ง Generic Method และ Parameter Method มีให้ใช้หลายโอเวอร์โหลด

Mapper.CreateMap<class1, class2>();

Mapper.CreateMap(typeof(class1), typeof(class2));

 

2. AutoMapper.Mapper.Reset() เป็นเม็ธธอดที่ใช้ในการล้างค่า config ที่เคยได้สร้างไว้ก่อนหน้านี้

3. AutoMapper.Mapper.Map() เป็นเม็ธธอดที่ใช้ในการรับ source object เข้าไปแม๊พและรีเทิร์น target object ที่ต้องการออกมา โดยมีให้ใช้ทั้ง Generic Method และ Parameter Method มีให้ใช้หลายโอเวอร์โหลดครับ

var objClass2 = Mapper.Map<class1, class2>(srcClass1);

var objClass2 = Mapper.Map(srcClass1, typeof(class1), typeof(class2));

 

4. AutoMapper.Mapper.DynamicMap() คือเม็ธธอดที่รองรับการแม๊พค่ากับกลุ่ม anonymous object

5. ในด้าน Performance ของ AutoMapper นั้นเป็นข้อที่ต้องพิจารณาให้มาก เนื่องจาก AutoMapper ช้ากว่า Manual Map อยู่ประมาณ 7 เท่า เพราะหากมีจำนวน object collection จำนวนมาก ๆ จะทำให้ระบบทำงานช้าลง 

ปล. ในรุ่นถัดไปทางทีมงานจะปรับปรุงประสิทธิภาพการทำงานไปใช้ Parallel Library ของ .NET ซึ่งจะทำให้ performance ดีขึ้น

 

6 Demo:

มาดูวิธีการใช้งานกันครับ ว่าการใช้งาน AutoMapper ว่าลดเวลางานการพัฒนา และช่วยให้ระบบของเรายืดหยุ่นได้ยังไง  โดยจะขออธิบายการทำงานที่เป็นพื้นฐานและใช้งานกันบ่อย ๆ

 

DEMO 1:  Simple Type + Nested Type Mapping

ต้องการส่งค่าจาก Person (Domain) ไปยัง PersonDTO และมี Nested Type เป็น Address class โดยมีโครงสร้างและชื่อ property เหมือนกัน

//Domain Class
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }
}
public class Address
{
    public string Road { get; set; }
    public string State { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

// DTO  Class
public class PersonDTO
{
    public string Name { get; set; }
    public int Age { get; set; }
    public AddressDTO Address { get; set; }
}
public class AddressDTO
{
    public string Road { get; set; }
    public string State { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

   1.1. กรณีเขียนโค้ดส่งค่าเอง (Manual Mapping) จะเห็นได้ว่าต้องกำหนดค่าเองทุก property

//สร้าง object person และกำหนดค่า
var objPerson = new Person
        {
            Name = "Calos Santana",
            Age = 53,
            Address = new Address
            {
                Road = "Platoo",
                State = "CA",
                Postcode = "11000",
                Country = "USA"
            }
        };

//ทำการ map ค่าจาก objPerson -> dtoPerson ด้วย object initializer (c# 3.0 feature)
var personDto = new PersonDTO
                           {
                                 Name = objPerson.Name,
                                 Age = objPerson.Age,
                                 Address = new AddressDTO
                                                  {
                                                      Road = objPerson.Address.Road,
                                                      State = objPerson.Address.State,
                                                      Postcode = objPerson.Address.Postcode,
                                                      Country = objPerson.Address.Country
                                                   }
                            };

   1.2. กรณีใช้งาน AutoMapper:

//config บอกว่า class Person จะ map ไปยัง PersonDTO
AutoMapper.Mapper.CreateMap<Person, PersonDTO>();
//config Nested Type บอกว่า class Address จะ map ไปยัง AddressDTO
AutoMapper.Mapper.CreateMap<Address, AddressDTO>();
 
// Map value โดยใช้ AutoMapper
var dtoPerson = AutoMapper.Mapper.Map<Person, PersonDTO>(objPerson);

วิเคราะห์โค้ดจาก Demo 1 : 

การ map ค่าจะสั่งให้ AutoMapper รู้ว่า Class ใด Map เข้ากับ Class ใดด้วยการใช้ CreateMap Method และการ map ค่านั้นสามารถสั่งได้ด้วย 3 บรรทัดดังที่เห็นในตัวอย่าง 

 


DEMO 2: Collection Mapping

เป็นการแม๊พคลาสกลุ่มที่เป็น Array, List<T> ไปมาระหว่างกัน

มาดูโดเมนคลาสที่มีดังนี้ Person จะมี Address เป็น List<T> ซึ่งเป็น collection ประเภทหนึ่ง

//Domain Class
public class Person
{
   public string Name { get; set; }
   public int Age { get; set; }
   public List<Address> Addresses { get; set; }
}
public class Address
{
   public string Road { get; set; }
   public string State { get; set; }
   public string PostCode { get; set; }
   public string Country { get; set; }
}

และเราก็มี DTO Class ที่มีโครงสร้างเหมือนกันกับ Domain Class ดังนี้

// DTO  Class
public class PersonDTO
{
   public string Name { get; set; }
   public int Age { get; set; }
   public List<AddressDTO> Addresses { get; set; }
}
public class AddressDTO
{
   public string Road { get; set; }
   public string State { get; set; }
   public string PostCode { get; set; }
   public string Country { get; set; }
}

ใช้ AutoMapper แม๊พค่า Collection

//ดึง persons list object collectionขึ้นมา
List<Person> lsPersons = GetPersons();
//แปลง list ไปเป็น array object เพือใช้ทดสอบ
Person[] arPersons = lsPersons.ToArray();

//Config Mapper ให้รู้จัก Soruce Type และ Target Type
Mapper.CreateMap<Person, PersonDTO>();
Mapper.CreateMap<Address, AddressDTO>();

//สั่งแม๊พค่า lsPersons List Object ไปเป็น lsPersonDto List Object
var lsPersonDto = Mapper.Map<List<Person>, List<PersonDTO>>(lsPersons);
//สั่งแม๊พค่า arPersons array object ไปเป็น arPersonDto array Object
var arPersonDto = Mapper.Map<Person[], PersonDTO[]>(arPersons);

วิเคราะห์โค้ดจาก Demo 2:

จะเห็นได้ว่าโค้ดส่วนที่ได้ทำ Hilight ไว้นั้น จะเป็นส่วนที่บอกให้ AutoMapper รู้ว่า Source และ Target ที่ต้องการจะแม๊พค่า ณ ตอนนั้นเป็น Collection ประเภทใด และเรายังสามารถแม๊พค่าจาก List –> Array ได้เนื่องจาก implement IEnumerable เหมือนกัน

 


DEMO 3: Custom Mapping

คือการสร้างแม๊พขึ้นเองในกรณีที่ source class กับ target class นั้นมีความแตกต่างกันมาก ไม่ว่าจะเป็น Property Name/Type  ที่ต่างกัน

//Domain Class
public class Person
{
   public string Name { get; set; }
   public int Age { get; set; }

   public DateTime Birthdate { get ;set; } // Birthdate แบบ DateTime
   public Address Address { get; set; }
// Address แบบ class
}
public class Address
{
   public string Road { get; set; }
   public string State { get; set; }
   public string Postcode { get; set; }
   public string Country { get; set; }
}

ในส่วนของ dto class จะมีโครงสร้างแตกต่างกับ domain class คือมี property Birthdate และ Address เป็น Type แบบ string

// DTO  Class
public class PersonDTO
{
   public string Name { get; set; }
   public int Age { get; set; }

   public string Birthdate { get; set; } // Birthdate แบบ string
   public string Address { get; set; }
// Address แบบ string
}

วิธีแรกใช้ ForMember() Method : 

สำหรับ ForMember() method เป็น fluent syntax ที่ช่วยให้เรากำหนดค่าให้แก่ property ปลายทางที่ต้องการได้ และในตัวอย่างนี้ผมแสดงให้เห็นว่าเราต้องการแม๊พค่าจาก

Person.Birthdate (DateTime) –> PersonDTO.Birthdate (string) โดยการใช้ DateTime.ToString() และ

Person.Address (class) –> PersonDTO.Address (string) โดยการใช้ string.format(format string, object) ดังตัวอย่างด้านล่าง

//Config บอกว่า classs Person จะ map ไปยัง PersonDTO และกำหนด custom mapping
Mapper.CreateMap<Person, PersonDTO>()
  .ForMember(dest => dest.Birthdate, opt => opt.MapFrom( 
      src => src.Birthdate.ToShortDateString()))
   .ForMember(dest => dest.Address, opt => opt.MapFrom(
src => string.Format("{0} {1} {2} {3}", src.Address.Road, src.Address.State,       src.Address.Postcode, src.Address.Country)));

// Map value โดยใช้ AutoMapper
var dtoPerson = AutoMapper.Mapper.Map<Person, PersonDTO>(objPerson);

วิธีที่สองคือใช้ TypeConverter<TSource, TTarget> abstract class

สำหรับการใช้ TypeConverter นั้นเป็นการ override method ที่ชื่อ ConvertCore และ return Type เป็น TTarget ดังนี้

//Custom mapping Person -> PersonDTO
public class PersonToDtoConverter : TypeConverter<Person, PersonDTO>
{
   protected override PersonDTO ConvertCore(Person source)
   {
      return new PersonDTO
      {
         Name = source.Name,
         Age = source.Age,
         Birthdate = source.Birthdate.ToShortDateString(), // DateTime -> String
         Address = string.Format("{0} {1} {2} {3}",        // Class –> String
            source.Address.Road, source.Address.State,
            source.Address.Postcode, source.Address.Country)
      };
    }
}

นำ PersonToDtoCoverter ไปใช้งานใน Map ดังนี้

//สร้าง object person และกำหนดค่า
var objPerson = new Person
{
   Name = "Calos Santana",
   Age = 53,  Birthdate = DateTime.Now,
   Address = new Address
   {
      Road = "Platoo", State = "CA",
      Postcode = "11000", Country = "USA"
    }
};
 
//บอก mapping ชุดนี้ว่าให้ใช้ PersonToDtoConverter ที่เราสร้างขึ้น
Mapper.CreateMap<Person, PersonDTO>().ConvertUsing<PersonToDtoConverter>();
var personDto = AutoMapper.Mapper.Map<Person, PersonDTO>(objPerson);

 

ผลลัพธ์ของ custom mapping ทั้งสองวิธีของ personDto ตามภาพด้านล่างนี้

image

 

 


DEMO 4: Flattening Mapping

คือการทำ value ของ nested class ให้ขึ้นมาอยู่ใน level เดียวกับ root class

จากโดเมนคลาสที่เห็นข้างล่างนี้จะมี nested class ที่ชื่อ Address

//Domail Class
public class Person
{
   public string Name { get; set; }
   public Address Address { get; set; }
}
public class Address
{
   public string Detail { get; set; }
   public string Country { get; set; }
}

มาดูการสร้าง DTO โดยใน PersonDetail Class ให้สร้าง property โดยใช้ชื่อ class name + property name ของ Domain Class มาสร้างดังนี้

//DTO Class
public class PersonDetail
{
   public string Name { get; set; }
   public string AddressDetail { get; set; } // flatten Address.Detail
   public string AddressCountry { get; set; } // flatten Address.Country
}

จากนั้นทดลอง Map ค่าดูด้วยโค้ดด้านล่างนี้

var person = new Person
{
   Name = "A la cart",
Address = new Address
   {
      Detail = "602 sukumwit road, bangkok 10110",
      Country = "Thailand"
   }
};

Mapper.CreateMap<Person, PersonDetail>();
var dtoPersonDetail = Mapper.Map<Person, PersonDetail>(person);

ผลลัพธ์ได้ค่าออกมาตามภาพด้านล่างนี้

image

 

 


DEMO 5: Interface Mapping

เป็นที่ทราบการว่าประโยชน์ของการใช้งาน Interface ช่วยลดการยึดเกาะของ implementation ในแต่ละส่วนออกจากกัน (loosely couple) ดังนั้นจึงมีความสามารถในการแม๊พ เช่นการแม๊พ object ไปเป็น Interface โดยที่ Class นั้นไม่ต้อง implement interface มาดูกันครับ

// domain class 
public class Customer
{
   public string Code { get; set; }
   public string Name { get; set; }
}
 
// interface 
public interface ICustomerService
{
   string Code { get; set; }
   string Name { get; set; }
}

จากนั้นมาลองดูโค้ดกัน

//สร้าง cust object
var cust = new Customer() { Code = "102", Name = "John Mayer"};
 
//กำหนด cust mapping ไปยัง interface
Mapper.CreateMap<Customer, ICustomerService>();
 
//สั่งให้ map ค่าไปยัง interface
var icustsvc = Mapper.Map<Customer, ICustomerService>(cust);

 

ผลลัพธ์หลังการรันจะเห็นได้ตามภาพด้านล่างนี้ เห็นได้ว่า AutoMapper ได้สร้าง dynamic proxy ขึ้นมาสำหรับคืนค่ากลับไปให้ icussvc ที่เป็น interface

image

 

 


DEMO 6: Dynamic Mapping

เป็นการแม๊พค่าอ๊อบเจ็คที่เป็น anonymous type ไปยัง strong typed มีอยู่หลายวิธีและรูปแบบการทำงานด้งนี้

Anonymous Type Mapping

มาดูโมเดลสำหรับเทสกันครับ ผมประกาศไว้ทั้ง class และ interface มีโครงสร้างเหมือนกันดังนี้

// domain class
public class Customer
{
   public string Code { get; set; }
   public string Name { get; set; }
}

// domain interface
public interface ICustomerService
{
   string Code { get; set; }
   string Name { get; set; }
}

จากนั้นผมได้สร้าง anonymous type ด้วย code line นี้ ให้มี property เหมือนกัน

// create anonymous type object
var cust = new {Code = "C001", Name = "Anderson"};

จากนั้นก็สั่ง map ข้อมูล โดยใช้ class และ interface เข้าไปแม๊พค่าจาก cust anonymous object ดังนี้

// call dynamic map to Customer Class
var res1 = Mapper.DynamicMap<Customer>(cust);
// call dynamic map tp ICustomerService interface
var res2 = Mapper.DynamicMap<ICustomerService>(cust);

มาดู output ของตัวแปร res1, res2 กันครับ

image  image

 

IQueryable Anonymous Type Mapping

หนีไม่พ้นกันกับ interface ตัวนี้ครับ เนื่องจากเราเขียนโปรแกรมกับ LINQ ดังนั้น มาดููวิธีการกันครับ

ผมมี Domain Class ดังนี้ครับ

//Domain Class
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; } public DateTime Birthdate  {get; set; }
    public Address Address { get; set; }
}
public class Address
{
    public string Road { get; set; }
    public string State { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

ผมสร้าง DTO class ไว้ตามนี้

//DTO class
public class CustomerInfo
{
   public string Info1 { get; set; }
   public string Info2 { get; set; }
   public string Info3 { get; set; }
}

และผมมี Mapping Helper Method ไว้ช่วยทำการแม๊พค่าจาก IQueryable ดังนี้

//
public static List<T> MapDynamicType<T>(IQueryable query) where T : class
{
   var output = new List<T>();
   var sourceType = query.GetType().GetGenericArguments()[0];
   var destType = output.GetType().GetGenericArguments()[0];

   foreach (var src in query)
   {
      var mySrc = Mapper.DynamicMap(src, src.GetType(), destType);
      output.Add(Mapper.DynamicMap<T>(mySrc));
   }
   return output;
}

จำลองสร้าง person เป็น list collection เอาไว้ ดสอบดังนี้

//สร้าง person list collection
var persons = new List<Person>();

//วนลูปสร้าง person object 10 ตัว
for (int i = 0; i < 10; i++)
{
   //สร้าง object person และกำหนดค่า
   var objPerson = new Person
   {
      Name = "Calos Santana"+i,
      Age = 20+i,
      BirthDate = DateTime.Now.AddDays(i),
      Address = new Address
      {
         Road = "Platoo"+i,
         State = "CA"+i,
         Postcode = "1100"+i,
         Country = "USA"+i
      }
   };
   persons.Add(objPerson);
}

 

จากนั้นทำการดึงข้อมูลให้ออกมาในรูป IQueryable Anonymous Type ดังนี้

//ใช้ LINQ ดึง person ออกมาในรูป anonymus type โดย return เป็น IQueryable
var query = from p in persons.AsQueryable()
   select new {
      Info1 = "Name is " + p.Name + ", Age " + p.Age,
      Info2 = " Birthdate is " + p.BirthDate.ToShortDateString(),
      Info3 = " and Address " + p.Address.Road + p.Address.State
     + p.Address.Postcode + p.Address.Country
    };

และสั่งให้แม๊พค่าดังนี้ โดยใช้ Helper medthod ที่สร้างไว้ดังนี้

// ส่ง query ที่ได้เข้าไป map กับ CustomerInfo class
var respx = MapDynamicType<CustomerInfo>(query);

ซึ่งจะได้ผลลัพธ์ดังนี้ครับ

image

Conclusion

เราจะเห็นได้ว่าประโยชน์ของ AutoMapper นั้นมีค่อนข้างมากมาย เพียงแค่ให้รู้จักใช้และทดสอบดี ๆ เพื่อไม่ให้เกิด overhead ของการใช้งานมากเกินไป ไว้บทความหน้าผมจะนำ AutoMapper ไปใช้งานร่วมกับการทำ  Application Architecture ครับ

 

 


สามารถ download source code ได้ที่นี่

Download Source Code Here

Chalermpon Areepong Nine (นาย)

Microsoft MVP Thailand ASP.NET

email : nine_biz-talk.net at hotmail dot com

2 Comments »

  1. มีประโยขน์มากครับ ขอบคุณครับ

    Comment by rungwiroon — 28/03/2011 @ 3:05 pm

    • ยินดีที่เป็นประโยชน์ครับ🙂

      Comment by Nine MVP ASP.NET — 28/03/2011 @ 11:46 pm


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: