แนวทางจัดการกับ Date-time อย่างที่ควรจะเป็น สำหรับ Web Application (C# ASP.NET)

ในการพัฒนา Web App หลายคนอาจจะเคยเจอพบปัญหาการแสดงผลเกี่ยวกับ Date-time UTC-Local ที่ชวนปวดหัว เคยรันเครื่องนี้ไป แต่ไปรันอีกเครื่องนึง กลับไม่เป็นดังใจ

วันนี้ผมจะมาเล่าสิ่งที่เราควรจะทำ เพื่อจัดการกับเรื่อง Date-time ให้ Web App ของเราอยู่รอดปลอดภัย อ้างอิงมาจากประสบการณ์ส่วนตัว ที่เคยเจอแล้วชวนหัวร้อนมากๆ

โดยจะแบ่งเป็น 3 จุดด้วยกันคือ
- Database
- Web Server
- Web Client

หลังบ้านจะเป็น MS-SQL + Entity Framework
ส่วนหน้าบ้านก็เป็น C# ASP.NET MVC + JavaScript jQuery

ซึ่งเดี๋ยวจะแบ่งเป็น 2 ขาด้วยกัน จะได้เห็นภาพคือ
1. เรียกข้อมูลจาก DB ไปแสดงที่หน้าบ้าน
2. หน้าบ้านส่งข้อมูลไปหาหลังบ้านเพื่อ save ลง DB

ไปดูกันเลย

การเรียกข้อมูลจาก Database ไปแสดงที่หน้าบ้าน

Web Server กับ Database

เริ่มจาก ขาระหว่าง Web Server กับ Database สิ่งที่สำคัญในขั้นตอนนี้คือ

  • กำหนดประเภท (Kind) ของ Date-time ที่วิ่งอยู่ในระบบ Web Server ให้เป็นค่าเดียวกันให้หมด เลือกเอาระหว่าง UTC กับ Local อย่างใดอย่างหนึ่ง เพื่อความง่ายต่อการคำนวณ (ส่วนตัวชอบแบบ UTC)
  • กรณีที่ไม่ได้สร้าง Database เอง ก็อาจจะตั้งค่าตามที่ต้องการไม่ได้ ก็ควรจะรู้ว่า ข้อมูลประเภท Date-time ที่เก็บใน Database นั้นเก็บด้วย UTC หรือ Local เพื่อที่ว่าตอน query มาใช้ จะได้นำมาแปลงให้เป็น Timezone เดียวกับในระบบ

สำหรับ class DateTime ใน C# จะมี property ตัวหนึ่งที่ชื่อว่า Kind จะเป็นตัวบอกว่า object DateTime ที่แสดงผลขณะนั้นเป็นประเภท UTC ,Local หรือ Unspecified (อ่านเพิ่มเติม)

เวลาที่เรา parse value จาก DB มาใส่ object เราสามารถกำหนด Kind ของ DateTime ให้เหมาะสมกับข้อมูลของเรา โดยใช้ method DateTime.SpecificKind ได้

Web API

พอเราได้ข้อมูล DateTime object มาวิ่งในระบบเราแล้ว ก็ถึงเวลาที่จะส่งออกไปให้ Web Client ผ่าน Web API Restful Service ในรูปแบบของ JSON

ข้อควรคำนึงต่อไปคือ Browser ต้องการนำข้อมูลไปทำอะไร ต้องการแค่แสดงผลหรือต้องไปคำนวณหน้าบ้านต่อ ซึ่งจะมีผลต่อการออกแบบ Model ที่ Web API จะส่งให้

ถ้าต้องการนำข้อมูลไปคำนวณต่อ เราควรแปลงข้อมูลให้อยู่ในรูปแบบที่หน้าบ้านสามารถเข้าใจได้ง่าย สำหรับ format ที่นิยมมีอยู่หลายแบบ โดยที่แนะนำคือ format ตามมาตรฐาน ISO-8601 โดยแปลงเป็น UTC แล้วบอก Kind (ตัว ‘Z’) ติดท้ายไปด้วยเสมอ

//format
"yyyy-MM-dd'T'HH:mm:ss.fffK"
//example
"2018–12–28T17:00:52.123Z"

เราสามารถตั้งค่า response ให้ทุกครั้งที่มีการ Serialize object type DateTime ไปเป็น JSON มี format และ DateTimeKind ตามที่ต้องการได้ที่ไฟล์ Global.asax.cs ใน Method App_Start()

Global.asax.cs — App_Start() ตั้งค่า Json Serializer DateTime ให้เป็น UTC และ ISO format

แต่ถ้าต้องการแสดงเพียงอย่างเดียว ไม่ได้คำนวณต่อ เราสามารถ แปลง Date-time ให้อยู่ใน format ที่ต้องการ แล้วส่งเป็น string ออกไปเลย ก็เป็นอีกทางเลือกหนึ่ง

ตัวอย่างการแสดงผล DateTime ตาม format และ culture ที่กำหนด

Web Browser

เมื่อเราเรียกข้อมูลจาก API เราจะได้ข้อมูล Date-time ที่เป็น string มาในรูปแบบที่ Web API กำหนด เราต้องแปลง string ให้เป็น Datetime object ของ Javasctipt ก่อน จึงจะสามารถนำไปคำนวณอย่างอื่นต่อได้

โดยเรามี Library ตัวช่วยจัดการ Datetime สำหรับ Javascript อยู่ นั่นก็คือ momentjs ซึ่งช่วยเราได้ดีมากๆ ทั้งการ parse Datetime, การคำนวณระยะเวลา การบวกลบ, การแสดงผล หรือเรื่อง Timezone ต่างๆ

ตัวอย่างการ parse และ แสดงผล Datetime ด้วย momentjs

เท่านี้ Web ของเราก็จะอยู่รอดปลอดภัยขึ้นมาอีกระดับหนึ่ง

การส่งข้อมูลไปหาหลังบ้านเพื่อ save ลง Database

สำหรับใน C# ปัญหา classic แบบตกม้าตาย ที่เคยเจอก็คือ ข้อมูลประเภท Date-time ที่จัดเก็บใน Database ไม่เป็น Timezone เดียวกันทั้งหมด ซึ่งตาม common sense แล้ว เราควรระลึกไว้เสมอว่า Date-time ที่เก็บในระบบควรจะมีแบบเดียว

สมมติเรา design table ให้มี field จำพวกข้อมูล Date-time ที่กำเนิดมาจากระบบเราเอง เช่น
- CreatedDatetime
- ModifiedDatetime
- ExpiredDatetime

ซึ่งเรากำหนด DateTimeKind ไว้เป็น UTC

กับอีกพวกนึงคือ ข้อมูลที่ input มาจากหน้าบ้าน เช่น
- BirthDate
- ScheduleDate
- บลาๆๆๆ

แต่เราไม่ได้กำหนด DateTimeKind ไว้ ประมาณว่าหน้าบ้านส่ง อะไรมาฉันก็ save ไปตามนั้น ไม่ได้มีการตรวจสอบอะไรเลย

ดังนั้นก่อนที่เราจะนำข้อมูลเหล่านี้ มาบันทึกลง Database เราควรจะมั่นใจก่อนว่า ข้อมูลนั้นได้แปลงเป็น Date-time ที่ถูกต้องสัมพันธ์กับค่าที่เรากำหนดไว้

วิธีนี้แก้ได้โดย ก่อนที่จะบันทึกข้อมูล Date-time ลง Database ให้แปลง DateTimeKind เป็นค่าที่เรากำหนด สมมติเราอยากให้เป็น UTC ทั้งหมด เราก็โดยใช้คำสั่ง

DateTime.ToUniversalTime();

ในกรณีที่เรารับ date string มาจากหน้าบ้าน แล้วต้องทำการ parse string ให้เป็น DateTime ถ้าเรารู้ว่าข้อมูลที่ Client ส่งมาเป็น Timezone เดียวกับ Server แน่ๆเราสามารถ parse แล้ว แปลงเป็น UTC แล้วนำไปใช้ได้ทันที

string dateString = "2018-12-19 20:15";
DateTime dt;
DateTime.TryParseExact(dateString, "yyyy-dd-MM hh:mm", out dt);
dt.ToUniversalTime();

ถ้าเว็บเราถูกใช้งานจากหลายประเทศที่มี Timezone แตกต่างกัน แล้วหน้าบ้านไม่ได้ส่ง Timezone มา ก็เป็นเรื่องยากที่จะจัดการ ดังนั้นหน้าบ้านควรบอก Timezone หรือ DateTimeKind มาด้วย

ใน Javascript Date object (รวมไปถึง momentjs) มี function toJSON() และ toISOString() สำหรับแปลง Date-time ให้เป็น UTC แล้วจัดรูปแบบให้กลายเป็น string ซึ่งช่วยให้ Server สามารถนำไปใช้งานได้อย่างถูกต้อง

new Date().toJSON();
new Date().toISOString();
moment().toJSON();
moment().toISOString();
// "2018-12-19T17:00:52.123Z"

เพียงเท่านี้เราก็จะได้เวลาที่บอก DateTimeKind ชัดเจน สามารถนำไปใช้งานต่อไป

สรุป

หลักการง่ายๆ ในบทความนี้คือ การตั้งมาตรฐาน Date-time ที่วิ่งอยู่ในระบบเราให้เป็นแบบเดียวกันทั้งหมดน ไม่ว่าข้อมูลจะเข้าจะออก ก็ต้องแปลงมาให้อยู่ในมาตรฐานที่ App เราเข้าใจนั่นเอง

--

--

Aekasit Nakarad (IIwuDaFFK)
Arcadia Software Development

ติ่งเป็นงานหลัก devเป็นงานรอง ส่วนใหญ่จะ ITsupport เลี้ยงดู carry ให้เติบโต