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

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

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

Blog at WordPress.com.