Skip to main content

Oracle Cloud Infrastructure Provider

Vali-Blob.OCI provides OCIStorageProvider, implementing IStorageProvider, IResumableUploadProvider, and IPresignedUrlProvider backed by OCI Object Storage via the official OCI .NET SDK.

Presigned URLs are implemented using OCI Pre-Authenticated Requests (PARs) — unlike AWS and GCP which generate signed URLs locally in memory, OCI requires an API call to create each PAR. See Presigned URLs for details.


Installation

dotnet add package Vali-Blob.Core
dotnet add package Vali-Blob.OCI

OCIStorageOptions Reference

OptionTypeRequiredDescription
NamespacestringYesOCI Object Storage namespace (unique per tenancy).
BucketNamestringYesObject Storage bucket name.
RegionstringYesOCI region identifier, e.g. "us-ashburn-1", "eu-frankfurt-1".
TenancyIdstringYesOCID of the tenancy, e.g. "ocid1.tenancy.oc1..aaa...".
UserIdstringYesOCID of the API signing user, e.g. "ocid1.user.oc1..aaa...".
FingerPrintstringYesFingerprint of the API signing key, e.g. "xx:yy:zz:...".
PrivateKeystringYesRSA private key in PEM format used to sign API requests.

DI Registration

using Vali-Blob.Core;
using Vali-Blob.OCI;

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "oci")
.AddProvider<OCIStorageProvider>("oci", opts =>
{
opts.Namespace = builder.Configuration["OCI:Namespace"]!;
opts.BucketName = builder.Configuration["OCI:BucketName"]!;
opts.Region = builder.Configuration["OCI:Region"]!;
opts.TenancyId = builder.Configuration["OCI:TenancyId"]!;
opts.UserId = builder.Configuration["OCI:UserId"]!;
opts.FingerPrint = builder.Configuration["OCI:FingerPrint"]!;
opts.PrivateKey = builder.Configuration["OCI:PrivateKey"]!;
})
.WithPipeline(p => p
.UseValidation(v =>
{
v.MaxFileSizeBytes = 500_000_000;
v.AllowedExtensions = [".jpg", ".png", ".pdf", ".mp4"];
})
.UseContentTypeDetection()
.UseConflictResolution(ConflictResolution.ReplaceExisting)
);
Protect your private key

The RSA private key signs every API request. Store it in OCI Vault, a Kubernetes Secret, or an environment variable. Never commit .pem files or PEM content to source control.


Finding Your Namespace

The Object Storage namespace is a short alphanumeric string unique to your tenancy.

Via OCI Console:

  1. Click the profile icon (top-right) → Tenancy: <name>
  2. The Object Storage Namespace appears in the tenancy details page.

Via OCI CLI:

oci os ns get

Obtaining API Signing Credentials

Generate an RSA Key Pair

# Generate 2048-bit RSA private key
openssl genrsa -out oci_api_key.pem 2048

# Extract the public key for upload to OCI
openssl rsa -in oci_api_key.pem -pubout -out oci_api_key_public.pem

# Compute the fingerprint (matches what OCI shows after uploading the public key)
openssl rsa -in oci_api_key.pem -pubout -outform DER | openssl md5 -c

Upload the Public Key to OCI

  1. OCI Console → Identity & SecurityUsers
  2. Open your API user (or create a dedicated service user)
  3. Scroll to API KeysAdd API KeyPaste a public key
  4. Paste the contents of oci_api_key_public.pem
  5. Copy the displayed fingerprint — this is your FingerPrint value

Load Credentials in Your Application

// Option 1: From environment variable (recommended for containers)
opts.PrivateKey = Environment.GetEnvironmentVariable("OCI_PRIVATE_KEY_PEM")
?? throw new InvalidOperationException("OCI_PRIVATE_KEY_PEM is not set.");

// Option 2: From a mounted secret file (Kubernetes)
opts.PrivateKey = File.ReadAllText("/run/secrets/oci-private-key");

// Option 3: From configuration (only for development; store via dotnet user-secrets)
opts.PrivateKey = builder.Configuration["OCI:PrivateKey"]!;

IAM Policies

Grant the API user (or a group) access to Object Storage operations:

# Full object operations on a specific bucket
Allow group valiblob-group to manage objects in tenancy
where target.bucket.name = 'my-app-uploads'

# Allow namespace and bucket reads (required for list operations)
Allow group valiblob-group to read objectstorage-namespaces in tenancy
Allow group valiblob-group to read buckets in compartment my-compartment

For minimum-privilege access:

Allow group valiblob-group to use object-family in compartment my-compartment
where target.bucket.name = 'my-app-uploads'

Navigate to Identity & SecurityPoliciesCreate Policy in the OCI Console.


Creating a Bucket

# Create a bucket
oci os bucket create \
--compartment-id ocid1.compartment.oc1..aaa... \
--name my-app-uploads \
--namespace axjklopqrstu \
--versioning Disabled

# Verify
oci os bucket get \
--bucket-name my-app-uploads \
--namespace axjklopqrstu

Presigned URLs (Pre-Authenticated Requests)

OCIStorageProvider implements IPresignedUrlProvider using OCI Pre-Authenticated Requests (PARs).

How OCI PARs differ from AWS/GCP presigned URLs

This is an important architectural difference to understand before using presigned URLs with OCI:

