Skip to main content

Calendar System Architecture

This reference document describes the internal architecture of the Resgrid calendar system, including all-day and multi-day event handling, iCal export, calendar feed subscriptions, and the data model.

Data Model

CalendarItem

The CalendarItem entity is the core model stored in the CalendarItems table. All changes in this enhancement are backwards-compatible with the existing table schema.

PropertyTypeDescription
TitlestringEvent name
StartDateTimeEvent start (always populated; auto-normalized for all-day events)
EndDateTimeEvent end (always populated; auto-normalized for all-day events)
IsAllDayboolWhether the event is an all-day event
DescriptionstringEvent description
LocationstringEvent location
RecurrenceTypeintRecurrence schedule (none, weekly, monthly, yearly)
RecurrenceEndDateTime?When recurrence stops
ReminderintReminder type
AttendeesstringEvent attendees

IsMultiDay() Helper

A [NotMapped] pure helper method on CalendarItem:

public bool IsMultiDay() => Start.Date != End.Date;

This is used by the UI and API to determine display formatting without additional database queries.

UserProfile — CalendarSyncToken

A new nullable column on the UserProfiles table stores the per-user calendar sync GUID:

ColumnTypeMax LengthNullableDescription
CalendarSyncTokenstring128YesGUID for validating iCal feed subscription tokens

Migration: M0047_AddingCalendarSyncToken (SQL Server) and M0047_AddingCalendarSyncTokenPg (PostgreSQL).

No new repository or DI registrations are required — UserProfile is managed by the existing IUserProfilesRepository with Dapper SaveOrUpdate. The new column is a nullable string on the existing entity.

All-Day Event Normalization

When IsAllDay is true, the service layer automatically normalizes the Start and End times:

FieldNormalized ValuePurpose
StartStart.Date (midnight 00:00:00)Ensures consistent date boundary
EndEnd.Date.AddDays(1).AddTicks(-1) (23:59:59.9999999)Ensures the event covers the full end date

This normalization is applied in:

  • CalendarService.AddNewCalendarItemAsync — after the existing UTC conversion block
  • CalendarService.UpdateCalendarItemAsync — same location
  • CalendarItem.CreateRecurranceItem — when generating recurrence children

The [Required] attributes on Start and End are preserved — they are always populated. The "no time required" behavior is purely a UI/service-layer concern.

Calendar Sync Token System

Token Generation Flow

  1. User clicks Activate Calendar Sync in the web UI
  2. CalendarService.ActivateCalendarSyncAsync generates a new GUID
  3. The GUID is saved to UserProfile.CalendarSyncToken
  4. An encrypted token payload is created: "{departmentId}|{userId}|{calendarSyncToken}"
  5. The payload is encrypted via IEncryptionService.Encrypt()
  6. The encrypted bytes are encoded as URL-safe Base64 (+-, /_, trailing = trimmed)
  7. The full subscription URL is returned to the user

Token Validation Flow

  1. External calendar app requests CalendarFeed/{token}
  2. Token is decoded from URL-safe Base64
  3. Token is decrypted via IEncryptionService
  4. Payload is split into departmentId, userId, and calendarSyncToken
  5. UserProfile is loaded and CalendarSyncToken is compared
  6. If the GUID matches, the department iCal feed is generated and returned
  7. If the GUID does not match (e.g., user regenerated the key), 401 is returned

Token Regeneration

RegenerateCalendarSyncAsync overwrites the existing GUID with a new one, instantly invalidating all previously issued subscription URLs.

iCal Export Service

The CalendarExportService (implementing ICalendarExportService) generates standard RFC 5545 iCalendar output using the Ical.Net NuGet package.

Service Methods

MethodDescription
GenerateICalForItemAsync(int calendarItemId)Single VEVENT as .ics string
GenerateICalForDepartmentAsync(int departmentId)Full department calendar as .ics with all items

Event Mapping Rules

CalendarItemIcal.Net CalendarEventNotes
TitleSummary
DescriptionDescription
LocationLocation
StartDtStartCalDateTime with date-only when IsAllDay
EndDtEndDate-only + 1 day (exclusive) when all-day
IsAllDayIsAllDayEmits VALUE=DATE in output
ReminderAlarms (VALARM)Trigger from GetMinutesForReminder()
AttendeesAttendeesOnly if populated

Recurrence Handling

Each materialized recurrence instance is emitted as a separate VEVENT. No RRULE properties are used because Resgrid pre-expands recurrences in the database. The IsAllDay flag is propagated to recurrence children.

Registration

CalendarExportService is registered as ICalendarExportService in ServicesModule.cs.

Configuration

Calendar-specific config values are stored in CalendarConfig.cs in the Resgrid.Config project, following existing patterns:

public static class CalendarConfig
{
/// <summary>Feature flag to enable/disable the external iCal feed endpoint.</summary>
public static bool ICalFeedEnabled = true;

/// <summary>PRODID value used in generated iCal files.</summary>
public static string ICalProductId = "-//Resgrid//Calendar//EN";

/// <summary>How long (in minutes) a feed response can be cached by the subscribing client.</summary>
public static int ICalFeedCacheDurationMinutes = 15;
}

