Nine MVP's Blog

04/06/2015

Use JSON File as Configuration File

Filed under: C# — Tags: , — Nine MVP @ 1:05 am

แนะนำวิธีการใช้ json file แทนการใช้ app.config/web.config

http://blogs.mvcrocks.net/2015/06/use-json-file-as-configuration-file/

Advertisements

03/11/2013

ASP.NET MVC Series : Photo Upload Tool (PLUpload, Bootstrap)

Filed under: ASP.NET, ASP.NET MVC, C#, html5, javascript — Tags: , , , , — Nine MVP @ 7:55 pm

ตอนนี้จะเป็นบทความสุุดท้ายที่จะโพสใน wordpress นี้ซึ่งผมจะย้ายไป blog ส่วนตัวซึ่งจะจัดการรายละเอียดต่างๆได้สะดวกกว่านี้

ตอนนี้จะมาแนะนำการสร้างเครื่องมือในการทำ Photo Upload สำหรับ MVC และเช่นเคยไม่เกริ่นมากมาย ก็เข้าเรื่องกันเลย

มาดู library ที่จะใช้งาน

plupload  library ตัวนี้มีดีที่รองรับ multi browser สามารถสลับ engine ไปใช้ html4, html5, flash, siverlight, etc. ตามที่ browser จะรองรับหรือมีติดตั้งไว้ และสามารถทำงานแบบ multiple file selection, file filter (GPLv2 License) ซึ่งใครไม่เข้าใจ plupload ให้อ่านที่นี่ก่อน เพราะผมจะไม่อธิบายการทำงานในบทความนี้
http://www.plupload.com

ตอนนี้จะแนะนำการใช้งานโดยสร้าง UI ขึ้นมาใช้งานเองโดยเพิ่มเติมความสามารถเรื่อง thumbnail, image dimension และตรวจสอบว่า browser รองรับการทำงานของฟังชั่นที่เราเพิ่มเติมเข้าไปหรือไม่

หน้าตาและตัวอย่างการทำงาน

สำหรับ Browser ที่รองรับ HTML5

สำหรับ browser ที่ไม่รองรับ HTML5

ตัวอย่าง code ใน Razor View

10-31-2013 3-02-37 PM

Line 3: เป็นการเรียกใช้ _layout.cshtml ซึ่งจะมี javascript พื้นฐานพวก jquery, css ต่างๆไว้ใช้งาน
Line 9: เรียกใช้ css สำหรับ xPhotoUpload
Line 11-17: ประกาศ tag สำหรับ เรียกใช้ตัว xphotoupload และกำหนดค่าต่าง ๆ ผ่าน data-* property

  • data-multiple-upload = true/ false ต้องการใช้งานโหมด single/multiple upload
  • data-upload-url = action for upload url
  • data-flash-url = กำหนด url path สำหรับ flash engine ของ plupload
  • data-filter-extension =  นามสกุลไฟล์ที่ต้องการมองเห็น
  • data-size-limit = ขนาดของไฟล์ที่อนุญาตให้ upload
  • data-chunk-size = ขนาดชิ้นงานที่จะทำการอัพโหลดต่อครั้ง
  • data-show-console = แสดงคอนโซลการทำงานของ upload tool

โค้ดส่วน Controller

10-31-2013 6-42-22 PM

Line 16-19: เป็นการนำวิว Index.cshtml ส่งกลับไปให้ client เพื่อแสดงผล
Line 22: รับเข้าเป็น HttpPostedFileBase parameter เพื่อง่ายในการเรียกใช้  หรือเปลี่ยนไปอ่านจาก Request.File
Line 25-30: เป็นการสร้าง Upload Directory ตามวันที่
Line 33: สั่งให้บันทึก File ที่ upload มาลงตาม path ดังกล่าว
Line 35: ส่งค่ากลับ

ส่วนของ script/style bundle

11-1-2013 2-24-17 PM

Line 28: kendo core เอาไว้ใช้ทำ template
Line 29: plupload script ใช้เป็น engine สำหรัย upload
Line 30: XPhotoUpload script ที่เราเขียนไว้เพิ่มเติมใช้ในงานนี้
Line 33-35: เป็น css ที่ใช้งานทั้งหมด

โครงสร้างไฟล์ในโปรเจคที่เดโม

ใน Content จะมีสองส่วนคือ plupload folder กับ xphotoupload.css ส่วน bootstrap ถือว่าต้องใช้เป็นพื้นในงานนี้อยู่แล้ว
11-3-2013 4-16-56 PM

ต่อมา Scripts จะมี plupload, kendo.core.min.2013.319.js, XPhotoUpload.js ซึ่งใช้ในการทำงานส่วน javascript
11-3-2013 4-17-15 PM

แนะนำ code ใน XPhotoUpload.js บางบรรทัดที่น่าสนใจ

การเข้าถึง File จาก browser

image
การเข้าถึง file upload โดยใช้ HTML5 API ทุก browser จะมี object สองตัวนี้ให้เรียกใช้งานซึ่งจะมี standard function ในการใช้ทำงานกับ file ได้

หารายละเอียดของรูปที่จะอัพโหลดเพื่อตรวจสอบ dimension

image
Line 98: สร้าง image object ขึ้น
Line 108: เรียกใช้ _URL object โดยให้สร้าง url address ของไฟล์ที่เลือกเขามาใน plupload และส่งให้ img object
Line 99-102: เป็นการผูก function ให้กับ img เพื่อให้ทำงานเมื่อมีการโหลดภาพเข้ามา ซึ่งจะเกิดทันทีที่กำหนดค่าให้ img.src ที่บรรทัด 108

การทำ progress bar

image
เป็น event ที่เกิดขึ้นโดย plupload และแสดงผลโดยใช้ css class ของ bootstrap  โดยการขยายความกว้างไปพร้อมๆ กำหนด % ของไฟล์ที่ plupload ส่งค่ามาให้ใน event นี้

การใช้ Kendo Template ในการทำ html template binding

image
Line 19: table template สำหรับแสดงผลตารางเพื่อสร้างกรอบการทำงานของทูลอัพโหลด
Line 20: table row แบบไม่แสดงผล thumbnail ในกรณีที่ browser ไม่รองรับการทำงานของ HTML5
Line 21: table row แบบแสดงผล thumbnail หาก browser รองรับการทำงาน

ใน table row ทั้งสองแบบจะมี tag ลักษณะ  #= property name # เพื่อบอกให้ kendo มองหา property ของ object นั้นมาแทนค่าลงไปโดยใช้ code ชุดข้างล่างนี้ซึ่งมี 2 แบบ
แบบสั่ง bind โดยไม่มี object
image
และแบบมี object
image

ส่งท้าย

หากเรามีความรู้ในส่วนของ client script อย่าง javascript ให้ลึกพอ เราก็จะสามารถสร้างสรรค์งานขึ้นตามที่เราต้องการได้ ซึ่งก็เป็นเรื่องที่สมควรจะต้องมีสำหรับ web developer ทุกท่าน Smile

