AWS S3
(Simple storage service) is an AWS service that provides users the ability to store and retrieve static data seamlessly and efficiently. In this article, we are exploring how to use AWS S3 in a .NET
application. The code for this article is available here.
Firstly, some common terms in S3
:
Bucket: A container for storing objects in Amazon S3. All objects are stored in a bucket. Each bucket has a globally unique name within Amazon S3.
Object: A unit of data stored in Amazon S3. An object consists of the data itself, a key (which is the unique identifier within a bucket), and metadata.
Key: The unique identifier for an object within a bucket. The combination of the bucket name and the object key forms the object's unique address.
Metadata: Additional information associated with an object, typically in the form of key-value pairs. Metadata provides details about the object, such as content type, creation date, etc.
Versioning: A feature that allows you to keep multiple versions of an object in the same bucket. This is useful for maintaining a version history and recovering from accidental deletions or overwrites.
Let's get started!
Create an AWS Account
First and foremost, to use any AWS service, we need an AWS account. Go over to the website here and create an account.
Create an S3 Bucket
Search for S3 on AWS and click on it to open the dashboard
Click on
Create bucket
to create a new bucketGive your bucket a name and save. (We would leave other default settings as is.)
Call your S3 bucket via your .NET
application
Our .NET
application would be an Asp.net
API responsible for uploading customer's profile pictures.
Create a new Asp.net core API project
App
AWSSDK.S3
NuGet package to the projectNow, we will implement our code.
Add the following file structure
Add the following code to their respective files:
ProfilePictureController.cs
using Microsoft.AspNetCore.Mvc; namespace Customers.Api.Controllers; [Route("api/user/{userId:guid}/profile-picture")] [ApiController] public class ProfilePictureController : ControllerBase { private readonly IStorageService _storageService; public ProfilePictureController(IStorageService storageService) { _storageService = storageService; } [HttpPost] public async Task<IActionResult> SavePicture([FromForm] IFormFile image, [FromRoute] Guid userId) { var result = await _storageService.UploadImageAsync(image, userId); return result switch { true => CreatedAtAction(nameof(GetImage), new { userId }, new {}), _ => BadRequest() }; } [HttpGet(Name = nameof(GetImage))] public async Task<IActionResult> GetImage([FromRoute] Guid userId) { var result = await _storageService.GetImageAsync(userId); if (result.responseStream is not {}) { return NotFound(); } return File(result.responseStream, result.contentType); } [HttpDelete] public async Task<IActionResult> DeleteImage([FromRoute] Guid userId) { var result = await _storageService.RemoveImageAsync(userId); return result switch { true => NoContent(), _ => BadRequest() }; } }
IStorageService.cs
namespace Customers.Api; public interface IStorageService { Task<bool> RemoveImageAsync(Guid id); Task<(Stream? responseStream, string contentType)> GetImageAsync(Guid id); Task<bool> UploadImageAsync(IFormFile image, Guid id); }
StorageService.cs
using System.Net; using Amazon.S3; using Amazon.S3.Model; using Ardalis.GuardClauses; using Microsoft.Extensions.Options; namespace Customers.Api; public class StorageService : IStorageService { private readonly IAmazonS3 _amazonS3Client; private readonly IOptions<StorageConfig> _storageOptions; private readonly ILogger<StorageService> _logger; public StorageService(IAmazonS3 amazonS3Client, IOptions<StorageConfig> storageOptions, ILogger<StorageService> logger) { _amazonS3Client = amazonS3Client; _storageOptions = storageOptions; _logger = logger; } public async Task<bool> RemoveImageAsync(Guid Id) { try { Guard.Against.Default(Id); var request = new DeleteObjectRequest() { BucketName = _storageOptions.Value.S3Bucket, Key = $"profile-picture/{Id}", }; var response = await _amazonS3Client.DeleteObjectAsync(request); if (response.HttpStatusCode == HttpStatusCode.NoContent) { return true; } } catch (Exception e) { _logger.LogError(e, $"an error has occured in {nameof(RemoveImageAsync)}"); } return false; } public async Task<(Stream? responseStream, string contentType)> GetImageAsync(Guid Id) { try { Guard.Against.Default(Id); var request = new GetObjectRequest() { BucketName = _storageOptions.Value.S3Bucket, Key = $"profile-picture/{Id}", }; var response = await _amazonS3Client.GetObjectAsync(request); if (response.HttpStatusCode == HttpStatusCode.OK) { return (responseStream: response.ResponseStream, contentType: response.Headers.ContentType); } } catch (Exception e) { _logger.LogError(e, $"an error has occured in {nameof(GetImageAsync)}"); } return default; } public async Task<bool> UploadImageAsync(IFormFile image, Guid id) { try { Guard.Against.Null(image); Guard.Against.Default(id); var putObjectRequest = new PutObjectRequest() { BucketName = _storageOptions.Value.S3Bucket, Key = $"profile-picture/{id}", InputStream = image.OpenReadStream(), ContentType = image.ContentType, Metadata = { ["x-amz-meta-original-file-name"] = image.FileName, ["x-amz-meta-original-file-extension"] = Path.GetExtension(image.FileName), } }; var response = await _amazonS3Client.PutObjectAsync(putObjectRequest); if (response.HttpStatusCode == HttpStatusCode.OK) { return true; } } catch (Exception e) { _logger.LogError(e, $"an error has occured in {nameof(UploadImageAsync)}"); } return false; } }
appsettings.json
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "StorageConfig": { "S3Bucket" : "**YOUR S3 BUCKET NAME**" } }
Program.cs
using Amazon.S3; using Customers.Api; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddScoped<IAmazonS3, AmazonS3Client>(); builder.Services.AddScoped<IStorageService, StorageService>(); builder.Services.Configure<StorageConfig>(builder.Configuration.GetSection("StorageConfig")); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.MapControllers(); app.Run();
That's it! Now, let's test our implementation
Run the just-created app and you should see a swagger documentation page like this:
To test our code, we would use Postman to call our API endpoints.
Calling our endpoint with a
POST
request, we receive a201
response code, indicating our image has been successfully uploadedCalling our endpoint with a
GET
request, we receive a200
response code, along with the imageAnd finally, calling our endpoint with a
DELETE
request, we received a204
response code, indicating our image has been successfully deleted
And that's it! The code for this article is available here.
Until next time, Cheers!