Nine MVP's Blog

23/09/2013

Object Mapping Library Performance Testing

ตอนนี้ปั่นไวๆ ไม่มีแบบแผนอะไรมาก เนื่องจากมีการ discuss กันในกลุ่ม ASP.NET & MVC Developers Thailand เกี่ยวกับการทำ mapping object
และได้หยิบยกประเด็นเรื่อง
Performance ว่าน่าวิตกแค่ไหนสำหรับการใช้ Library กลุ่มนี้

ผมจึงได้หยิบออกมาทดสอบด้วยกัน 4 วิธีคือ

  1. Custom Mapping
  2. EmitMapper
  3. ValueInjection
  4. AutoMapper

ทุกตัวใช้ version ล่าสุดจาก nuget (prerelease option)

เพื่อหา performance ของ simple object collection จำนวน 1M ล้านตัว เพื่อเปรียบเทียบดู ก็ได้ผลตามนี้

หลักการคือต้องการ .ToList() เพื่อทำการ copy object ไปยังตัวแปรที่รับค่าจริงๆ

เห็นได้ชัดว่าแย่ที่สุดคือ ValueInjection

แย่รองลงมาคือ EmitMapper

ผลเทสดีอันดับที่สองคือ Custom Mapping

และประสิทธิภาพดีที่สุดและทำงานได้ไวสุดยกให้ AutoMapper

มาดู code ที่ใช้ทดสอบ

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using AutoMapper;

using Omu.ValueInjecter;

namespace DemoPartialUpdate

{


public class User

{


public int Id { get; set; }

public string UserName { get; set; }

public string Password { get; set; }

public DateTime LastLogin { get; set; }

}


class Program

{


static void Main(string[] args)

{


int objectLenght = 1000000;


List<User> users = new List<User>(objectLenght);


for (int i = 0; i < objectLenght; i++)

{


User user = new User();

user.Id = i;

user.UserName = “User” + i;

user.Password = “1” + i + “2” + i;

user.LastLogin = DateTime.Now;

users.Add(user);

}


Stopwatch st = new Stopwatch();


//Custom Mapping

st.Start();

var userList = users.Select(o => new User{ Id = o.Id, UserName = o.UserName, Password = o.Password, LastLogin = o.LastLogin}).ToList();

st.Stop();

Console.ForegroundColor = ConsoleColor.Green;

Console.WriteLine(“Custom mapping {0} objects within {1}, objectLenght, st.Elapsed.ToString(“g”));


//EmitMapper

st.Start();

var map = EmitMapper.ObjectMapperManager.DefaultInstance.GetMapper<User, User>();

IEnumerable<User> emitUsers = users.Select(o => map.Map(o)).ToList();

st.Stop();

Console.ForegroundColor = ConsoleColor.Blue;

Console.WriteLine(“EmitMapper mapping {0} objects within {1}, objectLenght, st.Elapsed.ToString(“g”));


//ValueInject

st = new Stopwatch();

st.Start();

IEnumerable<User> valueUsers = users.Select(o => (User)new User().InjectFrom(o)).ToList();

st.Stop();

Console.ForegroundColor = ConsoleColor.Red;

Console.WriteLine(“ValueInjecter mapping {0} objects within {1}, objectLenght, st.Elapsed.ToString(“g”));


//AutoMapper

st = new Stopwatch();

st.Start();

var userMap = Mapper.Map<IEnumerable<User>>(users).ToList();

st.Stop();

Console.ForegroundColor = ConsoleColor.Yellow;

Console.WriteLine(“AutoMapper mapping {0} objects within {1}, objectLenght, st.Elapsed.ToString(“g”));


Console.ReadKey();


}

}

}

จบครับ

Advertisements

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

Create a free website or blog at WordPress.com.