สำหรับ project demo สามารถ download ได้ที่ https://bitbucket.org/NineMvp/mvc-upload #Git
สำหรับ zip file https://bitbucket.org/NineMvp/mvc-upload/get/b68e85296709.zip

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  

จบครับ

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();


}

}

}

จบครับ

02/02/2013

ASP.NET MVC Series: High Performance JSON Parser for ASP.NET MVC

Filed under: ASP.NET, ASP.NET MVC, C#, javascript — Nine MVP @ 9:17 pm

กลับมาอีกครั้งกับบทความตอนใหม่ จากที่ผมได้พูดเปรยไว้ในโพสนึงในกลุ่ม ASP.NET & MVC Developers Thailand Group ว่าอย่าทนใช้ Serializer ของเดิมๆ จึงขอขยายความด้วยบทความตอนนี้

ในปัจจุบัน JSON ได้ถูกใช้งานกันอย่างกว้างขวางจากหลาย application ซึ่งเป็นที่นิยมบน HTTP ใช้ในการส่งข้อมูลกันไปมา Request/Response

โดยปกติใครที่เขียน MVC/JQuery บ่อยๆก็มักจะได้ใช้งาน JSON และแม้ว่าปกติก็ทำงานได้อยู่แล้ว #ทำงานได้ไม่ได้หมายความว่าทำได้ดีที่สุด#

ดังนั้นสำหรับตอนนี้เราจะมาแนะนำให้รู้จักเจ้า JavaScriptSerializer Class ที่มีอยุ่ใน .NET Framework และก็ถูกเรียกใช้เยอะหลายจุดเลยทีเดียวใน MVC โดยบทความตอนนี้จะนำ Serializer ที่ดีกว่าจะหาของที่ดีกว่ามาแทนที่ของเดิมโดยทำงานเร็วกว่าหลายเท่าตัว

Performance Issue:

ก่อนอื่นทำความรุ้จักกับ JavaScriptSerializer Class เป็นคลาสที่เอาไว้ช่วยทำการแปลงจาก .NET Object ไปเป็น JSON (Serialization) และใช้แปลง JSON ไปเป็น .NET Object (Deserialization) นั่นเอง

การใช้งานก็เรียกใช้แบบนี้

string jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();

//Call Deserialize to .NET object
Person person = serializer.Deserialize<Person>(jsonIn);

//modify object value
person.Name = "ASP.NET MVC";
person.Salary = 10000;

//Call Serialize to JSON string
string jsonOut = serializer.Serialize(person);

ซึ่งหากไม่คิดอะไรมากก็ใช้งานได้แล้วครับ เพียงแต่อยากให้ดูผลการทดสอบของเว็บไซต์ที่ผมนำมาอ้างอิงกันก่อน J

The results (in milliseconds) as well as the average payload size (for each of the 100K objects serialized) are as follows.

Graphically this is how they look:



ref: http://theburningmonk.com/2012/08/performance-test-json-serializers-part-iii/

จากกราฟด้านบนจะเห็นได้ว่า JavaScripSerializer นั้นทำงานได้ช้ามาก (แต่ JayRock แย่สุด) เมื่อเทียบกับ library อื่นๆแล้ว แต่นี่ก็ยังไม่ใช่ผลการทดสอบของเรา

ผมจึงหยิบเอาตัวที่ใกล้ชิดกับเรามากที่สุดมาทำการทดสอบคือ JavaScriptSerializer, JSON.NET (มีอยู่ใน MVC project) และ ServiceStack.Text (ผลการทดสอบดีที่สุด

และจะนำมาใช้งานในบทความนี้)

JSON.NET : http://json.codeplex.com/

ServiceStack.Text : https://github.com/ServiceStack/ServiceStack.Text

 

TEST CASE:

การทดสอบจะใช้ Stopwatch ในการจับเวลาการทำงานโดย for loop 1,000,000 ครั้ง

โดยทดสอบทั้งการ Serialize และ Deserialize ดังนี้

int times = 1000000;
string output = "";


//JavaScriptSerializer Class
var startMSJson = Stopwatch.StartNew();
var msjs = new System.Web.Script.Serialization.JavaScriptSerializer();
for (int i = 0; i < times; i++)
{
var testObject = new { Name = "Nine" + i.ToString(), Salary = 500.00 };
var temp = msjs.Serialize(testObject);
}
startMSJson.Stop();
output += "Javascript Serializer : " + startMSJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

startMSJson = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var temp = msjs.Deserialize<Person>(jsonIn);
}
startMSJson.Stop();
output += "Javascript Deserializer : " + startMSJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;



//JSON.NET
var startJSONNET = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var testObject = new { Name = "Nine" + i.ToString(), Salary = 500.00 };
var temp = Newtonsoft.Json.JsonConvert.SerializeObject(testObject);
}
startJSONNET.Stop();
output += "JSON.NET Serializer : " + startJSONNET.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

startJSONNET = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var temp = Newtonsoft.Json.JsonConvert.DeserializeObject<Person>(jsonIn);
}
startJSONNET.Stop();
output += "JSON.NET Deserializer : " + startJSONNET.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;



//ServiceStack.Text
var startSSTJson = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var testObject = new { Name = "Nine" + i.ToString(), Salary = 500.00 };
var temp = ServiceStack.Text.JsonSerializer.SerializeToString(testObject);
}
startSSTJson.Stop();
output += "ServiceStack.Text Serializer : " + startSSTJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

startSSTJson = Stopwatch.StartNew();
for (int i = 0; i < times; i++)
{
var jsonIn = "{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }";
var temp = ServiceStack.Text.JsonSerializer.DeserializeFromString<Person>(jsonIn);
}
startSSTJson.Stop();
output += "ServiceStack.Text Deserializer : " + startSSTJson.ElapsedMilliseconds.ToString() + " ms" +
Environment.NewLine;

 

Output

  • Javascript Serializer : 11589 ms
  • Javascript Deserializer : 7769 ms
  • JSON.NET Serializer : 3783 ms
  • JSON.NET Deserializer : 6641 ms
  • ServiceStack.Text Serializer : 2069 ms
  • ServiceStack.Text Deserializer : 1211 ms

หลังทดสอบพบว่าผลการทำงานของ JavaScriptSerializer นั้นแย่มากจริงๆ รองลงมา JSON.NET ก็ทำได้ดี แต่ ServiceStack.Text ทำได้ดีที่สุด ซึ่งวัตถุประสงค์หลักของบทความนี้ต้องการจะบอกว่าใน ASP.NET MVC มีการใช้งาน Serializer ตัวที่แย่ที่สุดนั้นเอง

และเราจะนำตัว ServiceStack.Text มาใช้งานทดแทนเจ้า JavaScriptSerializer กัน แต่ว่าถูกใช้งานอยู่ที่ไหนบ้างหละ?

 

Where is JavaScriptSerializer used in ASP.NET MVC?

ต่อไปจะพากันไปหาจุดที่มีการเรียกใช้เจ้า JavaScriptSerializer ซึ่งมีอยู่หลายจุด แต่ขอนำจุดที่สำคัญของระบบจริงๆ มาแก้ไขกันในบทความนี้ ซึ่งจะเน้นไปที่ Input และ Output ซึ่งมีอยู่ 2 จุดคือ

