A modern .NET implementation of RFC 8984 (JSCalendar) - a JSON representation of calendar data.
- Full RFC 8984 Compliance - Complete implementation of the JSCalendar specification
- Native .NET 10 - Built with the latest .NET features
- Type-Safe - Strongly-typed models with comprehensive validation
- JSON Serialization - Seamless integration with System.Text.Json
- Recurrence Support - Full support for recurring events with overrides
- Localization - Multi-language support via localizations
- Time Zones - Complete time zone handling
dotnet add package JSCalendar.NetOr via NuGet Package Manager:
Install-Package JSCalendar.Net
using JSCalendar.Net;
using JSCalendar.Net.Enums;
using System.Text.Json;
var meeting = new Event
{
Uid = "meeting-001",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0)),
Duration = new Duration { Hours = 1, Minutes = 30 },
Title = "Team Meeting",
Description = "Weekly team sync",
Status = EventStatus.Confirmed
};
// Serialize to JSON
var json = JsonSerializer.Serialize(meeting, new JsonSerializerOptions
{
WriteIndented = true
});
Console.WriteLine(json);Output:
{
"@type": "Event",
"uid": "meeting-001",
"updated": "2024-12-02T10:00:00Z",
"start": "2024-12-15T14:00:00",
"duration": "PT1H30M",
"title": "Team Meeting",
"description": "Weekly team sync",
"status": "confirmed"
}var json = """
{
"@type": "Event",
"uid": "event-123",
"updated": "2024-12-02T10:00:00Z",
"start": "2024-12-15T14:00:00",
"title": "Conference Call"
}
""";
var evt = JsonSerializer.Deserialize<Event>(json);
Console.WriteLine($"Event: {evt.Title} at {evt.Start}");Events represent scheduled calendar items with a specific start time.
var conference = new Event
{
Uid = "conf-2024",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(new DateTime(2024, 12, 15, 9, 0, 0)),
Duration = new Duration { Hours = 8 },
Title = "Tech Conference 2024",
Locations = new Dictionary<string, Location>
{
["venue"] = new Location
{
Name = "Convention Center",
Coordinates = "geo:52.520008,13.404954"
}
},
VirtualLocations = new Dictionary<string, VirtualLocation>
{
["stream"] = new VirtualLocation
{
Name = "Live Stream",
Uri = "https://stream.example.com/conf2024",
Features = new Dictionary<VirtualLocationFeature, bool>
{
[VirtualLocationFeature.Video] = true,
[VirtualLocationFeature.Chat] = true
}
}
}
};Tasks represent action items with optional due dates.
var task = new Task
{
Uid = "task-001",
Updated = DateTimeOffset.UtcNow,
Title = "Prepare presentation",
Due = new LocalDateTime(new DateTime(2024, 12, 14, 17, 0, 0)),
Progress = ProgressStatus.InProcess,
PercentComplete = 60,
Priority = 1
};Groups allow you to organize related events and tasks together.
var projectGroup = new Group
{
Uid = "project-2024",
Updated = DateTimeOffset.UtcNow,
Title = "Product Launch 2024",
Description = "All events and tasks related to the product launch",
Categories = new Dictionary<string, bool>
{
["project"] = true,
["marketing"] = true
},
Entries = new List<IJSCalendarObject>
{
new Event
{
Uid = "kickoff-meeting",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(new DateTime(2024, 12, 1, 9, 0, 0)),
Duration = new Duration { Hours = 2 },
Title = "Project Kickoff Meeting",
Description = "Initial project planning session"
},
new Task
{
Uid = "prepare-marketing",
Updated = DateTimeOffset.UtcNow,
Title = "Prepare marketing materials",
Due = new LocalDateTime(new DateTime(2024, 12, 10, 17, 0, 0)),
Priority = 1,
Progress = ProgressStatus.Needs_Action
},
new Event
{
Uid = "launch-event",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(new DateTime(2024, 12, 15, 10, 0, 0)),
Duration = new Duration { Hours = 4 },
Title = "Product Launch Event",
Status = EventStatus.Confirmed,
Locations = new Dictionary<string, Location>
{
["venue"] = new Location
{
Name = "Main Auditorium"
}
}
}
}
};
// Serialize to JSON
var json = JsonSerializer.Serialize(projectGroup, new JsonSerializerOptions
{
WriteIndented = true
});Output:
{
"@type": "Group",
"uid": "project-2024",
"updated": "2024-12-03T10:00:00Z",
"title": "Product Launch 2024",
"description": "All events and tasks related to the product launch",
"categories": {
"project": true,
"marketing": true
},
"entries": [
{
"@type": "Event",
"uid": "kickoff-meeting",
"updated": "2024-12-03T10:00:00Z",
"start": "2024-12-01T09:00:00",
"duration": "PT2H",
"title": "Project Kickoff Meeting",
"description": "Initial project planning session"
},
{
"@type": "Task",
"uid": "prepare-marketing",
"updated": "2024-12-03T10:00:00Z",
"title": "Prepare marketing materials",
"due": "2024-12-10T17:00:00",
"priority": 1,
"progress": "needs-action"
},
{
"@type": "Event",
"uid": "launch-event",
"updated": "2024-12-03T10:00:00Z",
"start": "2024-12-15T10:00:00",
"duration": "PT4H",
"title": "Product Launch Event",
"status": "confirmed",
"locations": {
"venue": {
"name": "Main Auditorium"
}
}
}
]
}Create events that repeat with flexible recurrence rules.
var weeklyMeeting = new Event
{
Uid = "weekly-standup",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(new DateTime(2024, 12, 2, 10, 0, 0)),
Duration = new Duration { Minutes = 30 },
Title = "Daily Standup",
RecurrenceRules = new List<RecurrenceRule>
{
new RecurrenceRule
{
Frequency = RecurrenceFrequency.Weekly,
ByDay = new List<NDay>
{
new NDay { Day = DayOfWeek.Monday },
new NDay { Day = DayOfWeek.Tuesday },
new NDay { Day = DayOfWeek.Wednesday },
new NDay { Day = DayOfWeek.Thursday },
new NDay { Day = DayOfWeek.Friday }
},
Until = new LocalDateTime(new DateTime(2024, 12, 31, 23, 59, 59))
}
}
};Override specific instances of recurring events.
var recurringEvent = new Event
{
Uid = "meeting-recurring",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(new DateTime(2024, 12, 1, 10, 0, 0)),
Title = "Weekly Meeting",
RecurrenceRules = new List<RecurrenceRule>
{
new RecurrenceRule { Frequency = RecurrenceFrequency.Weekly }
},
RecurrenceOverrides = new Dictionary<LocalDateTime, PatchObject>
{
// Special meeting on Dec 15
[new LocalDateTime(new DateTime(2024, 12, 15, 10, 0, 0))] = new PatchObject
{
["title"] = "Special Year-End Meeting",
["duration"] = "PT2H", // 2 hours instead of default
["locations/main/name"] = "Large Conference Room"
},
// Cancel meeting on Dec 25 (Christmas)
[new LocalDateTime(new DateTime(2024, 12, 25, 10, 0, 0))] = new PatchObject
{
["excluded"] = true
}
}
};Add participants with roles and participation status.
var meeting = new Event
{
Uid = "team-meeting",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(DateTime.Now),
Participants = new Dictionary<string, Participant>
{
["organizer"] = new Participant
{
Name = "Alice Smith",
Email = "alice@example.com",
Roles = new Dictionary<ParticipantRole, bool>
{
[ParticipantRole.Owner] = true,
[ParticipantRole.Chair] = true
},
ParticipationStatus = ParticipationStatus.Accepted
},
["attendee1"] = new Participant
{
Name = "Bob Johnson",
Email = "bob@example.com",
Roles = new Dictionary<ParticipantRole, bool>
{
[ParticipantRole.Attendee] = true
},
ParticipationStatus = ParticipationStatus.Tentative,
ExpectReply = true
}
}
};Set up reminders and notifications.
var eventWithAlerts = new Event
{
Uid = "important-meeting",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0)),
Alerts = new Dictionary<string, Alert>
{
["reminder15"] = new Alert
{
Trigger = new OffsetTrigger
{
Offset = "-PT15M", // 15 minutes before
RelativeTo = TriggerRelation.Start
},
Action = AlertAction.Display
},
["reminder1day"] = new Alert
{
Trigger = new OffsetTrigger
{
Offset = "-P1D", // 1 day before
RelativeTo = TriggerRelation.Start
},
Action = AlertAction.Email
}
}
};Support multiple languages.
var internationalEvent = new Event
{
Uid = "global-event",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(DateTime.Now),
Title = "International Conference",
Description = "Annual technology conference",
Locale = "en",
Localizations = new Dictionary<string, PatchObject>
{
["de"] = new PatchObject
{
["title"] = "Internationale Konferenz",
["description"] = "Jährliche Technologiekonferenz"
},
["fr"] = new PatchObject
{
["title"] = "Conférence Internationale",
["description"] = "Conférence technologique annuelle"
},
["es"] = new PatchObject
{
["title"] = "Conferencia Internacional",
["description"] = "Conferencia tecnológica anual"
}
}
};JSCalendar uses ISO 8601 duration format:
// Various duration examples
var durations = new[]
{
new Duration { Hours = 1 }, // PT1H
new Duration { Minutes = 30 }, // PT30M
new Duration { Hours = 2, Minutes = 15 }, // PT2H15M
new Duration { Days = 1 }, // P1D
new Duration { Weeks = 2 }, // P2W
new Duration { Days = 1, Hours = 3 } // P1DT3H
};
// From TimeSpan
var duration = Duration.FromTimeSpan(TimeSpan.FromHours(1.5));
Console.WriteLine(duration.ToString()); // PT1H30MPatchObjects allow you to modify properties without redefining entire objects.
var patch = new PatchObject
{
// Update top-level property
["title"] = "Updated Title",
// Update nested property using JSON Pointer notation
["locations/venue/name"] = "New Venue Name",
// Remove a property
["alerts"] = null,
// Add complex nested data
["participants/newPerson/email"] = "new@example.com"
};
// Validation
if (patch.Validate())
{
Console.WriteLine("Patch is valid!");
}Handle time zones properly:
var zonedEvent = new Event
{
Uid = "zoned-event",
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0)),
TimeZone = "Europe/Berlin",
TimeZones = new Dictionary<string, TimeZone>
{
["Europe/Berlin"] = new TimeZone
{
TzId = "Europe/Berlin",
Updated = DateTimeOffset.UtcNow
}
}
};JSCalendar.Net includes built-in validation:
// PatchObject validation
var patch = new PatchObject
{
["title"] = "Valid",
["items/0/name"] = "Invalid" // Array indices not allowed
};
if (!patch.Validate())
{
Console.WriteLine("Invalid patch!");
}
// Check forbidden properties for recurrence overrides
if (PatchObject.IsForbiddenForRecurrence("uid"))
{
Console.WriteLine("Cannot patch 'uid' in recurrence overrides");
}var options = new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(myEvent, options);// Good
var evt = new Event
{
Uid = Guid.NewGuid().ToString(),
Updated = DateTimeOffset.UtcNow,
Start = new LocalDateTime(DateTime.Now)
};
// Bad - Missing required properties will cause errors
var evt = new Event();// Good - Descriptive and unique
Uid = $"meeting-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid():N}"
// Bad - Not unique
Uid = "meeting"if (myPatchObject.Validate())
{
var json = JsonSerializer.Serialize(myPatchObject);
}// For floating time (no time zone)
Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0))
// For zoned time
Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0)),
TimeZone = "Europe/Berlin"Run the test suite:
cd tests/JSCalendar.Net.Tests
dotnet testContributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
git clone https://github.com/JMAP-Net/JSCalendar.Net
cd JSCalendar.Net
dotnet restore
dotnet build
dotnet testThis library implements the complete JSCalendar specification as defined in RFC 8984.
Supported Sections:
- Section 2: Object Model (Event, Task, Group)
- Section 4: Common Properties
- Section 5: Type-Specific Properties
- Recurrence Rules (Section 4.3)
- PatchObject (Section 1.4.9)
- Time Zones (Section 4.7)
- Participants (Section 4.4)
- Alerts (Section 4.5)
- Localization (Section 4.6)
This project is licensed under the MIT License - see the LICENSE file for details.
- Uses System.Text.Json for high-performance JSON serialization
- Built with ❤️ for the .NET community
- Made with ☕ and .NET