Export files to YouTube in .NET C# using open source libraries
Exporting videos to YouTube programmatically is essential for automating video content management. This guide uses Google's official open-source .NET client library to upload videos to the YouTube Data API from a desktop or command-line application, with user authorization, progress reporting, and error handling.
Prerequisites
- .NET 6.0 or later
- Google Cloud Project with YouTube Data API enabled
- OAuth 2.0 credentials of type Desktop app from Google Cloud Console (download your
client_secrets.jsonfile), with an OAuth consent screen and an authorized test user if applicable - Google.Apis.YouTube.v3 NuGet package
Setting up the YouTube API client
First, install the required NuGet package:
dotnet add package Google.Apis.YouTube.v3 --version 1.76.0.4262
Use the following factory method to create a YouTube service instance. The first authorization opens
a browser so the channel owner can grant access. YouTube uploads require user OAuth credentials;
service accounts cannot replace this flow. The token directory stores refresh tokens and must be
private to the application user. The two partial class blocks below belong to the same project.
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.Upload;
using Google.Apis.YouTube.v3;
using Google.Apis.Util.Store;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
public partial class YouTubeUploader : IDisposable
{
private readonly YouTubeService _youtubeService;
private YouTubeUploader(YouTubeService youtubeService)
{
_youtubeService = youtubeService;
}
public static async Task<YouTubeUploader> CreateAsync(string credentialsPath, string tokenDirectory)
{
using var stream = File.OpenRead(credentialsPath);
var clientSecrets = GoogleClientSecrets.FromStream(stream).Secrets;
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
clientSecrets,
new[] { YouTubeService.Scope.YoutubeUpload },
"channel-owner",
CancellationToken.None,
new FileDataStore(tokenDirectory, true));
var youtubeService = new YouTubeService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = "YOUR_APP_NAME"
});
return new YouTubeUploader(youtubeService);
}
public void Dispose() => _youtubeService.Dispose();
}
Uploading videos
The method below handles video uploads with progress tracking and comprehensive error handling:
using System;
using System.IO;
using System.Threading.Tasks;
using Google.Apis.Upload;
using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data;
public partial class YouTubeUploader
{
public async Task<string> UploadVideoAsync(
string filePath,
string title,
string description,
string[] tags,
IProgress<IUploadProgress>? progress = null)
{
var video = new Video
{
Snippet = new VideoSnippet
{
Title = title,
Description = description,
Tags = tags,
CategoryId = "22" // People & Blogs category
},
Status = new VideoStatus
{
PrivacyStatus = "private" // or "public", "unlisted"
}
};
using var fileStream = File.OpenRead(filePath);
var videosInsertRequest = _youtubeService.Videos.Insert(
video,
"snippet,status",
fileStream,
"video/*");
videosInsertRequest.ChunkSize = ResumableUpload.MinimumChunkSize;
if (progress != null)
{
videosInsertRequest.ProgressChanged += progress.Report;
}
try
{
var uploadResponse = await videosInsertRequest.UploadAsync();
uploadResponse.ThrowOnFailure();
if (uploadResponse.Status != UploadStatus.Completed ||
string.IsNullOrEmpty(videosInsertRequest.ResponseBody?.Id))
{
throw new InvalidOperationException("Video upload did not complete.");
}
return videosInsertRequest.ResponseBody.Id;
}
catch (Google.GoogleApiException ex) when (ex.Error != null &&
(ex.Error.Code == 403 || ex.Error.Code == 429 || ex.Error.Code == 503))
{
throw new InvalidOperationException("YouTube rejected the upload. Check channel access, project quota, and service availability.", ex);
}
finally
{
if (progress != null) videosInsertRequest.ProgressChanged -= progress.Report;
}
}
}
Handling quotas and rate limits
Check your project's limits and the current YouTube quota documentation
before setting a local budget. Uploads have their own quota bucket, and daily quotas reset at
midnight Pacific Time. The following guard limits concurrency and reserves a configurable budget
before each operation, including failed attempts. Share one instance per bucket within a process.
It is an in-memory guard, so restarts, SDK retries, and other application instances still require
centralized quota monitoring. Pass the current cost of the operation as quotaCost.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public class QuotaManager : IDisposable
{
private readonly SemaphoreSlim _uploadSemaphore;
private readonly Dictionary<DateTime, int> _quotaUsage;
private readonly int _dailyLimit;
private readonly TimeZoneInfo _quotaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles");
public QuotaManager(int dailyLimit, int maxConcurrentUploads = 3)
{
if (dailyLimit <= 0 || maxConcurrentUploads <= 0) throw new ArgumentOutOfRangeException();
_dailyLimit = dailyLimit;
_uploadSemaphore = new SemaphoreSlim(maxConcurrentUploads);
_quotaUsage = new Dictionary<DateTime, int>();
}
public async Task<T> ExecuteWithQuotaAsync<T>(
Func<Task<T>> operation,
int quotaCost)
{
if (quotaCost <= 0 || quotaCost > _dailyLimit) throw new ArgumentOutOfRangeException(nameof(quotaCost));
await _uploadSemaphore.WaitAsync();
try
{
lock (_quotaUsage)
{
var today = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, _quotaTimeZone).Date;
if (!_quotaUsage.ContainsKey(today))
{
_quotaUsage.Clear();
_quotaUsage[today] = 0;
}
if (quotaCost > _dailyLimit - _quotaUsage[today])
{
throw new InvalidOperationException("Daily upload budget would be exceeded");
}
_quotaUsage[today] += quotaCost;
}
return await operation();
}
finally
{
_uploadSemaphore.Release();
}
}
public void Dispose() => _uploadSemaphore.Dispose();
}
Best practices for production use
- Implement retry logic with exponential backoff to handle transient errors such as network issues or temporary rate limits.
- Store your credentials securely to protect your OAuth 2.0 secrets.
- Monitor upload progress to give feedback and troubleshoot issues in real time.
- Ensure robust error handling to cover quota exceedance, authentication failures, and file size or network constraints.
Implement retry logic
The SDK handles resumable upload retries. Use the helper below only for operations that are safe
to repeat, such as read requests. Retrying an entire videos.insert operation can create duplicate
videos if the first upload succeeded but its response was lost.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Google;
public class RetryExamples
{
public async Task<T> RetryWithExponentialBackoff<T>(
Func<Task<T>> operation,
int maxAttempts = 3)
{
if (maxAttempts <= 0) throw new ArgumentOutOfRangeException(nameof(maxAttempts));
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
return await operation();
}
catch (Exception ex) when (IsTransientException(ex))
{
if (attempt == maxAttempts) throw;
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
await Task.Delay(delay);
}
}
throw new Exception("Retry attempts exhausted");
}
private bool IsTransientException(Exception ex)
{
return ex is HttpRequestException ||
(ex is GoogleApiException gex && (gex.Error?.Code == 429 || gex.Error?.Code == 503));
}
}
Store credentials securely
Configure YOUTUBE_CREDENTIALS_PATH in your process environment to point to the downloaded desktop
client JSON file. Keep that file and the token directory out of source control, restrict filesystem
access, and use a managed secret store for hosted deployments. This helper only locates the file;
it does not encrypt credentials. Web applications need Google's server-side OAuth flow instead of
the desktop browser flow shown here.
using System;
public class SecureCredentialManager
{
public string GetCredentialsPath()
{
var path = Environment.GetEnvironmentVariable("YOUTUBE_CREDENTIALS_PATH");
if (string.IsNullOrEmpty(path))
{
throw new InvalidOperationException("YOUTUBE_CREDENTIALS_PATH is not configured");
}
return path;
}
}
Monitor upload progress
using System;
using Google.Apis.Upload;
public class UploadProgressHandler : IProgress<IUploadProgress>
{
public void Report(IUploadProgress progress)
{
var status = progress.Status switch
{
UploadStatus.Uploading => $"Uploading: {progress.BytesSent} bytes sent",
UploadStatus.Failed => "Upload failed",
UploadStatus.Completed => "Upload completed",
_ => $"Status: {progress.Status}"
};
Console.WriteLine(status);
}
}
Conclusion
Exporting files to YouTube in .NET C# requires up-to-date authentication practices, careful quota management, and robust error handling. By integrating the Google APIs client library with asynchronous credential loading and following best practices for production deployments, you can build a resilient video upload solution. For a more streamlined approach to handling file exports and video processing at scale, consider using Transloadit's file exporting service.