Model Binder (INPUT)

จุดแรก model binder จะทำหน้าที่จับเอา request โดยตรวจดู content type ว่าเป็นชนิด json จากนั้นก็นำค่าไปทำการ deserialize ไปเป็น .net object ตามคลาสที่ประกาศไว้ใน Action นั้นๆ เช่น

public JsonResult NewPerson(Person person)
{
return Json(new Person());
}

จากตัวอย่างด้านบน Person จะเป็นคลาสที่กำหนดไว้ใน NewPerson Action ของ Controller เมื่อมีการ request กลับมาที่ Controller/NewPerson ด้วยการ json post ตัวอย่างด้านล่างนี้

"{ \"Name\" : \"Nine\", \"Salary\" : 500.00 }"

ก่อนที่จะเข้ามาใน NewPerson Action จะมีการสร้าง Person object จากส่งกลับเข้ามาโดยไปเรียก ModelBinder (หากไม่มี custom binder จะไปเรียก DefaultModelBinder มาทำงาน) โดยดักจับ content-type ว่าเป็น request ประเภทใด

ในกรณีนี้ส่งมาเป็น JSON ก็จะไปเรียกตัว JsonValueProviderFactory (ValueProviderFactory เป็น base class) ซึ่งเจ้านี่เองที่มีการใช้งาน JavaScriptSerializer Class ในการแปลง JSON มาเป็น object เพื่อส่งกลับมาใน Action

แกะดู code เจ้า JsonValueProviderFactory กัน

public sealed class JsonValueProviderFactory : ValueProviderFactory 

{

private static void AddToBackingStore(JsonValueProviderFactory.EntryLimitedDictionary backingStore, string prefix, object value){ … }

private static object GetDeserializedObject(ControllerContext controllerContext)

{

if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))

return (object) null;


string input = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();



if (string.IsNullOrEmpty(input))

return (object) null;


else

return new JavaScriptSerializer().DeserializeObject(input);

}



public override IValueProvider GetValueProvider(ControllerContext controllerContext){ }


private static string MakeArrayKey(string prefix, int index) { }


private static string MakePropertyKey(string prefix, string propertyName) { }


private class EntryLimitedDictionary


{


………

private static int GetMaximumDepth()


{


NameValueCollection appSettings = ConfigurationManager.AppSettings;


if (appSettings != null)


{


string[] values = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers");


int result;

if (values != null && values.Length > 0 && int.TryParse(values[0], out result))


return result;


}


return 1000;


}


}


}

จะพบว่าถูกเรียกใน GetDesirializedObject() (ซึ่งถูก GetValueProvider() เรียกอีกที) นั่นเอง ทีนี้เราจะมาสร้างของเราใช้งานเองครับ J

ผมสร้าง Class ขึ้นใหม่โดย Inherit เจ้า ValueProviderFactory และใช้เจ้า ServiceStack.Text ใน GetValueProvider() ตรงๆแบบนี้ โดยใช้ AsExpandoObject() Extension Method แทนการทำงานอื่นๆของ JsonValueProviderFactory

public sealed class JsonServiceStackValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");

if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return null;

var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream).BaseStream;

return new DictionaryValueProvider<object>(
ServiceStack.Text.JsonSerializer.DeserializeFromStream<Dictionary<string, object>>(reader).AsExpandoObject(),
CultureInfo.CurrentCulture);
}
}

มาดู Extension Method ตัวที่ว่ากันครับ

public static class CollectionExtensions
{
public static ExpandoObject AsExpandoObject(this IDictionary<string, object> dictionary)
{
var epo = new ExpandoObject();
var epoDic = epo as IDictionary<string, object>;

foreach (var item in dictionary)
{
bool processed = false;

if (item.Value is IDictionary<string, object>)
{
epoDic.Add(item.Key, AsExpandoObject((IDictionary<string, object>)item.Value));
processed = true;
}
else if (item.Value is ICollection)
{
var itemList = new List<object>();
foreach (var item2 in (ICollection)item.Value)
if (item2 is IDictionary<string, object>)
itemList.Add(AsExpandoObject((IDictionary<string, object>)item2));
else
itemList.Add(AsExpandoObject(new Dictionary<string, object> { { "Unknown", item2 } }));

if (itemList.Count > 0)
{
epoDic.Add(item.Key, itemList);
processed = true;
}
}

if (!processed)
epoDic.Add(item);
}

return epo;
}
}

การนำไปใช้งานก็ไปที่ Global.asax.cs วางโค้ดลงไปตาม

protected void Application_Start() 
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();

ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new JsonServiceStackValueProviderFactory());


}

การทำงานคือเราจะไป remove เจ้า JsonValueProviderFactory ออกจาก ValueProviderFactories Class และเพิ่ม JsonServiceStackValueProviderFactory อันใหม่ของเราเข้าไป

JsonResult (Output)

จุดที่สองจะเป็นการ render JSON string กลับออกไปยัง Client เรามาดูโค้ดของ JsonResult Class ที่ ASP.NET MVC ใช้อยู่ในปัจจุบัน

public class JsonResult : ActionResult
{

public override void ExecuteResult(ControllerContext context)
{

if (context == null)

throw new ArgumentNullException("context");

if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet && String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))

throw new InvalidOperationException(MvcResources.JsonRequest_GetNotAllowed);

HttpResponseBase response = context.HttpContext.Response;

response.ContentType = string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;

if (this.ContentEncoding != null) response.ContentEncoding = this.ContentEncoding;

if (this.Data == null) return;

JavaScriptSerializer scriptSerializer = new JavaScriptSerializer();

if (this.MaxJsonLength.HasValue)


scriptSerializer.MaxJsonLength = this.MaxJsonLength.Value;

if (this.RecursionLimit.HasValue)


scriptSerializer.RecursionLimit = this.RecursionLimit.Value;


response.Write(scriptSerializer.Serialize(this.Data));

}

}

ต่อไปเราจะสร้าง JsonResult ของเราเอง โดยสร้างคลาสใหม่ขึ้นมา แล้ว Inherit เจ้า JsonResult ให้กับคลาส

แล้วก็ทำการ override ExecuteResult() ใหม่ตามนี้


public class JsonServiceStackResult : JsonResult
{

public override void ExecuteResult(ControllerContext context)
{


HttpResponseBase response = context.HttpContext.Response;


response.ContentType = !String.IsNullOrEmpty(ContentType) ? ContentType : "application/json";



if (ContentEncoding != null)
{


response.ContentEncoding = ContentEncoding;


}

if (Data != null)
{


response.Write(ServiceStack.Text.JsonSerializer.SerializeToString(Data));


}


}


}

ตั้งชื่อซะยาวเรียกใช้คงลำบาก งั้นเราก็ไปสร้าง Base Controller ของเราไว้ใช้งานแทนเจ้า return Json(); แบบนี้


