Calendar Store | Getting Started
A cross-platform library for accessing device calendars and events on iOS, Mac Catalyst, macOS, Android, and Windows. Provides full CRUD operations on calendars and events, a fluent async query builder, Shiny’s AccessState permission model, and dependency injection integration. Built on Shiny.Core, so it runs in any Shiny host — with or without .NET MAUI.
| GitHub | |
| Downloads |
Features
Section titled “Features”- Calendars & events CRUD — calendar management via
GetAll/GetById/Create/Update/Delete(the calendar entity is implied by the interface), plusGetEvents,CreateEvent,UpdateEvent,DeleteEvent. - Fluent async querying —
Query()returns aCalendarEventQuerybuilder (ForCalendar/Between/Where/OrderBy/Skip/Take→ToListAsync(ct)). The calendar id and start/end window are pushed down to the native fetch; the rest runs in-memory, off the UI thread. - Rich model — events carry attendees, reminders, availability, URL, and read-only recurrence/organizer.
- Native backends — EventKit (iOS/Mac Catalyst/macOS), CalendarContract (Android),
AppointmentStore(Windows). - Permissions — Shiny.Core
AccessState, withCalendarAccessTypefor iOS 17 read/write/write-only. - AOT & trimmer compatible.
Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install the plugin:
claude plugin install shiny@shinyOne plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.
Step 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install the plugin:
copilot plugin install shiny@shinyOne plugin installs all 35 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.
Install the package:
dotnet add package Shiny.CalendarRegister the store (the app must call .UseShiny() so platform services like permissions are wired up):
using Shiny;
builder.UseShiny();builder.Services.AddCalendarStore();Or generate the full set of files — packages, registration, manifests, and entitlements — with the builder below:
See Permissions for the per-platform manifest keys.
Quick Start
Section titled “Quick Start”public class CalendarService(ICalendarStore store){ public async Task<string> AddMeeting() { var access = await store.RequestAccess(CalendarAccessType.ReadWrite); if (access != AccessState.Available) throw new InvalidOperationException("Calendar access not granted");
var evt = new CalendarEvent("Team Sync", DateTimeOffset.Now.AddHours(1), DateTimeOffset.Now.AddHours(2)) { Location = "Room 3", Description = "Weekly sync" }; evt.Reminders.Add(new EventReminder(TimeSpan.FromMinutes(15))); evt.Attendees.Add(new EventAttendee("Alice", "alice@example.com"));
return await store.CreateEvent(evt); }}Models
Section titled “Models”Calendar
Section titled “Calendar”| Property | Type | Notes |
|---|---|---|
| Id | string? |
Platform identifier |
| Name | string |
Display name |
| Color | string? |
Hex, e.g. #FF3B30 |
| IsReadOnly | bool |
Subscribed/holiday calendars, etc. |
| Account | string? |
Owning account / source |
CalendarEvent
Section titled “CalendarEvent”| Property | Type | Notes |
|---|---|---|
| Id | string? |
|
| CalendarId | string? |
Target calendar |
| Title | string |
|
| Description | string? |
Notes |
| Location | string? |
|
| Start / End | DateTimeOffset |
|
| IsAllDay | bool |
|
| Availability | EventAvailability |
Busy, Free, Tentative, Unavailable |
| Url | string? |
|
| IsRecurring | bool |
Read-only |
| RecurrenceRule | string? |
Read-only |
| Reminders | List<EventReminder> |
Offset = lead time before start |
| Attendees | List<EventAttendee> |
Name, Email, Role, Status, IsOrganizer |
| Organizer | EventAttendee? |
Read-only |
Attendees & Contacts
Section titled “Attendees & Contacts”Native calendars expose attendees as name + email + status — not contact references. Shiny.Calendar
references Shiny.Contacts and ships a bridge so you can resolve an attendee back to a device contact by email:
var contact = await attendee.ResolveContact(contactStore); // matches on Email, returns Shiny.Contacts.Contact?Deleting a Recurring Event
Section titled “Deleting a Recurring Event”DeleteEvent takes a deleteSeries flag. It is ignored for non-recurring events, so it is always
safe to pass — but for a recurring one, don’t guess. Check CalendarEvent.IsRecurring and let the
user choose, the way the native calendar apps do:
if (evt.IsRecurring){ var choice = await dialogs.ActionSheet( $"Delete \"{evt.Title}\"?", "Cancel", "Delete All Future Events", "Delete This Event");
if (choice is not ("Delete This Event" or "Delete All Future Events")) return;
await store.DeleteEvent(evt.Id!, choice == "Delete All Future Events");}else{ await store.DeleteEvent(evt.Id!);}| Platform | deleteSeries: false |
deleteSeries: true |
|---|---|---|
| iOS / Mac Catalyst / macOS | EKSpan.ThisEvent |
EKSpan.FutureEvents |
| Android | Inserts a cancellation exception for the occurrence | Deletes the Events row (whole series) |
| Windows | No effect — AppointmentStore has no per-instance delete |
Deletes the appointment |
Platform Caveats
Section titled “Platform Caveats”- Recurrence is read-only everywhere.
- Android instance granularity: events are read as series masters from the
Eventstable, not as expanded instances, sodeleteSeries: falsecancels the series’ ownDTSTART— the first occurrence. - Apple (EventKit): attendees cannot be written — attendees you set on create/update are ignored.
- Android (CalendarContract): event CRUD works with permissions; calendar creation uses sync-adapter semantics.
- Windows (AppointmentStore): best-effort — reads cover all calendars, but create/update/delete only work inside an app-owned calendar; writes to a system calendar throw
NotSupportedException.


