Ваш ответ был довольно близок. Было несколько проблем:
Вы предполагали, что сегодня в данном часовом поясе была та же дата, что и сегодня в UTC. В зависимости от часового пояса это могут быть разные дни. Например, 1:00 UTC 2019-10-18, это 8:00 вечера по центральному времени США 2019-10-17.
Если вы проектируете вокруг ", это уже произошлосегодня ", вы можете пропустить законные случаи. Вместо этого гораздо проще просто подумать о том, «что будет в будущем в будущем».
Вы не делали ничего, чтобы обрабатывать недопустимые или неоднозначные локальные времена, например, происходящие при запускеили конец летнего времени и с изменениями в стандартное время. Это важно для повторяющихся событий.
Итак, к коду:
// Get the current UTC time just once at the start
var utcNow = DateTimeOffset.UtcNow;
foreach (var schedule in schedules)
{
// schedule notification only if not already scheduled in the future
if (schedule.LastScheduledDateTime == null || schedule.LastScheduledDateTime.Value < utcNow)
{
// Get the time zone for this schedule
var tz = TimeZoneInfo.FindSystemTimeZoneById(schedule.User.TimeZone);
// Decide the next time to run within the given zone's local time
var nextDateTime = nowInZone.TimeOfDay <= schedule.PreferredTime
? nowInZone.Date.Add(schedule.PreferredTime)
: nowInZone.Date.AddDays(1).Add(schedule.PreferredTime);
// Get the point in time for the next scheduled future occurrence
var nextOccurrence = nextDateTime.ToDateTimeOffset(tz);
// Do the scheduling
BackgroundJob.Schedule<INotificationService>(x => x.Notify(schedule.CompanyUserID), nextOccurrence);
// Update the schedule
schedule.LastScheduledDateTime = nextOccurrence;
}
}
Я думаю, вы обнаружите, что ваш код и данные намного понятнее, если высделайте ваш LastScheduledDateTime
a DateTimeOffset?
вместо DateTime?
. Приведенный выше код предполагает это. Если вы не хотите, то вы можете изменить эту последнюю строку на:
schedule.LastScheduledDateTime = nextOccurrence.UtcDateTime;
Также обратите внимание на использование ToDateTimeOffset
, который является методом расширения. Поместите это в статический класс где-нибудь. Его целью является создание DateTimeOffset
из DateTime
с учетом определенного часового пояса. Он применяет типичные проблемы планирования при работе с неоднозначным и недействительным местным временем. (Я в последний раз писал об этом в этом другом ответе переполнения стека , если вы хотите прочитать больше.) Вот реализация:
public static DateTimeOffset ToDateTimeOffset(this DateTime dt, TimeZoneInfo tz)
{
if (dt.Kind != DateTimeKind.Unspecified)
{
// Handle UTC or Local kinds (regular and hidden 4th kind)
DateTimeOffset dto = new DateTimeOffset(dt.ToUniversalTime(), TimeSpan.Zero);
return TimeZoneInfo.ConvertTime(dto, tz);
}
if (tz.IsAmbiguousTime(dt))
{
// Prefer the daylight offset, because it comes first sequentially (1:30 ET becomes 1:30 EDT)
TimeSpan[] offsets = tz.GetAmbiguousTimeOffsets(dt);
TimeSpan offset = offsets[0] > offsets[1] ? offsets[0] : offsets[1];
return new DateTimeOffset(dt, offset);
}
if (tz.IsInvalidTime(dt))
{
// Advance by the gap, and return with the daylight offset (2:30 ET becomes 3:30 EDT)
TimeSpan[] offsets = { tz.GetUtcOffset(dt.AddDays(-1)), tz.GetUtcOffset(dt.AddDays(1)) };
TimeSpan gap = offsets[1] - offsets[0];
return new DateTimeOffset(dt.Add(gap), offsets[1]);
}
// Simple case
return new DateTimeOffset(dt, tz.GetUtcOffset(dt));
}
(В вашем случае вид всегда не указан, так что вы можете удалить эту первую проверку, если хотите, но я предпочитаю сохранять ее полностью функциональной в случае другого использования.)
Кстати, вам не нужна проверка if (!schedules.HasAny()) { return; }
. Entity Framework уже проверяет изменения во время SaveChangesAsync
и ничего не делает, если их нет.