public abstract class MyBaseController : Controller
{


protected new JsonResult Json(object data, JsonRequestBehavior behavior)


{ return Json(data, "application/json", Encoding.UTF8, behavior); }


protected new JsonResult Json(object data)
{

return Json(data, "application/json", Encoding.UTF8, JsonRequestBehavior.DenyGet);


}



protected override JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
{

return new JsonServiceStackResult
{

Data = data,


ContentType = contentType,


ContentEncoding = contentEncoding


};


}


}

ตอนจะใช้งานก็แค่นำ MyBaseController ไป inherit แทนที่ Controller Class แล้วค่อยเรียก Json() เหมือนปกติเดิม J

public class HomeController : MyBaseController
{


public JsonResult NewPerson(Person person)
{

return Json(new Person());

}

}

 
 
 

Conclusion

ในบทความนี้หวังว่าจะมีประโยชน์สำหรับผู้ที่ต้องการเพิ่มประสิทธิภาพให้กับตัว MVC App โดยบางท่านอาจจะยังไม่ทราบส่วนนี้ จนกว่าจะต้องทำ Performance tuning กันเพื่อเพิ่มประสิทธิภาพในการทำงานในจุดที่ช้าและทำงานได้ไม่ดี

และจากบทความตอนนี้ท่านสามารถนำสิ่งเหล่านี้ไปใช้งานได้ทั้ง ASP.NET MVC และ ASP.NET WEB API (ตัวนี้เหมาะที่จะเปลี่ยนใหม่เพราะทำงานกับ Data จำนวนมาก)

สำหรับตอนนี้ขอจบไว้เพียงเท่านี้ เจอกันใหม่ตอนหน้าครับ

 


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

24/10/2012

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

Filed under: ASP.NET, ASP.NET MVC, C#, html5, javascript, knockout, WEB, websockets — Tags: , , , , , , , — Nine MVP @ 4:10 pm

จาก ตอนที่ I ผมได้แนะนำการทำงานพื้นฐานของคำสั่งทั้ง javascript และ protocol กันไปแล้ว และเมื่อที่ผ่านมา ตอนที่ II.1 ได้พูดถึงการทำ Push Mail Service ด้วย SSE

สำหรับตอนนี้เราจะมาทดสอบตัว websockets โดยใช้โจทย์เดิมกัน  ซึ่งสำหรับตอนนี้จะเป็นการเปลี่ยนการทำงานจากการใช้ SSE มาเป็น WebSockets Engine ในการสื่อสารกับ Push Mail Service    ซึ่งจะมีการปรับเปลี่ยนทั้ง client javascript  และ Web API Service มาดูกัน

การทำงานและ output จะเหมือนกับโปรเจ็ค SSE ก่อนนี้ทุกอย่าง ดังนั้นผมจะอธิบายเพียงส่วนที่แตกต่างกันครับ

 

แนะนำ MvcPushWS Project

ส่วนทีต้องติดตั้งเพิ่มเติมสำหรับ MvcPushWS Project ก็คือ

Microsoft.WebSockets โดยหาได้จาก nuget windows ตามภาพด้านล่าง

image

 

สำหรับ Flow โปรแกรมมีจำนวน step มี 9 จุดที่น่าสนใจ

ลำดับที่ 0.-3. ผมจะข้ามไปโดยหากสนใจจะอ่านให้กลับไปดู ตอนที่ II.1 ครับ

image

 

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

ใน javascript function InitialWS(); จะเป็นการสร้างและกำหนดค่าการทำงานของ WebSockets  ให้ชี้ไปยัง EmailServiceController (WebAPI) ซึ่งเป็น push service ที่เราสร้างขึ้นมาสำหรับส่ง email ใหม่ที่ส่งเข้ามาจากผู้ใช้อื่นๆ โดยจะมีการ add event ไว้ 1 คือ onmessage

   1: //WS Setup

   2:     function InitialWS() {

   3:         if ("WebSocket" in window) {

   4:             var uri = 'ws://' + window.location.host + '/api/EmailService/?email=' + viewModel.email();

   5:             socket = new WebSocket(uri);

   6:             socket.onmessage = function(e) {

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

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

   9:                 //alert from email

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

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

  12:                 viewModel.messages.valueWillMutate();

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

  14:                 viewModel.messages.valueHasMutated();

  15:                 //sort inbox item

  16:                 SortMessageDesc();

  17:             };

  18:         } else {

  19:             // the browser doesn't support WebSockets

  20:             alert("WebSockets NOT supported here!\r\n\r\nBrowser: " + navigator.userAgent + "\r\n\r\n");

  21:         }

  22:     }

  1. line3: ทำการตรวจสอบว่า user browser รองรับการทำงานของ websockets หรือไม่
  2. line4: สร้าง url ปลายทางไปยัง push mail service และส่ง email pameter ไปด้วย
  3. line5: สร้างตัวแปร WebSocket และส่ง url ให้เพื่อทำการติดต่อกับ service
  4. line6-8: ทำการกำหนดการทำงานให้กับ onmessage event โดยให้เอาข้อมูลที่ส่งมาจากทางฝั่ง service ทำการ log และแปลงค่าเป็น json object
  5. line10-16: ทำการแสดงผล popup, โยนเข้า knockout model แล้วจัดลำดับ email ใน inbox ให้ DESC ReceivedDate

เมื่อมีการทำการ new WebSocket(url) ตรงนี้จะทำงานทันที โดยติดต่อไปยัง EmailServiceController.Get() Action โดยมี request header หน้าตาแบบนี้

           WebSockets Request Header

image

มีพารามิเตอร์ที่เกี่ยวข้องกับ websocket ดังนี้

  • Upgrade: websocket
  • Connection: xxx, Upgrade

เป็นค่าเพื่อบอก push email service ว่านี่เป็นการติดต่อมาจาก websocket  ให้ทำการสลับการทำงานจาก HTTP Protocol ไปเป็น WebSocket Protocol

  • Sec-WebSocket-Key: kmDqIkNBFU0iWMWb5fNiPQ==

จะอยู่ในรูปของ base64 encode  ทาง push email service จะเอาค่านี้ไปใช้งานเพื่อสร้าง handshake สำหรับคุยกันระหว่าง websocket client/server

ส่วนของ origin url จะถูกสร้างโดย browser

  • Sec-WebSocket-Version: 13

เป็นการบอก websocket client ว่าทางฝั่ง Client Browser นั้นใช้ protocol version ไหน ซึ่ง version 13 นั้นเป็น final spec ที่ทาง IEFT กำหนดให้เป็น standard

 

5. Create PushEmailHandler for new subscriber (EmailServiceController)

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

