A hero starts a mission "today". Whose today? The user in Brisbane is already on Wednesday while the API server in UTC is still on Tuesday. If the frontend sends UTC, or a full client DateTimeOffset, the backend has to guess which calendar day the user meant, and a mission can land on the wrong day.
When the value you need is a calendar date as the user sees it, send that date and nothing else. Capture it with TimeProvider.GetLocalNow(), turn it into a DateOnly, and let the backend attach the business-context timezone when it stores a DateTimeOffset.
DateTimeOffset (see storing points in time as DateTimeOffset with TimeProvider)Use an injected TimeProvider (not DateTime.Now) so the clock stays testable. In Blazor WebAssembly, GetLocalNow() returns the browser's local DateTimeOffset. Convert that to DateOnly before you call the API.
public class StartMissionForm(TimeProvider timeProvider, IMissionApi api){public async Task StartAsync(Guid teamId){// Client offset is sent to the API - the team's region may be differentvar clientNow = timeProvider.GetLocalNow();await api.StartMissionAsync(new StartMissionRequest(teamId, clientNow));}}
❌ Figure: Bad example - Sending the client's DateTimeOffset mixes the browser timezone into a business date that should use the team's region
public class StartMissionForm(TimeProvider timeProvider, IMissionApi api){public async Task StartAsync(Guid teamId){var localNow = timeProvider.GetLocalNow();var missionDate = DateOnly.FromDateTime(localNow.DateTime);await api.StartMissionAsync(new StartMissionRequest(teamId, missionDate));}}
✅ Figure: Good example - DateOnly carries the calendar day the user sees, with no time or offset attached
In Blazor Server or Interactive Server, this code runs on the server, so GetLocalNow() uses the server's timezone, not the user's. Get the date from the browser instead, for example through JavaScript interop, and send that as the DateOnly.
Avoid sending UTC for a "today" value. Near midnight, the user's local date and the UTC date can differ in either direction, so the mission lands on the wrong day.
DateOnly missionDate = DateOnly.FromDateTime(timeProvider.GetUtcNow().UtcDateTime);
❌ Figure: Bad example - UTC "today" is not the user's today when they are ahead of or behind UTC
The API receives the pure date and combines it with the timezone that belongs to the business context (for example the team's region). Store the result as a DateTimeOffset. In this example, StartedAt represents the start of the selected mission day in the team's timezone. CreatedAt records the instant the API creates the record.
public class StartMissionHandler(ApplicationDbContext db, TimeProvider timeProvider){public async Task Handle(StartMissionRequest request, CancellationToken ct){var team = await db.Teams.SingleAsync(t => t.Id == request.TeamId, ct);var region = TimeZoneInfo.FindSystemTimeZoneById(team.TimeZoneId);var localMidnight = request.MissionDate.ToDateTime(TimeOnly.MinValue);// This example assumes a one-hour daylight saving gap at midnightif (region.IsInvalidTime(localMidnight)){localMidnight = localMidnight.AddHours(1);}var startedAt = new DateTimeOffset(localMidnight,region.GetUtcOffset(localMidnight));db.Missions.Add(new Mission{TeamId = team.Id,StartedAt = startedAt,CreatedAt = timeProvider.GetUtcNow()});await db.SaveChangesAsync(ct);}}
✅ Figure: Good example - The user's date plus the team's region becomes one DateTimeOffset the rest of the system can trust
In a few regions, daylight saving starts at midnight, so that midnight never happens. For a one-hour gap, AddHours(1) moves to the first valid time at 1:00 AM. This example assumes that gap; applications that support other transitions need a policy for choosing a valid start of day or rejecting the date.
Note: DateOnly is for calendar dates (birthdays, due dates, the user's "today"). Points in time still use DateTimeOffset from an injected TimeProvider - see using DateTimeOffset and TimeProvider for the current time.
| Approach | Keeps user's "today" | Uses business timezone | Notes |
|---|---|---|---|
Client DateTimeOffset | ✅ | ❌ | Browser offset may not match the team |
UTC DateTime / UTC date | ❌ | ❌ | Loses the local calendar day near midnight |
Client DateOnly + backend region | ✅ | ✅ | Frontend sends the day, backend owns the zone |
DateTimeOffset from timeProvider.GetUtcNow()DateOnly as entered; you do not need GetLocalNow() for it