Syncing Staff Assignments to Google Calendar in a Legacy ASP.NET WebForms App
Syncing Staff Assignments to Google Calendar in a Legacy ASP.NET WebForms App
I recently worked on Staffpoint, a staffing and scheduling platform built by DeloLogic. My main piece was the backend for the Google Calendar integration for personnel (ticket "[SP-4.0] Google calendar integration for Personnel - Backend"). This post covers what Staffpoint is, the stack it runs on, and how the calendar sync works under the hood.
What Staffpoint is
Staffpoint is a workforce management system for staffing agencies. An agency uses it to manage clients, locations, and job types, then create shifts (called "assignments" in the product) and fill them with qualified personnel. It handles the whole lifecycle: open shifts, offers, confirmations, cancellations, availability, qualifications and accreditations, payroll and billing rates, invoices, KPI reports, and even automated phone calls and SMS through Twilio to offer shifts to staff.
There are three faces to the product. Agency administrators use the main scheduling portal. Clients get a portal to request shifts and see who is coming. Personnel get a portal where they see "My Assignments," accept or decline offers, and manage their availability. The Google Calendar feature lives on the personnel side.
The stack
Staffpoint is a classic Microsoft web application, and a large one.
- Runtime: .NET Framework 4.8, ASP.NET WebForms as a dynamically compiled Web Site (no csproj; code lives in App_Code and code-behind files).
- UI: .aspx pages with a master page per portal, plus a shared "StaffpointUI" component library the team has been migrating legacy pages onto.
- Data: SQL Server with LINQ-to-SQL through a custom data context. Domain classes such as Staff and Shifts double as both entity and repository.
- APIs: many thin .ashx HTTP handlers for AJAX endpoints, plus a routed REST-style API and a mobile API.
- Auth: Windows Authentication with impersonation on the server side.
- Logging: log4net.
- Integrations: Twilio (voice and SMS), Mandrill (email), QuickBooks, JobDiva, and now Google Calendar via the Google.Apis.Calendar.v3 client library.
- Hosting: IIS or IIS Express, folder publish with web.config transforms per environment.
There is no automated test suite. Validation happens by running locally under IIS Express against a dev database and smoke-testing the flows you touched. That shapes how you write code here: lots of guard clauses, lots of logging, and small targeted changes.
The feature: Google Calendar integration for personnel
The ask was simple to state. A staff member connects their Google account once. From then on, every assignment they hold shows up on their Google Calendar, updates when the shift changes, and disappears when the shift is cancelled or unassigned.
Connecting an account: the OAuth flow
The personnel portal has a Calendar Integration page with a single "Google Calendar Sync" button. Clicking it builds an authorization URL with the Google OAuth client library, requesting the Calendar scope with access_type=offline so we get a refresh token, and prompt=consent so the refresh token is always returned. The logged-in staff ID is passed as the OAuth state parameter.
Google redirects back to a small .ashx handler. The handler exchanges the authorization code for tokens, serializes the whole token response as JSON, and stores it on the staff record in a new GoogleOAuthToken column. It then kicks off a backfill of all the user's current assignments and redirects to the integration page with a "connected" banner.
Disconnecting posts the refresh token to Google's revoke endpoint and clears the stored token.
Storing the link between a shift and an event
Each shift row got a new nullable googleCalendarEventId column. That one column is the whole sync state. If it is set, we update or delete that event. If it is empty, we insert a new one. The event also carries the shift ID as a private extended property so it is traceable from the Google side.
The sync rules
A single method, SyncGoogleCalendarForShift, decides what to do for a given shift. It runs through the rules in order:
- If the shift does not exist or has no staff assigned, do nothing.
- If Google is not configured, or this staff member has never connected, do nothing.
- If the shift starts before tomorrow, it is outside the sync window. Delete any existing event and stop. We deliberately do not push past or same-day shifts into calendars.
- If the shift is not in Assigned or Completed status, it should not be on the calendar. Delete any existing event and stop.
- Otherwise create or update the event, and persist the returned event ID back onto the shift if it changed.
That method is called from every write path that can change an assignment's owner, time, or status: assigning a staff member, editing shift times, confirming, cancelling, unassigning, and bulk operations on recurring shift series. Each call site goes through a Try wrapper that catches and logs, so a Google API failure never breaks the core scheduling action. Scheduling has to work even when Google is down.
Event content
The event summary is "Staffpoint Assignment #12345." Location is the client name. The description lists the location, start and end, duration in hours, and a link back to the My Assignments page. Start and end are sent as ISO 8601 strings with the server's local UTC offset so they land at the right wall-clock time regardless of the user's calendar time zone.
Problems I ran into
Expired tokens. Google access tokens live for about an hour. The first version stored the token and used it blindly, so sync silently failed for anyone who had connected more than an hour earlier. The fix was to record IssuedUtc at exchange time, check expiry before every API call, and refresh through the Google client library's UserCredential. When a refresh comes back with invalid_grant, which means the user revoked access from their Google account, we delete the stored token so the UI correctly shows "Not Connected" instead of failing forever.
Cancellations leaving orphan events. Cancelling a shift cleared the event ID on the row before the delete call ran, so the deletion had nothing to delete. The fix was to capture the staff ID and event ID before mutating the row, run the database update, and only then remove the Google event. The same pattern had to be applied to recurring shift series, which remove many shifts in one transaction and need to collect the removals and process them after commit.
Reconnect backfill. When a user connects, they expect their existing assignments to appear immediately. The callback handler queries all Assigned or Completed shifts from tomorrow onward for that staff member and syncs each one.
Missing configuration. Not every deployment has Google credentials. Every entry point checks for client ID, secret, and redirect URL and quietly disables the feature if any is missing, with the integration page showing an "Unavailable" state instead of throwing.
What I would do differently
The sync is synchronous inside the web request. Assigning a shift means a round trip to Google before the page responds. It is wrapped so failures are non-fatal, but a slow Google API still slows the user. A queue and a background worker would be the right next step, and would also make retries possible.
Tokens are stored as plain JSON in the database. Encrypting that column, or moving to a dedicated secrets store, would be a sensible hardening step before wider rollout.
Takeaways
Integrating a modern OAuth API into a legacy WebForms codebase was less about the Google library and more about finding every code path that mutates a shift and making sure the calendar follows. One nullable column, one decision method, and a Try wrapper at every call site turned out to be a workable design. Log everything, because without tests the logs are how you find out what actually happened in production.
Comments
Post a Comment