Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 81 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@

## Google Sheets API .NET Wrapper Library
> [!IMPORTANT]
> Using this library requires you to use a Service Account to access your Google Sheets spreadsheets. Please review the authentication section farther down for more details on how to set this up.
> This library supports two authentication methods:
> - **Service Account Authentication** - for server-to-server scenarios
> - **OAuth 2.0 User Authentication** - for accessing organization Shared Drives and user-specific resources
>
> Please review the authentication section below for details on how to set up each method.

This library allows you to use strongly typed objects against a Google Sheets spreadsheet without having to have knowledge on the Google Sheets API methods and protocols.

Expand Down Expand Up @@ -478,8 +482,18 @@ using (var stream = new FileStream(filepath, FileMode.Create))
```

## Authentication

This library supports two authentication methods:
1. **Service Account Authentication** (recommended for server-to-server scenarios)
2. **OAuth 2.0 User Authentication** (recommended for accessing organization Shared Drives)

> [!NOTE]
> Service accounts cannot access Shared Drives that are restricted to organization members. Use OAuth 2.0 authentication if you need to access organization Shared Drives.

### Option 1: Service Account Authentication

> [!IMPORTANT]
> You need to setup a Google API Service Account before you can use this library.
> You need to setup a Google API Service Account before you can use this authentication method.

1. If you have not yet created a Google Cloud project, you will need to create one before you create a service account. Documentation on this can be found below,

Expand Down Expand Up @@ -520,3 +534,68 @@ sheetHelper.Init(settings.JsonCredential);
Another good article on how to setup a Google Service Account can be found below on Robocorp's documentation site,

https://robocorp.com/docs/development-guide/google-sheets/interacting-with-google-sheets#create-a-google-service-account

### Option 2: OAuth 2.0 User Authentication

OAuth 2.0 authentication allows users to authenticate with their Google account, which enables access to organization Shared Drives and resources restricted to organization members.

1. Create OAuth 2.0 credentials in your Google Cloud project:

https://developers.google.com/workspace/guides/create-credentials#oauth-client-id

2. Enable the ````Google Sheets API```` for your Google Cloud project (if not already enabled).

3. Install the necessary NuGet package for OAuth authentication in your project:
```bash
dotnet add package Google.Apis.Auth
```

4. Implement OAuth 2.0 flow and initialize ````SheetHelper```` with user credentials:

```csharp
using Google.Apis.Auth.OAuth2;
using Google.Apis.Util.Store;
using System.IO;
using System.Threading;

// Load client secrets from the OAuth credentials file you downloaded
UserCredential credential;
using (var stream = new FileStream("client_secrets.json", FileMode.Open, FileAccess.Read))
{
string credPath = "token.json";
credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.FromStream(stream).Secrets,
new[] { SheetsService.Scope.Spreadsheets },
"user",
CancellationToken.None,
new FileDataStore(credPath, true));
}

// Create a SheetHelper without service account email (not needed for OAuth)
var sheetHelper = new SheetHelper<TestRecord>(
settings.GoogleSpreadsheetId,
settings.GoogleMainSheetName);

// Initialize with the OAuth credential
sheetHelper.Init(credential);
```

You can also use OAuth credentials with ````SheetAppender```` and ````SheetExporter````:

```csharp
// Using SheetAppender with OAuth
var appender = new SheetAppender(
settings.GoogleSpreadsheetId,
settings.GoogleMainSheetName);
appender.Init(credential);

