Google Calendar API Integration in .NET with Rest APIs and React frontend
In my recent project, Polydesk, a medical appointment scheduling system, I needed to integrate Google Calendar. The goal was to allow users to sign in with their Google account through the frontend and automatically sync their calendar whenever an appointment is created or updated in the application.
This post explains the full process step by step, using .NET with C# for the backend and React for the frontend. I went through several problems due to unclear documentation, so I decided to write this guide to make the process easier for others.
Table of Contents
- Step 1: Setting Up Google Cloud project
- Step 2: Setting Up the .NET Backend
- Step 3: Frontend Integration with React
- Step 4: Syncing appointments with Google Calendar
- Conclusion
Step 1: Setting Up Google Cloud project
Before everything else, you need to set up a Google Cloud Project, add a OAuth 2.0 key, and enable the Google Calendar API.
- Create a Google Cloud Project:
- Go to the Google Cloud Console
- Create a new project or select an existing one
-
Enable the Google Calendar API:
- Navigate to the “APIs & Services” > “Library”
- Search for “Google Calendar API” and enable it
-
Configure OAuth 2.0 Credentials:
- Go to “Credentials” and click “Create Credentials”.
- Choose “OAuth 2.0 Client IDs”.
- Configure the OAuth consent screen
- Under “Authorized JavaScript origins” and also under ” Authorized redirect URIs ” add the URL where the application is hosted (in my case
http://localhost:3000
for local development)
-
Save the credentials:
- Once created take note of the
Client ID
andClient Secret
. You will need these in the .NET application. In my cased I saved them in theappsettings.json
file like this:
- Once created take note of the
public class GoogleSettings
{
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
}
You will need these to authenticate users and access their Google Calendar.
Step 2: Setting Up the .NET Backend
In this step, we will set up the .NET backend to handle OAuth 2.0 authentication and interact with the Google Calendar API.
First of all, you will need the following packages in your .NET project:
dotnet add package Google.Apis.Auth
dotnet add package Google.Apis.Calendar.v3
First one is for authentication and the second one is for interacting with the Google Calendar API.
To let users interact with their Google Calendar, we need to let them authenticate with their Google account with the OAuth 2.0 flow and saving their access credentials somwehere.
In their official documentation, Google uses a json
file to save the credentials, but in my case I preferred to save them in the database. So I created a GoogleToken
model like this:
public class GoogleToken
{
public required string Id { get; set; }
public User? User { get; set; }
public required string Value { get; set; }
}
This has a foreign key to the User
model, so we can associate the Google credentials with the user who authenticated with their Google account.
For the autentication we need first to create the login method that will interact with the frontend and authenticate the user with their Google account.
I am using MediatoR with CQRS pattern, so I created a GoogleLoginUserCommand
like this:
public class GoogleLoginUserCommand(
GoogleLoginUserDto googleLoginUserDto,
string username
)
: IRequest<Result<bool>>
{
public GoogleLoginUserDto GoogleLoginUserDto { get; set; } = googleLoginUserDto;
public string Username { get; set; } = username;
}
// - Handler
public sealed class
GoogleLoginInternalUserCommandHandler : IRequestHandler<GoogleLoginUserCommand,
Result<bool>>
{
private readonly GlobalSettings _globalSettings;
private readonly PolydeskContext _context;
private readonly AuthenticationManager _authenticationManager;
private readonly InternalSecurityLogsManager _internalSecurityLogsManager;
// Gogole calendar scope
private const string GoogleCalendarScope = "https://www.googleapis.com/auth/calendar";
public GoogleLoginInternalUserCommandHandler(
AuthenticationManager authenticationManager,
InternalSecurityLogsManager internalSecurityLogsManager,
PolydeskContext context, GlobalSettings globalSettings
)
{
_authenticationManager = authenticationManager;
_internalSecurityLogsManager = internalSecurityLogsManager;
_context = context;
_globalSettings = globalSettings;
}
public async Task<Result<bool>> Handle(
GoogleLoginUserCommand request,
CancellationToken cancellationToken
)
{
// here we get the user from the database
var userResult = await _authenticationManager.GetUserAsync(request.Username, request.UserType);
if (userResult.IsFailed)
{
return userResult.ToResult();
}
var user = userResult.Value;
// check if already has a token
var userTokens = _context.GoogleTokens
.Where(x => x.Id == user.Id)
.ToList();
if (userTokens.Any())
{
return Result.Fail(new ValidationError("AlreadyLoggedIn",
ValidationErrorCodes.NotAllowed,
title: "User already logged in with Google OAuth"
)
);
}
var clientId = _globalSettings.Google.ClientId;
var clientSecret = _globalSettings.Google.ClientSecret;
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
{
throw new Exception("Google settings not found");
}
var initializer = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = clientId,
ClientSecret = clientSecret
},
Scopes = new[] { GoogleCalendarScope },
DataStore = new GoogleDatabaseDataStore(_context)
};
var authFlow = new GoogleAuthorizationCodeFlow(initializer);
var accessToken = await authFlow.ExchangeCodeForTokenAsync(
user.Id, request.GoogleLoginUserDto.Code,
// the redirect URI must match the one used in the OAuth consent screen
"http://localhost:3000",
cancellationToken
);
if (accessToken == null)
{
return Result.Fail(new ValidationError("Authentication",
ValidationErrorCodes.InvalidValue,
title: "There was an error communicating with Google, please try again later"
)
);
}
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken.AccessToken);
var response =
await httpClient.GetStringAsync("https://www.googleapis.com/oauth2/v2/userinfo", cancellationToken);
if (!string.IsNullOrEmpty(response))
{
// convert response to object
var googleUserInfo = JsonSerializer.Deserialize<GoogleUserInfo>(response);
if (googleUserInfo == null)
{
return Result.Fail(new ValidationError("Authentication",
ValidationErrorCodes.InvalidValue,
title: "There was an error communicating with Google, please try again later"
)
);
}
}
else
{
return Result.Fail(new ValidationError("Authentication",
ValidationErrorCodes.InvalidValue,
title: "There was an error communicating with Google, please try again later"
)
);
}
}
return Result.Ok(true);
}
}
In summary, this command does the following:
- Retrieves the user from the database using the provided username.
- Checks if the user already has a Google token associated with their account.
- Initializes the Google Authorization Code Flow with the client ID and secret.
- Exchanges the authorization code for an access token using the
ExchangeCodeForTokenAsync
method. - If successful, it retrieves the user’s information from Google and saves the token in the database (not needed)
To use the database as DataStore you will need to create the GoogleDatabaseDataStore
class that implements the IDataStore
interface from the Google.Apis.Auth.OAuth2
namespace. This class will handle saving and retrieving the tokens from the database.
public class GoogleDatabaseDataStore : IDataStore
{
private readonly PolydeskContext _context;
public GoogleDatabaseDataStore(PolydeskContext context)
{
_context = context;
}
public async Task StoreAsync<T>(string key, T value)
{
var serializedValue = JsonSerializer.Serialize(value);
var existingToken = await _context.GoogleTokens.FindAsync(key);
if (existingToken == null)
{
_context.GoogleTokens.Add(new GoogleToken
{
Id = key,
Value = serializedValue
});
}
else
{
existingToken.Value = serializedValue;
}
await _context.SaveChangesAsync();
}
public async Task DeleteAsync<T>(string key)
{
var token = await _context.GoogleTokens.FindAsync(key);
if (token != null)
{
_context.GoogleTokens.Remove(token);
await _context.SaveChangesAsync();
}
}
public async Task<T> GetAsync<T>(string key)
{
var entity = await _context.GoogleTokens.FindAsync(key);
if (entity == null)
{
return default;
}
return JsonSerializer.Deserialize<T>(entity.Value);
}
public Task ClearAsync()
{
return Task.CompletedTask;
}
}
The DTO for the command is simple:
public class GoogleLoginUserDto
{
public required string Code { get; set; }
}
This code is sent from the frontend after the user has authenticated with their Google account. The code is the authorization code returned by Google after the user has granted access to their calendar.
For the controllers I created the following endpoints
// - Google
[HttpPost("google/login")]
public async Task<IActionResult> GoogleLogin([FromBody] GoogleLoginUserDto dto)
{
var requestInformationDto = HttpContext.ToRequestInformationDto();
var command = new GoogleLoginUserCommand(dto, User.Identity!.Name!, UserType.Internal, requestInformationDto);
var result = await _mediator.Send(command);
return result.ToActionResult();
}
// gogole logout
[HttpPost("google/logout")]
public async Task<IActionResult> GoogleLogout()
{
var requestInformationDto = HttpContext.ToRequestInformationDto();
var command = new GoogleLogoutCommand(User.Identity!.Name!, UserType.Internal, requestInformationDto);
var result = await _mediator.Send(command);
return result.ToActionResult();
}
For the logout you can find the GoogleLogoutCommand
in the repository here
Step 3: Frontend Integration with React
Now that the backend is set up to handle Google OAuth 2.0 authentication, we can move on to the frontend part of the application.
For the frontend I used the package “@react-oauth/google” that you can find here. It provides a simple way to integrate Google OAuth 2.0 in React applications.
To use it, you need to wrap your application with the GoogleOAuthProvider
and pass the Client ID
import { GoogleOAuthProvider } from '@react-oauth/google';
<GoogleOAuthProvider clientId="<your_client_id>">...</GoogleOAuthProvider>;
Then, you can use the GoogleLogin
component to handle the login process.
This is my component (a button) that handles the login process:
import { Preference } from '@/components/common/Preference.component';
import { handleError } from '@/core/utils/errorUtils';
import { handleSuccess } from '@/core/utils/genericUtils';
import { showErrorNotification } from '@/core/utils/notificationsUtils';
import {
useGetUserQuery,
useGoogleAuthMutation,
useGoogleLogoutMutation,
} from '@/features/settings/settingsApi';
import { Button, Text } from '@mantine/core';
import { modals } from '@mantine/modals';
import { nprogress } from '@mantine/nprogress';
import { CodeResponse, useGoogleLogin } from '@react-oauth/google';
import { GoogleIcon } from './GoogleIcon.component';
export const GoogleSync = () => {
// this is one internal api to get the user data
// this checks if the user is logged in and has a Google account linked (using the GoogleeToken FK)
const { data: user } = useGetUserQuery(undefined);
// - Mutations
const [googleAuth, { isLoading }] = useGoogleAuthMutation();
const [googleLogout, { isLoading: logoutIsLoading }] =
useGoogleLogoutMutation();
// - Handler
const handleAuth = (res: CodeResponse) => {
nprogress.start();
googleAuth({ code: res.code })
.unwrap()
.then((loggedIn) => {
if (loggedIn) {
handleSuccess('Successfully logged in with Google');
} else {
showErrorNotification({
title: 'There was an error logging in with Google',
});
}
})
.catch((err) =>
handleError(err, undefined, 'There was an error logging in with Google')
)
.finally(() => nprogress.complete());
};
const handleLogout = () => {
modals.openConfirmModal({
title: <Text fw="bold">Do you want to logout from Google?</Text>,
centered: true,
children: (
<Text size="sm">
This will remove your Google Calendar integration and you will need to
log in again to sync your calendar.
</Text>
),
labels: {
confirm: 'Yes, logout',
cancel: 'Cancel',
},
confirmProps: { color: 'red' },
onConfirm: () => {
nprogress.start();
googleLogout()
.unwrap()
.then((loggedOut) => {
if (loggedOut) {
handleSuccess('Successfully logged out from Google');
} else {
showErrorNotification({
title: 'There was an error logging out from Google',
});
}
})
.catch((err) =>
handleError(
err,
undefined,
'There was an error logging out from Google'
)
)
.finally(() => nprogress.complete());
},
});
};
const login = useGoogleLogin({
onSuccess: (codeResponse) => handleAuth(codeResponse),
flow: 'auth-code',
redirect_uri: `http://localhost:3000`,
scope: 'https://www.googleapis.com/auth/calendar',
});
if (!user) return null;
return (
<Preference
label="Google Calendar"
description={
'Sync your Google Calendar to manage your appointments'
}
>
{!user.google && (
<Button
leftSection={<GoogleIcon />}
variant="default"
onClick={login}
loading={isLoading}
>
Login with Google
</Button>
)}
{user.google == true && (
<Button
leftSection={<GoogleIcon />}
color="dark"
onClick={handleLogout}
loading={logoutIsLoading}
>
Logout from Google
</Button>
)}
</Preference>
);
};
This component checks if the user is logged in and has a Google account linked. If not, it shows a button to log in with Google. If the user is already logged in with Google, it shows a button to log out.
The useGoogleLogin
hook handles the login process and calls the handleAuth
function with the authorization code returned by Google. This code is then sent to the backend to authenticate the user and save their Google credentials.
Step 4: Syncing appointments with Google Calendar
To insert or update appointments in the Google Calendar, we need first need to get the user calendar object ( UserCalendar
). This object is used to interact with the Google Calendar API and perform operations like creating or updating events.
In my case I wanted to create a new calendar where I can add all the appointments.
This is the method that gets the calendar service for the user:
private async Task<UserCalendar?> GetCalendarService(string username,
int? timeslotResourceId,
CancellationToken cancellationToken = default
)
{
var clientId = _globalSettings.Google.ClientId;
var clientSecret = _globalSettings.Google.ClientSecret;
var user = await _userManager
.FindByNameAsync(username);
if (user == null)
{
throw new Exception("User not found");
}
// get user ŧoken
var googleToken = await _context.GoogleTokens.FindAsync([user.Id], cancellationToken: cancellationToken);
// in this case means the user has not logged in with Google yet
if (googleToken == null || string.IsNullOrEmpty(googleToken.Value))
{
// should stop gracefully
return null;
}
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets
{
ClientId = clientId,
ClientSecret = clientSecret
},
new[] { GoogleCalendarScope },
user.Id,
cancellationToken,
dataStore: new GoogleDatabaseDataStore(_context)
);
var service = new CalendarService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = _globalSettings.SiteName
});
var calendarId = user.GoogleCalendarId;
// if the user does not have a calendar, create one
if (string.IsNullOrEmpty(calendarId))
{
var resourceName = $"{_globalSettings.CustomerName} - {user.UserName}";
// create a new calendar
var calendar = new Calendar
{
Summary = resourceName
};
var createdCalendar = await service.Calendars.Insert(calendar).ExecuteAsync(cancellationToken);
calendarId = createdCalendar.Id;
user.GoogleCalendarId = calendarId;
await _userManager.UpdateAsync(user);
}
return new UserCalendar
{
CalendarId = calendarId,
Service = service
};
}
So this method does the following:
- Retrieves the user from the database using the provided username.
- Checks if the user has a Google token associated with their account.
- If the user has a token, it creates a
GoogleCredential
object using theGoogleWebAuthorizationBroker.AuthorizeAsync
method. - Initializes the
CalendarService
with the credential and application name. - Checks if the user has a Google Calendar Id. If not, it creates a new calendar with the user’s name and saves the calendar ID in the user object.
Finally, it returns a UserCalendar
object that contains the calendar ID and the CalendarService
instance. We can use this object to interact with the Google Calendar API and perform operations like creating or updating events.
So now we can use the following APIs to create or update events in the user’s Google Calendar.
- Create an event:
var createdEvent = await userCalendar.Service.Events.Insert(@event, userCalendar.CalendarId).ExecuteAsync();
- Update an event:
await userCalendar.Service.Events.Update(@event, userCalendar.CalendarId, @event.Id).ExecuteAsync();
- Delete an event:
await userCalendar.Service.Events.Delete(userCalendar.CalendarId, eventId).ExecuteAsync();
The @event
object is an instance of the Event
class from the Google.Apis.Calendar.v3.Data
. You can create this object and set its properties.
Sometimes Google limits the number of requests you can make to their API, so I put all my calls in a BackgroundJob using Hangfire. This way, I can queue the requests and process them in the background without blocking the main thread. And if they fail, I can retry them later.
BackgroundJob.Enqueue<TimeslotsExternalSyncer>(x => x.SyncCreation(timeslot.Id, @event, username));
You can configure the retry attempts and delay between retries in the Hangfire configuration.
Conclusion
I hope that this helped someone to integrate Google Calendar API in their .NET application. The process can be a bit tricky due and I hope this cleared some doubts.
If you have any questions or suggestions, feel free to reach out to me via email.