Get(HttpRequestMessage request)  ซึ่งจะมีการสร้าง WebSocketHandler class (มีใน Microsoft.Websockets จาก Nuget) ขี้นใหม่สำหรับ PushMailWSHandler เอาไว้ช่วยจัดการกับ ws request

   1: public class EmailServiceController : ApiController

   2: {

   3:     //Get

   4:     public HttpResponseMessage Get(HttpRequestMessage request)

   5:     {

   6:         //accept only websockets request

   7:         if (!HttpContext.Current.IsWebSocketRequest)

   8:             return request.CreateErrorResponse(HttpStatusCode.Forbidden, "the service support only websocket client");

   9:  

  10:         //get user's email

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

  12:  

  13:         //user's email is required parameter

  14:         if (string.IsNullOrEmpty(email.Trim()))

  15:             return request.CreateErrorResponse(HttpStatusCode.BadRequest, "the service required users email parameter.");

  16:  

  17:         //register websockets handler to context

  18:         HttpContext.Current.AcceptWebSocketRequest(new PushMailWSHandler(email));

  19:         return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);

  20:     }

  21: }

  1. line7-8: ตรวจสอบว่าเป็นการส่งค่าสั่งมาจาก websocket หรือไม่ถ้าไม่ก็ส่ง error response แบบ forbidden พร้อม error message กลับไป
  2. line11: อ่าน email parameter มาจาก QueryString
  3. line14-15: หากไม่พบ email parameter ก็ส่ง error response แบบ badrequest พร้อม error message กลับไป
  4. line18-19: กำหนดให้ HttpContext ทำการรับ websocket handler ตัวใหม่สำหรับ pushmail ที่สร้างไว้ (เล่าตอนหลัง) และสร้าง response ตาม handler ที่เพิ่งกำหนดเข้าไปใหม่ ก็คือ switch to websocket protocol

  อธิบายตัว PushMailHandler class

ได้ทำการสืบทอดจาก WebSocketHandler Class ที่มีมากับ Microsoft.WebSockets โดยเมื่อมีการเรียก constructor ให้ทำงานโดย remove old pushmail handler ตัวเก่าออกไปก่อน แล้วค่อย add ตัวใหม่ที่เพิ่งสร้างเข้าไป (ทำตามแนวคิด *มาทีหลังดังกว่า)

   1: using MvcPushWS.Models;

   2: using Newtonsoft.Json;

   3: using Microsoft.Web.WebSockets;

   4:  

   5: namespace MvcPushWS.Controllers

   6: {

   7:     public class PushMailWSHandler : WebSocketHandler

   8:     {

   9:         //EFcontext

  10:         private MailBoxDBContext mbctx = new MailBoxDBContext();

  11:         //message pool

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

  13:         //subscriber pools

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

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

  16:  

  17:         //constructor

  18:         public PushMailWSHandler(string email)

  19:         {

  20:             // add subscriber to pool

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

  22:             {

  23:                 if (_subscribers.ContainsKey(email))

  24:                 {

  25:                     SubscriberPushMail dead;

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

  27:                     {

  28:                         dead.client.Close();

  29:                         dead = null;

  30:                     }

  31:                 }

  32:                 _subscribers.TryAdd(email, new SubscriberPushMail

  33:                 {

  34:                     Email = email,

  35:                     client = this

  36:                 });

  37:  

  38:             }

  39:         }

  40:  

  41: ...............

*ไม่ขออธิบาย code เพราะการทำงานจะคล้ายกับตอนที่แล้ว

 

6. เมื่อตอบกลับการทำงานไปยัง WebSocket Client

ตัว WebSocketHandler จะเพิ่มตัวแปะลงใน Response Header เพื่อตอบกลับการทำงานไปยัง client

          WebSocket Response Header

image

  • Upgrade: Websocket
  • Connection: Upgrade
  • Sec-WebSocket-Accept: teTd3/I5X/xGOf2RHcLCvf49Jb4=

เมื่อมี response ตอบกลับมาตัว HTTP Code เราจะมองเห็นเป็น 101 Switching Protocols โดยการทำงานต่อจากนี้เราจะไม่สามารถดูได้จาก browser dev tool อีกเนื่องจากทำงานจะมีการส่งข้อมูลกันเป็น frame ไปมาตามรูปด้านล่างนี้

image

image

 

 

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

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

โดยใช้ตัว websocket object โดยให้ส่ง viewModel.newEmail ไปด้วยในรูปแบบของ json string และให้ทำการล้างค่าของ newEmail หลังจากทำงานเสร็จแล้ว

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

   2:     

   3:     //binding mapping to viewModel

   4:     ko.applyBindings(viewModel);

   5:  

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

   7:         

   8:         if (socket.readyState != WebSocket.OPEN)

   9:             return;

  10:  

  11:         var data = JSON.stringify(ko.mapping.toJS(viewModel.newEmail));

  12:  

  13:         //use websocket send json string to pushmail service

  14:         socket.send(data);

  15:        

  16:         //clear value

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

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

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

  20:     });

  21:  

  22: });

  1. line14: เป็นการใช้ websocket ส่งข้อมูลกลับไปที่ push mail service ของเรา

หลังจากที่ websocket client ได้ส่ง json string เข้ามาจะไปทำงานที่ PushMailHandler ตัวที่เราได้เก็บเอาไว้ก่อนหน้านี้ โดยจะเกิด Event OnMessage(string data) ขึ้น

   1: public class PushMailWSHandler : WebSocketHandler

   2: {

   3:     .........................

   4:     .....................

   5:  

   6:     //User send a email message to service

   7:     public override void OnMessage(string message)

   8:     {

   9:         var dtomail = JsonConvert.DeserializeObject<MailMessageView>(message);

  10:         try

  11:         {

  12:             var user = mbctx.Users.SingleOrDefault(o => o.UserName == dtomail.Username);

  13:             if (user == null)

  14:                 this.Send(JsonConvert.SerializeObject(new {success = false, msg = "not found user"}));

  15:  

  16:             var dt = DateTime.Now;

  17:  

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

  19:             var mail = new MailMessage

  20:             {

  21:                 Id = dtomail.Id = Guid.NewGuid(),

  22:                 From = dtomail.From,

  23:                 To = dtomail.To,

  24:                 Title = dtomail.Title,

  25:                 Message = dtomail.Message,

  26:                 IsRead = false,

  27:                 ReceivedDate = dt,

  28:                 User_Id = user.Id

  29:             };

  30:             mbctx.MailMessages.Add(mail);

  31:             mbctx.SaveChanges();

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

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

  34:             {

  35:                 _newMails.Add(dtomail);

  36:                 //push email to subscriber

  37:                 PushMessageToClient(mail.To);

  38:             }

  39:         }

  40:         catch (Exception ex)

  41:         {

  42:             //this.Send(JsonConvert.SerializeObject(new { success = false, msg = ex.Message }));

  43:         }

  44:  

  45:     }

  46:  

  47:     //push email to target subscriber

  48:     private void PushMessageToClient(string toEmail)

  49:     {

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

  51:         foreach (var subscriber in subscribers)

  52:         {

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

  54:             if (mails.Count > 0)

  55:             {

  56:                 subscriber.Value.client.Send(JsonConvert.SerializeObject(mails));

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

  58:             }

  59:         }

  60:  

  61:  

  62:     }

  63: }

  1. line7: คือ method ที่เราได้ override ไว้เมื่อมี client ได้ส่งข้อมูลกลับมาที่ service
  2. line9-31: ทำการ convert json string กลับมาเป็น object เพื่อใช้บันทึกลง database
  3. line37: เรียก pushmessagetoClient ไปยัง client ตามอีเมลที่ส่งเข้ามา
  4. line56: เมื่อหา client ปลายทางเจอแล้วก็เรียกใช้ PushMailWSHandler ที่เราเก็บไว้ตอนแรกใน flow ที่ 5.  ทำการเรียก Send(json string); ส่งไป

 