// Using SheetExporter with OAuth
var exporter = new SheetExporter(
settings.GoogleSpreadsheetId,
settings.GoogleMainSheetName);
exporter.Init(credential);
```

For more information on OAuth 2.0 authentication with Google APIs:
- https://developers.google.com/identity/protocols/oauth2
- https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth
100 changes: 100 additions & 0 deletions src/GoogleSheetsWrapper.Tests/AuthenticationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using NUnit.Framework;
using Google.Apis.Auth.OAuth2;

namespace GoogleSheetsWrapper.Tests
{
[TestFixture]
public class AuthenticationTests
{
[Test]
public void SheetHelperConstructorWithoutServiceAccountEmailDoesNotThrow()
{
// Arrange & Act
var sheetHelper = new SheetHelper("testSpreadsheetId", "testTab");

// Assert
Assert.That(sheetHelper, Is.Not.Null);
Assert.That(sheetHelper.SpreadsheetID, Is.EqualTo("testSpreadsheetId"));
Assert.That(sheetHelper.TabName, Is.EqualTo("testTab"));
}

[Test]
public void SheetHelperConstructorWithServiceAccountEmailDoesNotThrow()
{
// Arrange & Act
var sheetHelper = new SheetHelper("testSpreadsheetId", "service@account.com", "testTab");

// Assert
Assert.That(sheetHelper, Is.Not.Null);
Assert.That(sheetHelper.SpreadsheetID, Is.EqualTo("testSpreadsheetId"));
Assert.That(sheetHelper.ServiceAccountEmail, Is.EqualTo("service@account.com"));
Assert.That(sheetHelper.TabName, Is.EqualTo("testTab"));
}

[Test]
public void GenericSheetHelperConstructorWithoutServiceAccountEmailDoesNotThrow()
{
// Arrange & Act
var sheetHelper = new SheetHelper<TestObjects.TestRecord>("testSpreadsheetId", "testTab");

// Assert
Assert.That(sheetHelper, Is.Not.Null);
Assert.That(sheetHelper.SpreadsheetID, Is.EqualTo("testSpreadsheetId"));
Assert.That(sheetHelper.TabName, Is.EqualTo("testTab"));
}

[Test]
public void SheetAppenderConstructorWithoutServiceAccountEmailDoesNotThrow()
{
// Arrange & Act
var appender = new SheetAppender("testSpreadsheetId", "testTab");

// Assert
Assert.That(appender, Is.Not.Null);
}

[Test]
public void SheetExporterConstructorWithoutServiceAccountEmailDoesNotThrow()
{
// Arrange & Act
var exporter = new SheetExporter("testSpreadsheetId", "testTab");

// Assert
Assert.That(exporter, Is.Not.Null);
}

[Test]
public void SheetHelperInitWithICredentialMethodExists()
{
// Arrange
var sheetHelper = new SheetHelper("testSpreadsheetId", "testTab");

// Act & Assert - verifies the method signature exists and accepts ICredential
// Note: We can't fully test Init without a real Google Sheets service
var initMethod = typeof(SheetHelper).GetMethod("Init", new[] { typeof(ICredential) });
Assert.That(initMethod, Is.Not.Null, "Init(ICredential) method should exist");
}

[Test]
public void SheetAppenderInitWithICredentialMethodExists()
{
// Arrange
var appender = new SheetAppender("testSpreadsheetId", "testTab");

// Act & Assert - verifies the method signature exists and accepts ICredential
var initMethod = typeof(SheetAppender).GetMethod("Init", new[] { typeof(ICredential) });
Assert.That(initMethod, Is.Not.Null, "Init(ICredential) method should exist");
}

[Test]
public void SheetExporterInitWithICredentialMethodExists()
{
// Arrange
var exporter = new SheetExporter("testSpreadsheetId", "testTab");

// Act & Assert - verifies the method signature exists and accepts ICredential
var initMethod = typeof(SheetExporter).GetMethod("Init", new[] { typeof(ICredential) });
Assert.That(initMethod, Is.Not.Null, "Init(ICredential) method should exist");
}
}
}
23 changes: 21 additions & 2 deletions src/GoogleSheetsWrapper/SheetAppender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class SheetAppender
private readonly SheetHelper _sheetHelper;

/// <summary>
/// Constructor
/// Constructor for use with service account authentication
/// </summary>
/// <param name="spreadsheetID"></param>
/// <param name="serviceAccountEmail"></param>
Expand All @@ -28,6 +28,16 @@ public SheetAppender(string spreadsheetID, string serviceAccountEmail, string ta
_sheetHelper = new SheetHelper(spreadsheetID, serviceAccountEmail, tabName);
}

/// <summary>
/// Constructor for use with OAuth or other credential types
/// </summary>
/// <param name="spreadsheetID"></param>
/// <param name="tabName"></param>
public SheetAppender(string spreadsheetID, string tabName)
{
_sheetHelper = new SheetHelper(spreadsheetID, tabName);
}

/// <summary>
/// Constructor
/// </summary>
Expand All @@ -38,14 +48,23 @@ public SheetAppender(SheetHelper sheetHelper)
}

/// <summary>
/// Initializes the SheetAppender object with authentication to Google Sheets API
/// Initializes the SheetAppender object with service account authentication to Google Sheets API
/// </summary>
/// <param name="jsonCredentials"></param>
public void Init(string jsonCredentials)
{
_sheetHelper.Init(jsonCredentials);
}

/// <summary>
/// Initializes the SheetAppender object with a credential (supports OAuth user credentials, service account credentials, etc.)
/// </summary>
/// <param name="credential">Google credential (e.g., UserCredential from OAuth, ServiceAccountCredential, etc.)</param>
public void Init(Google.Apis.Auth.OAuth2.ICredential credential)
{
_sheetHelper.Init(credential);
}

/// <summary>
/// Appends a CSV file and all its rows into the current Google Sheets tab
/// </summary>
Expand Down
23 changes: 21 additions & 2 deletions src/GoogleSheetsWrapper/SheetExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class SheetExporter
private readonly SheetHelper _sheetHelper;

/// <summary>
/// Constructor
/// Constructor for use with service account authentication
/// </summary>
/// <param name="spreadsheetID"></param>
/// <param name="serviceAccountEmail"></param>
Expand All @@ -26,6 +26,16 @@ public SheetExporter(string spreadsheetID, string serviceAccountEmail, string ta
_sheetHelper = new SheetHelper(spreadsheetID, serviceAccountEmail, tabName);
}

/// <summary>
/// Constructor for use with OAuth or other credential types
/// </summary>
/// <param name="spreadsheetID"></param>
/// <param name="tabName"></param>
public SheetExporter(string spreadsheetID, string tabName)
{
_sheetHelper = new SheetHelper(spreadsheetID, tabName);
}

/// <summary>
/// Constructor
/// </summary>
Expand All @@ -36,14 +46,23 @@ public SheetExporter(SheetHelper sheetHelper)
}

/// <summary>
///
/// Initializes the SheetExporter object with service account authentication to Google Sheets API
/// </summary>
/// <param name="jsonCredentials"></param>
public void Init(string jsonCredentials)
{
_sheetHelper.Init(jsonCredentials);
}

/// <summary>
/// Initializes the SheetExporter object with a credential (supports OAuth user credentials, service account credentials, etc.)
/// </summary>
/// <param name="credential">Google credential (e.g., UserCredential from OAuth, ServiceAccountCredential, etc.)</param>
public void Init(Google.Apis.Auth.OAuth2.ICredential credential)
{
_sheetHelper.Init(credential);
}

/// <summary>
/// Exports the current Google Sheet tab to a CSV file
/// </summary>
Expand Down
53 changes: 50 additions & 3 deletions src/GoogleSheetsWrapper/SheetHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public class SheetHelper
private bool IsInitialized;

/// <summary>
/// Constructor
/// Constructor for use with service account authentication
/// </summary>
/// <param name="spreadsheetID"></param>
/// <param name="serviceAccountEmail"></param>
Expand All @@ -65,6 +65,17 @@ public SheetHelper(string spreadsheetID, string serviceAccountEmail, string tabN
TabName = tabName;
}

/// <summary>
/// Constructor for use with OAuth or other credential types that don't require a service account email
/// </summary>
/// <param name="spreadsheetID"></param>
/// <param name="tabName"></param>
public SheetHelper(string spreadsheetID, string tabName)
{
SpreadsheetID = spreadsheetID;
TabName = tabName;
}

/// <summary>
/// Initializes the SheetHelper object
/// </summary>
Expand Down Expand Up @@ -106,6 +117,34 @@ public void Init(string jsonCredentials, Google.Apis.Http.IHttpClientFactory htt
UpdateTabName(TabName);
}

/// <summary>
/// Initializes the SheetHelper object with a credential (supports OAuth user credentials, service account credentials, etc.)
/// </summary>
/// <param name="credential">Google credential (e.g., UserCredential from OAuth, ServiceAccountCredential, etc.)</param>
public void Init(ICredential credential)
{
Init(credential, default);
}

/// <summary>
/// Initializes the SheetHelper object with a credential and optional HTTP client factory
/// </summary>
/// <param name="credential">Google credential (e.g., UserCredential from OAuth, ServiceAccountCredential, etc.)</param>
/// <param name="httpClientFactory">Optional HTTP client factory</param>
public void Init(ICredential credential, Google.Apis.Http.IHttpClientFactory httpClientFactory)
{
var service = new SheetsService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
HttpClientFactory = httpClientFactory,
});

Service = service;
IsInitialized = true;

UpdateTabName(TabName);
}

/// <summary>
/// Throws ArgumentException if the Init() method has not been called yet.
/// </summary>
Expand All @@ -114,7 +153,7 @@ protected void EnsureServiceInitialized()
{
if (!IsInitialized)
{
throw new ArgumentException("SheetHelper requires the Init(string jsonCredentials) method to be called before using any of its methods.");
throw new ArgumentException("SheetHelper requires an Init() method to be called before using any of its methods.");
}
}

Expand Down Expand Up @@ -550,14 +589,22 @@ private BatchUpdateSpreadsheetResponse CreateNewTab(string newTabName)
public class SheetHelper<T> : SheetHelper where T : BaseRecord
{
/// <summary>
/// Constructor
/// Constructor for use with service account authentication
/// </summary>
/// <param name="spreadsheetID"></param>
/// <param name="serviceAccountEmail"></param>
/// <param name="tabName"></param>
public SheetHelper(string spreadsheetID, string serviceAccountEmail, string tabName)
: base(spreadsheetID, serviceAccountEmail, tabName) { }

/// <summary>
/// Constructor for use with OAuth or other credential types that don't require a service account email
/// </summary>
/// <param name="spreadsheetID"></param>
/// <param name="tabName"></param>
public SheetHelper(string spreadsheetID, string tabName)
: base(spreadsheetID, tabName) { }

/// <summary>
/// Adds a record to the next row in the Google Sheet tab
/// </summary>
Expand Down
Loading