Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

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 GitHub stars for shinyorg/shiny
Downloads NuGet downloads for Shiny.Calendar
Frameworks
.NET MAUI
.NET
  • Calendars & events CRUD — calendar management via GetAll / GetById / Create / Update / Delete (the calendar entity is implied by the interface), plus GetEvents, CreateEvent, UpdateEvent, DeleteEvent.
  • Fluent async queryingQuery() returns a CalendarEventQuery builder (ForCalendar/Between/Where/OrderBy/Skip/TakeToListAsync(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, with CalendarAccessType for iOS 17 read/write/write-only.
  • AOT & trimmer compatible.
claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One 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.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One 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.

View shiny-calendarstore Skill

Install the package:

Terminal window
dotnet add package Shiny.Calendar

Register 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:

Shiny.CalendarNuGet package Shiny.Calendar
Shiny.Hosting.MauiNuGet package Shiny.Hosting.Maui

See Permissions for the per-platform manifest keys.

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);
}
}
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
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

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?

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
  • Recurrence is read-only everywhere.
  • Android instance granularity: events are read as series masters from the Events table, not as expanded instances, so deleteSeries: false cancels the series’ own DTSTART — 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.