Meziantou.Framework.Scheduling
4.1.2
Prefix Reserved
See the version list below for details.
dotnet add package Meziantou.Framework.Scheduling --version 4.1.2
NuGet\Install-Package Meziantou.Framework.Scheduling -Version 4.1.2
<PackageReference Include="Meziantou.Framework.Scheduling" Version="4.1.2" />
<PackageVersion Include="Meziantou.Framework.Scheduling" Version="4.1.2" />
<PackageReference Include="Meziantou.Framework.Scheduling" />
paket add Meziantou.Framework.Scheduling --version 4.1.2
#r "nuget: Meziantou.Framework.Scheduling, 4.1.2"
#:package Meziantou.Framework.Scheduling@4.1.2
#addin nuget:?package=Meziantou.Framework.Scheduling&version=4.1.2
#tool nuget:?package=Meziantou.Framework.Scheduling&version=4.1.2
Meziantou.Framework.Scheduling
This package supports 2 schedule formats:
- Recurrence rules (RRULE) as defined in RFC5545 and RFC2445
- Cron expressions
Recurrence rules (RRULE)
Parse recurrence rules:
var rrule = "FREQ=DAILY;UNTIL=20000131T140000Z;BYMONTH=1";
if (RecurrenceRule.TryParse(rrule, out var rule, out var error))
{
var nextOccurrences = rule.GetNextOccurrences(DateTime.Now).Take(50).ToArray();
}
Convert a recurrence rule to human-readable text:
var culture = CultureInfo.GetCultureInfo("en-US");
RecurrenceRule.Parse("FREQ=DAILY").GetHumanText(culture); // every day
RecurrenceRule.Parse("FREQ=WEEKLY;INTERVAL=3;BYDAY=TU;UNTIL=20150101").GetHumanText(culture); // every 3 weeks on Tuesday until January 1, 2015
Supported languages for human-readable text:
- English (
en,en-*, and invariant culture) - French (
fr,fr-*)
Time zones
GetNextOccurrences also accepts a time zone. The occurrences keep their wall-clock time across a daylight
saving transition, so each one carries the UTC offset in effect at that moment:
var rrule = RecurrenceRule.Parse("FREQ=DAILY;BYHOUR=9");
var timeZone = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
foreach (var occurrence in rrule.GetNextOccurrences(new DateTime(2024, 03, 09), timeZone).Take(3))
{
Console.WriteLine(occurrence);
}
// 2024-03-09 09:00:00 -05:00
// 2024-03-10 09:00:00 -04:00 <- the offset changes, the wall clock does not
// 2024-03-11 09:00:00 -04:00
The start date is read as a wall-clock time in that time zone, and a DateTimeOffset overload accepts an
instant instead. A time zone identifier can be passed directly, which TimeZones.Find resolves:
var occurrences = rrule.GetNextOccurrences(startDate, "America/New_York");
A local time that a transition makes invalid or ambiguous is resolved as RFC 5545 section 3.3.5 requires:
an ambiguous time keeps its first occurrence, and an invalid time is read with the UTC offset in effect
before the gap, so 02:30 on a spring-forward day surfaces as 03:30 at the new offset. This is what
errata 4271 settles for recurrence instances; only an invalid
date, such as February 30, is dropped from the recurrence set.
Reading a gap that way maps it onto the instants of the hour that follows it, so a sub-hourly recurrence
would otherwise repeat them. Those duplicates are ignored, per RFC 5545 section 3.8.5.3, and do not count
towards COUNT. Across a backward transition the repeated hour is visited once, so its second pass is not
produced.
UNTIL is honoured as an instant when it is a UTC value, and as a wall-clock reading when it is floating.
CronExpression supports the same overloads.
UNTIL
A UTC UNTIL (20240110T100000Z), or one carrying an offset, is parsed as a Utc EndDate and denotes an instant.
A floating UNTIL (20240110T100000) or a date (20240110) is parsed as an Unspecified EndDate and denotes a
wall-clock reading, a date being its first instant; a date is written back as a date.
Without a time zone, an instant bounds the occurrences of a Utc or Local start date by instant, and the occurrences
of an Unspecified start date by wall clock. A wall-clock UNTIL always bounds them by wall clock.
iCalendar
InternetCalendar reads and writes events in the iCalendar format.
Reading
InternetCalendar.Parse reads an iCalendar object from a string, a ReadOnlySpan<char>, a TextReader or
a UTF-8 Stream, and TryParse reports why the content was rejected instead of throwing:
var calendar = InternetCalendar.Parse(File.ReadAllText("invite.ics"));
foreach (var @event in calendar.Events)
{
Console.WriteLine($"{@event.Start:g} {@event.Summary}");
}
if (!InternetCalendar.TryParse(content, out var parsed, out var error))
{
Console.WriteLine(error);
}
The parser unfolds content lines, decodes TEXT values and reads the three date-time forms of RFC 5545
section 3.3.5: 20240102T080000Z becomes a Utc value, 20240102T080000 a floating (Unspecified) one,
and DTSTART;TZID=America/New_York:20240102T080000 a wall-clock value together with Event.TimeZone.
A leap second (235960) is read as the 59th second.
The identifier of a TZID parameter is resolved, in order:
- as a time zone of the platform (
TimeZoneInfo.FindSystemTimeZoneById); - from the
VTIMEZONEcomponent of the calendar with thatTZID, which builds a customTimeZoneInfowhoseIdis the identifier, so the event is written back with it. Every onset of the sub-components (DTSTART,RDATE, bounded or open-endedRRULE) is expanded, and each year becomes one adjustment rule holding its standard offset and at most one daylight saving period, including a period spanning the new year and a change of the standard offset (the latter needs .NET 6 or later). An open-endedSTANDARD/DAYLIGHTpair expressible as a floating (BYDAY=-1SU) or fixed (BYMONTHDAY=22) transition becomes a single open-ended rule; any other open-ended recurrence is expanded for 300 years. A year whose offset changes more often than that, such as a daylight saving period suspended during Ramadan, cannot be expressed by aTimeZoneInfo, so its shortest periods take the offset of the one before; - as the IANA identifier ending a prefixed one, such as
/mozilla.org/20050126_1/America/New_York.
An identifier that still cannot be resolved does not reject the calendar: DTSTART and DTEND are read as
floating values and Event.TimeZone stays null. Only DTSTART and DTEND determine Event.TimeZone;
CREATED, LAST-MODIFIED and DTSTAMP are always read as Utc values, converted from their TZID when they
carry one, and taken as UTC when they are floating.
A DATE value, DTSTART;VALUE=DATE:20240101 (or a date without the parameter), sets Event.IsAllDay and is
read as the first instant of the day, without a time zone. An event without STATUS has a null Event.Status.
An event property the model does not have, such as X-MICROSOFT-CDO-BUSYSTATUS:OOF, goes to
Event.AdditionalProperties when its TEXT value writes it back unchanged. Any other one — a property with
parameters (EXDATE;TZID=Europe/Paris:20240104T100000), a repeated one, or a structured value
(GEO:37.38;-122.08, CATEGORIES:WORK,MEETING) — goes to Event.RawProperties, which keeps its name,
parameters and value verbatim. Calendar properties go to InternetCalendar.AdditionalProperties and
InternetCalendar.RawProperties the same way. The components the model does not represent — VTODO,
VJOURNAL, VFREEBUSY and VALARM — are skipped.
Properties written
Content lines longer than 75 UTF-8 octets are folded, never inside a character.
CREATED,LAST-MODIFIEDandDTSTAMPare written in UTC (anUnspecifiedvalue is taken as UTC), and are omitted when not set, as areDTENDandSTATUS. RFC 5545 requiresDTSTAMP, so setEvent.DateTimeStampto produce a conforming event; the library does not use the current time, which keeps the output deterministic.Event.IsAllDaywrites the date part ofStartandEndasDTSTART;VALUE=DATE:/DTEND;VALUE=DATE:, ignoringEvent.TimeZone.An attendee or an organizer without an address is not written.
A time zone identifier containing
:,;or,, such as(UTC+01:00) Amsterdam, Berlin, is quoted in theTZIDparameter and escaped in theVTIMEZONETZIDproperty. An identifier containing a"or a control character cannot be written, andToIcsthrows anInvalidOperationExceptionbefore writing anything.AdditionalPropertiesvalues are escaped asTEXT.RawPropertiesare written verbatim; anInternetCalendarPropertyvalidates its name, parameters and value when it is created, so it cannot inject a line:@event.RawProperties.Add(new InternetCalendarProperty("EXDATE", [new("TZID", "Europe/Paris")], "20240104T100000")); @event.RawProperties.Add(new InternetCalendarProperty("GEO", "37.386013;-122.082932"));A property of either collection named after one the writer emits itself —
BEGIN,END,VERSIONandPRODIDfor the calendar;UID,STATUS,ORGANIZER,ATTENDEE,CREATED,LAST-MODIFIED,DTSTAMP,DTSTART,DTEND,RRULE,SUMMARYandDESCRIPTIONas well for an event — is not written.
Writing
InternetCalendar writes events in the iCalendar format. Setting Event.TimeZone writes the start and end
as DTSTART;TZID=/DTEND;TZID= and emits a matching VTIMEZONE component:
var calendar = new InternetCalendar();
calendar.Events.Add(new Event
{
Start = new DateTime(2024, 01, 02, 08, 00, 00),
End = new DateTime(2024, 01, 02, 09, 00, 00),
TimeZone = TimeZoneInfo.FindSystemTimeZoneById("America/New_York"),
});
var ics = calendar.ToIcs();
BEGIN:VTIMEZONE
TZID:America/New_York
BEGIN:DAYLIGHT
DTSTART:20070311T020000
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20071104T020000
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
END:VTIMEZONE
...
DTSTART;TZID=America/New_York:20240102T080000
A Start or End whose Kind is Unspecified is taken as a wall-clock reading in that time zone; a Utc
or Local value denotes an instant and is converted to it. The component describes the adjustment rule in
effect for the events, expanded to an open-ended yearly recurrence.
Cron expressions
The library also provides CronExpression to parse and evaluate cron schedules.
var cron = CronExpression.Parse("0 */15 * * * *");
var occurrences = cron.GetNextOccurrences(DateTime.Now).Take(10).ToArray();
Supported formats
- 5 fields:
minute hour day-of-month month day-of-week - 6 fields:
second minute hour day-of-month month day-of-week - 7 fields:
second minute hour day-of-month month day-of-week year
Fields are separated by spaces or tabs. When using the 5-field format, seconds are implicitly set to 0.
Field ranges
- second:
0-59 - minute:
0-59 - hour:
0-23 - day-of-month:
1-31 - month:
1-12orJAN-DEC - day-of-week:
0-7orSUN-SAT(0and7= Sunday, so1-7means every day) - year (optional):
1970-2099
Operators and special values
For all fields:
*or?: any valuea,b,c: list. Each item can be a value, a range, a step,*or*/n(for example*/15,7). A*item means any value.?is only valid as the whole field.a-b: range*/n: step from field minimuma-b/n: stepped rangea/n: step starting ata
A range whose start is greater than its end wraps around the end of the field, except in the year field where it is invalid:
22-2in the hour field means22,23,0,1,222-2/2in the hour field means22,0,2FRI-MONin the day-of-week field means5,6,0,1NOV-FEBin the month field means11,12,1,2
Day-of-month field additionally supports:
L: last day of monthL-n: nth day before end of month (for exampleL-2,nin0-30)LW: last weekday of monthnW: nearest weekday to dayn(nin1-31), without leaving the month
Day-of-week field additionally supports:
nL: last occurrence of weekdaynin monthn#m: m-th occurrence of weekdaynin month (min1-5)
Predefined schedules
@yearly/@annually@monthly@weekly@daily/@midnight@hourly
Notes
- Parsing is case-insensitive for month/day names, special values (
L,W), and predefined schedules. - Occurrences are whole seconds. A start date with a fractional second starts at the next whole second.
day-of-monthandday-of-weekare combined with AND semantics. A date must satisfy both fields to match.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Memory (>= 4.6.3)
-
net10.0
- No dependencies.
-
net11.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Meziantou.Framework.Scheduling:
| Package | Downloads |
|---|---|
|
Immediate.Jobs
A reflection-free background job scheduler for .NET using source generation. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.1.3 | 608 | 9/20/2026 |
| 4.1.2 | 228 | 9/15/2026 |
| 4.1.1 | 392 | 9/6/2026 |
| 4.1.0 | 120 | 9/4/2026 |
| 4.0.5 | 118 | 9/3/2026 |
| 4.0.4 | 96 | 9/3/2026 |
| 4.0.3 | 148 | 8/30/2026 |
| 4.0.2 | 257 | 8/16/2026 |
| 4.0.1 | 599 | 7/8/2026 |
| 4.0.0 | 166 | 7/5/2026 |
| 3.0.2 | 303 | 6/13/2026 |
| 3.0.1 | 298 | 5/23/2026 |
| 3.0.0 | 159 | 5/17/2026 |
| 2.0.12 | 194 | 5/11/2026 |
| 2.0.11 | 365 | 3/22/2026 |
| 2.0.10 | 440 | 1/16/2026 |
| 2.0.9 | 386 | 11/2/2025 |
| 2.0.8 | 195 | 10/19/2025 |
| 2.0.7 | 288 | 9/3/2025 |
| 2.0.6 | 318 | 3/1/2025 |