Adventures with Azure Storage: Read/Write Files to Blob Storage from Azure Functions

azure storage

In my last article, Adventures with Azure Storage: Read/Write Files to Blob Storage from a .NET Core Web API, we looked at uploading and downloading files from Azure Blob Storage using a .NET Core Web API, in this article, we are going to perform the same task, but this time, we will use Azure Functions in place of the .NET Core Web API.

Create an Azure Storage account or use an existing one.

Open Visual Studio or VS Code and Create a new Azure Function project.

Add a folder called Helpers.

Add two files, one called AzureStorageBlobOptions.cs and another called AzureStorageBlobOptionsTokenGenerator.cs.

Configuration will be provided by AzureStorageBlobOptions and the SAS Token will be generated by AzureStorageBlobOptionsTokenGenerator.

public class AzureStorageBlobOptions
{
    public string AccountName { get; set; }
    public string FilePath { get; set; }
    public string ConnectionString { get; set; }

    public AzureStorageBlobOptions()
    {
        this.AccountName =
            Environment.GetEnvironmentVariable(
                $"{nameof(AzureStorageBlobOptions)}:AccountName");
        this.ConnectionString =
            Environment.GetEnvironmentVariable(
                $"{nameof(AzureStorageBlobOptions)}:ConnectionString");
        this.FilePath =
            Environment.GetEnvironmentVariable(
                $"{nameof(AzureStorageBlobOptions)}:FilePath");
    }
}   

The AzureStorageBlobOptions gets tweaked to pull configuration from environment variables.

public class AzureStorageBlobOptionsTokenGenerator
{
    private readonly IOptions _options;

    public AzureStorageBlobOptionsTokenGenerator(
        IOptions options)
    {
        _options = options;
    }

    public string GenerateSasToken(
        string containerName)
    {
        return this.GenerateSasToken(
            containerName,
            DateTime.UtcNow.AddSeconds(30));
    }

    public string GenerateSasToken(
        string containerName,
        DateTime expiresOn)
    {
        var cloudStorageAccount =
            CloudStorageAccount.Parse(_options.Value.ConnectionString);
        var cloudBlobClient =
            cloudStorageAccount.CreateCloudBlobClient();
        var cloudBlobContainer =
            cloudBlobClient.GetContainerReference(containerName);

        var permissions = SharedAccessBlobPermissions.Read | SharedAccessBlobPermissions.Write;

        string sasContainerToken;

        var shareAccessBlobPolicy =
            new SharedAccessBlobPolicy()
            {
                SharedAccessStartTime = DateTime.UtcNow.AddMinutes(-5),
                SharedAccessExpiryTime = expiresOn,
                Permissions = permissions
            };

        sasContainerToken =
            cloudBlobContainer.GetSharedAccessSignature(shareAccessBlobPolicy, null);

        return sasContainerToken;
    }
}

In the local.settings.json,  Add configuration keys and Update the key values based on your Azure Storage account and Blob container.

{
     "IsEncrypted": false,
     "Values": {
         "AzureWebJobsStorage": "UseDevelopmentStorage=true",
         "FUNCTIONS_WORKER_RUNTIME": "dotnet",
         "AzureStorageBlobOptions:AccountName": "AZURE_STORAGE_ACCOUNT_NAME",
         "AzureStorageBlobOptions:ConnectionString": "AZURE_STORAGE_ACCOUNT_CONNECTION_STRING",
         "AzureStorageBlobOptions:FilePath": "AZURE_STORAGE_ACCOUNT_BLOB_CONTAINER_NAME"
     },
     "Host": {
         "CORS": "*"
     }
 }

Note, I also added the CORS option so I would not run into any issues when working locally.

Add a class called Startup.cs.

[assembly: FunctionsStartup(typeof(MyProject.FuncApp.Startup))]
namespace MyProject.FuncApp
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {            
        }
    }
}

Add the NuGet package for Microsoft.Azure.Functions.Extensions.

In the Startup.cs file we will add dependency injection support for the AzureStorageBlobOptions and AzureStorageBlobOptionsTokenGenerator classes.

builder.Services.AddSingleton();

builder.Services.AddSingleton();

Add a new HttpTrigger function called FileUploadHttpTrigger.cs.

public class FileUploadHttpTrigger
{
    private readonly AzureStorageBlobOptions _azureStorageBlobOptions;
    private readonly AzureStorageBlobOptionsTokenGenerator _azureStorageBlobOptionsTokenGenerator;

    public FileUploadHttpTrigger(
        AzureStorageBlobOptions azureStorageBlobOptions,
        AzureStorageBlobOptionsTokenGenerator azureStorageBlobOptionsTokenGenerator)
    {
        _azureStorageBlobOptions = azureStorageBlobOptions;
        _azureStorageBlobOptionsTokenGenerator = azureStorageBlobOptionsTokenGenerator;
    }

