I’ve written a few posts about building an API using .Net Core recently. One of the most recent requirements was to integrate the API with Azure blob storage so that we could upload files and return a list of files to the front end.
This particular application required the blob storage container to be private, and for the files only to be accessible from the front end application. The React front end used Azure Active Directory authentication to allow users access.
This post gives the basic example of how we implemented this functionality in the .Net Core API using C#.
Table of contents
Getting started
Prerequisites
I have presumed you already have an Azure account set up. Also that you’re comfortable in setting up an Azure Storage account, and creating a few containers within that.
I’ve also presumed that you’ve built a .Net Core API before, and that you’re really just here for code examples of uploading, downloading, and deleting files in Azure file storage from a C# .Net Core API.
Blob storage details
Our Azure blob storage was set up with 3 containers, one for our Dev environment, one for Staging and one for Prod. We stored the environment container name in the appsettings file, but you could just have one for development purposes to try this out.
In my examples here I’ve shown the container name in place to keep the examples focused only on Azure storage blobs and how to use them.
Once the containers are set up in Azure, the structure looks like this:
- blob-storage // this is the Azure storage account name
--- dev-storage
--- staging-storage
--- prod-storage
Store files in a Private container
Each of these containers was set to an access level of Private. This can be changed by clicking the 3 dots menu on each container in the containers list view in Azure. Then select ‘Change Access Level’ as shown here.
Install Nuget packages
You’ll need a few Nuget packages to work with Azure file storage. If you are using the command line :
dotnet add package Microsoft.Extensions.Azure
dotnet add package Azure.Storage.Blobs
Otherwise use the Nuget Package Manager and search for the package names to install them.
Now we start coding.
Add code to Program.cs
Next you need to add some code to your Program.cs file to create your BlobServiceClient instance and to register our Azure storage interface and repository.
You will need the connection string to the Azure Storage, which will be of this format:
"DefaultEndpointsProtocol=https;AccountName=storage-container-name;AccountKey=account-key;EndpointSuffix=core.windows.net"
Replace "Azure-File-Storage-Connection-String"
in the following code with your Azure Storage connection string.
using Azure.Identity; //.... builder.Services.AddAzureClients(clientBuilder => { // Add a Storage account client clientBuilder.AddBlobServiceClient("Azure-File-Storage-Connection-String")); // Use DefaultAzureCredential by default clientBuilder.UseCredential(new DefaultAzureCredential()); }); //.... //register our interface and repository builder.Services.AddTransient<IAzureStorage, AzureStorage>(); //....
Create DTO Models
In order to pass data to and from an API it’s standard practice to use DTOs (Data Transfer Objects). In this application we need two – BlobDto
, which will describe the file being passed in or out of the API. And BlobResponseDto
which as the name suggests will describe the response from the endpoints.
public class BlobDto { public string Uri { get; set; } public string Name { get; set; } public string ContentType { get; set; } public Stream Content { get; set; } public DateTimeOffset CreatedOn { get; set; } //optional parameter, so we can send and receive a custom //meta description public string? Description { get; set; } //I've included a custom comparer to enable date sorting public static int CompareByDate(BlobDto file1, BlobDto file2) { return DateTimeOffset.Compare(file1.CreatedOn, file2.CreatedOn); } }
public class BlobResponseDto { public string? Status { get; set; } public bool Error { get; set; } public BlobDto Blob { get; set; } public BlobResponseDto() { Blob = new BlobDto(); } }
Create interface and repository for storage code
Next we’ll create an interface for all the functionality associated with accessing the Azure blob storage container. We want to be able to :
- Get a list of files
- Upload a file
- Download a file
- Delete a file
So we create a definition for each method.
using DemoProject.Dto; namespace DemoProject.Interfaces { public interface IAzureStorage { Task<List<BlobDto>> ListAsync(string folderName); Task<BlobResponseDto> UploadAsync( string folderName, IFormFile file, string description); Task<BlobDto> DownloadAsync( string folderName, string blobFilename); Task<BlobResponseDto> DeleteAsync( string folderName, string blobFilename); } }
Next up we’ll create the repository file that implements this interface.
Set up the AzureStorage repository
Using Dependency Injection you pass the reference of the Blob Client into the constructor like this:
public class AzureStorage : IAzureStorage { private readonly BlobServiceClient _blobServiceClient; public AzureStorage(BlobServiceClient blobServiceClient) { _blobServiceClient = blobServiceClient; } //.... }
And now we can start working to download and upload files.
Get a list of files from Azure Blob Storage
This method retrieves a list of files from the specified folder and container.
/// <summary> /// Returns a list of files from the folder specified /// </summary> /// <param name="folderName"></param> /// <returns></returns> public async Task<List<BlobDto>> ListAsync(string folderName) { List<BlobDto> files = new List<BlobDto>(); //get the container var containerClient = _blobServiceClient.GetBlobContainerClient("container-name"); //loop through all files in the container await foreach (BlobItem file in containerClient.GetBlobsAsync(prefix: folderName)) { // Add each file retrieved from the storage container to the //files list by creating a BlobDto object string uri = containerClient.Uri.ToString(); string name = file.Name.Split("/")[1]; string type = file.Name.Split('.').Last(); string fullUri = $"{uri}/{folderName}/{name}"; files.Add(new BlobDto { Uri = fullUri, Name = name, ContentType = type, CreatedOn = (DateTimeOffset)file.Properties.CreatedOn }); } //make sure the list is sorted from newest file to oldest files = files.OrderByDescending(x => x.CreatedOn).ToList(); // Return all files to the requesting method return files; }
Note this line with the ‘prefix’ property:
await foreach (BlobItem file in containerClient.GetBlobsAsync(prefix: folderName))
Passing in the prefix: folderName
property ensures that the list of blobs returned are only from the folderName provided.
If you omit this property like this:
await foreach (BlobItem file in containerClient.GetBlobsAsync())
… will return ALL blobs from every sub folder in the container.
Upload a file to Azure blob storage container
Next we will create the method that we will use to upload files to our private Azure blob storage container.
public async Task<BlobResponseDto> UploadAsync( string folderName, IFormFile blob, string description) { // Create new upload response object that we can return to //the requesting method BlobResponseDto response = new(); var containerClient = _blobServiceClient.GetBlobContainerClient("container-name"); try { //set path for sub folder string storageContainerPath = $"/{folderName}/{blob.FileName}"; // Get a reference to the blob just uploaded from the API in a container //from configuration settings BlobClient client = containerClient.GetBlobClient(storageContainerPath); // Open a stream for the file we are uploading await using (Stream? data = blob.OpenReadStream()) { // Upload the file await client.UploadAsync(data); } response.Status = $"File {blob.FileName} Uploaded Successfully"; response.Error = false; response.Blob.Uri = client.Uri.AbsoluteUri; response.Blob.Name = client.Name; } // If the file already exists, we catch the exception and do not upload it catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobAlreadyExists) { _logger.LogError($"File with name {blob.FileName} already exists in container.'"); response.Status = $"File with name {blob.FileName} already exists. Please use another name to store your file."; response.Error = true; return response; } catch (RequestFailedException ex) { // Log error to console and create a new response we can return to the requesting method _logger.LogError($"Unhandled Exception. ID: {ex.StackTrace} : {ex.Message}"); response.Status = $"Unexpected error: {ex.StackTrace}. Check log with StackTrace ID."; response.Error = true; return response; } return response; }
Upload file to subfolder in Azure blob storage
Take note of this line of code, where we define the container path that we want to upload the file to. If we just specified the filename then it would work too. But if you want to upload the file to a subfolder then you define the subfolder in the container path. And if the subfolder doesn’;’t exist then it will be automatically created 🙂 :
string storageContainerPath = $"/{folderName}/{blob.FileName}";
Download a file from Azure blob storage container
Now let’s create the method to download a file from the private Azure blob storage container.
public async Task<BlobDto> DownloadAsync( string folderName, string blobFilename) { var containerClient = _blobServiceClient.GetBlobContainerClient("container-name"); try { var file = containerClient.GetBlobClient($"{folderName}/{blobFilename}"); if (await file.ExistsAsync()) { var data = await file.OpenReadAsync(); Stream blobContent = data; //Download the file var content = await file.DownloadContentAsync(); //Construct the BlobDto object string name = blobFilename; string contentType = content.Value.Details.ContentType; return new BlobDto { Content = blobContent, Name = name, ContentType = contentType }; } } catch (RequestFailedException ex) { // Log error to console _logger.LogError($"Download request for file {blobFilename} failed."); throw; } // File does not exist, return null and handle that in requesting method return null; }
Delete a file from Azure blob storage container
Finally we will write the code to delete a file from Azure blob storage.
public async Task<BlobResponseDto> DeleteAsync( string folderName, string blobFilename) { var containerClient = _blobServiceClient.GetBlobContainerClient("container-name"); try { //set path for the folder string storageContainerPath = $"/{folderName}/{blobFilename}"; BlobClient client = containerClient.GetBlobClient(storageContainerPath); // Delete the file await client.DeleteAsync(); } catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound) { // File did not exist, log to console and return new //response to requesting method _logger.LogError($"File {blobFilename} was not found."); return new BlobResponseDto { Error = true, Status = $"File with name {blobFilename} not found." }; } // Return a new BlobResponseDto to the requesting method return new BlobResponseDto { Error = false, Status = $"File: {blobFilename} has been successfully deleted."}; }
Now that all of the Azure storage repository methods have been defined we can build the controller.
Defining the endpoints in the controller
The controller will define 4 endpoints – one for each of the repository methods we just defined.
using AutoMapper; using Azure; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Identity.Web.Resource; using DemoProject.AzureBlobStorage.Models; using DemoProject.Dto; using DemoProject.Exceptions; using DemoProject.Interfaces; namespace DemoProject.Controllers { [Authorize] [ApiController] [Route("files")] public class FilesController : BaseController { private readonly IMapper _mapper; private readonly ILogger<FilesController> _logger; private readonly IAzureStorage _azureStorage; public FilesController( ILogger<FilesController> logger, IMapper mapper, IAzureStorage azureStorage) { _logger = logger; _mapper = mapper; _azureStorage = azureStorage; } [HttpGet("{folder}")] public async Task<IActionResult> GetFilesFromFolder(string folder) { IEnumerable<BlobDto>? files = await _azureStorage.ListAsync(folder); if (files.Count() == 0) return NoContent(); return Ok(files); } [HttpPost("upload/{userId}")] public async Task<IActionResult> UploadFileForUser( int userId, [FromForm]IFormFile file) { try { BlobResponseDto? response = await _azureStorage.UploadAsync( folderName, file, description); if (response.Error == true) { // We got an error during upload ModelState.AddModelError("BadRequest", response.Status); return BadRequest(new ApiBadRequestResponse(ModelState)); } return Ok(response); } catch (RequestFailedException rfe) { return BadRequest(new ErrorDto() { Message = "The attempt to upload the file failed.", SystemMessage = rfe.Message }); } catch (Exception ex) { _logger.LogError($"Something went wrong inside the Upload action: {ex}"); return StatusCode(500, ex); } } [HttpGet("download/{folder}")] public async Task<IActionResult> DownloadFile( string folder, [FromQuery] string filename) { BlobDto? file = await _azureStorage.DownloadAsync(folder, filename); if (file == null) return NotFound($"File {filename} was not found."); //return file to the client return File(file.Content, file.ContentType, file.Name); } [HttpDelete("delete/{folder}")] public async Task<IActionResult> DeleteFile(string folder, [FromQuery] string fileName) { //log the deletion and who is deleting the file _logger.LogDebug($"{base.CurrentUserName} has requested to delete the file : {fileName}"); var response = await _azureStorage.DeleteAsync(folder, fileName); if (response.Error) return NotFound(response.Status); _logger.LogDebug($"{base.CurrentUserName} has successfully deleted the file : {fileName} on {DateTime.Now}"); return Ok(response); } } }
Summary
This should give you everything you need to build a .Net Core Api which interacts with Azure blob storage files – an interface which defines upload, delete, list, and get file. A repository which implements the interface. And a controller which defines each of the endpoints and uses the repository methods.
You might notice there is no error handling in the controller, which is something I’ve written about before. It’s best practice to implement global exception handling in a .Net Core API, which centralises the error handling in one place.