Skip to content

Timing

Granit.Timing provides testable time abstractions for the entire framework. Replaces all calls to DateTime.Now / DateTime.UtcNow with injectable services. Uses AsyncLocal for per-request timezone propagation.

PackageRoleDepends on
Granit.TimingIClock, ICurrentTimezoneProvider, TimeProviderGranit.Core
[DependsOn(typeof(GranitTimingModule))]
public class AppModule : GranitModule { }

The primary abstraction for accessing the current time and performing timezone conversions:

public interface IClock
{
DateTimeOffset Now { get; }
bool SupportsMultipleTimezone { get; }
DateTimeOffset Normalize(DateTimeOffset dateTime);
DateTimeOffset ConvertToUserTime(DateTimeOffset utcDateTime);
DateTimeOffset ConvertToUtc(DateTimeOffset dateTime);
}

Usage in a service:

public class AppointmentService(IClock clock, AppDbContext db)
{
public async Task<bool> IsAvailableAsync(
Guid doctorId, DateTimeOffset requestedTime, CancellationToken cancellationToken)
{
DateTimeOffset utcTime = clock.Normalize(requestedTime);
return !await db.Appointments
.AnyAsync(a => a.DoctorId == doctorId
&& a.ScheduledAt <= utcTime
&& a.EndAt > utcTime,
cancellationToken)
.ConfigureAwait(false);
}
public Appointment CreateAppointment(Guid doctorId, DateTimeOffset scheduledAt)
{
return new Appointment
{
DoctorId = doctorId,
ScheduledAt = clock.Normalize(scheduledAt),
CreatedAt = clock.Now, // Always UTC
};
}
}

Per-request timezone context backed by AsyncLocal<string?>. Set it early in the request pipeline (e.g., from a header, claim, or user preference):

app.Use(async (context, next) =>
{
var timezoneProvider = context.RequestServices.GetRequiredService<ICurrentTimezoneProvider>();
timezoneProvider.Timezone = context.Request.Headers["X-Timezone"].FirstOrDefault()
?? "Europe/Brussels";
await next(context).ConfigureAwait(false);
});

Then IClock.ConvertToUserTime() converts UTC dates to the user’s local time:

public class AppointmentResponse
{
public required DateTimeOffset ScheduledAtUtc { get; init; }
public required DateTimeOffset ScheduledAtLocal { get; init; }
}
// In the handler:
var response = new AppointmentResponse
{
ScheduledAtUtc = appointment.ScheduledAt,
ScheduledAtLocal = clock.ConvertToUserTime(appointment.ScheduledAt),
};

Opt out of automatic UTC normalization for properties that store user-local times (e.g., birth dates where timezone is irrelevant):

public class Patient
{
[DisableDateTimeNormalization]
public DateTimeOffset DateOfBirth { get; set; }
}

Replace TimeProvider in tests to control time:

var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 3, 15, 10, 0, 0, TimeSpan.Zero));
services.AddSingleton<TimeProvider>(fakeTime);
// IClock.Now will return 2026-03-15T10:00:00Z
// Advance: fakeTime.Advance(TimeSpan.FromMinutes(30));
ServiceLifetimeRationale
TimeProviderSingletonTimeProvider.System is stateless and thread-safe
ICurrentTimezoneProviderSingletonBacked by AsyncLocal — value is per-async-context
IClockSingletonStateless — delegates to TimeProvider and ICurrentTimezoneProvider
PropertyDefaultDescription
DefaultTimezonenullFallback timezone when none is set per-request (IANA or Windows ID)
CategoryKey typesPackage
ModuleGranitTimingModule
ServicesIClock, ICurrentTimezoneProvider, CurrentTimezoneProvider, ClockGranit.Timing
AttributesDisableDateTimeNormalizationAttributeGranit.Timing
OptionsClockOptionsGranit.Timing
ExtensionsAddGranitTiming()Granit.Timing