AWS S3 / GCP Cloud StorageOCI Object Storage
How the URL is generatedSigned locally in memory using your credentials (HMAC-SHA256 / RSA) — no network callA PAR object is created on OCI's servers via an API call — requires a round-trip
Network cost per URLZero — pure CPU operationOne HTTP request to OCI per URL generated
LatencySub-millisecondDepends on network latency to OCI (~50–200 ms)
Server-side lifecycleURL is stateless — OCI/AWS/GCP have no record of itPAR is a real object: it can be listed, deactivated, or deleted from the Console or CLI
Rate limitsNone for URL generationOCI imposes API rate limits on PAR creation

Practical impact: if your application generates presigned URLs at high frequency (e.g. one per file request in a high-traffic API), the extra round-trip adds latency per URL. In low-to-moderate-traffic scenarios this is negligible.

Usage

The API is identical to other providers — Vali-Blob abstracts the difference:

var provider = factory.Create("oci");

if (provider is IPresignedUrlProvider presigned)
{
// Creates a PAR on OCI granting PUT access for 15 minutes
var uploadUrl = await presigned.GetPresignedUploadUrlAsync(
StoragePath.From("uploads", userId, "report.pdf"),
expiresIn: TimeSpan.FromMinutes(15));

// Creates a PAR on OCI granting GET access for 2 hours
var downloadUrl = await presigned.GetPresignedDownloadUrlAsync(
"private/salary-report.pdf",
expiresIn: TimeSpan.FromHours(2));

return Results.Ok(new
{
uploadUrl = uploadUrl.Value,
downloadUrl = downloadUrl.Value
});
}

Caching recommendation

Because each call to GetPresignedUploadUrlAsync / GetPresignedDownloadUrlAsync makes an HTTP request to OCI, cache the resulting URLs when the same user accesses the same resource repeatedly within the validity window.

Always include the user in the cache key

Cache keys must be scoped per user. A PAR gives unauthenticated access to the file for its entire lifetime — sharing the same URL across different users is a security risk even if the bucket is private.

// Always scope the cache key to the user AND the path
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier)!;
var cacheKey = $"oci-par:{userId}:{path}";

if (!cache.TryGetValue(cacheKey, out string? url))
{
var expiration = TimeSpan.FromHours(2);
var result = await presigned.GetPresignedDownloadUrlAsync(path, expiration);
url = result.Value;
cache.Set(cacheKey, url, expiration * 0.9); // expire cache before PAR expires
}
PAR lifecycle management

PARs are real server-side objects in OCI. You can view, deactivate, or delete them under Storage → Buckets → <bucket> → Pre-Authenticated Requests in the OCI Console, or via the OCI CLI (oci os preauth-request list). This gives you the ability to revoke access to a URL after it has been issued — something that is not possible with AWS or GCP presigned URLs.


OCI Region Identifiers

RegionIdentifier
US East (Ashburn)us-ashburn-1
US West (Phoenix)us-phoenix-1
EU (Frankfurt)eu-frankfurt-1
EU (Amsterdam)eu-amsterdam-1
UK (London)uk-london-1
AP (Tokyo)ap-tokyo-1
AP (Sydney)ap-sydney-1
Brazil (Sao Paulo)sa-saopaulo-1
Canada (Toronto)ca-toronto-1
Middle East (Dubai)me-dubai-1

Resumable Uploads (OCI Multipart Upload)

Vali-Blob maps IResumableUploadProvider to OCI's native multipart upload API:

Vali-Blob OperationOCI API Operation
StartResumableUploadAsyncCreateMultipartUpload
UploadChunkAsyncUploadPart
CompleteResumableUploadAsyncCommitMultipartUpload
AbortResumableUploadAsyncAbortMultipartUpload

OCI multipart upload constraints:

  • Minimum part size: 1 MiB (except the last part)
  • Maximum parts per upload: 10,000
  • Recommended part size: 5–100 MiB

Supported Operations

OperationSupportedNotes
UploadAsyncYesSingle PUT or multipart
DownloadAsyncYesIncluding byte range
DeleteAsyncYes
DeleteFolderAsyncYesBatch list + delete by prefix
ExistsAsyncYesHeadObject
CopyAsyncYesServer-side copy
GetMetadataAsyncYesHeadObject + custom metadata
SetMetadataAsyncYesCopy-in-place with new metadata
ListFilesAsyncYesListObjects with prefix + pagination
ListFoldersAsyncYesListObjects with delimiter
GetUrlAsyncYesPublic URL or PAR URL
StartResumableUploadAsyncYesOCI CreateMultipartUpload
UploadChunkAsyncYesOCI UploadPart
CompleteResumableUploadAsyncYesOCI CommitMultipartUpload
AbortResumableUploadAsyncYesOCI AbortMultipartUpload
GetPresignedUploadUrlAsyncYesPre-Authenticated Request (PAR)
GetPresignedDownloadUrlAsyncYesPre-Authenticated Request (PAR)

Troubleshooting

401 Unauthorized

  • Verify that TenancyId, UserId, and FingerPrint exactly match what the OCI Console shows (these values are case-sensitive).
  • Confirm the API key is Active in the user's API key list.
  • Ensure the private key PEM corresponds to the uploaded public key.

404 Not Found on Bucket Operations

  • Verify the Namespace is correct — it is case-sensitive.
  • Confirm the bucket exists in the specified region and compartment.
  • Check that IAM policies grant the user access to the compartment containing the bucket.

Clock Skew Errors

OCI request signatures include a Date header. Requests are rejected if the server clock differs by more than 5 minutes from OCI's servers. Ensure your server uses NTP and system time is accurate.

# Check system time (Linux)
timedatectl status