Skip to content

Working with recurring elements

axunonb edited this page Sep 23, 2025 · 11 revisions

Preface: It's impossible to provide an example for every type of recurrence scenario, so here are a few that represent some common use cases.

How to think about occurrences

If you create an event or alarm that happens more than once, it typically has a start time, an end time, and some rules about how and when it repeats:

  • "Daily, forever"
  • "Daily, forever — except for a certain date"
  • "Every other Tuesday until the end of the year"
  • "The fourth Thursday of every November"

You then want to search for occurrences of that event during a given time period. This triggers the machinery that generates the set of occurrences matching your search criteria.

How to create a recurring event

It's like creating a normal event, but you add one or more RecurrencePattern objects to the event to make it recurring.

Note

Make sure the event's start date is the first occurrence in the series.

var recurrence = new RecurrencePattern
{
    Frequency = FrequencyType.Daily,
    Interval = 2,
    Count = 5
    // Add other properties like ByDay, ByMonth, etc.
};

var calendarEvent = new CalendarEvent
{
    DtStart = new CalDateTime(2025, 07, 10),
    // Add the rule to the event.
    RecurrenceRules = [recurrence]
};

// Get all occurrences of the series.
IEnumerable<Occurrence> allOccurrences = calendarEvent.GetOccurrences();
Assert.That(allOccurrences.Count(), Is.EqualTo(5));
5 occurrences:
Start: 07/10/2025
  Period: P1D
  End: 07/11/2025
Start: 07/12/2025
  Period: P1D
  End: 07/13/2025
Start: 07/14/2025
  Period: P1D
  End: 07/15/2025
Start: 07/16/2025
  Period: P1D
  End: 07/17/2025
Start: 07/18/2025
  Period: P1D
  End: 07/19/2025

How to limit returned occurrences of a recurring event

After creating a recurring event, you can call GetOccurrences() to get all occurrences in the series, or GetOccurrences(CalDateTime startTime) to get all occurrences after a given date. You can further limit the evaluation by adding TakeWhileBefore(CalDateTime periodEnd) to get occurrences up to a specific point in time.

IEnumerable<Occurrence> occurrences = 
   calendarEvent.GetOccurrences().TakeWhileBefore(CalDateTime periodEnd);

Note

The end date provided to the TakeWhileBefore method is exclusive.

Warning

Calculating a series with no end may result in an EvaluationOutOfRangeException. Make sure you use TakeWhileBefore to avoid this.

Tip

You can call the GetOccurrences method on both CalendarEvent and Calendar.

Recurrence Examples

Daily, Interval, Count

Suppose you want to create an event for July 10, between 09:00 and 10:00, that recurs every other day for 2 occurrences.

// Create the CalendarEvent
var start = new CalDateTime(2025, 07, 10, 09, 00, 00, "Europe/Zurich");
var recurrence = new RecurrencePattern
{
    Frequency = FrequencyType.Daily,
    Interval = 2,
    Count = 2
};

var calendarEvent = new CalendarEvent
{
    DtStart = start,
    DtEnd = start.AddHours(1),
    RecurrenceRules = [recurrence]
};

// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);

// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);

// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();
Assert.That(occurrences.Count(), Is.EqualTo(2));

ICS string:

BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250710T100000
DTSTART;TZID=Europe/Zurich:20250710T090000
RRULE:FREQ=DAILY;INTERVAL=2;COUNT=2
END:VEVENT
END:VCALENDAR

Occurrences:

2 occurrences:
Start: 07/10/2025 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/10/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/12/2025 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/12/2025 10:00:00 +02:00 Europe/Zurich

Yearly, ByMonthDay, Until

Suppose you want to create a series of events for July 10 and 12, between 09:00 and 10:00, that recurs every year for two years.

Warning

The UNTIL date must have either UTC or no time zone.

Note

The UNTIL date is inclusive.

