Home » dotnet » Access files in Azure blob storage from a .Net Core Api

Access files in Azure blob storage from a .Net Core Api

By Emily

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

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.

Change access level on Azure File Storage container

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 :

  1. Get a list of files
  2. Upload a file
  3. Download a file
  4. 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.