Nine MVP's Blog

24/09/2013

Entity Framework: Partial Update Object

Introduce

เป็นปากเสียงกันได้เลยสำหรับคนใช้ Entity Framework (EF) และ NHibernate (NH) ถึงเรื่องความสามารถต่างๆ
อาจจะเพราะ EF เกิดมาช้าเลยพากันขัดตาไปเสียหมด อาจจะเพราะว่า NH ได้ถูกนำโค้ดจาก Hibernate ที่เป็น JAVA มาเขียนเป็น C# ใหม่ด้วยกระมัง
ดังนั้นจึงมีของให้ใช้เยอะและตรงใจผู้พัฒนาค่อนข้างมาก

มาถึงเหตุให้ต้องเขียนตอนนี้ขึ้นมาเพราะมีคำถามว่า EF มี Merge() method สำหรับการ Update เฉพาะ property ที่เรากำหนดค่าได้หรือไม่

ยกตัวอย่างเช่นว่า มี 


public class User
{
    public int Id { get; set; }     

public string UserName { get; set; }

    public string Password { get; set; }

    public DateTime LastLogin { get; set; }

}

แต่ต้องการแค่อยากจะอัพเดทแค่ Password กับ LastLogin property เท่านั้นแบบนี้ โดยที่ไม่กำหนด property อื่นๆ
และเมื่อส่งไปอัพเดทใน database ต้องไม่ทำให้ property อื่นๆที่ไม่ได้กำหนดค่ากลายเป็น Null แบบตัวอย่างข้างล่างนี้


//pass user object and specify update properties
userDto.Password = “P@ssw0rd”;
userDto.LastLogin = DateTime.Now;db.PartialUpdate(userDto);

db.SaveChanges();

 

คำตอบคือ ไม่มีหรอกเสียใจด้วยครับ

แต่……ช้าก่อน  เราทำได้ครับ EF เก่งพอที่จะให้เราโชว์ฝีเท้าใส่โค้ดเข้าเพิ่มเติมเข้าไปเองได้

..

SOLUTION 1 Passing Property Name

วิธีแรกก็คือ ก็ส่งชื่อ Property ที่ต้องการจะ update เข้าไปเองก็สิ้นเรื่อง แต่ก็ไม่ถึงกับต้องมานั่งพิมพ์ค่า property name ใส่ string variable เข้าไปกันเองหรอกนะ มีวิธีที่ดีกว่านั้น


เริ่มต้นด้วยการติดตั้ง
Entity Framework ลงในโปรเจคก่อน
จากนั้นสร้าง
User class ขึ้นตาม introduce ด้านบน
ต่อมากก็สร้าง UserContext class สำหรับใช้งานตาม code ด้านล่างนี้


public partial class UserContext : DbContext
{
    public UserContext() : base(“name=UserDB”) { }    

public DbSet<User> Users { get; set; }

}

เสร็จแล้วให้เพิ่ม class file อีกอัน สำหรับสร้าง Partial Class เพื่อเพิ่ม method สำหรับทำ PartialUpdate ตาม codeด้านล่าง ซึ่งจะเห็นได้ว่ามีแต่ Method Name และ Signature ตัว code จะเพิ่มให้ใน block หลังๆครับ


public partial class UserContext
{     

public void PartialUpdate<T>(T entity, params Expression<Func<T, object>>[] propsToUpdate) where T : class {}    

private IEnumerable<string> ConfigPartialUpdate<T>(T entity, out DbEntityEntry<T> entry) where T : class { }

private IEnumerable<string> GetPrimaryKeys<T>() where T : class {}

}

จากโค้ดด้านบนจะเห็นได้ว่ามี 3 methods ซึ่งเป็น public 1, private 2  ซึ่งขออธิบายดังนี้

GetPrimaryKeys() Method เราเอาไว้ดึง primary key ทั้งหมดที่มีใน T class ที่เราส่งเข้ามาเพื่อทำการอัพเดท โดยดึง property name ออกมาเพื่อที่จะให้ข้ามการอัพเดทไป เพราะจะทำให้เกิด Error หากสั่งอัพเดทรายการนี้


private IEnumerable<string> GetPrimaryKeys<T>() where T : class
{    

ObjectContext objectthis = ((IObjectContextAdapter)this).ObjectContext;

ObjectSet<T> set = objectthis.CreateObjectSet<T>();

IEnumerable<string> keyNames = set.EntitySet.ElementType.KeyMembers.Select(k => k.Name);

    return keyNames;

}

ConfigPartialUpdate() Method จะมีหน้าที่ในการตรวจสอบ object ที่จะทำการอัพเดทโดยป้องกันการ attach object ซ้ำซ้อนใน DbContext


private IEnumerable<string> ConfigPartialUpdate<T>(T entity, out DbEntityEntry<T> entry) where T : class
{

    //set validate on save 
   

this.Configuration.ValidateOnSaveEnabled = false;


//get primary keys 

var pks = GetPrimaryKeys<T>();


//get object entry

entry = this.Entry(entity);


//if dbthis is already tracking T object then remove it all and attach new object

if (entry.State == EntityState.Detached && this.ChangeTracker.Entries<T>().Any())

{

foreach (DbEntityEntry<T> item in this.ChangeTracker.Entries<T>())

{

this.Entry<T>(item.Entity).State = EntityState.Detached;

}

this.Set<T>().Attach(entity);

}

//if T object is detached 

else if (entry.State == EntityState.Detached)

{

this.Set<T>().Attach(entity);

}

return pks;

}

PartialUpdate() Method เราจะใช้ในการกำหนด property ที่มีการแก้ไขค่าและบอกให้ EF ทำการอัพเดท
โดยใช้ params Expression<Func<T, object>>[] มาช่วยในการรับค่า property ที่ต้องการจะทำการอัพเดท


public void PartialUpdate<T>(T entity, params Expression<Func<T, object>>[] propsToUpdate) where T : class
{   

DbEntityEntry<T> entry;

var pks = ConfigPartialUpdate(entity, out entry);

foreach (var prop in propsToUpdate)

{

//if prop.name is pk go next loop

if (pks.Contains(prop.Name))

continue;

var errors = entry.Property(prop).GetValidationErrors();

if (errors.Count == 0)

{

entry.Property(prop).IsModified = true;

}

else

{

throw new DbEntityValidationException(string.Format({0} Model has errors please check.”, typeof(T).Name), errors.Select(o => new DbEntityValidationResult(entry, errors)));

}

}

}

..

SOLUTION 1 DEMO TEST

มาทดลอง method ที่เราเขียนกันไว้ดูด้วย code ข้างล่างนี้

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DemoPartialUpdate.DAL;
using DemoPartialUpdate.Domain;
namespace DemoPartialUpdate
{

class Program

{

static void Main(string[] args)

{

using (var db = new UserContext())

{

//If first run

if (db.Database.CreateIfNotExists())

{

for (int i = 1; i < 3; i++)

{

User user = new User();

user.Id = i;

user.UserName = “User” + i;

user.Password = “1234”;

user.LastLogin = DateTime.Now;

db.Users.Add(user);

}

db.SaveChanges();

}


var userx = db.Users.AsNoTracking().ToList();

Console.ForegroundColor = ConsoleColor.Yellow;

Console.WriteLine(“Before Update……..”);


foreach (var user in userx)

{

Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”));

}

Console.WriteLine();


//Simulate DTO object

var userDto = new User();

userDto.Id = 1; //Id is a key it will not update

userDto.Password = “P@ssw0rd”; //Update

userDto.LastLogin = DateTime.Now.AddDays(1); //Update


//pass user object and specify update properties

db.PartialUpdate(userDto, x => x.Password, x => x.LastLogin);

db.SaveChanges();

userx = db.Users.AsNoTracking().ToList();

Console.ForegroundColor = ConsoleColor.Green;

Console.WriteLine(“After Update……..”);

foreach (var user in userx)

{

Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”));

}

}

}

}

}

จากโค้ดด้านบนเป็นการสั่งให้ Create Database หากไม่มีอยู่
และให้ทำการ
loop 2 เพื่อสร้าง User Object ลงไปใน Database
จากนั้นทำการ simulate ด้วยการสมมติว่ารับค่ามาจากภายนอก โดยกำหนด userDto Object ขึ้นมาและระบุ Id = 1 เพื่อบอกว่าให้อัพเดทค่าไปที่ User.Id =1 ต่อมากำหนดค่า 2 property คือ Password, LastLogin เพื่อต้องการให้อัพเดทลง Database
แล้วเราก็ดึงออกมาแสดงผลหลังทำการ
Insert ลงไปครั้งแรกโดยระบุ AsNoTracking() เพื่อไม่ให้ DbContext เก็บค่า object เอาไว้ติดตามในระบบ

และถึงเวลาทดสอบ เราก็เรียก db.PartialUpdate() โดยส่ง userDto เข้าไปและใช้ lambada x=> กำหนด property ที่ต้องการจะให้ update เข้าไป

ต่อมาก็สั่ง SaveChanges() เพื่อให้ commit รายการลง Database

สุดท้ายก็ดึงค่า ขึ้นมาจาก Database ทั้ง 2 User เพื่อแสดงค่าที่เปลี่ยนแปลงไปของ 2 property ที่สั่ง PartialUpdate ไป

เราไปดูใน Database หลังจากที่รันโค้ดชุดด้านบนนี้ไปแล้ว จะพบว่าเจอ 2 records ใน Users Table และมีการ update ค่าถูกต้องที่ User.Id = 1

ซึ่งเราไม่ได้กำหนดค่าให้ UserName Property หากใช้ Update โดยการ Attach() ของ DbContext ตรงๆ จะทำให้กลายเป็น Null ไปนั่นเอง

..