// Create the CalendarEvent
var start = new CalDateTime(2025, 07, 10, 09, 00, 00, "Europe/Zurich");
var recurrence = new RecurrencePattern
{
    Frequency = FrequencyType.Yearly,
    ByMonthDay = [10, 12],
    // 2027-07-10 09:00:00 Europe/Zurich (07:00:00 UTC)
    Until = start.AddYears(2).ToTimeZone("UTC")
};

var calendarEvent = new CalendarEvent
{
    DtStart = start,
    DtEnd = start.AddHours(1),
    RecurrenceRules = [recurrence]
};

// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);

// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);

// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();

ICS string:

BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250710T100000
DTSTART;TZID=Europe/Zurich:20250710T090000
RRULE:FREQ=YEARLY;UNTIL=20270710T070000Z;BYMONTHDAY=10,12
END:VEVENT
END:VCALENDAR

Occurrences:

5 occurrences:
Start: 07/10/2025 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/10/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/12/2025 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/12/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/10/2026 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/10/2026 10:00:00 +02:00 Europe/Zurich
Start: 07/12/2026 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/12/2026 10:00:00 +02:00 Europe/Zurich
Start: 07/10/2027 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/10/2027 10:00:00 +02:00 Europe/Zurich

Tip

Notice the last occurrence has the same start as the UNTIL date of the recurrence. This is the meaning of 'inclusive' (valid as long as DTSTART <= UNTIL).

Monthly, ByDay, Count, RDate (add occurrence)

Suppose you decide to play poker with your friends every last Sunday of the month for the next three months, and also on July 10th just to stay sharp.

// Create the CalendarEvent
var start = new CalDateTime(2025, 06, 29, 16, 00, 00, "Europe/Zurich");
var recurrence = new RecurrencePattern
{
    Frequency = FrequencyType.Monthly,
    ByDay = [new(DayOfWeek.Sunday, FrequencyOccurrence.Last)],
    Count = 3
};

var calendarEvent = new CalendarEvent
{
    DtStart = start,
    DtEnd = start.AddHours(4),
    RecurrenceRules = [recurrence],
};
// Add additional an occurrence to the series.
calendarEvent.RecurrenceDates
    .Add(new CalDateTime(2025, 07, 10, 09, 00, 00, "Europe/Zurich"));

// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);

// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);

// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();

ICS string:

BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250629T200000
DTSTART;TZID=Europe/Zurich:20250629T160000
RDATE;TZID=Europe/Zurich:20250710T090000
RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=-1SU
END:VEVENT
END:VCALENDAR

Note

See the RDATE property, which defines our additional occurrence.

Occurrences:

 4 occurrences:
 Start: 06/29/2025 16:00:00 +02:00 Europe/Zurich
   Period: PT4H
   End: 06/29/2025 20:00:00 +02:00 Europe/Zurich
 Start: 07/10/2025 09:00:00 +02:00 Europe/Zurich
   Period: PT4H
   End: 07/10/2025 13:00:00 +02:00 Europe/Zurich
 Start: 07/27/2025 16:00:00 +02:00 Europe/Zurich
   Period: PT4H
   End: 07/27/2025 20:00:00 +02:00 Europe/Zurich
 Start: 08/31/2025 16:00:00 +02:00 Europe/Zurich
   Period: PT4H
   End: 08/31/2025 20:00:00 +02:00 Europe/Zurich

Note

We only specified the start of our additional occurrence; the duration is inherited from the base series.

Hourly, Until, ExDate (remove occurrence)

Suppose you decide to read for 15 minutes every full hour until midnight — except at 22:00, when you have dinner.

// Create the CalendarEvent
var start = new CalDateTime(2025, 07, 10, 20, 00, 00, "UTC");
var recurrence = new RecurrencePattern
{
    Frequency = FrequencyType.Hourly,
    Until = start.AddHours(4)
};

var calendarEvent = new CalendarEvent
{
    DtStart = start,
    DtEnd = start.AddMinutes(15),
    RecurrenceRules = [recurrence],
};
// Add the exception date to the series.
calendarEvent.ExceptionDates
    .Add(new CalDateTime(2025, 07, 10, 22, 00, 00, "UTC"));

// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);

// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);

// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();

ICS string:

BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND:20250710T201500Z
DTSTART:20250710T200000Z
EXDATE:20250710T220000Z
RRULE:FREQ=HOURLY;UNTIL=20250711T000000Z
END:VEVENT
END:VCALENDAR

Note

See the EXDATE property, which defines our removed occurrence.

Occurrences:

4 occurrences:
Start: 07/10/2025 20:00:00 +00:00 UTC
  Period: PT15M
  End: 07/10/2025 20:15:00 +00:00 UTC
Start: 07/10/2025 21:00:00 +00:00 UTC
  Period: PT15M
  End: 07/10/2025 21:15:00 +00:00 UTC
Start: 07/10/2025 23:00:00 +00:00 UTC
  Period: PT15M
  End: 07/10/2025 23:15:00 +00:00 UTC
Start: 07/11/2025 00:00:00 +00:00 UTC
  Period: PT15M
  End: 07/11/2025 00:15:00 +00:00 UTC

Daily, Interval, Count, Exception (moved occurrence)

Suppose you decide to go for a walk every other day at 09:00 (4 times) — but not on the third occurrence, when you have a packed schedule and can only go for 13 minutes at 13:00.

Since the start, end, duration, and title of the event are changing, you need to create a new event and tell the series to replace an occurrence with this 'special' event (child event). Link the child event to the series by using the same UID, and link it to the original occurrence by adding the original occurrence's start date in the RECURRENCE-ID property.

Note

Link moved events:

  1. Create a CalendarEvent with the changes (child event).
  2. Link the child event to the series by using the same Uid (UID).
  3. Link the child event to the original occurrence date by using the RecurrenceId (RECURRENCE-ID).
// Create the CalendarEvent
var start = new CalDateTime(2025, 07, 10, 09, 00, 00, "Europe/Zurich");
var recurrence = new RecurrencePattern
{
    Frequency = FrequencyType.Daily,
    Interval = 2,
    Count = 4
};

var calendarEvent = new CalendarEvent
{
    // UID links master with child.
    Uid = "my-custom-id",
    Summary = "Walking",
    DtStart = start,
    DtEnd = start.AddHours(1),
    RecurrenceRules = [recurrence],
    Sequence = 0 // default value
};

var startMoved = new CalDateTime(2025, 07, 13, 13, 00, 00, "Europe/Zurich");
var movedEvent = new CalendarEvent
{
    // UID links master with child.
    Uid = "my-custom-id",
    // Overwrite properties of the original occurrence.
    Summary = "Short after lunch walk",
    // Set new start and end time.
    DtStart = startMoved,
    DtEnd = startMoved.AddMinutes(13),
    // Set the original date of the occurrence (2025-07-14 09:00:00).
    RecurrenceId = start.AddDays(4),
    // The first change for this RecurrenceId
    Sequence = 1
};

// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);
calendar.Events.Add(movedEvent);

// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);

// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();

ICS-string:

BEGIN:VCALENDAR
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250710T100000
DTSTART;TZID=Europe/Zurich:20250710T090000
RRULE:FREQ=DAILY;INTERVAL=2;COUNT=4
SUMMARY:Walking
UID:my-custom-id
END:VEVENT
BEGIN:VEVENT
DTEND;TZID=Europe/Zurich:20250713T131300
DTSTART;TZID=Europe/Zurich:20250713T130000
RECURRENCE-ID;TZID=Europe/Zurich:20250714T090000
SUMMARY:Short after lunch walk
UID:my-custom-id
END:VEVENT
END:VCALENDAR

Note

See the SUMMARY property, which we have overridden.

Occurrences:

4 occurrences:
Start: 07/10/2025 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/10/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/12/2025 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/12/2025 10:00:00 +02:00 Europe/Zurich
Start: 07/13/2025 13:00:00 +02:00 Europe/Zurich
  Period: PT13M
  End: 07/13/2025 13:13:00 +02:00 Europe/Zurich