ถ้าอยากเขียน WebSocket Server เองไม่ต้องการใช้ Microsoft.WebSocket ?

ก็เขียนแบบ TCP Server ปกติได้เลยครับ เพียงแต่ตอน response กลับก็เขียน header กลับไปให้ครบตามนี้

   1: var listener = new TcpListener(IPAddress.Loopback, 8181);

   2: listener.Start();

   3: using (var client = listener.AcceptTcpClient())

   4: using (var stream = client.GetStream())

   5: using (var reader = new StreamReader(stream))

   6: using (var writer = new StreamWriter(stream))

   7: {

   8:     writer.WriteLine("HTTP/1.1 101 Web Socket Protocol Handshake");

   9:     writer.WriteLine("Upgrade: WebSocket");

  10:     writer.WriteLine("Connection: Upgrade");

  11:     writer.WriteLine("WebSocket-Origin: http://localhost:8080");

  12:     writer.WriteLine("WebSocket-Location: ws://localhost:8181/websession");

  13:     writer.WriteLine("");

  14: }

  15: listener.Stop();

 

ทิ้งท้ายก่อนจบ

สำหรับตอนนี้เป็นการทดสอบการใช้งานตัว WebSocket และ WebSocket Server ขึ้นใช้งานเอง เพื่อสร้างความเข้าใจในการทำงาน  แต่ตัวอย่างข้างต้นที่ได้หยิบยกมายังไม่เหมาะจะนำไปใช้งานจริง ดังนั้นอาจจะต้องแก้ไขและปรับแต่งหรืออาจจะไปมองหา WebSocket Server ที่มีอยู่มากมายตอนนี้ ซึ่งน่าสนใจอยู่หลายตัวครับ SuperWebSocket.Net, XSockets.NET หรืออีกมากมายที่ http://en.wikipedia.org/wiki/Comparison_of_WebSocket_implementations 

ปล. ก่อนจะเอาโปรเจ็ค demo ไปรันทดสอบรบกวนตรวจสอบ browser ของคูณกันก่อนนะครับ Smile  http://websocketstest.com/

image

 

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

15/03/2012

Application Design : Logging 1

Filed under: C#, Core System — Tags: , — Nine MVP @ 2:58 am

ตอนที่2
https://nine69.wordpress.com/2012/04/21/application-design-logging-2/

หายไปนาน กลับมากับหัวข้อที่ว่าด้วยเรื่องของการซับพอทโปรแกรมและระบบที่เรากำลังจะสร้างกัน เพราะคงน้อยมากที่นักพัฒนาหลังจากเขียนโปรแกรมเสร็จแล้ว จะหายไปไม่ต้องสนใจโปรแกรมของตนอีกเลย หลังจากมีผู้ใช้งานเริ่มใช้งานระบบนั้น ๆ  เราเรียกว่าการซัพพอทหลังการพัฒนาระบบ ไม่ว่าจะเป็น bug, human error, system failed, system down, system maintenance เมื่อเกิดขึ้นแล้ว ปัญหาจะวิ่งกลับมายังผู้พัฒนาหรือบุคคลที่ต้องเป็นแพะ (admin support) หากไม่ได้ออกแบบส่วนนี้ไว้หรือทำได้ไม่ดีพอ  เราจะได้เห็นฝ่าย IT เจ้าของระบบนั้นๆ นั่งทำงานคิ้วขมวดทั้งวันทั้งคืนเป็นแน่

 

ปัญหาหากโปรแกรมขาดการเก็บ Log 

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

เนื่องจากต้องไปหาสาเหตุจากต้นตอของปัญหา ไม่ว่าจะเกิดจากผู้ใช้หรือระบบ, หน้าจอไหน, ข้อมูลใด, กลุ่มคลาสหรือเมดธอดอะไร ข้อมูลเหล่านี้มีความจำเป็นในการตรวจสอบ ซึ่งจะช่วยให้ผู้พัฒนาทำการทดสอบระบบด้วยกรณีเดียวกันได้เพื่อให้ทราบถึงปัญหา   ซึ่งโปรแกรมที่เราๆใช้กันอยู่ไม่ว่าจะเป็น Windows 7, Office, หรืออื่น ๆ ต่างก็มีการบันทึก log การทำงานทุกอย่างเพื่อให้ผู้ใช้งานสามารถเข้าไปดูเพื่อวิเคราะห์และแก้ไขปัญหานั่นเอง

สำหรับนักพัฒนาหน้าใหม่ อาจจะไม่ได้ดักจับ error ไว้ในโปรแกรมเลย หรือไม่ก็อาจจะใช้ MessageBox.Show(); พวกข้อความที่ error บอกผู้ใช้ระบบไปตรงๆ       ซึ่งผู้ใช้งานระบบคงไม่เข้าใจว่าเกิดอะไรขึ้น  ก็ต้อง print screen ส่งมาบอกผู้พัฒนาด้วยตัวเอง และบอกว่าทำอะไรไป ทำหน้าไหน ใส่ข้อมูลอะไร กดอะไรไป  

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

ดังนั้นผู้พัฒนาอย่างเราๆ จึงจำเป็นที่จะต้องออกแบบระบบการเก็บ log การทำงานของโปรแกรมให้รองรับปัญหาที่จะเกิดขึ้นด้วย  

 

เราควรเก็บ Log อะไรบ้าง?

การบันทึก log นั้น อยู่ที่วัตถุประสงค์ของแต่ระบบที่ได้ออกแบบและตกลงกันไว้ แต่วัตถุประสงค์หลัก ๆ ของการเก็บ log นั้นมีอยู่ 2 ส่วนคือ

1. System Support เช่น การบันทึก Error, Information, System Status เป็นต้น

2. Audit Support กรณีที่่ต้องคอยให้ข้อมูลเพื่อการตรวจสอบการทำงานของผุ้ใช้ หรือตัวระบบเอง เช่นไม่ว่าจะ login, logout, access page, access service, approved, commit task เป็นต้น

