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, LINQ query support, 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. - LINQ querying —
Query()returnsIQueryable<CalendarEvent>where the calendar id and start/end window are pushed down to the native fetch; the rest runs in-memory. - 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/skills Step 2 — Install plugins:
claude plugin install shiny-client@shiny claude plugin install shiny-maui@shiny claude plugin install controls@shiny claude plugin install shiny-mediator@shiny claude plugin install shiny-data@shiny claude plugin install shiny-aspire@shiny claude plugin install shiny-extensions@shiny Step 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skills Step 2 — Install plugins:
copilot plugin install shiny-client@shiny copilot plugin install shiny-maui@shiny copilot plugin install controls@shiny copilot plugin install shiny-mediator@shiny copilot plugin install shiny-data@shiny copilot plugin install shiny-aspire@shiny copilot plugin install shiny-extensions@shiny 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.