Start: 07/16/2025 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 07/16/2025 10:00:00 +02:00 Europe/Zurich

Recurrence with Time Zone Changes

Suppose you want to create a weekly meeting every Monday at 09:00 in the "Europe/Zurich" time zone, starting before a daylight saving time change and continuing after. The event's local time remains 09:00, but the UTC offset changes due to daylight saving.

// Create the CalendarEvent
var start = new CalDateTime(2025, 03, 24, 09, 00, 00, "Europe/Zurich"); // Before DST starts
var recurrence = new RecurrencePattern
{
    Frequency = FrequencyType.Weekly,
    Count = 3 // Three Mondays: before, on, and after DST change
};

var calendarEvent = new CalendarEvent
{
    DtStart = start,
    DtEnd = start.AddHours(1),
    RecurrenceRules = [recurrence]
};

// Add CalendarEvent to Calendar
var calendar = new Calendar();
calendar.Events.Add(calendarEvent);

// Serialize Calendar to string
var calendarSerializer = new CalendarSerializer();
var generatedIcs = calendarSerializer.SerializeToString(calendar);

// Calculate all occurrences
IEnumerable<Occurrence> occurrences = calendar.GetOccurrences();

ICS string:

 BEGIN:VCALENDAR
 BEGIN:VEVENT
 DTEND;TZID=Europe/Zurich:20250324T100000
 DTSTART;TZID=Europe/Zurich:20250324T090000
 RRULE:FREQ=WEEKLY;COUNT=3
 END:VEVENT
 END:VCALENDAR

Occurrences:

3 occurrences:
Start: 03/24/2025 09:00:00 +01:00 Europe/Zurich
  Period: PT1H
  End: 03/24/2025 10:00:00 +01:00 Europe/Zurich
Start: 03/31/2025 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 03/31/2025 10:00:00 +02:00 Europe/Zurich
Start: 04/07/2025 09:00:00 +02:00 Europe/Zurich
  Period: PT1H
  End: 04/07/2025 10:00:00 +02:00 Europe/Zurich

Note

Notice how the UTC offset changes from +01:00 to +02:00 after the daylight saving time change, while the local time remains consistent at 09:00.

Advanced Recurrence Rule Examples

Every other Tuesday until the end of the year

var rrule1 = new RecurrencePattern(FrequencyType.Weekly, 2)
{
    Until = new CalDateTime(2026, 1, 1)
};

The 2nd day of every month for 5 occurrences

var rrule2 = new RecurrencePattern(FrequencyType.Monthly)
{
    ByMonthDay = [2],  // Your day of the month goes here
    Count = 5
};

The 4th Thursday of every November

// The 4th Thursday of November every year
var rrule3 = new RecurrencePattern(FrequencyType.Yearly, 1)
{
    Frequency = FrequencyType.Yearly,
    Interval = 1,
    ByMonth = [11],
    ByDay = [new WeekDay { DayOfWeek = DayOfWeek.Thursday, Offset = 4 }],
};

Every day in 2025, except Sundays

// Every day in 2025, except Sundays
var rrule4 = new RecurrencePattern(FrequencyType.Daily)
{
    // Start: 2025-01-01, End: 2025-12-31
    Until = new CalDateTime(2025, 12, 31),
    // Exclude Sundays
    ByDay = [
        new WeekDay(DayOfWeek.Monday),
        new WeekDay(DayOfWeek.Tuesday),
        new WeekDay(DayOfWeek.Wednesday),
        new WeekDay(DayOfWeek.Thursday),
        new WeekDay(DayOfWeek.Friday),
        new WeekDay(DayOfWeek.Saturday)
    ]
};

FAQ (Recurrence)

Can I add multiple recurrence rules?

Yes, you can. The RecurrenceRules property is a list of RecurrencePattern objects, so you can add as many as you need.

Clone this wiki locally