A high-performance .NET 10.0 External Logic component for OutSystems Developer Cloud (ODC) that provides a seamless integration with Google Cloud Storage (GCS). Designed for enterprise-grade scalability, security, and developer efficiency.
- Architecture
- Prerequisites
- Quick Start
- Authentication
- Action Reference
- Data Structures
- Project Structure
- Build and Deployment
- Best Practices
- License
GoogleCloudStorage_ODC/
├── GoogleCloudStorage.csproj # Project definition
├── IGoogleCloudStorage.cs # ODC External Logic Interface
├── GoogleCloudStorage.cs # Implementation logic (Adapter)
├── Resources/ # Embedded branded icons
└── Structures/ # Strongly-typed ODC structures
The connector is architected as an adapter. It bridges the OutSystems Developer Cloud runtime with the official Google Cloud Storage .NET SDK using the Bridge Pattern. This ensures that the OutSystems application logic remains decoupled from the low-level SDK implementation details.
- Cached, thread-safe clients:
StorageClientandUrlSignerinstances are cached per service account (keyed by a SHA-256 hash of the credentials, never the raw key) and reused across requests. This avoids re-parsing the RSA private key and allocating a newHttpClienton every call — both types are thread-safe, so sharing them is safe under high concurrency and prevents socket exhaustion. - Actionable errors: Google API failures are translated into clear, actionable messages (missing bucket vs. object, access denied, unauthenticated, bucket-not-empty, credential mismatch), with the original exception preserved as the inner exception for diagnostics.
- V4 Signed URLs: Offloads large file data transfers directly to the client browser, bypassing the ODC server to optimize memory and bandwidth.
- Resource Embedding: Branded icons are embedded directly into the assembly to provide a premium integrated experience in Service Studio.
- OutSystems Developer Cloud (ODC)
- .NET 10.0 SDK
- An active Google Cloud Project with Billing enabled.
- A Service Account with the following IAM roles:
Storage Object Admin(full object control)Storage Admin(required for bucket management)Service Account Token Creator(mandatory for Signed URLs)
# Build the project
dotnet build GoogleCloudStorage.csproj -c Release
# Publish for ODC (standard deployment)
dotnet publish GoogleCloudStorage.csproj -c Release -f net10.0 --no-self-containedAfter publishing, zip the contents of the publish/ folder (excluding OutSystems.ExternalLibraries.SDK.dll) and upload it to the ODC Portal.
Authentication is handled via the Authentication structure. Credentials should be stored securely in ODC App Settings (Site Properties) and passed to each action at runtime.
| Parameter | Source in GCP JSON | Description |
|---|---|---|
ProjectId |
project_id |
Your Google Cloud Project ID |
ClientEmail |
client_email |
Service Account identification email |
PrivateKey |
private_key |
Full RSA Private Key (with BEGIN/END headers) |
Persists a file to a specific GCS bucket, optionally with custom metadata.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Destination bucket |
objectName |
Text |
Full path/filename in the bucket |
file |
File |
Structure containing Binary Content and ContentType |
metadata |
List of MetadataEntry |
Optional custom key-value metadata to store with the object (e.g. tenant, document type). Retrievable via Object_GetMetadata. Leave empty for none. |
Retrieves a file and its metadata from GCS.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Source bucket |
objectName |
Text |
Full path/filename in the bucket |
Outputs:
| Output | Type | Description |
|---|---|---|
file |
File |
Structure containing Binary Content and system ContentType |
Lists objects in a bucket, optionally filtered by prefix, with support for pagination (MaxResults/PageToken) and folder-style navigation (Delimiter).
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Source bucket |
prefix |
Text |
Prefix filter for hierarchical navigation |
maxResults |
Integer |
Maximum objects to return in this call; 0 returns everything. When greater than 0, use NextPageToken to fetch the next page. |
pageToken |
Text |
Continuation token from a previous call's NextPageToken; empty starts from the first page |
delimiter |
Text |
Typically / — groups nested objects into PrefixList for folder-style browsing; empty lists recursively |
Outputs:
| Output | Type | Description |
|---|---|---|
objects |
List of Object |
Collection of GCS object metadata |
nextPageToken |
Text |
Non-empty when more results exist (paged mode only) — pass it as PageToken in the next call |
prefixList |
List of Prefix |
The "folders" found directly under Prefix when Delimiter is set |
Pagination: pass a
MaxResultsgreater than0to return a single page, then feed the returnedNextPageTokenback asPageTokenuntil it comes back empty. WithMaxResults = 0every object is returned in one call (noNextPageToken).
Checks whether an object exists in a bucket via a lightweight metadata probe.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Source bucket |
objectName |
Text |
Full path/filename to check |
Outputs:
| Output | Type | Description |
|---|---|---|
exists |
Boolean |
True if the object exists |
Retrieves an object's full metadata (size, content type, hashes, generation, storage class, timestamps, and custom metadata) without downloading its content. Returns Exists = False if the object is not found, leaving the metadata and customMetadata outputs empty.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Source bucket |
objectName |
Text |
Full path/filename to inspect |
Outputs:
| Output | Type | Description |
|---|---|---|
exists |
Boolean |
True if the object was found |
metadata |
ObjectMetadata |
Full object metadata (only populated when exists is True) |
customMetadata |
List of MetadataEntry |
The object's custom key-value metadata. Empty when the object has none or does not exist. |
Permanently removes an object from a bucket.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Source bucket |
objectName |
Text |
Full path/filename to delete |
Copies an object to another location, within the same bucket or across buckets, without downloading its content. Overwrites the destination if it exists.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
sourceBucketName |
Text |
Bucket that currently contains the object |
sourceObjectName |
Text |
Full path/filename of the source object |
destinationBucketName |
Text |
Bucket to copy into (can equal the source) |
destinationObjectName |
Text |
Full path/filename for the destination |
Moves an object to another location (copy + delete of the source), within the same bucket or across buckets. Use the same source and destination bucket to rename. Overwrites the destination if it exists.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
sourceBucketName |
Text |
Bucket that currently contains the object |
sourceObjectName |
Text |
Full path/filename of the source object |
destinationBucketName |
Text |
Bucket to move into (can equal the source) |
destinationObjectName |
Text |
Full path/filename for the destination |
Note: Move is copy-then-delete and is not atomic. If the copy succeeds but the source can't be deleted, the action reports explicitly that both objects now exist.
Changes an object's content headers and custom metadata without re-uploading its content. Only the fields you provide change: empty text inputs are left untouched, and an empty metadata list leaves custom metadata untouched. Within metadata, an entry with an empty Value removes that key. A call with nothing to update is rejected. The write is guarded by a metageneration precondition, so concurrent metadata updates fail cleanly instead of silently overwriting each other.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Source bucket |
objectName |
Text |
Full path/filename of the object to update |
contentType |
Text |
New MIME type. Empty = unchanged. |
contentEncoding |
Text |
New content encoding (e.g. gzip). Empty = unchanged. |
contentDisposition |
Text |
New content disposition (e.g. attachment; filename="report.pdf"). Empty = unchanged. |
cacheControl |
Text |
New cache control (e.g. public, max-age=3600). Empty = unchanged. |
metadata |
List of MetadataEntry |
Custom metadata changes. Empty list = unchanged. An entry with an empty Value removes that key; others are set/overwritten. |
Deletes every object under a prefix (a "folder" and everything below it) server-side, and returns how many were deleted. The prefix is mandatory and cannot be empty, as a safety guard against wiping an entire bucket. Concurrent deletions are tolerated; a mid-operation failure reports exactly how many objects were already deleted.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Source bucket |
prefix |
Text |
All objects whose names start with this prefix are deleted (e.g. uploads/2025/). Cannot be empty. |
Outputs:
| Output | Type | Description |
|---|---|---|
deletedCount |
Long Integer |
Number of objects that were deleted |
Note: Move is copy-then-delete and is not atomic — the source is removed only after a successful copy.
Generates a time-limited V4 signed URL for secure, direct-to-browser file access. The operation controls the action the URL permits: download, upload, or delete.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Source bucket |
objectName |
Text |
Full path/filename |
expirationMinutes |
Integer |
Link validity duration, 1–10080 (a V4 signed URL is valid for at most 7 days). Values outside this range raise a clear error. |
operation |
Text |
Optional. Download (GET), Upload (PUT), or Delete (DELETE). Case-insensitive. Defaults to Download. |
contentType |
Text |
Optional, for Upload URLs. The exact Content-Type the client will send in the PUT request. It becomes part of the signature, so Google rejects uploads with a different Content-Type. Leave empty to allow any. |
Outputs:
| Output | Type | Description |
|---|---|---|
url |
Text |
Temporary secure URL. For Upload, the client sends an HTTP PUT with the file as the body. |
Multi-upload: signed URLs are bound to a specific object path, so request one
UploadURL per file (pass each file'sobjectName).Content-Type binding: if you pass
contentType, the client's PUT must send exactly thatContent-Typeheader, or Google rejects the upload with a signature mismatch. Leave it empty to accept any content type.
Lists all buckets in the specified project.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
Outputs:
| Output | Type | Description |
|---|---|---|
buckets |
List of Bucket |
Collection of project bucket metadata |
Provisions a new globally unique storage container.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Globally unique name |
location |
Text |
Geographic region (e.g., US, EU, asia-east1) |
Decommissioning of an empty storage container.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Name of the bucket to delete |
Checks whether a bucket exists and is accessible to the service account, without listing its contents.
Arguments:
| Argument | Type | Description |
|---|---|---|
authentication |
Authentication |
GCP credentials |
bucketName |
Text |
Name of the bucket to check |
Outputs:
| Output | Type | Description |
|---|---|---|
exists |
Boolean |
True if the bucket exists and the service account can access it |
Encapsulates Google Cloud Service Account credentials.
ProjectId: TextClientEmail: TextPrivateKey: Text
Used for binary data exchange.
Content: Binary DataContentType: Text (MIME type)
Represents object metadata.
Name: Text (Full path)Size: Long IntegerContentType: TextUpdated: Date Time (UTC)
Represents storage container metadata.
Name: TextLocation: TextStorageClass: TextCreated: Date Time (UTC)
A folder-style entry returned by Object_List when Delimiter is set — a common prefix shared by the objects grouped under it.
Value: Text (e.g.,images/thumbnails/)
A single custom metadata key-value pair stored with an object. Used by Object_Upload (input), Object_GetMetadata (output), and Object_UpdateMetadata (input).
Key: Text (e.g.,tenant,documentType)Value: Text (an emptyValueremoves the key inObject_UpdateMetadata)
Represents the complete metadata of an object (returned by Object_GetMetadata).
Name: Text (Full path)Bucket: TextSize: Long IntegerContentType: TextContentEncoding: TextContentDisposition: TextCacheControl: TextMD5Hash: TextCrc32c: TextETag: TextGeneration: Long IntegerMetageneration: Long IntegerStorageClass: TextMediaLink: TextTimeCreated: Date Time (UTC)Updated: Date Time (UTC)
GoogleCloudStorage_ODC/
├── GoogleCloudStorage.csproj # Dependencies: Google.Cloud.Storage.V1, Google.Apis.Auth
├── IGoogleCloudStorage.cs # OSInterface & OSAction definitions
├── GoogleCloudStorage.cs # StorageClient implementation & credential handling
├── Resources/ # Branding assets
│ ├── app_icon.png # Library icon
│ └── action_icon.png # Action-level icon
└── Structures/ # ODC-compatible structs
├── Authentication.cs # Credential model
├── File.cs # Binary wrapper
├── Bucket.cs # Container metadata
├── Object.cs # File metadata (list entry)
├── ObjectMetadata.cs # Full object metadata
├── Prefix.cs # Folder-style entry (Object_List with Delimiter)
└── MetadataEntry.cs # Custom key-value metadata pair
- Publish: Run
dotnet publishas shown in Quick Start. - Clean: Delete
OutSystems.ExternalLibraries.SDK.dllfrom thepublish/directory. - Zip: Compress all remaining files into a flat structure (no subfolders).
- Deploy: Upload to ODC Portal > External Logic.
A full test suite lives in tests/ and covers every action — no Google account or credentials required:
dotnet test # offline + emulator integration
dotnet test --filter "FullyQualifiedName~OfflineTests" # offline only, no network- Offline tests (signed URLs, validation, caching) use a throwaway in-memory RSA key — V4 signing is local cryptography.
- Integration tests run the connector against fake-gcs-server via the
GCSCONNECTOR_EMULATOR_HOSThook. This variable is honored only for local testing and is never set on a real ODC server, where the connector always talks to production GCS. The test fixture starts the emulator automatically and skips (rather than fails) if it can't. Seetests/README.mdfor details.
- Security: Mark
PrivateKeyas a Secret App Setting in ODC to ensure it is encrypted and masked in logs. - Efficiency: For files larger than 100MB, always use
Object_GetSignedUrlto avoid server-side memory pressure. - Naming: Follow GCS bucket naming constraints (3-63 characters, lowercase letters, numbers, and hyphens).
- Tag your objects: Use the
metadatainput onObject_Upload(orObject_UpdateMetadatalater) to attach business context — tenant, owner, document type — that you can read back cheaply withObject_GetMetadatawithout downloading the file. - Edit metadata in place: Use
Object_UpdateMetadatato fix aContentTypeor relabel objects without re-uploading their content; it is metageneration-guarded so concurrent edits fail cleanly. - Delete folders safely: Use
Object_DeleteByPrefixto remove a whole "folder" in one call — the mandatory, non-empty prefix guards against wiping an entire bucket by accident.
This project is licensed under the MIT License - see the LICENSE file for details.