مدیریت قطعی زمان در NET.: چرا باید استفاده از DateTime.UtcNow را ممنوع کنیم؟
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۱۹ ۱۰:۰۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
DateTime.UtcNow یک فراخوانی ساده و بیضرر نیست، بلکه ایجاد یک وابستگی پنهان (Hidden Dependency) به ساعت سختافزاری سرور است. این وابستگی، تستپذیری قطعی (Deterministic Testing) را غیرممکن کرده و پایداری سیستم را به شانس واگذار میکند.TimeProvider در .NET 8، مایکروسافت یک انتزاع (Abstraction) رسمی، پایدار و درونساخت برای مدیریت زمان ارائه داده است تا به این کابوس پایان دهد. در این مقاله بررسی میکنیم که چرا روشهای قدیمی منسوخ شدهاند، چگونه با ابزار تحلیل کد (Analyzer) استفاده از آنها را ممنوع کنیم و چطور به یک معماری تستپذیر و قابل اطمینان دست یابیم.public bool IsTokenExpired(DateTime issuedAt, TimeSpan validity)
{
return DateTime.UtcNow > issuedAt + validity;
}Func و پاس دادن () => DateTime.UtcNow در محیط عملیات (یک راهحل موقت و غیراستاندارد).IClock یا ISystemClock (اختراع دوباره چرخ در هر پروژه).ISystemClock در ASP.NET Core استفاده میکنند:public interface IClock
{
DateTime UtcNow { get; }
}ISystemClock را در ASP.NET Core منسوخ (Deprecated) کرده و TimeProvider را جایگزین آن نموده است. ادامه استفاده از ابزارهای موازی یا شخصی، صرفاً باعث سردرگمی توسعهدهندگان جدید و نگهداری کدهای زائد میشود. وقتی پلتفرم حرکت کرده است، معماری سیستم شما نیز باید همگام شود.TimeProvider در فضای نام System قرار دارد و سطح دسترسی آن عمداً کوچک و بهینه نگه داشته شده است:public abstract class TimeProvider
{
public static TimeProvider System { get; }
public virtual DateTimeOffset GetUtcNow();
public virtual DateTimeOffset GetLocalNow();
public virtual TimeZoneInfo LocalTimeZone { get; }
public virtual long GetTimestamp();
public virtual long TimestampFrequency { get; }
public virtual ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period);
}نکته برای پروژههای قدیمیتر: کلاسTimeProviderبه صورت بومی از .NET 8 در دسترس است. با این حال، اگر پروژهای روی .NET 6 یا .NET 7 دارید، نیازی به بازنویسی ساختار ندارید؛ میتوانید با نصب پکیج NuGet رسمیMicrosoft.Bcl.TimeProviderدقیقاً از همین قابلیتها بهرهمند شوید.
TimeProvider فقط یک پوسته دور DateTime نیست؛ این کلاس ساخت لایههای زمانسنج (ITimer) را نیز کنترل میکند. این یعنی رفتارهای مبتنی بر زمانِ شبکه یا تاخیرها نیز بدون نیاز به ترفندهای پیچیده Threading، کاملاً تستپذیر میشوند.public class TokenValidator
{
private readonly TimeProvider _time;
public TokenValidator(TimeProvider time)
{
_time = time;
}
public bool IsTokenExpired(DateTimeOffset issuedAt, TimeSpan validity)
{
return _time.GetUtcNow() > issuedAt + validity;
}
}Program.cs) تنها به یک خط کد نیاز دارد:services.AddSingleton(TimeProvider.System);
Microsoft.Extensions.TimeProvider.Testing را دقیقا برای سناریوهای تست ارائه داده است که کلاس FakeTimeProvider را در اختیار ما میگذارد. به نحوه تست بدون ثانیهای تاخیر واقعی دقت کنید:var fakeTime = new FakeTimeProvider(); fakeTime.SetUtcNow(new DateTimeOffset(2026, 6, 9, 12, 0, 0, TimeSpan.Zero)); var validator = new TokenValidator(fakeTime); var issuedAt = fakeTime.GetUtcNow().AddMinutes(-16); // به طور قطعی مشخص است که توکن منقضی شده است Assert.True(validator.IsTokenExpired(issuedAt, TimeSpan.FromMinutes(15)));
fakeTime.Advance(TimeSpan.FromMinutes(30));
UtcNow ختم میشود، اما TimeProvider مشکل سختتری را حل میکند: کنترل زمانسنجها و تاخیرها بدون معطل نگه داشتن دیتاسنتر یا سیستم تست.public async Task RetryAsync(Func<Task> operation, TimeProvider time)
{
for (int attempt = 0; attempt < 3; attempt++)
{
try
{
await operation();
return;
}
catch
{
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
// ایجاد توکن لغو مبتنی بر زمان از طریق واگذاری به سرویس ارائهدهنده زمان
await Task.Delay(delay, time.CreateCancellationTokenSource(delay).Token);
}
}
}FakeTimeProvider میتوان زمان را به صورت مجازی جلو برد تا رفتارهای درونی تحریک شوند:var fakeTime = new FakeTimeProvider(); var retryTask = RetryAsync(failingOperation, fakeTime); fakeTime.Advance(TimeSpan.FromSeconds(1)); // شبیهسازی و تحریک تلاش مجدد اول fakeTime.Advance(TimeSpan.FromSeconds(2)); // شبیهسازی و تحریک تلاش مجدد دوم fakeTime.Advance(TimeSpan.FromSeconds(4)); // شبیهسازی و تحریک تلاش مجدد سوم await retryTask;
Task.Delay(100) یا قفل کردن Thread اجرا میشوند.Microsoft.CodeAnalysis.BannedApiAnalyzers استفاده میکنیم.BannedSymbols.txt را ایجاد کرده و پیکربندی زیر را درون آن قرار دهیم. این فایل تمام راههای دسترسی مستقیم به ساعت سیستم (از جمله سیستم قدیمی ISystemClock) را مسدود میکند:T:System.DateTime;استفاده مستقیم از ساختار زمان به دلیل ایجاد وابستگی مخفی ممنوع است. از TimeProvider استفاده کنید. P:System.DateTime.UtcNow;دسترسی مستقیم به ساعت جهانی ممنوع است. لطفاً TimeProvider را تزریق کنید. P:System.DateTime.Now;دسترسی مستقیم به ساعت محلی ممنوع است. لطفاً TimeProvider را تزریق کنید. P:System.DateTimeOffset.UtcNow;دسترسی مستقیم به ساعت جهانی ممنوع است. لطفاً TimeProvider را تزریق کنید. P:System.DateTimeOffset.Now;دسترسی مستقیم به ساعت محلی ممنوع است. لطفاً TimeProvider را تزریق کنید. T:Microsoft.AspNetCore.Authentication.ISystemClock;این اینترفیس قدیمی و منسوخ شده است. به جای آن از TimeProvider استفاده کنید. T:Microsoft.Extensions.Internal.ISystemClock;این اینترفیس قدیمی و منسوخ شده است. به جای آن از TimeProvider استفاده کنید.
.csproj) یا فایل مشترک مدیریت ساختار پروژهها (Directory.Build.props) اضافه کنید:<ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> </ItemGroup> <ItemGroup> <AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" Link="Properties/BannedSymbols.txt" /> </ItemGroup>
public static class TimeContext
{
[ThreadStatic]
private static TimeProvider? _current;
public static TimeProvider Current
{
get => _current ?? TimeProvider.System;
set => _current = value;
}
}TimeContext.Current را با یک FakeTimeProvider مقداردهی کرده و در پایان تست (Teardown) آن را پاکسازی کنید. این کار به شما کمک میکند تا بدون بازنویسی کل سیستم، وابستگیهای مخفی را کنترل کنید. اما در نظر داشته باشید که در اولین فرصت، این کدهارا به مدل تزریق سازنده ارجاع دهید.TimeProvider را تزریق کنید.CreatedAt در زمان نگاشت دیتابیس یا فرمتهای نمایشی است؛ این موارد نیازی به کنترل زمان ندارند و میتوان آنها را به همان صورت رها کرد.TimeProvider در .NET یک ابزار آزمایشی یا فریمورک فرعی نیست، بلکه اصلاح یک نقص طراحی قدیمی است که از نسخههای اولیه داتنت وجود داشته است.