DateTime.Now la mot trong nhung lenh trong co ve vo hai cho den khi ban thu viet test cho no. Day la mot static property doc dong ho he thong, va khong co cach nao de kiem soat gia tri ma no tra ve neu khong boc no trong mot thanh phan co the inject.
Toi da tung boc DateTime.Now trong nhieu codebase, va nhieu nguoi khac cung lam nhu vay. Tiec la moi nguoi lai tu xay dung mot cach khac nhau. Tu .NET 8, chung ta khong con phai lap lai cung mot abstraction nua ma co the dung TimeProvider.
Vay tai sao bai viet nay lai noi ve mot tinh nang cua .NET 8? Vi toi duoc nhac den no khi chung toi phat hanh Duende IdentityServer v8. Viec tang major version cho phep chung toi thuc hien nhung thay doi breaking ma da tri hoan tu lau, trong do co viec thay the wrapper rieng cua minh, IClock, bang TimeProvider cua .NET.
Duoi day la phan nhac lai ve nhung gi TimeProvider co the lam.
Mục lục
Giai phap ma moi nguoi van dung
Giai phap tieu chuan de dat mot moc thoi gian cu the trong codebase tu truoc den nay la dinh nghia mot interface boc dong ho, sau do inject no nhu mot dependency de co the thay bang implementation gia trong cac test phu thuoc vao ngay gio. Thuong thi se co dang nhu sau:
public interface IDateTimeProvider
{
DateTimeOffset UtcNow { get; }
}
public class SystemDateTimeProvider : IDateTimeProvider
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
Trong moi truong production, ban dang ky SystemDateTimeProvider. Trong test, ban viet mot FakeDateTimeProvider voi property co the gan gia tri, sau do truyen vao thoi diem ma test can. Cach nay hoat dong, khong phuc tap, va moi team lai thuc hien hoi khac nhau.
Toi tung lam viec voi cac project co ISystemClock. Toi da thay IClock, IDateTimeProvider, ITimeService, va ca nhung codebase su dung nhieu loai cung luc, vi sao lai khong. Trong mot thoi gian, ASP.NET Core cung cung cap ISystemClock rieng trong Microsoft.AspNetCore.Authentication, nhung no khong tuong thich voi cac phien ban tu xay dung.
Giai phap tich hop: TimeProvider
.NET 8 gioi thieu TimeProvider, mot abstract class trong namespace System. No nam trong base class library, vi vay khong can them dependency NuGet. Y tuong van giong voi cac giai phap tu xay dung truoc day: thay vi goi truc tiep DateTime.UtcNow, ban goi timeProvider.GetUtcNow(). Trong production, ban dung TimeProvider.System. Trong test, ban dung mot fake.
Nhung phuong thuc chinh ban se thuong su dung:
// Lay thoi gian UTC hien tai, tra ve DateTimeOffset thay vi DateTime
DateTimeOffset utcNow = timeProvider.GetUtcNow();
// Lay gio dia phuong, duoc tinh tu GetUtcNow() va LocalTimeZone
DateTimeOffset localNow = timeProvider.GetLocalNow();
// Mui gio dia phuong lam co so cho GetLocalNow()
TimeZoneInfo zone = timeProvider.LocalTimeZone;
// Moc thoi gian do phan giai cao de do thoi gian troi qua
long start = timeProvider.GetTimestamp();
// ... thuc hien cong viec ...
TimeSpan elapsed = timeProvider.GetElapsedTime(start);
TimeProvider lam viec voi DateTimeOffset, kieu du lieu mang thong tin ve do lech mui gio, thay vi DateTime.
Singleton dung trong production la TimeProvider.System. No doc tu dong ho he thong, tuong tu DateTimeOffset.UtcNow. Ban can dang ky no khi khoi dong ung dung:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
Sau do, ban co the inject no vao bat ky noi nao can thoi gian hien tai:
public class SubscriptionReminderService
{
private readonly TimeProvider _timeProvider;
private readonly ISubscriptionRepository _repository;
public SubscriptionReminderService(
TimeProvider timeProvider,
ISubscriptionRepository repository)
{
_timeProvider = timeProvider;
_repository = repository;
}
public async Task<IEnumerable<Subscription>> GetExpiringSubscriptionsAsync()
{
var now = _timeProvider.GetUtcNow();
var cutoff = now.AddDays(7);
return await _repository.GetSubscriptionsExpiringBeforeAsync(cutoff);
}
}
Dong ho bay gio da tro thanh mot dependency, va dependency thi co the duoc thay the, chang han trong test.
Test voi FakeTimeProvider
.NET cung cap class FakeTimeProvider trong mot goi NuGet rieng:
dotnet add package Microsoft.Extensions.TimeProvider.Testing
Ban co the khoi tao no voi mot moc thoi gian bat dau co dinh, hoac de no dung ngay mac dinh da biet neu khong quan trong gia tri cu the:
// Gan vao mot thoi diem cu the
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 6, 1, 0, 0, 0, TimeSpan.Zero));
// Hoac su dung gia tri mac dinh
var fakeTime = new FakeTimeProvider();
Trong vi du truoc, SubscriptionReminderService nhan TimeProvider, la abstract base class, nen ban co the truyen truc tiep FakeTimeProvider:
using Microsoft.Extensions.Time.Testing;
[Fact]
public async Task GetExpiringSubscriptions_ReturnsOnlySubscriptionsWithin7Days()
{
// Gan dong ho vao ngay 1 thang 6
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 6, 1, 0, 0, 0, TimeSpan.Zero));
var repository = new InMemorySubscriptionRepository(new[]
{
new Subscription { Id = 1, ExpiresAt = new DateTimeOffset(2026, 6, 5, 0, 0, 0, TimeSpan.Zero) },
new Subscription { Id = 2, ExpiresAt = new DateTimeOffset(2026, 6, 30, 0, 0, 0, TimeSpan.Zero) },
});
var service = new SubscriptionReminderService(fakeTime, repository);
// Thuc thi
var expiring = await service.GetExpiringSubscriptionsAsync();
// Kiem tra: chi subscription 1 nam trong vong 7 ngay tu ngay 1 thang 6
Assert.Single(expiring);
Assert.Equal(1, expiring.First().Id);
}
Subscription 1 het han vao ngay 5 thang 6, tuc la con 4 ngay va nam trong khoang canh bao. Subscription 2 het han vao ngay 30 thang 6, nam ngoai khoang nay. Test cho ket qua xac dinh bat ke duoc chay vao luc nao.
Tien thoi gian
Gia tri lon nhat xuat hien trong cac kich ban co thoi gian troi qua. FakeTimeProvider co phuong thuc Advance(TimeSpan delta) de tien dong ho ve phia truoc theo khoang thoi gian chi dinh:
[Fact]
public async Task GetExpiringSubscriptions_AfterTimeAdvances_CapturesMoreSubscriptions()
{
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 6, 1, 0, 0, 0, TimeSpan.Zero));
var repository = new InMemorySubscriptionRepository(new[]
{
new Subscription { Id = 1, ExpiresAt = new DateTimeOffset(2026, 6, 5, 0, 0, 0, TimeSpan.Zero) },
new Subscription { Id = 2, ExpiresAt = new DateTimeOffset(2026, 6, 30, 0, 0, 0, TimeSpan.Zero) },
});
var service = new SubscriptionReminderService(fakeTime, repository);
// Ngay 1 thang 6: chi subscription 1 sap het han
var firstCheck = await service.GetExpiringSubscriptionsAsync();
Assert.Single(firstCheck);
// Ba tuan sau: hien tai la ngay 22 thang 6, subscription 2 het han ngay 30 thang 6
fakeTime.Advance(TimeSpan.FromDays(21));
var secondCheck = await service.GetExpiringSubscriptionsAsync();
Assert.Equal(2, secondCheck.Count());
}
Neu muon nhay den mot thoi diem chinh xac thay vi tien tuong doi, ban co the dung SetUtcNow(DateTimeOffset utcNow). Ca hai phuong thuc cung kich hoat moi timer dang cho ma thoi diem kich hoat nam trong khoang thoi gian duoc tien, se duoc noi them o phan sau.
Advance() dac biet huu ich khi kiem thu logic tich luy trang thai theo thoi gian, chang han nhu thoi han cache, retry back-off va tinh toan cua so truot. Ban kiem soat chinh xac gia tri cua “hien tai” ma khong can cho doi trong thuc te.
Timer
TimeProvider cung cung cap CreateTimer, phuong thuc tra ve mot ITimer tu System.Threading:
ITimer timer = timeProvider.CreateTimer(
callback: state => DoSomeWork(),
state: null,
dueTime: TimeSpan.FromMinutes(5),
period: TimeSpan.FromMinutes(5));
Cau truc nay giong het System.Threading.Timer, voi cung tham so va cung phuong thuc Change(TimeSpan dueTime, TimeSpan period) de len lich lai, nhung duoc xay dung tren abstraction. Khi dung FakeTimeProvider, viec goi Advance() vuot qua thoi diem kich hoat se chay callback. Ban khong can dung Thread.Sleep hay Task.Delay; cac test timer se chay nhanh nhat theo kha nang cua test host.
Dieu nay quan trong neu ban co background service hoac hosted worker thuc hien cong viec dinh ky bang timer. Khi chung chap nhan mot TimeProvider, toan bo chu ky len lich tro nen co the kiem thu.
Ket luan
TimeProvider mang den mot abstraction duy nhat va tieu chuan cho viec xu ly thoi gian trong .NET hien dai. Hay dung va inject TimeProvider.System trong production, dong thoi dung FakeTimeProvider trong test de viec xac minh logic phu thuoc vao thoi gian tro nen de dang hon.
Trong test, FakeTimeProvider cung cap Advance(), cho phep ban tien thoi gian ma khong phai cho doi. Nho do, ban co the kiem thu viec het han cache, retry back-off va logic cua so truot voi toc do toi da, khong phai cho den khi thoi gian thuc te troi qua. Cac timer duoc tao qua TimeProvider.CreateTimer() se chay callback khi ban tien qua thoi diem kich hoat, vi vay viec len lich background va cong viec dinh ky cung tro nen co the kiem thu.



