---
title: "Read an ICS calendar file in C#"
url: https://recursiveai.org/onemail/cookbook/read-ics-file/
description: "Read an .ics calendar file in C# and list every occurrence of recurring events, with RRULE, EXDATE, moved instances and time zones applied. Runnable code with OneMail.Calendar 0.3.0-preview.2."
version: "0.3.0-preview.2"
---

# Read an ICS calendar file in C#

Parse the file with `ContentDocument.Parse`, take the calendar, and call `CalendarRecurrence.Expand` for every occurrence in a date range. Repeat rules, exceptions, moved instances and time zones are applied for you. The program below reads a small calendar with a weekly meeting that crosses the US daylight saving change.

```shell
dotnet new console -o ReadIcs && cd ReadIcs
dotnet add package RecursiveAI.OneMail.Calendar --version 0.3.0-preview.2
```

Save this as `team.ics`: a standup on Mondays and Wednesdays in New York time, eight times, minus March 10, with March 15 moved to the afternoon, and an all-day launch.

```text
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example//Team calendar//EN
BEGIN:VEVENT
UID:standup@example.com
DTSTAMP:20270101T000000Z
SUMMARY:Team standup
LOCATION:Room 4
DTSTART;TZID=America/New_York:20270301T093000
DTEND;TZID=America/New_York:20270301T094500
RRULE:FREQ=WEEKLY;BYDAY=MO,WE;COUNT=8
EXDATE;TZID=America/New_York:20270310T093000
END:VEVENT
BEGIN:VEVENT
UID:standup@example.com
DTSTAMP:20270101T000000Z
RECURRENCE-ID;TZID=America/New_York:20270315T093000
SUMMARY:Team standup (moved)
DTSTART;TZID=America/New_York:20270315T140000
DTEND;TZID=America/New_York:20270315T141500
END:VEVENT
BEGIN:VEVENT
UID:launch@example.com
DTSTAMP:20270101T000000Z
SUMMARY:Launch day
DTSTART;VALUE=DATE:20270322
END:VEVENT
END:VCALENDAR
```

Put this in `Program.cs` and run `dotnet run`:

```csharp
using RecursiveAI.OneMail.Calendar;
using RecursiveAI.OneMail.Calendar.Ics;
using RecursiveAI.OneMail.Calendar.Recurrence;

ContentDocument doc = ContentDocument.Parse(File.ReadAllBytes("team.ics"));
IcsComponent calendar = IcsDocument.Calendars(doc).First();

// The events as written: one entry per VEVENT
foreach (IcsComponent ev in calendar.Events)
    Console.WriteLine($"{ev.Summary,-21} {ev.Start,-16} {ev.GetText("RRULE") ?? "-"}");

// Every occurrence: RRULE, EXDATE and moved instances applied, time zones resolved
Console.WriteLine();
var window = new OccurrenceWindow(new DateTime(2027, 3, 1), new DateTime(2027, 4, 1));
foreach (Occurrence o in CalendarRecurrence.Expand(calendar, window))
{
    string utc = o.StartUtc is { } u ? $"{u:yyyy-MM-dd HH:mm} UTC" : "all day";
    string format = o.Start.IsDate ? "ddd yyyy-MM-dd" : "ddd yyyy-MM-dd HH:mm";
    string local = o.Start.Value.ToString(format);
    Console.WriteLine($"{local,-20}  {utc,-20}  {o.Component.Summary}");
}
```

```text
Team standup          20270301T093000  FREQ=WEEKLY;BYDAY=MO,WE;COUNT=8
Team standup (moved)  20270315T140000  -
Launch day            20270322         -

Mon 2027-03-01 09:30  2027-03-01 14:30 UTC  Team standup
Wed 2027-03-03 09:30  2027-03-03 14:30 UTC  Team standup
Mon 2027-03-08 09:30  2027-03-08 14:30 UTC  Team standup
Mon 2027-03-15 14:00  2027-03-15 18:00 UTC  Team standup (moved)
Wed 2027-03-17 09:30  2027-03-17 13:30 UTC  Team standup
Mon 2027-03-22        all day               Launch day
Mon 2027-03-22 09:30  2027-03-22 13:30 UTC  Team standup
Wed 2027-03-24 09:30  2027-03-24 13:30 UTC  Team standup
```

The three `VEVENT`s become eight occurrences: the rule’s eight dates, minus the excluded March 10, with March 15 replaced by its moved copy. The standup stays at 9:30 in New York while its UTC time moves from 14:30 to 13:30 after daylight saving starts on March 14. All-day events have no UTC instant.

## What you get

- **The typed view.** `IcsComponent` has `Summary`, `Start`, `End`, `Location`, `Uid` and the rest. `GetText` reads any property.
- **Time zones.** A zone resolves from a `VTIMEZONE` in the file, the IANA database, or a Windows zone name.
- **Lossless.** An untouched file writes back byte for byte (`doc.Write(stream)`), and only the lines you edit are rewritten.
- **No exceptions on bad input.** Repairs and guesses are collected in an `ExpansionReport` you can pass to `Expand`.

## Limits

- **A preview.** 0.3.0-preview.2: the API can change before 1.0.
- **Mind the window.** A rule with no `COUNT` or `UNTIL` never ends. Expansion stops at the window’s end, and `ExpansionOptions` caps the count.
- **RSCALE is refused.** Non-Gregorian rules (RFC 7529) aren’t expanded.
- **Windows.** IANA names resolve through the Windows zone each maps to, so US dates before 2007 follow today’s rules there.
- **Outlook appointments** in MSG and PST files aren’t ICS: [convert them to ICS](https://recursiveai.org/onemail/cookbook/convert-msg-to-ics/) first.

Every rule and choice: [Recurrence and time zones](https://recursiveai.org/onemail/docs/recurrence/). The parser: [ICS and vCard](https://recursiveai.org/onemail/docs/calendar/).