    [FunctionName("FileUploadHttpTrigger")]
    public async Task Run(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = "files")] HttpRequestMessage req,
        ILogger logger)
    {
        logger.LogInformation(
            $"{nameof(FileUploadHttpTrigger)} trigger function processed a request.");

        var multipartMemoryStreamProvider =
            new MultipartMemoryStreamProvider();

        await req.Content.ReadAsMultipartAsync(multipartMemoryStreamProvider);

        var file =
            multipartMemoryStreamProvider.Contents.First();
        var fileInfo =
            file.Headers.ContentDisposition;

        logger.LogInformation(
            JsonConvert.SerializeObject(fileInfo, Formatting.Indented));

        var sasToken =
            _azureStorageBlobOptionsTokenGenerator.GenerateSasToken(
                _azureStorageBlobOptions.FilePath);

        var storageCredentials =
            new StorageCredentials(
                sasToken);

        var cloudStorageAccount =
            new CloudStorageAccount(storageCredentials, _azureStorageBlobOptions.AccountName, null, true);

        var cloudBlobClient =
            cloudStorageAccount.CreateCloudBlobClient();

        var cloudBlobContainer =
            cloudBlobClient.GetContainerReference(
                _azureStorageBlobOptions.FilePath);

        var blobName =
            $"{Guid.NewGuid()}{Path.GetExtension(fileInfo.FileName)}";

        blobName = blobName.Replace("\"", "");

        var cloudBlockBlob =
            cloudBlobContainer.GetBlockBlobReference(blobName);

        cloudBlockBlob.Properties.ContentType =
           file.Headers.ContentType.MediaType;

        using (var fileStream = await file.ReadAsStreamAsync())
        {
            await cloudBlockBlob.UploadFromStreamAsync(fileStream);
        }

        return new OkObjectResult(new { name = blobName });
    }
}	

Run the Azure Function and open up Postman.

Create a new POST request.

Enter the API URL, in my case it was https://localhost:7071/api/files.

Set Body to form-data.

Add key called file, make sure to change the type to File, defaults to Text.

Add the file to upload and Send the request.

If the call is successful, you should receive an OK response with the new name of the file uploaded.

Not the name property, we will need to remember that to test the download funciton.

Add a new HttpTrigger function called FileDownloadHttpTrigger.cs.

public class FileDownloadHttpTrigger
{
    private readonly AzureStorageBlobOptions _azureStorageBlobOptions;
    private readonly AzureStorageBlobOptionsTokenGenerator _azureStorageBlobOptionsTokenGenerator;

    public FileDownloadHttpTrigger(
        AzureStorageBlobOptions azureStorageBlobOptions,
        AzureStorageBlobOptionsTokenGenerator azureStorageBlobOptionsTokenGenerator)
    {
        _azureStorageBlobOptions = azureStorageBlobOptions;
        _azureStorageBlobOptionsTokenGenerator = azureStorageBlobOptionsTokenGenerator;
    }

    [FunctionName("FileDownloadHttpTrigger")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "")]
    public async Task Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "files/{name}")] HttpRequest req,
        string name,
        ILogger logger)
    {
        logger.LogInformation(
            $"{nameof(FileDownloadHttpTrigger)} trigger function processed a request.");
        var sasToken =
             _azureStorageBlobOptionsTokenGenerator.GenerateSasToken(
                _azureStorageBlobOptions.FilePath);

        var storageCredentials =
            new StorageCredentials(
                sasToken);

        var cloudStorageAccount =
            new CloudStorageAccount(storageCredentials, _azureStorageBlobOptions.AccountName, null, true);

        var cloudBlobClient =
            cloudStorageAccount.CreateCloudBlobClient();

        var cloudBlobContainer =
            cloudBlobClient.GetContainerReference(
                _azureStorageBlobOptions.FilePath);

        var blobName =
            name;

        var cloudBlockBlob =
            cloudBlobContainer.GetBlockBlobReference(blobName);

        var ms = new MemoryStream();

        await cloudBlockBlob.DownloadToStreamAsync(ms);

        return new FileContentResult(ms.ToArray(), cloudBlockBlob.Properties.ContentType);
    }
}

Run the Azure Function and open up Postman.

Create a new GET request in Postman.

Enter the API URL, in my case it was https://localhost:7071/api/files/RETURN_FILE_NAME, and Send the request.

Replace RETURN_FILE_NAME with the name of the file that returned in the previous step where we uploaded the file.

If the call is successful, you should see the image displayed in Postman.

Some final comments.

I thought I could write to Azure Blob Storage using the output bindings, but could not figure out how to set the name of the Blob, so opted to write to the Azure Blob Storage myself.

The code could be more readable and easier to maintain if I refactored the Blob commands to a common helper library.

Thanks for reading!

Leave a Reply

Your email address will not be published.