اگر به سورسهای ASP.NET Identity نگارشهای 2 و 3 دقت کنیم، این تفاوت به وضوح قابل مشاهدهاست:
در نگارش 2 public virtual DateTime? LockoutEndDateUtc { get; set; }
در نگارش 3 public virtual DateTimeOffset? LockoutEnd { get; set; }
و در کل، در طراحی تمام قسمتها و اجزای NET Core. بجای استفادهی از DateTime متداول، شاهد استفادهی گستردهای از DateTimeOffset هستیم که از زمان ارائهی NET 3.5. معرفی شدهاست. چرا؟
مشکل ساختار DateTime چیست؟
تمام کسانیکه مدتی با NET Framework. کار کردهاند، قطعا از ساختار DateTime برای ذخیره سازی اطلاعاتی زمانی محلی استفاده کردهاند. اما مشکل DateTime چیست؟
فرض کنید در حال استفادهی از یک وب سرویس قرار گرفتهی در یک منطقهی زمانی غربی هستید و این وب سرویس تاریخ تولد افراد را با یک چنین فرمتی ارائه میدهد:
2012-03-01 00:00:00-05:00
در این حالت برای استفادهی متداول از این زمان میتوان به صورت زیر عمل کرد:
var dateString = "2012-03-01 00:00:00-05:00";
var birthDay = DateTime.Parse(dateString);
هرچند این عملیات ساده به نظر میرسد، اما با توجه به قرارگیری سرور برنامه در یک منطقهی زمانی دیگر، زمان پردازش شده به صورت ذیل خواهد بود:
اتفاقی که رخ دادهاست، تبدیل DateTime رسیده به زمان محلی سرور است و در این حالت تاریخ تولد شخص از یکم ماه، به 29 ام ماه قبل تغییر کردهاست. علت آن هم وجود 05:00 یا offset (فاصلهی با UTC) در تاریخ ارائه شدهاست.
چگونه میتوان offset را در تاریخ ذکر کرد، اما از تبدیل آن به زمان محلی جلوگیری کرد؟ این مورد جاییاست که ساختار DateTimeOffset بکار خواهد آمد.
DateTimeOffset و ذخیرهی DateTime به همراه Offset
ساختار کلی DateTimeOffset بسیار واضح بوده و تشکیل شدهاست از Date + Time + Offset. اهمیت آن نیز به ذخیره سازی اطلاعات منطقهی زمانی، در قسمت Offset ساختار ارائه شده بر میگردد. ساختار DateTimeOffset در بسیاری از موارد با DateTime متداول یکسان است و تفاوتهای آن شامل خواص اضافی ذیل هستند:
- DateTime: قسمت DateTime مقدار را بدون توجه به offset باز میگرداند (به زمان محلی تبدیل نخواهد شد).
- LocalDateTime: قسمت DateTime را با توجه به منطقه زمانی سروری که برنامه بر روی آن اجرا میشود، بر میگرداند.
- Offset: فاصلهی زمانی با UTC را بیان میکند. یک TimeSpan است که فاصلهی با UTC را بیان میکند.
- UtcDateTime: قسمت DateTime را با توجه به UTC time ارائه میکند.
در این ساختار خواص Now و UtcNow نیز یک DateTimeOffset را باز میگردانند.
چه زمانی از DateTime و چه زمانی از DateTimeOffset استفاده کنیم؟
اگر هدف شما ذخیره سازی اطلاعات زمانی محلی (جایی که سرور برنامه قرار دارد) است، از DateTime استفاده کنید. اما اگر میخواهید مقادیر زمانی را در مناطق زمانی دیگری نیز مورد استفاده قرار دهید و علاقمندید که قسمت TimeZone این اطلاعات نیز حفظ شود، از DateTimeOffset استفاده نمائید.
در این حالت روش پردازش صحیح مثال ابتدای بحث به صورت ذیل خواهد بود:
string birthDay = "2012-03-01 00:00:00-05:00";
var dtOffset = DateTimeOffset.Parse(birthDay);
و در اینجا اگر علاقمند به مقایسهی این مقدار با یک زمان محلی هستیم، میتوان از خاصیت Date آن استفاده کرد:
var theDay = dtOffset.Date;
مطابق توصیهی تیم BCL، استفاده از DateTimeOffset روش ترجیح داده شدهی برای ذخیره سازی اطلاعات اکثر سناریوهای زمانی است.
SQL Server و پشتیبانی از DateTimeOffset
ساختار دادهای datetime در SQL Server نیز اطلاعات منطقهی زمانی را ذخیره نمیکند و درصورت بازیابی آن در برنامه، این زمان، به زمان محلی تبدیل خواهد شد. برای رفع این مشکل، از زمان ارائهی SQL Server 2008، ساختار DateTimeOffset نیز به نوعهای دادهآی SQL Server اضافه شدهاست:
این ساختار، اطلاعات +00:00 timezone را نیز ذخیره میکند.
مشکلات نوع datetime در بانکهای اطلاعاتی برای ذخیره سازی اطلاعات UTC در آنها
یکی از روشهای توصیه شدهی جهت ذخیره سازی اطلاعات زمانی در بانکهای اطلاعاتی، استفادهی از DateTime.UtcNow است. اما زمانیکه از DateTime.UtcNow برای ذخیره سازی اطلاعاتی زمانی استفاده میکنیم، به معنای دریافت زمان محلی بر اساس و نسبت به UTC است. در این حالت هنگامیکه آنرا از یک فیلد datetime بانک اطلاعاتی بازیابی میکنیم، از نوع Unspecified خواهد بود (DateTimeKind.Unspecified) و به صورت خودکار به DateTimeKind.Local ترجمه میشود. یعنی مقدار آن مجددا به زمان محلی شیفت پیدا خواهد کرد چون نوع datetime بانک اطلاعاتی درکی از DateTimeKind و منطقهی زمانی ندارد.
به همین جهت روش بازیابی صحیح این زمان UTC، نیاز به قید صریح DateTimeKind.Utc را خواهد داشت:
public static class SqlDataReaderExtensions
{
public static DateTime GetDateTimeUtc(this SqlDataReader reader, string name)
{
int fieldOrdinal = reader.GetOrdinal(name);
DateTime unspecified = reader.GetDateTime(fieldOrdinal);
return DateTime.SpecifyKind(unspecified, DateTimeKind.Utc);
}
}
اما اگر نوع فیلد را DateTimeOffset قرار دهیم و از DateTimeOffset.UTCNow برای ذخیره سازی اطلاعات زمانی استفاده کنیم، SqlDataReader بدون نیاز به تبدیلات فوق، قادر است اطلاعات آنرا به نحو صحیحی دریافت و پردازش کند.
خلاصهی بحث
اگر برنامهی وب شما امروز در یک سرور در اروپا هاست میشود و سال بعد در یک سرور کانادایی، استفادهی DateTime.UtcNow کمک زیادی به برنامه نکرده و خروجی SQL Server در این حالت DateTimeKind.Unspecified است و این زمان مجددا بر اساس محل سرور جدید و تنظیمات منطقهی زمانی آن، به حالت DateTimeKind.Local شیفت داده میشود که الزاما خروجی صحیحی را به همراه نخواهد داشت و یا اگر قرار است از وب سرویس شما در مناطق زمانی مختلفی استفاده کنند نیز DateTime.UtcNow انتخاب مناسبی نیست. جهت درج فاصلهی صحیح با UTC و ذخیره سازی آن در بانک اطلاعاتی، روش توصیه شده، استفاده از نوع DateTimeOffset است و در این حالت دیگر SQL Server اطلاعات را با فرمت زمانی Unspecified بازگشت نمیدهد و در سمت کلاینت نیازی به تبدیلات خاصی نخواهد بود.