These values can be overridden via ResgridConfig.json like all other config classes (handled by ConfigProcessor).

FullCalendar Upgrade (v3 → v6)

The web calendar UI is upgraded from FullCalendar v3 to FullCalendar v6:

Aspectv3 (Old)v6 (New)
APIjQuery plugin ($('#calendar').fullCalendar({...}))Vanilla JS (new FullCalendar.Calendar(el, {...}))
LibrarySingle fullcalendar package@fullcalendar/core, daygrid, timegrid, interaction, list
CSSfullcalendar.print.min.cssFullCalendar 6 CSS bundles
All-day renderingManual handlingNative support via allDay: true in event JSON
Multi-day renderingLimitedNative continuous banner rendering

FullCalendar JSON Changes

The GetV2CalendarEntriesForCal endpoint now includes:

  • allDay boolean property (set from IsAllDay)
  • For all-day events, end is set to End.Date.AddDays(1) (FullCalendar uses exclusive end dates)

Localization

All new user-facing strings use the existing IStringLocalizer / .resx localization system. New keys are added to Calendar.en.resx and Calendar.es.resx:

KeyEnglishSpanish
AllDayEventAll Day EventEvento de todo el día
DateRange{0} – {1}{0} – {1}
DownloadIcsDownload .icsDescargar .ics
SubscribeCalendarSubscribe to CalendarSuscribirse al calendario
CalendarSyncTitleCalendar SyncSincronización de calendario
CalendarSyncDescriptionSubscribe to your department calendar in Google Calendar, Microsoft Outlook, Apple Calendar, or any application that supports iCal feeds.Suscríbase al calendario de su departamento en Google Calendar, Microsoft Outlook, Apple Calendar o cualquier aplicación que admita fuentes iCal.
ActivateCalendarSyncActivate Calendar SyncActivar sincronización de calendario
RegenerateCalendarSyncRegenerate Sync KeyRegenerar clave de sincronización
CalendarSyncActivateHelpTo sync your department calendar with an external calendar application, you must first activate calendar sync. This generates a unique subscription URL. If the URL is compromised, you can regenerate it to invalidate the old one.Para sincronizar el calendario de su departamento con una aplicación de calendario externa, primero debe activar la sincronización del calendario. Esto genera una URL de suscripción única. Si la URL se ve comprometida, puede regenerarla para invalidar la anterior.
CopyToClipboardCopy to ClipboardCopiar al portapapeles
SubscriptionUrlSubscription URLURL de suscripción
WebCalLinkOpen in Calendar AppAbrir en aplicación de calendario
CalendarSyncActiveCalendar sync is active. Use the URL below to subscribe.La sincronización de calendario está activa. Use la URL a continuación para suscribirse.
CalendarSyncInactiveCalendar sync is not yet activated. Click the button below to generate your subscription URL.La sincronización del calendario aún no está activada. Haga clic en el botón a continuación para generar su URL de suscripción.

New and Modified Files Summary

New Files

FileDescription
CalendarConfig.csCalendar-specific configuration values
ICalendarExportService.csInterface for iCal generation
CalendarExportService.csiCal generation implementation using Ical.Net
CalendarExportController.csv4 API controller for export and feed endpoints
M0047_AddingCalendarSyncToken.csSQL Server migration for UserProfiles column
M0047_AddingCalendarSyncTokenPg.csPostgreSQL migration for UserProfiles column
CalendarExportServiceTests.csUnit tests for iCal export

Modified Files

FileChanges
CalendarItem.csAdded IsMultiDay() helper method
UserProfile.csAdded CalendarSyncToken property
ICalendarService.csAdded sync token management methods
CalendarService.csAll-day normalization, sync token implementation
ServicesModule.csRegistered CalendarExportService
CalendarController.cs (web)All-day normalization, sync actions, view model updates
CalendarController.cs (v4 API)Added IsMultiDay to calendar item results
CalendarItemV2Json.csAdded allDay property
GetAllCalendarItemResult.csAdded IsMultiDay property
IndexView.csAdded CalendarSyncToken and CalendarSubscriptionUrl
CalendarItemView.csAdded ExportIcsUrl
Index.cshtmlCalendar sync subscription panel
View.cshtmlAll-day display, multi-day date range, .ics download
New.cshtml / Edit.cshtmlAll-day date-only picker UX
resgrid.calendar.index.jsRewritten for FullCalendar v6 API
resgrid.calendar.newEntry.jsAll-day toggle hides time pickers
_UserLayout.cshtmlFullCalendar v6 CSS/JS references
libman.jsonFullCalendar v6 library references
Calendar.en.resx / Calendar.es.resxNew localization keys
CalendarServiceTests.csExpanded with all-day, multi-day, sync token tests

Backwards Compatibility

All changes are backwards-compatible:

  • The CalendarItems table schema is unchanged — no migration needed for calendar data
  • The UserProfiles table gains a single nullable column — existing rows are unaffected
  • Existing API contracts are additive only (new properties, new endpoints)
  • The IsAllDay normalization only activates when IsAllDay is true; existing events with IsAllDay = false are not modified
  • The FullCalendar v6 upgrade maintains the same data source URL (GetV2CalendarEntriesForCal)