Amazon S3 (Simple Storage Service) is one of the most popular cloud storage solutions for modern applications. If you’re a .NET developer looking to integrate S3 into your application using clean architecture principles and dependency injection, this comprehensive guide will walk you through the entire process step by step.
Why Use Amazon S3 with .NET?
#Amazon S3 offers numerous benefits for .NET applications:
- Scalability: Handle virtually unlimited storage needs
- Durability: 99.999999999% (11 9’s) data durability
- Cost-effective: Pay only for what you use
- Global accessibility: Access files from anywhere in the world
- Security: Enterprise-grade security features
- Integration: Seamless integration with other AWS services
Prerequisites
#Before we begin, ensure you have:
- .NET 9.0 or later installed
- An active AWS account
- Basic knowledge of C# and dependency injection
- Visual Studio, VS Code, or JetBrains Rider
Setting Up AWS S3 and IAM
#Step 1: Create an S3 Bucket
#- Log into the AWS Management Console
- Navigate to the S3 service
- Click “Create bucket”
- Choose a unique bucket name (e.g.,
your-app-storage-bucket) - Select your preferred AWS region
- Configure bucket settings (keep defaults for this tutorial)
- Click “Create bucket”
Step 2: Create an IAM User
#- Navigate to the IAM service in AWS Console
- Click “Users” → “Create user”
- Enter a username (e.g.,
s3-dotnet-user) - Select “Programmatic access”
- Click “Next: Permissions” and select your corresponding permissions like Amazon S3 FullAccess
Step 3: Generate Access Keys
#- Complete the user creation process
- Save the Access Key ID and Secret Access Key securely
- Important: Never commit these keys to version control
Project Setup and Dependencies
#Create a new .NET console application and install the required NuGet packages:
1
2
3
4
5
6
7
| dotnet new console -n AmazonS3Sample
cd AmazonS3Sample
dotnet add package AWSSDK.S3
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.DependencyInjection
|
Implementation Overview
#Our implementation follows clean architecture principles with:
- Configuration model for AWS settings
- Service interface for abstraction
- Service implementation with proper error handling
- Dependency injection setup for clean IoC
- Logging integration for monitoring
Creating the Configuration Model
#First, let’s create a configuration model to handle AWS settings:
namespace AmazonS3Sample.Configuration;
1
2
3
4
5
6
7
8
9
10
11
| namespace AmazonS3Sample.Configuration;
public sealed class AwsStorageConfiguration
{
public static string SectionName => "Aws:Storage";
public string AccessKeyId { get; set; } = null!;
public string SecretAccessKey { get; set; } = null!;
public string Region { get; set; } = null!;
public string Bucket { get; set; } = null!;
}
|
Implementing the Storage Service
#Service Interface
#Create a clean interface that defines our S3 operations:
using System.Net.Mime;
using Amazon.S3.Model;
namespace AmazonS3Sample.Services.Interfaces;
1
2
3
4
5
6
7
8
9
10
11
12
| using System.Net.Mime;
using Amazon.S3.Model;
namespace AmazonS3Sample.Services.Interfaces;
public interface IAwsStorageClientService
{
ValueTask<IEnumerable<string>> GetFileNamesAsync();
ValueTask<GetObjectResponse> GetFileAsync(string fileName);
ValueTask UploadFileAsync(string fileName, byte[] data, ContentType contentType);
ValueTask DeleteFileAsync(string fileName);
}
|
Service Implementation
#Now, implement the service with comprehensive error handling and logging:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
| using System.Net.Mime;
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using AmazonS3Sample.Configuration;
using AmazonS3Sample.Services.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace AmazonS3Sample.Services;
public class AwsStorageClientService : IAwsStorageClientService
{
private readonly IAmazonS3 _client;
private readonly IOptions<AwsStorageConfiguration> _options;
private readonly ILogger<AwsStorageClientService> _logger;
public AwsStorageClientService(IOptions<AwsStorageConfiguration> options, ILogger<AwsStorageClientService> logger)
{
_options = options;
_logger = logger;
_client = CreateAmazonS3Client();
}
public async ValueTask<IEnumerable<string>> GetFileNamesAsync()
{
_logger.LogInformation("Getting file names from bucket {Bucket}", _options.Value.Bucket);
try
{
var result = await _client.ListObjectsV2Async(new ListObjectsV2Request
{
BucketName = _options.Value.Bucket,
});
if (result.S3Objects == null || !result.S3Objects.Any())
{
_logger.LogInformation("No files found in bucket {Bucket}", _options.Value.Bucket);
return Array.Empty<string>();
}
return result.S3Objects.Select(x => x.Key);
}
catch (Exception e)
{
_logger.LogError(e, "Error getting file names from bucket {Bucket}", _options.Value.Bucket);
throw;
}
}
public async ValueTask<GetObjectResponse> GetFileAsync(string fileName)
{
_logger.LogInformation("Getting file '{FileName}' from bucket {Bucket}", fileName, _options.Value.Bucket);
try
{
var response = await _client.GetObjectAsync(_options.Value.Bucket, fileName);
return response;
}
catch (Exception e)
{
_logger.LogError(e, "Error getting file '{FileName}' from bucket {Bucket}", fileName, _options.Value.Bucket);
throw;
}
}
public async ValueTask UploadFileAsync(string fileName, byte[] data, ContentType contentType)
{
_logger.LogInformation("Uploading file '{FileName}' to bucket {Bucket}", fileName, _options.Value.Bucket);
try
{
using var stream = new MemoryStream(data);
var request = new PutObjectRequest
{
BucketName = _options.Value.Bucket,
Key = fileName,
InputStream = stream,
ContentType = contentType.ToString()
};
var response = await _client.PutObjectAsync(request);
_logger.LogInformation("File '{FileName}' uploaded successfully with status {StatusCode}",
fileName, response.HttpStatusCode);
}
catch (Exception e)
{
_logger.LogError(e, "Error uploading file '{FileName}' to bucket {Bucket}", fileName, _options.Value.Bucket);
throw;
}
}
public async ValueTask DeleteFileAsync(string fileName)
{
_logger.LogInformation("Deleting file '{FileName}' from bucket {Bucket}", fileName, _options.Value.Bucket);
try
{
await _client.DeleteObjectAsync(_options.Value.Bucket, fileName);
_logger.LogInformation("File '{FileName}' deleted successfully", fileName);
}
catch (Exception e)
{
_logger.LogError(e, "Error deleting file '{FileName}' from bucket {Bucket}", fileName, _options.Value.Bucket);
throw;
}
}
private AmazonS3Client CreateAmazonS3Client()
{
var options = _options.Value;
return new AmazonS3Client(options.AccessKeyId, options.SecretAccessKey,
RegionEndpoint.GetBySystemName(options.Region));
}
}
|
Setting Up Dependency Injection
#Create an extension method to register our services cleanly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using AmazonS3Sample.Configuration;
using AmazonS3Sample.Services;
using AmazonS3Sample.Services.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace AmazonS3Sample.DependencyInjection;
public static class AwsStorageDependencyInjection
{
public static IServiceCollection AddAwsStorageClientService(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<AwsStorageConfiguration>(configuration.GetSection(AwsStorageConfiguration.SectionName));
services.AddTransient<IAwsStorageClientService, AwsStorageClientService>();
return services;
}
}
|
Configuration and Environment Variables
#appsettings.json Setup
#Create your configuration file (never commit real credentials):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| {
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Warning",
"Microsoft": "Warning"
}
},
"Aws": {
"Storage": {
"AccessKeyId": "YOUR_ACCESS_KEY_ID",
"SecretAccessKey": "YOUR_SECRET_ACCESS_KEY",
"Region": "us-east-1",
"Bucket": "your-bucket-name"
}
}
}
|
Important: Use UserSecrets to store sensetive data during development.
Using the Service in Your Application
#Here’s how to set up and use the service in your Program.cs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| using AmazonS3Sample.DependencyInjection;
using AmazonS3Sample.Services.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
builder.Services.AddAwsStorageClientService(builder.Configuration);
using var host = builder.Build();
// Use the service
var storageService = host.Services.GetRequiredService<IAwsStorageClientService>();
// Example operations
try
{
// List files
var fileNames = await storageService.GetFileNamesAsync();
Console.WriteLine("Files in S3 bucket:");
foreach (var fileName in fileNames)
{
Console.WriteLine($"- {fileName}");
}
// Upload a file
var testContent = "Hello, S3 from .NET!"u8.ToArray();
await storageService.UploadFileAsync("test-file.txt", testContent,
new System.Net.Mime.ContentType("text/plain"));
Console.WriteLine("File uploaded successfully!");
// Download the file
var downloadedFile = await storageService.GetFileAsync("test-file.txt");
using var reader = new StreamReader(downloadedFile.ResponseStream);
var content = await reader.ReadToEndAsync();
Console.WriteLine($"Downloaded content: {content}");
// Clean up
await storageService.DeleteFileAsync("test-file.txt");
Console.WriteLine("File deleted successfully!");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
await host.RunAsync();
|
Complete Example Repository
#You can find the complete working example on GitHub: Amazon S3 .NET Sample Repository
Conclusion
#Integrating Amazon S3 with .NET using dependency injection provides a clean, maintainable, and testable solution for cloud storage needs. By following the patterns and best practices outlined in this guide, you’ll have a robust foundation for handling file operations in your .NET applications.
The combination of proper dependency injection, configuration management, and error handling ensures your S3 integration is production-ready and follows modern .NET development practices.
Remember to always follow security best practices, never commit credentials to version control, and implement proper monitoring and logging for production environments.