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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public class AzureStorageBlobOptionsTokenGenerator { private readonly IOptions<AzureStorageBlobOptions> _options; public AzureStorageBlobOptionsTokenGenerator( IOptions<AzureStorageBlobOptions> 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "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
.
1 2 3 4 5 6 7 8 9 10 |
[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.
1 2 3 |
builder.Services.AddSingleton<AzureStorageBlobOptions, AzureStorageBlobOptions>(); builder.Services.AddSingleton<AzureStorageBlobOptionsTokenGenerator, AzureStorageBlobOptionsTokenGenerator>(); |
Add a new HttpTrigger
function called FileUploadHttpTrigger.cs
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
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<IActionResult> 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 a 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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
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 = "<Pending>")] public async Task<IActionResult> 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!
Discover more from Matt Ruma
Subscribe to get the latest posts sent to your email.
Hello Matt,
This is Mani from India. I am trying to accomplish the same with Azure Functions but I am getting an error when registering the helper classes. Can you please share the completed Sample project (through GitHub/OneDrive/Email or so) which will help to understand.
Thanks a lot for sharing!
Hi Mani! Whoops! Sorry about that! You can find the code at https://github.com/mattruma/MJR030.
Hi, very nice, thanks!
Your Github repo is different than de example here.
From wish package IOptions came from? (Also not found in the repo)
Sorry @William! Let me see if I can find the original code … looks like in this one I ended up going with output bindings for the Azure Function. For more information on
IOptions
see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1. Did you have a specific question that I can help with? Just in case I can’t find my original code, and again, sorry for the confusion.wow, really fast reply!
Ok, don’t worry!
Could you provide full solution? Could not find startup class.
Thanks a lot.
In the step “Add a class called Startup.cs.” .. should that be in the helper folder?
On the step… Add a class called Startup.cs. Should this be done in the Helper Folder?
Can you show what the result of this step would produce?
In the Startup.cs file we will add dependency injection support for the AzureStorageBlobOptions and AzureStorageBlobOptionsTokenGenerator classes.
builder.Services.AddSingleton();
builder.Services.AddSingleton();