SOLUTION 2 Implement INotifyPropertyChanged

วิธีนี้จะไปยุ่งยากกับ Entity Object ในระบบทั้งหมด แต่จะทำให้ PartialUpdate method สะอาดไม่รกเต็มไปด้วย Paramter ว่าแล้วก็เริ่มลงมือ

แรกเริ่มเราจะเพิ่ม UpdatePartial() method เข้ามาใน partial class UserContext ใหม่อีก 1 method


public partial class UserContext
{

//////////


public void PartialUpdate<T>(T entity) where T : EntityBase

{


DbEntityEntry<T> entry;

var pks = ConfigPartialUpdate(entity, out entry);


foreach (var prop in entity.PropertyChangedList)

{

if (pks.Contains(prop))

continue;

var errors = entry.Property(prop).GetValidationErrors();


if (errors.Count == 0)

{

entry.Property(prop).IsModified = true;

}

else

{

throw new DbEntityValidationException(string.Format({0} Model has errors please check.”, typeof(T).Name), errors.Select(o => new DbEntityValidationResult(entry, errors)));

}

}

}

///////////////

}

เสร็จแล้วให้ทำการสร้าง NotifyPropertyChangedInvocatorAttribute Class เอาไว้ช่วยให้เราไม่ต้องระบุ property name ในตอนเรียก ตาม Code ด้านล่างนี้

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute {

public NotifyPropertyChangedInvocatorAttribute() }

public NotifyPropertyChangedInvocatorAttribute(string parameterName)

ParameterName = parameterName; }


public string ParameterName { get; private set; }

}

ต่อไปจะสร้าง BaseEntity สำหรับเป็นตัวช่วยให้ Entity Classของเราเก็บค่า Property Name ที่มีการเปลี่ยนแปลงค่าจากการ assign ค่าได้

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace DemoPartialUpdate.Domain
{

public abstract class EntityBase : INotifyPropertyChanged

{


protected EntityBase(){ }


private HashSet<string> _propertyChangeList = new HashSet<string>();


public HashSet<string> PropertyChangedList { get { return _propertyChangeList; } }


public void AddPropertyChanged(string propertyName)

{

if (!_propertyChangeList.Contains(propertyName))

_propertyChangeList.Add(propertyName);

}


public void ClearPropertyChanges()

{

_propertyChangeList.Clear();

}


public event PropertyChangedEventHandler PropertyChanged;

[NotifyPropertyChangedInvocator]

protected virtualvoid OnPropertyChanged([CallerMemberName] string propertyName = null)

{

PropertyChangedEventHandler handler = PropertyChanged;

if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));

AddPropertyChanged(propertyName);

}

}

}

จากนั้นไปเปิด User Class มาแก้ไขให้ได้ตามโค้ดด้านล่างนี้ โดยนำ EntityBase ไป Implement

using System;

using System.Collections.Generic;

using System.ComponentModel.DataAnnotations;

using System.Linq;

using System.Runtime.InteropServices;

using System.Text;

using System.Threading.Tasks;


namespace DemoPartialUpdate.Domain
{

public class User : EntityBase

{


private int _id;

private string _userName;

private string _password;

private DateTime _lastLogin;

public int Id {

get { return _id; }

set { _id = value; }

}

public string UserName {

get { return _userName; }

set {

_userName = value;

OnPropertyChanged();

}

}

public string Password {

get { return _password; }

set {

_password = value;

OnPropertyChanged();

}

}

public DateTime LastLogin {

get { return _lastLogin; }

set {

_lastLogin = value;

OnPropertyChanged();

}

}

}

}

SOLUTION 2 DEMO TEST

ให้เรา copy test class จาก Solution 1แล้วเปลี่ยนเป็น db.PartialUpdate(userDto);

ก่อนรันให้ไปลบ UserDB Database ทิ้งก่อน

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DemoPartialUpdate.DAL;
using DemoPartialUpdate.Domain;
namespace DemoPartialUpdate
{

class Program 

{

static void Main(string[] args)

{

using (var db = new UserContext())

{

//If first run

if (db.Database.CreateIfNotExists())

{

for (int i = 1; i < 3; i++) {

User user = new User();

user.Id = i;

user.UserName = “User” + i;

user.Password = “1234”;

user.LastLogin = DateTime.Now;

db.Users.Add(user);

}

db.SaveChanges();

}


var userx = db.Users.AsNoTracking().ToList();

Console.ForegroundColor = ConsoleColor.Yellow;

Console.WriteLine(“Before Update……..”);


foreach (var user in userx)

{

Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”));

}

Console.WriteLine();


//Simulate DTO object

var userDto = new User();

userDto.Id = 1; //Id is a key it will not update

userDto.Password = “P@ssw0rd”; //Update

userDto.LastLogin = DateTime.Now.AddDays(1); //Update

//pass user object and specify update properties

db.PartialUpdate(userDto);

db.SaveChanges();

userx = db.Users.AsNoTracking().ToList();

Console.ForegroundColor = ConsoleColor.Green;

Console.WriteLine(“After Update……..”);

foreach (var user in userx)

{

Console.WriteLine(“User Id {0}, UserName {1}, Password {2}, LastLogin {3}, user.Id, user.UserName, user.Password, user.LastLogin.ToString(“G”));

}

}

}

}

}

ซึ่งก็จะได้ผลลัพธ์เหมือนกับ Solution 1

Conclusion

สรุปว่าทั้งสองวิธีมีข้อดีและข้อเสียแตกต่างกัน แต่ได้ผลลัพธ์เดียวกัน

แบบที่ กำหนด property ที่ต้องการด้วยตัวเอง

ข้อดีคือ ใช้งานง่ายเหมาะกับระบบที่มีโมเดลอยู่แล้ว และไม่ต้องการจะไปรื้อโครงสร้างให้วุ่นวาย
ข้อเสียคือ จำเป็นต้องระบุ
property ที่ต้องการให้ Change ค่าเอง ซึ่งก็อาจจะหลงๆลืมๆส่งไปไม่ครบก็ได้ อาจจะทำงานผิดพลาด

แบบที่ ใช้ INotifyPropertyChanged เข้าช่วยในการตรวจสอบการเปลี่ยนแปลง

ข้อดีคือ ไม่ต้องกังวลว่าจะลืมส่ง Property อะไรไปบ้าง จุดเรียกใช้งานก็ใช้ง่าย
ข้อเสียคือ ต้องแก้ไข
entity class อาจจะกระทบงานเก่าที่มีอยู่

หวังว่าตอนนี้จะเป็นประโยชน์ ในการนำไปใช้งาน และช่วยเพิ่มความรู้และเทคนิคต่างๆให้เพื่อนๆกันได้ไม่มากก็น้อย

download source code  

จบครับ

Advertisements

15/10/2012

ASP.NET MVC Series: Web Push Technology II.1

Filed under: ASP.NET, ASP.NET MVC, Design Pattern, W3C, WEB — Tags: , , , , — Nine MVP @ 8:46 pm

จากตอนที่แล้ว web push technology I ได้แนะนำความเป็นมาของการทำงานแบบ push และเทคโนโลยีต่างๆที่มีให้ใช้งานกันไปแล้ว ตอนนี้เราจะมาเขียนโปรแกม โดยขอเริ่มที่การใช้งานตัว Server Sent Event (SSE) กันก่อน และจะไปจบที่ WebSockets (ws) ฮีโร่มัยซินของเราในตอนสุดท้ายของเรื่องนี้

เตรียมเครื่องมือ Tool:

  1. Visual Studio 2010+ (บทความใช้ vs2012)
  2. ASP.NET MVC 4.0
  3. HTML5 Browser (Chrome v20+, FF v14+, Opera v11+)
  4. jQuery latest version
  5. knockout.js latest version

โจทย์การทดสอบนี้คือโปรแกรม Push Mail

โจทย์ตัวอย่าง

  • เมื่อ load inbox ในครั้งแรกที่เข้าสู่ระบบ
  • ระบบสามารถ push email ไปยังผู้รับปลายทาง โดยเมื่อมีอีเมลเข้ามาใหม่ก็ส่งเข้า inbox ของ online user ทันที 
  • สามารถสร้างและส่งอีเมลไปหา user คนอื่นในระบบได้

มาถึงช่วงของการละเลงโค้ดในตอนนี้ผมขอนำเสนอการใช้งาน

 

Server Sent Event (EventSource) Solution

key technologies สิ่งที่วางไว้สร้าง SSE Solution นี้ก็จะมีดังนี้

  • Server จะเป็น ASP.NET WEB API  ในการบริการงานด้าน push mail ซึ่งส่งข้อมูลอีเมลใหม่ไปยัง user ที่ online อยู่หากมีการส่งอีเมล์เข้ามา ณ ตอนที่ออนไลน์อยู่ 
  • Client จะใช้เป็น ASP.NET MVC, jQuery, Knockout.js ในการทำงาน เข้าระบบ, บันทึกส่งเมล และแสดงผล push mail จาก server

image

จากภาพด้านบนอธิบายว่า

  1. เมื่อ user1 เข้าระบบมาแล้วก็ให้ทำการ load inbox เฉพาะทีมีคนส่งมาหากลับมาให้หน้าแรก

  2. พร้อมทั้ง subscribed เข้าใน pool ของ user online เพื่อใช้ในการ push new email

  3. return inbox data กลับไป

อาจจะ 10 นาทีต่อมามี user2  ทำการเข้าระบบมาส่งอีเมลโดย

  4. user2 เลือกส่งเมลหา user1

  5. พบ user1 ออนไลน์อยู่และทำการ push email ไปให้

 

SSE Demo Output

จำลองผู้ใช้งานขึ้น 2 คนชื่อ user1 และ user2 ตามภาพการทำงานด้านบน จะได้ผลตามลำดับภาพดังนี้