โดยข้อมุลที่ควรจะถูกจัดเก็บอาจจะมีข้อมูลคร่าว ๆ ดังนี้

  1. Id (auto gen id) อาจจะเป็น bigint, long, guid เป็นต้น
  2. DateTime : วันที่และเวลาที่ทำการบันทึก log (ใช้เวลาที่ error จริง ๆ ใน Application)
  3. LogLevel : อ่านด้านล่าง
  4. Priority : อ่านด้านล่าง
  5. Title : หัวข้อที่จะบันทึก
  6. Application : กรณีมีใช้งานสถานที่เก็บ log รวมกันเช่น windows event log, database table เป็นต้น
  7. Module : ใช้บ่งบอกว่าเป็นโมดูลการทำงานส่วนไหนภายในโปรแกรม เช่น Member Register เป็นต้น
  8. Action : ใช้บ่งบอกว่าเป็นการทำงานอะไร เช่น Create New Register เป็นต้น
  9. Data : อาจจะเป็นข้อมูลที่ต้องเอาไว้ใช้ตรวจสอบ หรืออ้างอิงถึงเช่น Id ของ register user หรืออาจจะเป็น OrderNumber
  10. Message : เป็น message ของ error ที่เกิดขึ้น
  11. Trace : เป็น StackTrace ของ error ที่เกิดขึ้น
  12. SupportNo : โปรแกรมอาจจะมี troubleshooting document link แนบไว้ให้กรณีที่มีคำแนะนำให้ผู้ support ระบบ
  13. Host : อาจจะเป็น computer name, ip address ของ server นั้น ๆ ที่ Application ติดตั้งอยู่
  14. ClientIPAddress : เป็น ip address จริงของ user ที่เข้าใช้งาน application
  15. UserId : อาจจะเป็น username, user id, domain account ของผู้ใช้ที่ทำการ login ในตอนนั้น

Log Level: ซึ่งนิยมใช้งานกันดังนี้

  1. Fatal\Critical : เป็นความผิดพลาดของระบบที่เกิดขึ้นเอง เช่น OS เกิดปัญหา, executable file corrupt, Hardware ระบบเสียหาย
  2. Error : การบันทึกเมื่อระบบเกิดปัญหาขึ้นเช่น Exception ในโปรแกรม
  3. Warning : การบันทึกเพื่อเตือนว่าอาจจะมีการทำงานผิดพลาดเกิดขึ้นหลังจากนี้
  4. Audit : การบันทึกเพื่อตรวจสอบการทำงานของผุ้ใช้ต่าง ๆ
  5. Information : การบันทึกเก็บข้อมูลบางอย่างเพื่อใช้เป็นการอ้างอิง
  6. Debug : การบันทึกเพื่อใช้ในการตรวจสอบเขียนโปรแกรม
  7. Trace : บันทึกการทำงาน ณ จุดต่าง ๆ ของโปรแกรมตั้งแต่ต้นจนจบ

Priority: ระดับความสำคัญในการแก้ปัญหา

  1. High: เป็นระดับความสำคัญสูงสุด ซึ่งต้องต้องรีบตรวจสอบหรือแก้ไขปัญหาในทันที เช่น ไม่สามารถบันทึกข้อมูลลงในระบบได้ ทำให้ user ไม่สามารถทำงานต่อไปได้
  2. Medium: เป็นปัญหาที่มีความสำคัญ แต่ระบบหลักยังคงทำงานได้ เพียงแต่อาจจะไม่สมบูรณ์ในบางโมดูล เช่น feed ข่าวจากเว็บมาแสดงไม่ได้ เป็นต้น
  3. Low: เป็นปัญหาที่กระทบต่อการทำงานของผู้ใช้ และควรจะแก้ไขใน released ต่อไปเช่น เกิด exception ใน loop แต่เรา try ข้ามไปและทำงานต่อได้ถูกต้อง เป็นต้น

เราสามารถบันทึก Log ต่าง ๆ เก็บไว้ที่ไหนได้บ้าง

  • File การบันทุึกเก็บลงไฟล์นั้น

มีข้อดี

  • สามารถสร้าง method ในการเขียนไฟล์ได้เลย
  • บันทึกข้อมูลได้รวดเร็ว

มีข้อเสีย

  • เรื่องการกำหนดขนาดไฟล์ไม่ให้ใหญ่มาก และการจัดเก็บไฟล์ให้เป็นระบบ
  • มีโอกาส file corrupt และ read/write locked
  • รวมไปถึงการเข้ามาอ่าน log file ทั้งเรื่องรูปแบบของไฟล์ สถานที่เก็บไฟล์
  • Database การบันทึกลง table ใน database

มีข้อดี

  • ในด้านการจัดการ โดยสามารถลดการกระจัดกระจายของ log โดยรวมไว้ที่เดียว
  • สามารถเข้าถึงและอ่าน log ได้ง่าย

ข้อเสีย

  • จะกินเนื้อในการเก็บข้อมูลจำนวนมาก
  • มี cost ที่เพิ่มขึ้นเช่น CPU time, Network bandwidth ของ DB Server
  • Windows Event Log

ข้อดี

  • เก็บไว้ในที่เป็นมาตรฐานของ OS
  • สามารถเรียกดูได้โดย remote event viewer หรือผ่านหน้าเครื่องตรงๆ

ข้อเสีย

  • มีผลต่อประสิทธิภาพของ OS
  • มีพื้นที่จำกัดในการเก็บบันทึก

และยังมีที่เก็บอีกมากมายแล้วแต่ความต้องการไม่ว่าจะเป็น Email, ยิง TCP, ส่งไปเข้า MSMQ เป็นต้น ซึ่งก็แล้วแต่ว่าเราจะเลือกสถานที่จัดเก็บเพื่อความสะดวกและเหมาะสม

 

รู้จักกับ Error ที่เกิดขึ้นในการเขียนโปรแกรม (Exception)

โค้ดจำนวนมากผู้พัฒนาได้เขียนออกไป กรณีหากเกิดการทำงานที่ผิดพลาดขึ้น ในตอน Run time ระบบจะทำการโยน (Throw) ชุดคำสั่งมีการทำงานที่ผิดพลาดกลับมายังโปรแกรมในรูปของ Exception ซึ่งอาจจะทำให้ระบบแฮ้งค้าง เกิดไดอะล็อกเตือน ไปจนหยุดการทำงานของโปรแกรม 

กรณีที่เราไม่ต้องการให้โปรแกรมหยุดการทำงาน และตรวจจับปัญหาของชุดคำสั่งได้นั้น แต่ละภาษาก็จะมีชุดคำสั่งในการดักจับ Error ที่เรียกว่า Try…Catch Block  ซึ่งเขียนได้ในรูปนี้

Code 1:
  1. Person person1 = null;
  2. try
  3. {
  4.     //person1 = new Person();
  5.     person1.FullName = "call null object";
  6.     
  7. }
  8. catch (Exception ex)
  9. {
  10.     MessageBox.Show(ex.ToString());
  11. }
  12. finally
  13. {
  14.     person1 = null;
  15. }

 

จาก code ตัวอย่างด้านบนจะเห็นได้ว่า

(line8-11) มีการ catch error ลง Exception Class เมื่อเกิด error ขึ้นใน try block ด้านบน (line 2-7)

ทำให้โปรแกรมเข้าไปทำงานต่อบล็อกของ catch พร้อมได้รายละเอียดของ error เพื่อนำไปจัดการปัญหาที่เกิดขึ้นต่อไป ในตัวอย่างแสดงแค่แสดง messagebox เท่านั้น

เมื่อทำการ  Watch ex object ดูก็จะเห็นข้อมูลดังนี้

Image 1: image 

