Working With AWS S3 in C#

Photo by Growtika on Unsplash

Working With AWS S3 in C#

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

  1. Search for S3 on AWS and click on it to open the dashboard

  2. Click on Create bucket to create a new bucket

  3. Give 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.

  1. Create a new Asp.net core API project

  2. App AWSSDK.S3 NuGet package to the project

  3. Now, 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 a 201 response code, indicating our image has been successfully uploaded

  • Calling our endpoint with a GET request, we receive a 200 response code, along with the image

  • And finally, calling our endpoint with a DELETE request, we received a 204 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!