image 

เมื่อ user1 เปิด browser เข้าระบบมาครั้งแรก

image 

เมื่อ user1 ทำการใส่ username แล้วกดปุ่ม load inbox หากเข้าระบบได้ก็จะมีรายการ inbox item และสามารถส่งอีเมลได้ ซึ่งถือว่า user1 online อยู่ในระบบของ push mail แล้ว

image 

เมื่อ user2 เปิด browser เข้าระบบมาครั้งแรก และทำการใส่ username แล้วกดปุ่ม load inbox หากเข้าระบบได้ก็จะมีรายการ inbox item และสามารถส่งอีเมลได้ ซึ่งถือว่า user2 online อยู่ในระบบของ push mail แล้ว  

image

user2 ทำการส่งอีเมลหา user1 ที่ user1@nine.com ตามรูป กด send

image 

อีเมลจาก user2 ได้ถูกส่งเข้าไปในระบบเรียบร้อย

image

ที่ browser ของ user1 จะมี new email ส่งเข้า inbox ไปแสดงผลตามภาพ และมีป๊ปบอัพโผล่บอกว่าใครส่งมา

 

อธิบาย MVCPushSSE Project

มาดู flow การทำงานของตัวโปรแกรมใน mvc application จากภาพด้านล่าง (เขียนโค้ดเรียบๆไม่เล่นกระบวนท่ามาก)

image

 

* การอธิบายโค้ดจะยึดตาม flow ด้านบนและชี้โค้ดให้เห็นเป็นส่วนๆ

 

0. user access to homepage

เมื่อ user เปิด browser แล้วเข้าเว็บมาครั้งแรก http://localhost:xxx/ จะไปเข้า /Email/Index (controller/action) ตามที่ได้ตั้งค่าไว้ใน routeconfig ทำให้มีการสร้าง viewmodel ของ knockout เกิดขึ้นตามนี้

   1: <script>

   2:  

   3:     //EventSource object

   4:     var source;

   5:     

   6:     //main view model

   7:     var viewModel = {

   8:         username: ko.observable(""),//map to username input

   9:         email: ko.observable(""), //map to email input

  10:         messages: ko.observableArray([]) //dynamic array inbox message (json) output

  11:     };

  12:  

  13:     //new email object viewmodel

  14:     viewModel.newEmail = {

  15:         Username: ko.computed(function () { return viewModel.username(); }), //get username value from viewModel.username

  16:         To: ko.observable(""), //map to To input

  17:         From: ko.computed(function () { return viewModel.email(); }), //get From value from viewModel.email

  18:         Title: ko.observable(""), //map to To input

  19:         Message: ko.observable("") //map to Body input

  20:     };

