Home β€Ί .NET on AWS β€Ί Using S3 with .NET

Using S3 with .NET

Working with Amazon S3 from .NET: uploads, downloads, presigned URLs, streaming large files, and CDK setup.

S3 from .NET

S3 looks simple. Put objects, get objects, list objects. But in a real application you need presigned URLs (so clients upload directly without proxying through your API), multipart uploads for large files, streaming downloads without loading everything into memory, and event-driven processing when files land.

The .NET SDK for S3 is mature and well-designed. Most operations are simple.

Basic Operations

Setup

using Amazon.S3;
using Amazon.S3.Model;

var s3Client = new AmazonS3Client(); // uses default credentials chain

Upload an object

await s3Client.PutObjectAsync(new PutObjectRequest
{
    BucketName = "my-bucket",
    Key = $"uploads/{userId}/{fileName}",
    InputStream = fileStream,
    ContentType = "application/pdf",
    Metadata =
    {
        ["x-amz-meta-uploaded-by"] = userId,
        ["x-amz-meta-original-name"] = fileName,
    },
});

Download an object

var response = await s3Client.GetObjectAsync(new GetObjectRequest
{
    BucketName = "my-bucket",
    Key = "uploads/user123/report.pdf",
});

// Stream it β€” don't load the whole thing into memory
await using var responseStream = response.ResponseStream;
await responseStream.CopyToAsync(outputStream);

Check if an object exists

try
{
    await s3Client.GetObjectMetadataAsync(new GetObjectMetadataRequest
    {
        BucketName = "my-bucket",
        Key = "uploads/user123/report.pdf",
    });
    // exists
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
    // doesn't exist
}

Presigned URLs

Presigned URLs let clients upload or download directly to/from S3 without your API proxying the bytes. Essential for large files. Your Lambda doesn't need the memory or the 30-second timeout.

Presigned upload URL

var request = new GetPreSignedUrlRequest
{
    BucketName = "my-bucket",
    Key = $"uploads/{userId}/{Guid.NewGuid()}/{fileName}",
    Verb = HttpVerb.PUT,
    Expires = DateTime.UtcNow.AddMinutes(15),
    ContentType = contentType,
};

var url = s3Client.GetPreSignedURL(request);
// Return this URL to the client β€” they PUT directly to S3

Presigned download URL

var request = new GetPreSignedUrlRequest
{
    BucketName = "my-bucket",
    Key = objectKey,
    Verb = HttpVerb.GET,
    Expires = DateTime.UtcNow.AddMinutes(60),
    ResponseHeaderOverrides = new ResponseHeaderOverrides
    {
        ContentDisposition = $"attachment; filename=\"{fileName}\"",
    },
};

var url = s3Client.GetPreSignedURL(request);

Presigned POST (for browser form uploads)

For browser-based uploads with size limits, content type restrictions, and other conditions:

// Note: presigned POST is not directly in the SDK β€” use presigned PUT 
// with CORS configured on the bucket, or use the Transfer Utility

Multipart Uploads

For files over 100MB, multipart uploads are more reliable (each part can be retried independently) and faster (parts upload in parallel).

Using TransferUtility (recommended)

using Amazon.S3.Transfer;

var transferUtility = new TransferUtility(s3Client);

await transferUtility.UploadAsync(new TransferUtilityUploadRequest
{
    BucketName = "my-bucket",
    Key = "large-files/backup.zip",
    FilePath = "/tmp/backup.zip", // or use InputStream
    PartSize = 10 * 1024 * 1024, // 10MB parts
    CannedACL = S3CannedACL.Private,
});

TransferUtility handles multipart upload/download automatically, including retries for individual parts.

Processing S3 Events with Lambda

When a file lands in S3, trigger a Lambda to process it:

using Amazon.Lambda.S3Events;

public async Task Handler(S3Event s3Event, ILambdaContext context)
{
    foreach (var record in s3Event.Records)
    {
        var bucket = record.S3.Bucket.Name;
        var key = Uri.UnescapeDataString(record.S3.Object.Key);
        var size = record.S3.Object.Size;
        
        context.Logger.LogInformation($"Processing {key} ({size} bytes) from {bucket}");
        
        var response = await s3Client.GetObjectAsync(bucket, key);
        await using var stream = response.ResponseStream;
        
        // Process the file...
        await ProcessFileAsync(stream, key);
    }
}

Important: Always URL-decode the key. S3 event notifications URL-encode special characters (spaces become +, etc.).

CDK Setup (C#)

using Amazon.CDK;
using Amazon.CDK.AWS.S3;
using Amazon.CDK.AWS.Lambda.EventSources;

var bucket = new Bucket(this, "UploadsBucket", new BucketProps
{
    BucketName = "my-app-uploads",
    Encryption = BucketEncryption.S3_MANAGED,
    BlockPublicAccess = BlockPublicAccess.BLOCK_ALL,
    RemovalPolicy = RemovalPolicy.RETAIN,
    LifecycleRules = new[]
    {
        new LifecycleRule
        {
            // Move to Infrequent Access after 30 days
            Transitions = new[]
            {
                new Transition
                {
                    StorageClass = StorageClass.INFREQUENT_ACCESS,
                    TransitionAfter = Duration.Days(30),
                },
            },
            // Delete after 1 year
            Expiration = Duration.Days(365),
        },
    },
    Cors = new[]
    {
        new CorsRule
        {
            AllowedMethods = new[] { HttpMethods.PUT, HttpMethods.GET },
            AllowedOrigins = new[] { "https://myapp.com" },
            AllowedHeaders = new[] { "*" },
            MaxAge = 3600,
        },
    },
});

// Trigger Lambda on upload
processorFunction.AddEventSource(new S3EventSource(bucket, new S3EventSourceProps
{
    Events = new[] { EventType.OBJECT_CREATED },
    Filters = new[] { new NotificationKeyFilter { Prefix = "uploads/" } },
}));

// Grant presigned URL generation
bucket.GrantReadWrite(apiFunction);

Tips for .NET Developers

  • Always stream, never buffer large files. GetObjectResponse.ResponseStream lets you process data without loading it all into memory. Critical in Lambda where memory = cost.
  • Use TransferUtility for anything over 10MB. It handles multipart automatically and is more resilient than PutObjectAsync for large files.
  • S3 is eventually consistent for overwrites and deletes. If you PUT to the same key twice quickly, readers might get the old version briefly. Plan for this in your application logic.
  • Don't use S3 as a database. Listing objects is slow and expensive at scale. Store metadata in DynamoDB and use S3 for the actual blobs.
  • Enable versioning for anything important. It protects against accidental overwrites and deletions.

Further Reading

Looking for hands-on help? View my .NET on AWS services β†’

Building file handling into your .NET app?

Drop me a message β€” I typically respond within one business day.