Exception Class เป็นเบสคลาสของ exception class อื่นๆ ซึ่งพบบ่อยว่ามักใช้ในการดักจับ error ทุกประเภทไม่ว่าจะ IO , Network , Permission , COM ซึ่งค่อนข้างใช้งานได้กว้างขวาง โดยโครงสร้างของคลาสจะมี property ของข้อมูลพื้นฐาน ให้ใช้งานดังนี้

Property Name

Description

Data จะบอกข้อมูลของ Data ที่เกี่ยวข้องกับ Error นั้น
HelpLink เป็น link ไปยัง document เพื่อช่วยอธิบายปัญหา หาก exception นั้นมีใส่ไว้
HResult เป็น number ที่กำหนดให้ error นั้น
InnerException เป็น exception ที่เป็นสาเหตุให้เกิด exception นี้
Message ข้อความของ error
Source ชื่อของโปรแกรมที่ error
StackTrace เป็นข้อมูลที่เรียงลำดับการ call จาก Main Method ไปจนถึง line number ที่เกิด error
TargetSite ชื่อ Main Method ที่โยน Exception

ซึ่งข้อที่ควรสนใจคือ Exception Class ไม่ควรใช้งานเป็นคลาสหลัก เพราะเป็นเบสคลาสซึ่งจะทำให้เสียข้อมูลในส่วนของ exception ที่ควรจะได้ไม่ครบถ้วนของปัญหาที่เกิดขึ้น เช่น SqlException ที่จะมี Number property ของ Database แจ้งออกมาด้วยเป็นต้น แต่ถ้าคิดไม่ออกจริง ๆ ว่า try..catch block นี้จะเกิด exception ตัวไหนบ้างก็รอง catch สุดท้ายไว้ด้วย Exception Class ก็ได้ครับ

Exception Class ได้ถูกกำหนดให้เป็น Based class ซึ่งถูกนำไปใช้โดยแยกออกตามส่วนของระบบดังนี้

  1. SystemException Class  เป็นคลาสได้ที่กำหนดไว้แล้วใน Common Language Runtime Class ซึ่งจะใช้เป็น Base Class อีกครั้งสำหรับสร้าง exception class ที่แยกย่อยออกไปตามการทำงานเช่น NullReferenceException, ArithmaticException, IOException, SqlException เป็นต้น
  2. ApplicationException Class  เป็นคลาสที่อนุญาตให้ผู้พัฒนานำไปใช้เป็น base class สำหรับใช้สร้าง Exception class ที่จะใช้โปรแกรมนั้น เช่น InvalidBusinessRuleException เป็นต้น

    ลองดูโค้ดชุดนี้ที่มีการสร้าง Exception ต่างๆ ในแต่ละกรณีตัวอย่าง

    Code 2:
    1. try
    2. {
    3.     //Case 1: NullReferenceException
    4.     Person person1 = null;
    5.     person1.FullName = "call null object";
    6.  
    7.     //Case 2: ArithmeticException
    8.     int i = 5;
    9.     int x = 0;
    10.     var res = i / x;
    11.  
    12.     //Case 3: ApplicationException
    13.     throw new ApplicationException("My Error");
    14.  
    15. }
    16. catch (NullReferenceException ex) //Case1
    17. {
    18.     MessageBox.Show(ex.ToString());
    19. }
    20. catch (ArithmeticException ex) //Case2
    21. {
    22.     MessageBox.Show(ex.ToString());
    23. }
    24. catch (ApplicationException ex)  //Case3
    25. {
    26.     MessageBox.Show(ex.ToString());
    27. }
    28. catch (SystemException ex) //Case1, Case2
    29. {
    30.     MessageBox.Show(ex.ToString());
    31. }
    32. catch (Exception ex) //Case1, Case2, Case3
    33. {
    34.     MessageBox.Show(ex.ToString());
    35. }

Line 5:  จะเกิด error เนื่องจาก person1 ยังไม่ได้ทำการกำหนดค่าเริ่มต้น new Person(); ซึ่งจะวิ่งไปที่ Line 16: ซึ่งดักด้วย NullReferenceException  กรณีที่อ้างอิง object ที่เป็น null

Line 10: จะเกิด error เนื่องจากเป็นการหารด้วย 0  ซึ่งจะวิ่งไปทำงานที่ Line 20: ซึ่งจับด้วย ArithmeticException กลุ่มคณิต

Line 13: จะเกิด error เนื่องจากเป็นการสร้าง ApplicationException แล้ว throw ให้โปรแกรมเกิด error  จะวิ่งไปทำงานที่ Line 24: คือตรงตัว class

ส่วน Line 28:  หากลบโค้ดตั้งแต่ Line 16 – 27 ออก  Line 5 และ 10  จะวิ่งมาทำงานที่นี่เพราะเป็น base class ที่สืบสอดมาจาก Exception Class ใช้งานในกลุ่มของ CLR Exception ทั้งหมด

 

สำหรับ .NET App เราสามารถบันทึก Log ด้วยอะไรได้บ้าง?

Custom Log

โดยเป็นการเขียนโค้ดขึ้นมาเอง โดยอาจจะมี static method เอาไว้ใช้งานโดย ให้บันทึกลง File, ยิงเข้า StoreProcedure ลง Table, ส่ง Email เป็นต้น ซึ่งต้องเขียนจัดการเองทั้งหมด ซึ่งหากเขียนไว้ใช้เองก็จะพอดีกับงาน  แต่ค่อนข้างไม่ยืดหยุ่นแล้วมีข้อจำกัด รวมไปถึงมีความเสี่ยงในการจัดการของตัว Log ที่เขียนขึ้นเอง

3rd Party Library

การใช้กลุ่ม logging open source ถือว่าน่าสนใจมากกว่าเมื่อเปรียบเทียบข้อดีข้อเสียแล้ว  เพราะ library เหล่านี้ถูกพัฒนาขึ้นมาและใช้งานจากกลุ่มนั้น ๆ มีการพัฒนาและแก้ไขปรับปรุงโค้ดต่อเนื่อง รวมไปถึงความสามารถที่ช่วยลดการเกิดปัญหาเกี่ยวกับตัว Log เองเช่น การทำ file rolling, file size limit, various target source, multiple target source, Alternate Source,  write buffer, etc.  ผมขอแนะนำที่น่าสนใจ

1. Enterprise Library 5.0: Logging Block จาก Microsoft เอง ก็น่าสนใจตัวนึง มีการพัฒนาต่อเนื่อง และมี Document/Lab ให้เอาไปทดลองทำกันได้

2. Log4Net  เป็นการพัฒนาต่อยอดมาจาก Log4J มีการใช้งานกันอย่างกว้างขวาง แต่ก็ไม่เห็นมี update version มาซักระยะแล้ว

3. NLog  เป็นตัวที่จะนำมาแนะนำในบทความตอนต่อไปครับ

     

ส่งท้าย

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

ในตอนต่อไปจะแนะนำการใช้งาน NLog และแนะนำการเก็บบันทึกข้อมูลจาก Application ที่แตกต่างกันครับ

 


 

Chalermpon Areepong Nine (นาย)

Microsoft MVP Thailand ASP.NET

email : nine_biz-talk.net at hotmail dot com

Blog at WordPress.com.