และเมื่อ html document โหลดเสร็จเรียบร้อบก็จะถึงการสั่งให้ เอา viewmodel ด้านบนมา map เข้าใน knockout engine ด้วยคำสั่งใน line4

   1: $(document).ready(function () {

   2:  

   3:     //binding mapping to viewModel

   4:     ko.applyBindings(viewModel);

   5:  

 

จากนั้นเมื่อมีการเรียกเข้าไปที่ EmailController –> Index Action จะมี code ทำงานดังนี้

line4: สร้างตัวแปร efcontext ไว้ใช้งาน
line7-10: สร้าง action สำหรับไว้ดึง view ไปแสดงผลครั้งแรกที่เข้ามาโดยจะ return Index.cshtml ออกไป

   1: public class EmailController : Controller

   2: {

   3:  

   4:     MailBoxDBContext mbctx = new MailBoxDBContext();

   5:  

   6:     // GET: /Email/

   7:     public ActionResult Index()

   8:     {

   9:         return View();

  10:     }

 

1. เมื่อ user กด load mailbox

เมื่อ user ได้ใส่ username และกดส่งกลับเข้ามา javascript ใน index.cshtml ส่วนนี้จะทำงานตามที่ได้ hook event เอาไว้ โดยสั่ง GET username ส่งไปที่ /Email/GetMyInbox  ซึ่งจะไปโหลด user inbox  กลับออกมา

   1: $('#loadMailBox').live("click", function (e) {

   2:     viewModel.email('');

   3:     var username = $('#username').val();

   4:     $.getJSON("/Email/GetMyInbox", { username: viewModel.username() },

   5:         function (data) {

   6:             if (!data.success) {

   7:                 if (source != null) source.close();

   8:                 alert(data.msg);

   9:             } else {

  10:                 viewModel.email(data.email);

  11:                 InitialSSE();

  12:                 $('#inbox').empty();

  13:                 viewModel.messages(data.mails);

  14:             }

  15:         }

  16:     );

  17: });

  1. line4: เป็นการสั่ง getJSON() มี url, json object โดยดึงเอา viewModel.username ไปใช้  และทำการยิงไป “/Email/GetMyInbox”
  2. line10: ก็จะเอา json ที่ได้ส่ง email เข้าไปใน ko เพื่อไปแสดงผลใน input email
  3. line11: จากนั้นสั่งให้ setup ตัว SSE object ด้วย InitialSSE(); //ไว้อธิบายต่อตอนหลัง
  4. line12: จากนั้นก็สั่ง clear ทุกอย่างใน inbox
  5. line13: และส่ง item ที่โหลดมาจากได้ inbox ของ user ส่งไป binding ผ่าน viewModel.messages

 

2. การทำงานของ GetMyInbox Action 

หลังจากที่ GET มาเข้าที่ EmailController –> GetMyInbox () Action ซึ่งเป็นการจำลองว่า user login เข้ามาก็จะได้ inbox ของตัวเองกลับไป

   1: [HttpGet]

   2: public JsonResult GetMyInbox(string username)

   3: {

   4:     try

   5:     {

   6:         var user = mbctx.Users.SingleOrDefault(o => o.UserName == username);

   7:         //not found user

   8:         if (user == null)

   9:             return Json(new { success = false, msg = string.Format("not found user {0}", username) },

  10:                         JsonRequestBehavior.AllowGet);

  11:  

  12:         //get mailbox send to this users email

  13:         var mails = mbctx.MailMessages.Where(o => o.To.ToLower().Trim() == user.Email.ToLower().Trim())

  14:             .OrderByDescending(o => o.ReceivedDate).ToList()

  15:             .Select(o => new MailMessageView

  16:                     {

  17:                         Id = o.Id,

  18:                         To = o.To,

  19:                         From = o.From,

  20:                         ReceivedDate = o.ReceivedDate.ToString("dd/MM/yyyy HH:mm:ss"),

  21:                         Title = o.Title

  22:                     }).ToList();

  23:  

  24:         //return json to client

  25:         return Json(new { success = true, email = user.Email, mails = mails }, JsonRequestBehavior.AllowGet);

  26:  

  27:     }

  28:     catch (Exception ex)

  29:     {

  30:         return Json(new { success = false, msg = ex.Message }, JsonRequestBehavior.AllowGet);

  31:     }

  32: }

  1. line15-45: ดึงข้อมูลอีเมลทั้งหมดที่ส่งมาถึง To username นี้กลับออกไปในรูปของ JSON ทั้งหมดกลับออกไป

 

3. เมื่อได้ json กลับมา ตัว Knockout จะเอา json ไปสร้างรายการอีเมลใน inbox

อ่านย้อนกลับไปที่ข้อ 1.2, 1.3, 1.4, 1.5

 

4. เมื่อ user มี email รีเทิร์นค่ากลับมาก็ให้ทำการ connect to Push Service

จากนั้นใน javascript function InitialSSE(); จะเป็นการกำหนดตัว EventSource object ให้ชี้ไปยัง EmailServiceController (WebAPI) ซึ่งเป็น push service ที่เราสร้างขึ้นมาสำหรับส่ง email ใหม่ที่ส่งเข้ามาจากผู้ใช้อื่นๆ โดยจะมีการ add event ไว้ 3 ตัวแต่ใช้งานจริงแค่  ‘message’ event

   1: //EventSource Setup

   2: function InitialSSE() {

   3:     if (!!window.EventSource) {

   4:         var root = "@Url.Content("~/")";

   5:         source = new EventSource(root + 'api/EmailService/?email=' + viewModel.email());

   6:  

   7:         //Add message event

   8:         source.addEventListener('message', function (e) {

   9:             console.log(e.data); // log 

  10:             var mails = JSON.parse(e.data);

  11:             //alert from email

  12:             showNewMessageAlert(mails[0].From);

  13:             //binding array object to viewmodel(template binding)

  14:             viewModel.messages.valueWillMutate();

  15:             ko.utils.arrayPushAll(viewModel.messages(), mails);

  16:             viewModel.messages.valueHasMutated();

  17:             //sort inbox item

  18:             SortMessageDesc();

  19:  

  20:         }, false);

  21:  

  22:         source.addEventListener('open', function (e) {

  23:             console.log("open!");

  24:         }, false);

  25:  

  26:         source.addEventListener('error', function (e) {

  27:             if (e.readyState == EventSource.CLOSED) {

  28:                 console.log("error!");

  29:             }

  30:         }, false);

  31:     } else {

  32:         // not supported!

  33:         alert('The EventSource is not available.');

  34:     }

  35: }

  1. line3: ตรวจสอบว่า browser ที่ทำงานอยู่ปัจจุบันสามารถใช้งาน SSE ได้หรือไม่
  2. line4-5: เป็นการสร้างตัวแปร EventSource และกำหนด url ของ push service พร้อมทั้งส่งค่าของ email ของ user คนนี้ไปด้วย
  3. line8-20: มีการ add event ‘message’ ให้ตัว eventsource และเขียนคุมการทำงานให้ทำการแสดงผลเมื่อมี message ส่งมาจาก push service โดยทำงานกับ knockout และ jquery ในการแสดงผลข้อมูลที่ Inbox
  4. line22-24: บันทึกลง log เมื่อมีการเชื่อมต่อกับ push service แล้ว

เมื่อตัว EventSource ได้ถูกสร้างขึ้นก็จะยิง GET ไปยัง /api/EmailService/ ก็จะไปเข้า API Controller ของเราข้างล่าง

 

5. create long polling for online user (EmailServiceController)

การทำงานในขั้นต่อมาคือ มีการสั่งยิงมาด้วย GET (EventSource ทำงานโดยใช้ GET) มาเข้า HttpGet Action ซึ่งมีอยู่ตัวเดียวใน EmailServiceController ก็คือ

Get(HttpRequestMessage request)

   1: //EFcontext

   2: private MailBoxDBContext mbctx = new MailBoxDBContext();

   3: //message pool

   4: private static List<MailMessageView> _newMails = new List<MailMessageView>();

   5: //online subscribe user

   6: private static ConcurrentDictionary<string, SubscriberPushMail> _subscribers = 

   7:      new ConcurrentDictionary<string, SubscriberPushMail>();

   8:  

   9: //Get

  10: public HttpResponseMessage Get(HttpRequestMessage request)

  11: {

  12:      HttpResponseMessage response = request.CreateResponse();

  13:      var email = request.QueryString("email");

  14:  

  15:      if (!string.IsNullOrEmpty(email.Trim()))

  16:      {

  17:            response.Content = new PushStreamContent(

  18:                (stream, content, tc) =>

  19:                   {

  20:                        StreamWriter streamwriter = new StreamWriter(stream);

  21:                        if(_subscribers.ContainsKey(email))

  22:                        {

  23:                           SubscriberPushMail dead;

  24:                           if(_subscribers.TryRemove(email, out dead))

  25:                           {

  26:                                dead.Stream.Dispose();

  27:                                dead = null;

  28:                           }

  29:                        }

  30:                        _subscribers.TryAdd(email, new SubscriberPushMail

  31:                           {   Email = email, Stream = streamwriter });

  32:                    }, "text/event-stream");

  33:      }

  34:      return response;

  35: }

  1. line2: สร้างตัวแปรสำหรับ efcontext,
  2. line4: _newMails สำหรับเก็บอีเมลที่มีการส่งเข้ามาใหม่ และจะถูกลบออกเมื่อส่งออกไปแล้ว
  3. line6-7: _subscribers จะเป็นผู้ใช้ที่มีการ logon ล่าสุดเข้ามาเพื่อเก็บ connection ไว้ส่งข้อมูลกลับ
  4. Line10-35: จะเป็น Get Action สำหรับ EventSource ทำการเชื่อมต่อเข้ามา พร้อมรับ email ของ user เข้ามาเพื่อเพิ่มลงไปใน _subscribers เก็บทั้ง Email และ StreamWriter ของ HttpResponse เพื่อทำการส่งข้อมูลกลับไปในตอนหลัง และมีการตรวจสอบหากมี email นี้ใน _subscribers แล้วก็ให้ลบออกไปจากระบบ *แก้บั๊ก

 

6. Connected to Push Service

เมื่อรับ response กลับมาจาก Get() Action ด้านบนก็จะเกิด ‘open’ event ซึ่งให้ย้อนกลับไปดูข้อ 4.4 ด้านบน

 

7. เมื่อ User2 ทำการส่งอีเมลไปหา User1

ต่อไปเมื่อมี user2 เปิด browser เข้ามา load mailbox ของตัวเอง และทำการส่งอีเมลถึง user1 หลังจากกดปุ่ม Send

   1: $("#sendMail").click(function () {

   2:     $.ajax({

   3:         url: "/api/EmailService/",

   4:         data: JSON.stringify(ko.mapping.toJS(viewModel.newEmail)),

   5:         cache: false,

   6:         type: 'POST',

   7:         dataType: "json",

   8:         contentType: 'application/json; charset=utf-8',

   9:         success: function (data) {

  10:             var json = JSON.parse(data);

  11:             if (!json.success) {

  12:                 alert(data.msg);

  13:             }

  14:         }

  15:     });

  16:     viewModel.newEmail.To('');

  17:     viewModel.newEmail.Title('');

  18:     viewModel.newEmail.Message('');

  19: });

จะเข้ามาทำงานใน click ของ jquery ที่เราได้ hook event เอาไว้ โดยให้ทำการส่งไปที่ “/api/EmailService/”  และกำหนดข้อมูลโดยให้ส่ง viewModel.newEmail ไปด้วยในรูปแบบของ POST   และให้ทำการล้างค่าของ newEmail หลังจากทำงานเสร็จแล้ว

       Save and Push

หลังจากที่ jquery ได้ยิง POST เข้ามาที่ EmailService Controller ก็จะมาเข้าที่ SendEmail() Action เนื่องจากเป็น POST action ตัวเดียวใน controller นี้

   1: //Post

   2: public string SendEmail(MailMessageView newEmail)

   3: {

   4:     try

   5:     {

   6:         var user = mbctx.Users.SingleOrDefault(o => o.UserName == newEmail.Username);

   7:         if (user == null)

   8:             return Json.Encode(new {success = false, msg = "not found user"});

   9:         var dt = DateTime.Now;

  10:         newEmail.ReceivedDate = dt.ToString("dd/MM/yyyy HH:mm:ss");

  11:         var mail = new MailMessage

  12:                        {

  13:                            Id = newEmail.Id = Guid.NewGuid(),

  14:                            From = newEmail.From,

  15:                            To = newEmail.To,

  16:                            Title = newEmail.Title,

  17:                            Message = newEmail.Message,

  18:                            IsRead = false,

  19:                            ReceivedDate = dt,

  20:                            User_Id = user.Id

  21:                        };

  22:         mbctx.MailMessages.Add(mail);

  23:         mbctx.SaveChanges();

  24:         //add alert email if the target subscriber is online

  25:         if (_subscribers.ContainsKey(mail.To))

  26:         {

  27:             _newMails.Add(newEmail);

  28:             //push email to subscriber

  29:             PushMessageToClient(mail.To);

  30:         }

  31:         return Json.Encode(new {success = true});

  32:     }

  33:     catch (Exception ex)

  34:     {

  35:         return Json.Encode(new { success = false, msg = ex.Message });

  36:     }

  37: }

  38:  

  39: [NonAction]

  40: private void PushMessageToClient(string toEmail)

  41: {

  42:     var subscribers = _subscribers.Where(o => o.Key.ToLower() == toEmail.ToLower().Trim());

  43:     foreach (var subscriber in subscribers)

  44:     {

  45:         var isNewMail = false;

  46:         var mails = _newMails.Where(o => o.To.ToLower().Trim() == toEmail.Trim().ToLower()).ToList();

  47:         if (mails.Count > 0)

  48:         {

  49:             subscriber.Value.Stream.WriteLine("data:" + JsonConvert.SerializeObject(mails) + "\n");

  50:             try { subscriber.Value.Stream.Flush(); }catch { }

  51:             _newMails.RemoveAll(o => o.To == toEmail);

  52:         }

  53:     }

  54:  

  55:  

  56: }

  1. line6-23: ใช้ในการบันทึก new email ลง database
  2. line25-31: เป็นการตรวจสอบ user ที่จะ push และทำการ add email ลง message collection จากนั้นเรียก PushMessageToClient เพื่อส่ง email ไปให้ user คนนั้น
  3. line40: จะเป็น method ที่ใช้ในการ push
  4. line42-43: เลือก user ที่เราแอดเก็บไว้ในตอนแรกจาก email ของ user และทำการ loop ตามจำนวนที่พบ
  5. line46-47: ค้นหา email ที่ถูกส่งเข้ามาหา user นี้ออกมาทั้งหมด เมื่อนับแล้วมีมากกว่า 0 message
  6. line49-51: ใช้ stream ของ user นั้นและทำการสร้าง  data: โดยใช้เอา email มาทำ serialize ให้อยู่ในรูปของ json string แล้วสั่ง write / flush ตามลำดับ เมื่อจบก็ให้ remove email เหล่านั้นออกจาก collection

 

ทิ้งท้าย

ตัวอย่างสำหรับ Web Push ในตอนที่ 2 นี้จะมีแค่ส่วนของ SSE ซึ่งผมพยายามหยิบยกตัวอย่างที่ไม่ใช่การ broadcast message ซึ่งน่าจะพอเป็นไอเดียนำไปประยุกต์ใช้ต่อได้  ในตอนต่อไปจะเป็นการนำ websockets มาใช้งาน

โปรดติดตามตอนต่อไป Smile

 

Download Demo Project *มี database script อยู่ใน project folder เอาไปสร้าง database และอย่าลืมเปลี่ยน connectionstring ใหม่


SONY DSC

About Me:

Chalermpon Areepong : Nine (นาย)

Microsoft MVP Thailand ASP.NET

ASP.NET & MVC Developers Thailand Group :http://www.facebook.com/groups/MVCTHAIDEV/

Greatfriends.biz Community Leader : http://greatfriends.biz

Email : nine_biz-talk.net at hotmail dot com

Blog : https://nine69.wordpress.com

21/02/2011

DI Framework Series : ออกแบบระบบให้ยืดหยุ่นด้วย Dependency Injection Framework

Filed under: Design Pattern — Tags: , , , , — Nine MVP @ 6:48 am

Series

  • DI Framework Series : ออกแบบระบบให้ยืดหยุ่นด้วย Dependency Injection Framework
  • DI Framework Series : ออกแบบ DAL ให้รอบรับอนาคตด้วย Repositoty Pattern

Programming Level:

  • Intermediate – Advance

Computer Skills:

  1. Object Oriented Programming (Interface/Abstract programing)
  2. Layer Architecture
  3. C# 3.0, LINQ
  4. ASP.NET

Development Tool and Library

  1. Visual Studio 2010

Background: Application Layer Design

มาถึงตอนสำคัญอีกตอนครับ สำหรับกลุ่มนักพัฒนาที่ชอบออกแบบและเขียนโปรแกรม  ในตอนนี้ผมขอเสนอเรื่องราวเกี่ยวกับการออกแบบระบบโดยใช้ PoEAA ตัวที่เรียกว่า Inversion of Control โดยวิธี Dependecy Injection ซึ่งเป็นเทคนิคและแนวทางที่กำลังนิยมอยู่ในปัจจุบัน ซึ่งช่วยให้ระบบมีความยืดหยุ่นในการทำงานทุกอย่าง แต่ก็มีข้อแลกเปลี่ยนซึ่งเป็นข้อเสียเช่นกัน  แต่เมื่อประมาณการแล้วข้อเสียนั้นเล็กน้อยมากเมื่อแลกกับข้อดีที่ได้มาเต็ม ๆ

แต่ก่อนที่ผมจะเข้าสู่การอธิบาย เจ้าด้วย DI ผมขอยกเรื่องราวตัวอย่างในปัญหาการพัฒนาโปรแกรมในชีวิตจริง ของพวกเรา ๆ ที่เริ่มเขียนโปรแกรมไว้ประมาณนี้ครับ

นายเอิธเป็นโปรแกรมเมอร์ที่เพิ่งเรียนจบ ตั้งใจจะรับงานฟรีแลนซ์ กะว่าทำงานคนเดียวคงรวยไม่ต้องแบ่งใคร ในเดือนแรกนายเอิธได้รับงานมาทำเป็น Windows App  ได้ไปเก็บรีไควเม้นท์ได้ ER Diagram, DB Dictionary, Screen  และ business กลับมา  จากนั้นก็เริ่มเขียนโปรแกรม โดยเริ่มจากการออกแบบดาต้าเบส  และเปิด visual studio ขึ้นมา และสร้างโปรเจ็คชื่อว่า EarthAccounting และสร้างหน้าจอสำหรับแสดงผลและบันทึกข้อมูล  โดยนายเอิธ มักจะกดดับเบิ้ลคลิ๊กปุ่มเพื่อให้เกิด click event แล้วก็เริ่มเขียนบันทึก อัพเดทข้อมูลกันภายในอีเว้นท์นั้น   นายเอิ๊กต้องทำ Form ของโปรแกรม 50 Form เลยทีเดียว  ปรากฎว่าเหนื่อยมาก  เวลาที่จะต้องส่งงานก็ใกล้เข้ามา  เงินที่ฝันไว้กำลังจะโดนหักหากส่งงานไม่ทันตามกำหนด  เนื่องจากทำอยู่คนเดียวงานจึงเสร็จล่าช้า  จึงตัดใจยอมดึงเพื่อนที่จบมาด้วยกันมาช่วยงานโดยแบ่ง form ให้ไปช่วยทำ   ทำไปได้ซักพักปรากฎว่าเริ่มมีฟังชั่นที่ใช้งานซ้ำ ๆ เกิดขึ้นจำนวนมากภายในฟอร์ม  นายเอิธจึงตกลงกับเพื่อนอีกคนในทีมว่า “เพื่อน เราจะแยกฟังชั่นที่ใช้ซ้ำ ๆ พวกนี้ไว้ใน utility class นะ  แล้วเพื่อนค่อยเอาไปใช้นะ”   หลังจากตกลงกันได้นายเอิธและเพื่อนก็เริ่ม refactoring code ในส่วนของฟังชั่นที่ได้ตกลงกันไว้  หลังจากทำงานไปได้ครึ่งทางปรากฎว่าเกิดมีการเปลี่ยน requirement โดยของแก้ business ส่วนกลางที่ใช้ในการคำนวนค่าทางบัญชี โดยการเปลี่ยนครั้งนี้กระทบกับ form ทั้งหมด 25 form 50 function เนื่องจากเป็นส่วนคำนวนที่ฝังโค้ดเอาไว้ในปุ่ม และยังมีงานเหลืออีก 15 Form ตั้งใจว่าจะ out source งานส่วนนี้ให้มือปืนรายอื่นเข้ามาช่วยงาน  แต่ต้องพบกับปัญหาที่ว่าไม่สามารถบอกโครงสร้างทางบิสซิเนสของลูกค้าให้บุคคลอื่นที่นอกเหนือจากสัญญาจ้างที่ได้เซ็นกันไว้ก่อนนี้  จนนายเอิธรู้สึกท้อใจกับการทำงานแนวทางนี้ จึงได้เปิดเว็บไปหากูเกิ้ล พบทางสว่างแห่งการลดปัญหาที่ว่า สามารถแบ่งงานกันทำได้ แก้ไขแล้วไม่กระทบระบบมากนัก ลดความซ้ำซ้อนและกระจายตัวของระบบงาน ก็คือการแบ่งโค้ดออกเป็น Layer โดยนายเอิธเห็นแนวทางว่าต้องแบ่งระบบออกเป็น 3 Layer (3 project) โดยมี

1. Presentation Layer (PL)  ส่วนที่ใช้แสดงผลได้แก่พวกกลุ่ม windows form, web form เป็นต้น

2. Business Logic Layer (BLL) แยกส่วนที่บรรจุลอจิกทาง business ออกมารวมกันไว้เป็นคลาส

3. Data Access Layer (DAL) เป็นส่วนที่ใช้ติดต่อกับดาต้าเบส

โดยทั้ง 3 layer มีความสัมพันธ์กันดังนี้ PL –> BLL –> DAL โครงสร้างที่นายเอิธใช้ในรอบแรกมีลักษณะเป็นแบบนี้ครับ

Sample01 Pattern : Basic structure

เริ่มต้นเริ่มแรกใครได้ลองเขียนแยก layer ของโปรเจ็คออกเป็นชิ้นย่อย ๆ PL,BLL,DAL หากจุดไหนใครอยากเรียกใครก็แค่ add reference เข้ามาแล้วก็สร้างอินแสตนท์และเรียกใช้งานกันตรงนั้นดังตัวอย่างนี้

image

กรณีใน Business Logic Layer (BLL) ที่จะเรียกใช้ Data Access Layer (DAL)image

จาก class diagram จะพบว่า CustomerBLL มี association กับ CustomerDAL ตรง ๆ อีกนัยก็คือได้ add reference ของ project เข้ามาใช้งาน ลองดูโค้ดด้านล่างนี้ครับ

image

จากภาพจะพบได้ว่าใน line 3,7 มีการประกาศตัวแปรทีและสร้างอินสแตนท์ของ CustomerDAL ขึ้นใช้งาน ซึ่งเป็นการอ้างถึง DAL ตรง ๆ จะทำให้เราต้องมีการสร้าง DAL ขึ้นก่อน ถึงจะสามารถเขียน BLL ต่อไปได้ หรือแม้กระทั่งการทำเทสก็ยากต่อการทดสอบในกรณีที่ต้องแยกเทสแต่ละ Layer เพราะ BLL ไม่สามารถเทสโดยข้ามการสร้างอินแสตนท์ของ DAL ไปได้ และ DAL ยังถูกเรียกใช้ในทุกๆ จุดของเม็ธธอดของ BLL นั่นเอง

และในส่วนของ dependency diagram จะเห็นว่ามี 2 DLL ที่เรียกใช้งานกันตรง ๆ ซึ่งเป็นคำพูดที่ว่า Tight Coupling นั่นเอง

image

ต่อมาแหม่ ทำงานพอได้ระบบใหญ่เข้าหน่อยชักฮึกเหิม ไม่นานนายเอิธก็บรรเจิดปิ๊งไอเดียที่ว่า อยากให้ EarthAccounting รองรับดาต้าเบสได้มากกว่า 1 ชนิด ต้องทำอย่างไร แน่นอนครับพึ่งพากูเกิ้ลอีก เสริชเข้าไปจนได้ความรุ้ที่ว่า “ก็ใช้ Class/Interface + Factory Method Pattern สิคร้าบบ” นายเอิธไม่รอนานรีบก่อนจะสายเวลาลงมือรื้อ BLL และ DAL ใหม่ทั้งยวง ตามตัวอย่างที่ 2 ข้างล่างนี้

Sample02 Pattern : Interface and Factory Class

image

ตัวอย่างที่2 ในกรณีที่ว่า ระบบต้องการที่จะรองรับ DATABASE มากกว่า 1 ชนิด ในที่นี้ผมมี DAL ที่เอาไว้ใช้ทั้ง MySql และ MSSql ไว้ซัพพอร์ทลูกค้ากรณีเขาเลือกใช้อย่างใดอย่างหนึ่ง เราจึงแยกโครงสร้างโปรเจ็คไว้ประมาณหน้าตาดังรูปด้านบนภาพด้านล่างนี้เป็น DAL Class Structure ครับ

image

1. ผมกำหนดโครงสร้างของ DAL ไว้เป็น Interface ที่ชื่อว่า ICustomerServiceDAL ซึ่งมีเม็ธธอด GetCustomerName() ไว้ที่โปรเจ็ก Domain02

2. คลาสที่ใช้ติดต่อกับดาต้าเบสทั้ง MySqlDAL02.MySqlDAL และ MSSQLDAL02.MSSQLDAL จะ implement Domain02.ICustomerServiceDAL ทั้งคู่

3. โปรเจ็ก DALFactory Class จะมี GetActiveDAL method โดยมี return type เป็น ICustomerServiceDAL และภายในจะมีการสร้างอินแสตนท์ของคลาส MySqlDAL, MSSQLDAL โดยตามที่อ่านค่าได้จาก configuration file ตามโค้ดด้านล่างนี้

image

4. ในโปรเจ็ค BLL02.CustomerBLL Class จะมีการเรียกใช้ Domain02.ICustomerServiceDAL และ DALFactory02.DALFactory ภายในดังนี้

image

ต่อมาลองดู dependency diagram ของตัวอย่างที่ 2 กันครับ

image

จากรูปด้านบนจะเห็นได้ว่าคลาส BLL02.CustomerBLL นั้นไม่ได้เรียกใช้ MSSqlDALและ MySqlDAL ตรง ๆ แต่จะเรียกใช้ผ่านคลาส DALFactoty02.DALFactory โดยอิงตามเสปคของ Domain02.ICustomerServiceDAL interface ซึง DALFactory เป็นคลาสที่ช่วยแยก DAL ของ Database ชนิดต่าง ๆ ออกไปจัดการภายใน factory method  ตรงนี้จุดที่เกิด tight coupling คือ CustomerBLL และ DALFactory

และในส่วนของ  Factory Class ก็ต้องรู้จักกับคลาส SqlDAL, MySqlDAL และ OracleDAL ตรง ๆ ซึ่งจากดีไซน์นี้ความเป็นจริงคือไม่ได้ลดความ Tight Coupling ของโครงสร้างลงเลย  แต่ว่าดีไซน์นี้มีกลิ่นไอของ Dependency Injection อยู่ระดับนึง

หลังจากที่นายเอิธพยายามกับโปรแกรมสุดเดิ้ล อย่าง EarthAccounting เพื่อเพิ่มความสามารถและสร้างความสามารถที่รองรับกับความต้องการของลูกค้าในเรื่องการเลือก database ได้ถึง 2 ชนิด  กลับมาเจอกับ challenge ใหม่ในหัวข้อที่ว่า ลูกค้าต้องการลอจิกในการคำนวณค่าบางตัวด้วยสูตรทางคณิตที่ซับซ้อน ซึ่งนายเอิธและเพื่อนไม่เก่งคณิตเอาซะเลย เลยตั้งใจจะ outsource งานส่วนนี้ไปให้คนอื่นทำ แต่มาติดปัญหาว่า คลาสใน BLL นั้นต้องมานั่งรอ คลาสอีกตัวของทาง outsource กว่าจะได้จัดส่ง มาให้ใช้งานใน BLL ถ้านั่งรอก็คงกินเวลาไปอีกเป็นอาทิตย์  ไหนกว่าจะได้งานมาก็มานั่งเขียนโปรแกรม นั่งเทส นั่ง build ทำ package งานเกินเวลาส่งแน่ ๆ จึงได้เปิดกูเกิ้ลและค้นหาจนได้พบกำคำว่า Dependency Injection

จากเรื่องราวด้านบน ทำให้เห็นได้ว่าสิ่งที่นายเอิธกำลังต้องการก็คือวิธีีการออกแบบและเฟรมเวิร์กที่จะช่วยแก้ปัญหา

1. ต้องการดีไซน์ที่ช่วยให้แยก Layer และ Dependency ให้เป็นอิสระจากกัน

2. ต้องการแยกเทสในแต่ละ Layer และ Dependency ที่เกี่ยวข้อง

3. ต้องการลดการ rebuild โปรแกรม กรณีที่มีการ update  DLL เพียงแค่บางตัว

4. สามารถกำหนดโครงสร้างของระบบ โดยใช้ Class, Interface เป็น contract ในการพัฒนาระหว่าง Layer และ Dependency ทั้งหมด

Dependency Injection Pattern

ถูกคิดค้นโดยนาย Martin Flower ในหัวข้อเรื่อง Inversion of control (IoC) มีวัตถุประสงค์เพื่อช่วยแก้ปัญหาการยึดเกาะระหว่างโมดูลของระบบให้แยกขาดออกจากกัน โดยกลับเส้นของ dependency จากผู้เรียกใช้กลายเป็นผู้ถูกเรียก (Hollywood Principal)   IoC เป็น 1 ในกลุ่มของ PoEAA โดยมีแนวทางการนำไปใช้งานได้ 2 แบบคือ Service Locator และ Dependency Injection   ซึ่งจะขอกล่าวถึงเพียง DI เท่านั้น  ดังนี้ผมขออธิบายแบบไม่อิงภาษา oo หรือแบบบ้าน ๆ ว่า DI นั้นคือการสร้างและยิง object ณ ตอน runtime เข้าไปยังกลุ่มคลาสที่ได้เรียกใช้ Interface ซึ่งเป็นข้อตกลงที่ object นั้นๆได้นำไป implement โดยการยิง object นั้นมีทั้งแบบ setter และ constructor

การนำ DI Pattern ไปใช้งานนั้นมีได้หลายวิธีครับ แบบ DIY (Do it your self) หรือใช้ Framework ซึ่งในปัจจุบันนั้นมีอยู่เยอะมากกกก ตามรายชื่อที่พอจะรวบรวมมาได้ด้านล่างนี้ (แต่ยังคงมีออีกเยอะ)

Name Performance(Transient : Singleton)* Current Version
Spring.Net n/a:n/a 1.3.1
Castle Windsor 2:4 2.5.1
Ninject 2:2 2.1.0.76
StructureMap 5:4 2.6.1.0
Unity (Microsoft) 4:4 2.0
AutoFac 3:4 2.2.4.9
MEF (Microsoft) N/A 1.0
Dynamo 5:5 1.0(beta)

* performance 1:bad,2:poor,3:normal,4:good,5:best

** มีหลายปัจจัยสำหรับการตัดสินใจเลือกใช้ framework เพราะแต่ละตัวนั้นมีความสามารถไม่เท่ากัน บางตัวสร้างมาเพื่อ DI โดยเฉพาะ บางตัวเป็น framework ที่มีการ integrate กับหลายส่วนงาน

DI Start up : Demo ASP.NET 3 Layer with Dependency Injection (Unity 2.0)

สำหรับเริ่มกับ DI ผมขอแนะนำการใช้งานเจ้า DI ตัวนึงก็คือ Unity ครับ เป็นของไมโครซอฟเองก่อนอื่นก็ไปดาวน์โหลดมาติดตั้งก่อนครับ ที่นี่ Unity 2.0 ติดตั้งเสร็จแล้วก็เริ่มใช้งานกันเลย

ลองดูโครงสร้างโปรเจ็คทั้งหมดครับ

image

และให้ add reference ตาม dependency diagram ตามภาพด้านล่างนี้

image

เราจะเห็นได้ว่า ไม่มีเส้น PL ไปเรียก BLL หรือ BLL ไปเรียก DAL ให้เห็นเลย ทุก Layer จะวิ่งไปมองที่ Domain ทั้งหมด

1. Domains03 Project

image

สำหรับโปรเจ็คนี้ ผมออกแบบไว้เพื่อกำหนดโครงสร้าง layer ทั้งหมดของ Application โดยภายในจะประกอบไปด้วย  Domain class เป็นกลุ่มคลาสที่เอาไว้ใช้งานข้ามระหว่าง layer , Service Interface เอาไว้กำหนดโครงสร้างให้ BLL , Repository Interface เอาไว้กำหนดโครงสร้างให้ DAL

    ตามรูปด้านล่างนี้

image

  • สำหรับ Domain03.Customer class เป็นโดเมนคลาส ที่จะใช้ในการทำงานกับทุก layer

    public class Customer
    {
       public string CustId { get; set; }
       public string FName { get; set; }
       public string LName { get; set; }
    }

  • ข้อกำหนดของ DAL ผมได้สร้าง Domain03.ICustomerRepository interface เอาไว้ใช้เป็นข้อกำหนด  โดยจะสร้างด้วยอะไรก็ได้ไม่ว่าจะเป็น Entity Framework, NHibernate หรือ Custom DAL with ADO.NET ก็ได้แล้วแต่ตามต้องการ  ซึ่งบังคับว่าคุณต้องใช้ interface ตัวนี้ในการทำงานเท่านั้น

public interface ICustomerRepository
{
   Customer GetCustomerById(string custId);
}

  • ผมสร้างส่วนของ Domain03.CustomerService interface เป็นข้อกำหนดว่า Busines Logic Class ของ CustomerService ใดๆที่จะพัฒนาและนำเข้ามาใช้งานต้องนำ interface นี้ไปใช้เท่านั้น

public interface ICustomerService

{
   Customer GetCustomer(string custId);
}

 

2. L2SDAL03 Project (DAL –> Domain)

image

สำหรับโปรเจ็คนี้ ผมได้นำ interface ที่ชื่อ ICustomerRepository เข้ามาใช้งานใน CustomerLinq2Sql Class โดยตามภาพไดอะแกรมด้านล่างนี้

image

ในส่วนของโค้ดผมได้สร้าง data สำหรับส่งค่ากลับไป ไม่ได้ติดต่อดาต้าเบสจริง ๆ ดังนี้

    public class CustomerLinq2Sql : ICustomerRepository
{
public Customer GetCustomerById(string custId)
{
if (!string.IsNullOrEmpty(custId))
{
var cust = new Customer{CustId = custId};                if (custId.ToLower().Contains(‘1’))
{
cust.FName = “Nine”;
cust.LName = “MVP”;
}
if (custId.ToLower().Contains(‘2’))
{
cust.FName = “SHISUKA”;
cust.LName = “SHAREPOINT”;
}
if (custId.ToLower().Contains(‘3’))
{
cust.FName = “JIENT”;
cust.LName = “BizTalk”;
}
return cust;
}
return null;
}
}

3. BLL03 Project  (BLL –> Domain)

image

ในส่วนของ BLL03 Project ก็คือ Business Logic Layer ของเรานั่นเอง ผมได้นำ ICustomerService interface เข้ามาอิมพลีเม้นให้กับคลาส CustomerService ตามไดอะแกรมด้านล่างนี้

image

ลองมาดูส่วนของโค้ดในคลาส CustomerService กันครับ

    public class CustomerService : ICustomerService
{
private ICustomerRepositorydal;        public CustomerService(ICustomerRepository repository)
{
dal = repository;
}

public Customer GetCustomer(string custId)
{
return dal.GetCustomerById(custId);
}

}

  • จะเห็นได้ว่าในบรรทัดที่ 3 จะมีการประกาศตัวแปรของ ICustomerRepository ไว้ที่นี่เนื่องจากวน BLL นั้นจะเป็นผ้ที่เรียกใช้ DAL นั่นเอง
  • บรรทัดที่ 5 คือการเปิดให้ DI นั้นยิง object เข้ามาทาง Constructor นั่นเอง
  • บรรทัดที่ 12 เป็นการเรียกใช้ DAL ที่ถูกยิง object เข้ามาโดย DI โดยเรียกผ่านทาง polymorphism ของ interface
4. WebUI03 Project (PL –> Domain)

หลังจากที่เราได้วางโครงสร้างและทำการสร้างกลุ่ม BLL, DALเสร็จเรียบร้อยแล้ว  ขั้นตอนต่อไปคือการนำไปใช้งาน โดยก่อนนี้การแบ่งเลเยอร์แบบปกติ ลำดับของผู้ที่จะทำการเรียกใช้ ก็คือ PL –> BLL –> DAL แต่เราจะเปลี่ยนเส้นให้ชี้ไปที่ PL –> Domain และสำหรับ PL นั่นจะมีส่วนที่พิเศษสำหรับการนำ DI เข้ามาใช้งาน เนื่องจากเป็นความแตกต่างของเทคโนโลยี ไม่ว่าจะเป็น ASP.NET, ASP.NET MVC, SilverLight, Windows App เป็นต้น

image

ขั้นแรก  ตั้งค่าคอนฟิกของ Unity ใน web.config  ดังนี้
  1. <configSections>
  2.   <sectionname=unitytype=Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration />
  3. </configSections>

 

เป็นการเพิ่มโมเดลคอนฟิกของ Unity ภายใน web.config

 

  1. <unity>
  2.   <typeAliases>
  3.     <!– Lifetime manager types –>
  4.     <typeAliasalias=singleton
  5.          type=Microsoft.Practices.Unity.ContainerControlledLifetimeManager,
  6.              Microsoft.Practices.Unity />
  7.     <typeAliasalias=perThread
  8.          type=Microsoft.Practices.Unity.PerThreadLifetimeManager,
  9.              Microsoft.Practices.Unity />
  10.     <typeAliasalias=external
  11.          type=Microsoft.Practices.Unity.ExternallyControlledLifetimeManager,
  12.              Microsoft.Practices.Unity />
  13.   </typeAliases>
  14.   <containers>
  15.     <containername=containerOne>
  16.       <types>
  17.         <!– map ICustomerService to CustomerService –>
  18.         <typetype=Domains03.ICustomerService, Domains03mapTo=BLL03.CustomerService, BLL03>
  19.           <lifetimetype=singleton />
  20.         </type>
  21.         <!– map ICustomerRepository to CustomerLinq2Sql –>
  22.         <typetype=Domains03.ICustomerRepository, Domains03mapTo=L2SDAL03.CustomerLinq2Sql, L2SDAL03>
  23.           <lifetimetype=singleton />
  24.         </type>
  25.       </types>
  26.     </container>
  27.   </containers>
  28. </unity>

Line 2 – 13 คือการตั้งชื่อย่อให้ component กลุ่ม LifeTime

Line 15 คือการกำหนด container name ที่จะบรรจุกุล่ม dependency เข้าไว้ในนี้

Line 16 – 25 คือการแม๊พ Interface เข้ากับ Class ที่นำอินเทอเฟสไปใช้งาน โดยจะเป็น BLL และ DAL ของเรา

 

ขั้นที่ 2 แก้ไขไฟล์ Global.asax

เพราะว่าเว็บเวลาจะ start up ขึ้นมาส่วนที่ทำหน้าที่ก่อนคือไฟล์นี้ครับ โดยแก้ไขตามนี้

Code Snippet
  1. public interface IContainerAccessor
  2. {
  3.     IUnityContainer Container { get; set; }
  4. }
  5. public class Global : System.Web.HttpApplication, IContainerAccessor
  6. {
  7.     private static IUnityContainer _container;
  8.     public IUnityContainer Container
  9.     {
  10.         get { return _container; }
  11.         set { _container = value; }
  12.     }
  13.     private static void BuildContainer()
  14.     {
  15.         IUnityContainer container = new UnityContainer();
  16.         //unityContainer = new UnityContainer();
  17.         UnityConfigurationSection section
  18.             = (UnityConfigurationSection)ConfigurationManager.GetSection(“unity”);
  19.         section.Containers[“containerOne”].Configure(container);
  20.         _container = container;
  21.     }
  22.     private static void CleanUp()
  23.     {
  24.         if (_container != null)
  25.         {
  26.             _container.Dispose();
  27.         }
  28.     }
  29.     void Application_Start(object sender, EventArgs e)
  30.     {
  31.         // Code that runs on application startup
  32.         BuildContainer();
  33.     }
  34.     void Application_End(object sender, EventArgs e)
  35.     {
  36.         //  Code that runs on application shutdown
  37.         CleanUp();
  38.     }
  39.     void Application_Error(object sender, EventArgs e)
  40.     {}
  41.     void Session_Start(object sender, EventArgs e)
  42.     {}
  43.     void Session_End(object sender, EventArgs e)
  44.     {}
  45. }

line 1-4 เป็น interface ที่เอาไว้ให้ส่วนอื่นสามารถเรียกใช้ container ได้จาก Global

line 6 อิมพลีเม้น IContainerAccessor ให้ Global

line 9 เป็นการประกาศตัวแปรของ container

line 11 เป็น Property จาก IContainerAccessor Interface

line 17-25 เป็นการโหลด container จาก web.config ที่เราได้ตั้งค่าไว้เข้ามา

 

ขั้นที่ 3 จัดการ set dependency ไปยังหน้า webpage (Setter Method)

ซึ่งเราจะสร้าง HttpModule เอาไว้สำหรับสั่งให้ DI เซ็ทค่าต่าง ๆ เข้าไปใน Web UI ณ จุด ที่มีการเรียกใช้กลุ่ม อินเทอเฟส ในทุกครั้งที่มีการเรียกถึง page นั้น ๆ ดังนี้

DIHttpModule.cs
  1. public class DIHttpModule : IHttpModule
  2.     {
  3.         private IUnityContainer container;
  4.         private void ContextPreRequestHandlerExecute(object sender, EventArgs e)
  5.         {
  6.             Page page = HttpContext.Current.CurrentHandler as Page;
  7.             if (page != null)
  8.             {
  9.                 page.PreInit += Page_PreInit;
  10.             }
  11.         }
  12.         private void BuildUp(object o)
  13.         {
  14.             container.BuildUp(o.GetType(), o);
  15.         }
  16.         private void Page_PreInit(object sender, EventArgs e)
  17.         {
  18.             Page page = sender as Page;
  19.             BuildUp(page);
  20.             BuildUpMaster(page.Master);
  21.             BuildUpControls(page.Controls);
  22.         }
  23.         private void BuildUpControls(ControlCollection controls)
  24.         {
  25.             foreach (Control c in controls)
  26.             {
  27.                 if (c is UserControl)
  28.                     BuildUp(c);
  29.                 BuildUpControls(c.Controls);
  30.             }
  31.         }
  32.         private void BuildUpMaster(MasterPage page)
  33.         {
  34.             if (page != null)
  35.             {
  36.                 BuildUp(page);
  37.                 BuildUpMaster(page.Master);
  38.             }
  39.         }
  40.         #region IHttpModule Members
  41.         public void Init(HttpApplication context)
  42.         {
  43.             container = ((IContainerAccessor)context).Container;
  44.             context.PreRequestHandlerExecute += ContextPreRequestHandlerExecute;
  45.         }
  46.         public void Dispose()
  47.         {
  48.         }
  49.         #endregion
  50.     }

เข้าไป register HttpModule ที่เราสร้างขึ้นใน web.config ภายในบริเวณ <system.web> ..  ดังนี้

  1.     <httpModules>
  2.       <addname=DIModtype=WebUI03.IoC.DIHttpModule, WebUI03 />
  3.     </httpModules>

 

 

ขั้นที่ 4  ทดสอบเรียกใช้งาน BLL ด้วย ICustomerService

หลังจากที่ได้ตั้งค่าและสร้าง Helper ในส่วนของ DI กันเสร็จแล้ว คราวนี้เราก็มาใช้งานกันได้แล้วครับ โดยแก้ไขหน้า default.aspx ออกมาประมาณนี้

image

และเขียนโค้ดไว้ด้านหลังแบบนี้

Code Snippet
  1. using System;
  2. using Domains03;
  3. using Microsoft.Practices.Unity;
  4. namespace WebUI03
  5. {
  6.     public partial class _Default : System.Web.UI.Page
  7.     {
  8.         [Dependency]
  9.         public ICustomerService custSvc { get; set; }
  10.         protected void Button1_Click(object sender, EventArgs e)
  11.         {
  12.             var cust = custSvc.GetCustomer(txtCustId.Text);
  13.             if (cust == null)
  14.             {
  15.                 lbcustid.BackColor = System.Drawing.Color.Red;
  16.                 return;
  17.             }
  18.             lbcustid.Text = cust.CustId;
  19.             lbfname.Text = cust.FName ?? “not found”;
  20.             lblname.Text = cust.LName ?? “not found”;
  21.         }
  22.     }
  23. }

 

Line 9-10  เป็นการประกาศตัวแปรสำหรับรับ object ของ BLL เข้ามาโดยกำหนด attribute [Dependency] ไว้บนหัว property ICustomerService custSvc   เมื่อมีการเรียก page นี้ให้ทำงาน  เจ้า DIHttpModule ที่เราเขียนไว้จะมาตรวจจับ Dependency attribute และ set BLL object (BLL03.CustomerBLL) เข้ามาที่ property นี้

Line 14  เป็นการเรียกใช้ CustomerService เพื่อดึงข้อมูลผ่าน DAL อีกทีนึง

ผลลัพธ์ที่ได้

image

Conclusion

หลังจากที่ได้ทดลองทำลองอ่านตามที่ผมนำเสนอไป หวังทุกท่านคงพอมองเห็นลู่ทางในการนำไปใช้งานกันต่อ เนื่องจากเจ้า DI นี้ช่วยเพิ่มความยืดหยุ่นให้ระบบของเราได้มากจริง ๆ อาจจะมีข้อแลกเปลี่ยนบ้างเช่น ระบบในตอน startup จะช้ากว่าปกติบ้าง และอาจจะต้องระวังเรื่องหน่วยความจำด้วย

สำหรับตอนนี้ผมเขียนไปยาวมาก จริงแล้วยังไม่จบ ยังไม่ได้พูดถึงเรื่องการทำ Testing ในแต่ละ layer และการ integrate กับ ASP.NET MVC อีกทั้งยังมี DI Framework ที่น่าสนใจจะมาแนะนำอีกหลายยี่ห้อ  ค่อนข้างยาวครับ แต่ผมขอจบตอนไว้แค่นี้ก่อน ไว้จะมาเขียนต่อในเรื่องที่ได้บอกไปข้างต้น  สวัสดีครับ


 

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

Download Source Code Here

 

 

Chalermpon Areepong Nine (นาย)

Microsoft MVP Thailand ASP.NET

email : nine_biz-talk.net at hotmail dot com

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

20/10/2009

Design Pattern Series I : What’s the Design Patterns

Filed under: Design Pattern — Nine MVP @ 8:19 pm

Design Pattern Series

  • Design Pattern Series I : What’s the Design Patterns

  • Design Pattern Series II : Abstract Factory Pattern  (next..)

Programming Level

  • OOP : Beginner
  • UML : Beginner

คำแนะนำ (Suggesstion)

ก่อนเข้าเรื่อง Design Pattern อยากแนะนำให้ทุกท่านเรียนรู้เกี่ยวกับ Object Oriented Analysis, Design and Programming ให้ได้ซักระดับ Beginner มาก่อนนะครับ เนื่องจากมีความสำคัญมากในการทำความเข้าใจและเรียนรู้เกี่ยวกับบทความ Design Pattern ทั้งหมดนี้

ความเป็นมาของบทความ Design Pattern ตอนแรกนี้ เนื่องจากต้องการแนะนำประวัติภาพกว้าง และความรู้เบื้องต้นต่าง ๆ ที่ควรรู้ไว้ก่อนครับ หากใครถามที่มาที่ไปกับเรา จะได้ยืดอกตอบได้ชัดเจน

What’s the Design Patterns

เนื่องจากการออกแบบและพัฒนาโปรแกรมในระบบ Object Oriented นั้น มักจะมีโจทย์ปัญหาที่เกิดขึ้นเสมอเกือบทุกครั้ง ทำให้มีการรวบรวมปัญหาต่าง ๆ และได้คิดค้นวิธีแนวทางในการแก้ไขโจทย์ปัญาเหล่านั้นขึ้น ซึ่งเกี่ยวข้องกับปัญหาด้าน Object Oriented Programming ทั้งหมด ดังนั้น Design Pattern ไม่ได้หมายถึงชุด Coding ที่เบ็ดเสร็จนะครับ แต่เป็นแค่เพียงแบบวิธีการปฏิบัติสามารถนำไปใช้งานใน Object Oriented Programming Language ได้ทุกภาษา ซึ่งต้องนำไปประยุกต์ใช้งานเพื่อแก้ปัญหาในระบบด้วยตัวเอง

History

Design Pattern ได้มีการคิดค้นมานานแล้วครับ โดยนาย Christopher Alexander ผู้เขียนหนังสือที่ชื่อว่า "A Pattern Language" ในปี 1977 จากนั้น ก็มีอีกหลายท่านช่วยกันคิดค้นสรุปแนวทางจนมาถึงปี 1995 ได้มีการตีพิมพ์หนังสือที่ชื่อว่า

Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. ISBN 0-201-63361-2” โดย Gamma, Erich; Richard Helm, Ralph Johnson และ John Vlissides ซึ่งได้เขียนสรุป Pattern ออกมาทั้งหมด 23 Patterns ซึ่งเป็นที่ยอมรับกันอย่างกว้างขวางในปัจจุบัน และได้ขนานนามกันว่า Gang of Four และยังมีอีกหลาย pattern จากหนังสือเล่มเป็นที่ยอมรับกันเช่นกัน

 

Gang of Four 23 Design Patterns

GoF design pattern มีทั้งหมด 23 Patterns นั้นเราได้แบ่งแยกออกตามลักษณะไว้ดังนี้

  1. Creational patterns เป็นกลุ่มแพทเทิร์นที่ใช้แก้ปัญหาในการสร้างกลุ่มอ๊อปเจ็ก ซึ่งช่วยให้เราสามารถควบคุมลักษณะ จำนวนและวิธีการสร้าง โดยมีทั้งหมด 5 Patterns
    • Abstract factory pattern:   next vol..
    • Factory method pattern: 
    • Builder pattern: 
    • Prototype pattern:
    • Singleton pattern:
  2. Structural patterns เป็นกลุ่มแพทเทิร์นที่ใช้แก้ปัญหาในการออกแบบโครงสร้างของอ๊อปเจ็กที่ต้องมีความสัมพันธ์กันในรูปแบบโครงสร้าง ซึ่งมีทั้งหมด 7 patterns
    • Adapter pattern:
    • Bridge pattern:
    • Composite pattern: 
    • Decorator pattern: 
    • Facade pattern: 
    • Flyweight pattern:
    • Proxy pattern:
  3. Behavioral patterns เป็นกลุ่มแพทเทิร์นที่ใช้แก้ปัญหาในเรื่องพฤติกรรมการทำงานระหว่าง object ด้วยกัน ซึ่งมีทั้งหมด 11 Patterns
    • Chain of responsibility pattern:
    • Command pattern:
    • Interpreter pattern:
    • Iterator pattern:
    • Mediator pattern: 
    • Memento pattern:
    • Observer pattern:
    • State pattern: 
    • Strategy pattern: 
    • Template method pattern:
    • Visitor pattern:

Design Pattern Documentation

หลังจากเหล่า GoF ได้ออกหนังสือเล่มที่กล่าวไปข้างต้นแล้ว ทั้ง 4 ท่านได้เขียนเอกสารไว้ใช้อ้างอิงของ design pattern ขึ้น โดยมีโครงสร้างสาระตามหัวข้อด้านล่างนี้ ซึ่งบทความ Design Patterns ที่ผมจะเขียนต่อไปจะอ้างอิงบางหัวข้อจาก documentation นี้ครับ

  • Pattern Name and Classification: ชื่อของแพทเทิร์นเอาไว้เรียกกันให้คุ้นปาก และรู็จักกัน
  • Intent: จุดมุ่งหมายในการแก้ปัญหาของแพทเทิร์นนั้น ๆ และเหตุผลในการใช้งาน
  • Also Known As: ชื่อเรียกอื่น ๆ ของแพทเทิร์น
  • Motivation (Forces): ตัวอย่างเหตุการณ์ปัญหาที่สามารถใช้แพทเทิร์นมาแก้ปัญหาได้
  • Applicability: การใช้แพทเทิร์นเข้าแก้ปัญหาSituations in which this pattern is usable; the context for the pattern.
  • Structure: รูปภาพไดอะแกรม ไว้สื่อสารเพื่อสร้างความเข้าใจ ปัจจุบันเน้นใช้งาน Class Diagram และ Interaction Diagram (UML)
  • Participants: รายชื่อของ class, object ต่าง ๆ ทั้งหมดในแพทเทิร์น และบทบาทต่าง ๆ ที่กำหนดในแพทเทิร์น
  • Collaboration: เป็นส่วนที่บรรยายว่า class และ object ทั้งหมดที่ใช้ในแพทเทิร์นนั้นมีการทำงานกันอย่างในภายในแพทเทิร์น
  • Consequences: เป็นส่วนที่บรรยายถึงผลลัพธ์ ผลข้างเคียง และข้อแลกเปลี่ยนต่าง ๆ หากต้องใช้งานแพทเทิร์นดังกล่าว
  • Implementation: บรรยายวิธีการนำแพทเทิร์นไปใช้งาน อาจจะยกตัวอย่างศึกษา
  • Sample Code: แสดงตัวอย่างการแปลง UML Diagram ไปเป็น code ของแต่ละภาษา
  • Known Uses: แสดงตัวอย่างใช้งานแพทเทิร์นจริง 
  • Related Patterns: กล่าวถึงแพทเทิร์นอื่นที่มีความเกี่ยวข้องกัน หรือมีลักษณะคล้าย ๆ กัน

 

Next..

ตอนหน้าจะเขียน Abstract Factory Pattern ครับ

Blog at WordPress.com.