Watch the recording of this lesson on YouTube 🎥.
The goal of this lesson is to use Blob storage input and output bindings which lets you read and write Blob data in your Functions. In addition you will create a Blob triggered Function that reacts to changes in Blob storage data.
This lessons consists of the following exercises:
📝 Tip - If you're stuck at any point you can have a look at the source code in this repository.
📝 Tip - If you have questions or suggestions about this lesson, feel free to create a Lesson Q&A discussion here on GitHub.
| Prerequisite | Exercise |
|---|---|
| Azurite (Storage Emulator) | 1-6 |
| Azure Storage Explorer | 1-7 |
| An empty local folder / git repo | 2-7 |
| Azure Functions Core Tools | 2-7 |
| VS Code with Azure Functions extension | 2-7 |
| Rest Client for VS Code or Postman | 2-6 |
See TypeScript prerequisites for more details.
What is the Azure Blob storage? Azure Blob storage is a service for storing large amounts of unstructured data that can be accessed from anywhere in the world via REST API i.e. HTTP or HTTPS calls. A single blob can be hundreds of gigabytes in size. Azure Blob storage offers three types of resources:
- The storage account
- Container(s) in the storage account. A storage account can contain multiple containers.
- A blob in a container. A container can contain multiple blobs. You can also use folders to structure the blobs in a container.
We will mostly use a local storage emulator instead of creating a storage account in Azure to deal with blobs. This is great for local development. In this section we will setup a sample container containing a folder and a blob.
-
Install Azurite as cross-platform emulator for the storage.
-
Install Azure Storage Explorer.
-
Create a directory for the data storage of Azurite e.g.
C:\Users\<Your_Name>\azurite-storage -
Start the Azurite with the command:
azurite -s -l C:\Users\<Your_Name>\azurite-storage -d C:\Users\<Your_Name>\azurite-storage\debug.log
📝 Tip This command starts all Azurite services and tells Azurite where to store the data including logs. You can also start the services exclusively e. g. via
azurite-blob -
Open the Azure Storage Explorer, expand Local & Attached > Storage Accounts > (Emulator - Default Ports) (Keys) > Right click on Blob containers and create a new container named
players. -
Create a folder called
inin theplayerscontainer. -
Drag the player-1.json file that we have provided there. You can create more
player-x.jsonfiles and add them if you like. -
You are now all set to work with local storage.
📝 Tip - Read about Azurite and Azure Storage Explorer.
In this exercise, we will create a HTTP-triggered Function and extend it with a Blob output binding in order to write a Player JSON object to a players/out path in the Blob storage.
-
Create a new directory
AzureFunctions.Blob, switch into the directory and start VSCode. -
In VSCode create a new HTTP Trigger Function App via the Azure Functions extension with the following settings:
- Location: AzureFunctions.Blob
- Language: TypeScript
- Template: HTTP trigger
- Function name: StorePlayerWithBlobOutput
- AccessRights: Function
-
After the Function App is created, execute
npm installto install the required dependencies. -
Open the
function.jsonfile in theStorePlayerWithBlobOutputdirectory and make the following changes:-
We want to support POST requests only, so we remove the
"get"from the array of support HTTP methods. -
We add a new output binding by adding the following lines of JSON after the input binding section:
{ "name": "playerBlob", "type": "blob", "path": "players/out/stored-input.json", "direction": "out" }🔎 Observation - The attribute
"name"defines the name that we use to address the bound object in yourindex.tsfile. The attribute"type"specifies the binding type, in our case the Blob binding. The attribute"path"tells the binding which path to use to store the blob. It consists of the name of the container (player), the directory (out) and the file name (players/out/stored-input.json). The attribute"direction"defines the binding direction, in this case an output binding.🔎 Observation - Notice that we left out the
connectionattribute for theBlobbinding. This means the storage connection of the Function App itself is used which is the"AzureWebJobsStorage"setting in thelocal.settings.jsonfile. The value of this setting should be:"UseDevelopmentStorage=true"when emulated storage is used. When an Azure Storage Account is used this value should contain the connection string to that Storage Account.
-
-
Go back to Function code in
index.tsfile and remove the existing content of the function's body, since we write a new implementation. -
Add some variables for the HTTP response object:
let responseStatusCode: number let responseMessage: string
-
We must distinguish the cases if we receive a body in our POST request or not. So add some basic
if-elselogic:if (req.body){ } else{ }
🔎 Observation - For the sake of this lesson, we leave out any further checks on the JSON object in the body of the request. In real life scenarios you certainly must place further validations in place to make sure that you store valid data.
-
Let us deal with the unhappy case first when the request does not contain a body. We return the corresponding information to the caller:
else { responseStatusCode = 400 responseMessage = "Provide player data in the request." }
-
In case we receive a body we store the JSON body in the Blob storage. As we put in place the output binding, this is straightforward, as we just need to transfer the JSON object in the body to the binding available via the Function context:
if (req.body){ context.bindings.playerBlob = req.body }
-
Complete the
if-case by adding the corresponding response information:if (req.body){ ... responseStatusCode = 201 responseMessage = "Player data created in Blob storage." }
-
Finalize the code by returning the information via the response object after the
if-elseclause:context.res = { status: responseStatusCode, body: responseMessage }
-
Start the Azure Function via
npm run startor by pressing F5 and make a POST call to theStorePlayerWithBlobOutputendpoint. Provide a valid JSON body with aPlayerobject:POST http://localhost:7071/api/StorePlayerWithBlobOutput Content-Type: application/json { "id": "{{$guid}}", "nickName": "Scarlet Witch", "email":"wanda.maximoff@avengers.com", "location": { "region": "novi grad", "country": "sokovia" } }
📝 Tip - The
{{$guid}}part in the body creates a new random GUID when the request is made. This functionality is part of the VSCode REST Client extension.
❔ Question - Is there a blob created in the blob storage? What is the exact path of the blob? What is the content type?
❔ Question - What do you think would happen if you run the Function again with the exact same input?
In this exercise, we will make use of binding expressions to add a unique ID to the stored data and avoid the overwriting of entries.
-
Commit your changes in the Function App directory, create a new branch called
binding-expressionand switch to the new branch. -
Adjust the
pathattribute in thefunction.jsonfile:... "path": "players/out/stored-input-{rand-guid}.json", ...
🔎 Observation - The {rand-guid} expression in path is a so-called binding expression. The expression creates a random GUID when the output binding is executed by the Azure Functions runtime. There are more expressions available as described in the documentation.
-
Start the Azure Function via
npm run startor by pressing F5 and make a POST call to theStorePlayerToBlobOutputendpoint. Provide a valid JSON body with aPlayerobject:POST http://localhost:7071/api/StorePlayerWithBlobOutput Content-Type: application/json { "id": "{{$guid}}", "nickName": "Scarlet Witch", "email":"wanda.maximoff@avengers.com", "location": { "region": "novi grad", "country": "sokovia" } }
❔ Question - What happens when you run the Function with the exact same input?
In this exercise, we making use of the data from the JSON body of the HTTP request to derive the file name we want to store.
The binding expression syntax enables us to use the attributes in the JSON file of our input, so we want to apply the following naming convention to our output file:
players/out/stored-input-<GUID from the input>-<Nickname>-<Country in Location>.json-
Commit your changes in the Function App directory, create a new branch called
binding-expression-payloadand switch to the new branch. -
Adjust the
pathattribute in thefunction.jsonfile:... "path": "players/out/stored-input-{id}-{nickName}-{location.country}.json", ...
🔎 Observation - We can access nested structures of the JSON body via dot notation.
-
Adjust the return message for the success case in the
index.tsin order to be able to check if the GUID of the request object was used:if (req.body) { ... responseMessage = `Player data with id ${req.body.id} was created in Blob storage.` ... }
-
Start the Azure Function via
npm run startor by pressing F5 and make a POST call to theStorePlayerToBlobOutputendpoint. Provide a valid JSON body with aPlayerobject:POST http://localhost:7071/api/StorePlayerWithBlobOutput Content-Type: application/json { "id": "{{$guid}}", "nickName": "Hulk", "email":"bruce.banner@avengers.com", "location": { "region": "massachusetts", "country": "usa" } }
❔ Question - Is the blob created as specified?
📝 Tip - In the previous sections we used a declarative binding specified in the
function.jsonfile. The naming of the output file is defined at design time. Although there is some flexibility via binding expressions to influence this you will face scenarios where the functionality is not enough to cover your requirements. Unfortunately, imperative binding patterns (also known as dynamic binding) is not supported for non-.NET languages. In case you run into limitations with the declarative binding, do not hesitate to use the Blob storage SDK to directly interact with the storage from withing your Function code.
In this exercise we want to explore how we can use the input binding to read data from a Blob storage. We will create an HTTP trigger Function that expects a player ID as URL parameter. Using this ID we will return the content from the Blob that matches it.
-
Create a new HTTP triggered Function and name it
GetPlayerFromBlob. -
We make some changes to the
function.jsonfile with respect to the HTTP trigger :-
Remove the
"post"value from the methods array, as we only support GET requests. -
Add a
routeattribute to theHTTPTriggerconfiguration and set it to:"route": "GetPlayerFromBlob/{id}"
📝 Tip - The optional route parameter allows you to define custom HTTP endpoints for the HTTP triggered Function. In addition, the route parameters, like
{id}in our setup, are available as input for the other bindings of the Function. -
Add the Blob input binding:
{ "name": "playerBlobIn", "type": "blob", "path": "players/in/player-{id}.json", "direction": "in" }🔎 Observation - The conventions for the binding attributes are the same as for the output binding we used before.
🔎 Observation - We left out the optional attribute
dataType. This attribute allows to specify the data type for dynamically typed languages. However, the possible values are restricted tostring,binaryandstream.
-
-
We adjust the code of the
index.tsfile to make use of the input binding and return the result fetched from the Blob storage to the caller:-
Remove all the code from the function's body.
-
Add some variables for the HTTP response object:
let responseStatusCode: number let responseMessage: string let responseHeaders: object
-
We must distinguish the cases if we receive a result from our binding or not. So we add a basic
if-elselogic and check the bound data:if (context.bindings.playerBlobIn){ } else{ }
-
Fill the response parameters in accordance to the branch of the
if-clause:if (context.bindings.playerBlobIn) { responseStatusCode = 200 responseHeaders = { "Content-Type": "application/json" } responseMessage = context.bindings.playerBlobIn } else { responseStatusCode = 404 responseHeaders = { "Content-Type": "text/plain" } responseMessage = `No result found` }
🔎 Observation - As we will return a JSON object we adjust the response header i. e. the
Content-Typeattribute accordingly. 🔎 Observation - We cheated a bit concerning the type of theresponseMessage. The Blob is a JSON, but the binding in thecontextparameter is typed asany. Due to the untyped nature of the transpiled JavaScript code, this works. In a real-life scenario you should distinguish the types to have a better safety at design time. -
Finally add the response object to the Function code:
context.res = { status: responseStatusCode, headers: responseHeaders, body: responseMessage }
-
-
Start the Azure Function via
npm run startor by pressing F5 and make a GET call to the GetPlayerFromBlob endpoint. Provide a valid ID:-
URL:
GET http://localhost:7071/api/GetPlayerFromBlob/1
-
Output: (this is the contents of player-1.json make sure it is in your local storage blob container, as described in the first section of this lesson.)
{ "id": "1", "nickName": "Starlord", "email":"peter.quill@avengers.com", "location": { "region": "missouri", "country": "usa" } }❔ Question - What happens if you leave the id out of the HTTP request?
-
As already discussed when dealing with the output binding, the bindings have some short comings when we want make things more dynamically or want to use typed bindings. This makes it sometimes necessary to use the Blob Storage SDK.
To show you how to use it, we make a short detour to cover the following use case: we want to enable the caller to either get a dedicated entry in our Blob storage or to get a list of all stored file. We will now cover the later part by using the Blob Storage SDK.
-
Commit your changes in the Function App directory, create a new branch called
blob-sdkand switch to the new branch. -
Adjust the
routeattribute in thefunction.jsonfile to make the{id}parameter optional:{ ... "route": "GetPlayerFromBlob/{id?}" ... } -
Add the dependency to the Blob Storage SDK in the
package.jsonfile and runnpm install"dependencies": { "@azure/storage-blob": "12.5.0" }
📝 Tip - Usually you would use
^12.0.0as dependency. Unfortunately, there is a bug in the current version 12.6.0 which will cause an error when executing the call. -
Adjust the
index.tsfile.-
Import the
BlobServiceClientfrom the SDK:import { BlobServiceClient } from "@azure/storage-blob"
-
Create an
if-elsebranch to check if a parameter is received and move the existing logic into theifbranch:if (req.params.id) { if (context.bindings.playerBlobIn) { responseStatusCode = 200 responseHeaders = { "Content-Type": "application/json" } responseMessage = context.bindings.playerBlobIn } else { }
-
Implement the logic to read the file names from the storage in the
else-branch. Take into account the directory which is treated as a prefix in the file path:else { const connectionString = "<YOUR CONNECTION STRING>" const containerName = "players" let blobData: string[] = new Array() const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString); const containerClient = blobServiceClient.getContainerClient(containerName); let i = 0 for await (const blob of containerClient.listBlobsFlat({ prefix: "in/" })) { let entry = `Blob ${i++}: ${blob.name}` blobData.push(entry) } responseStatusCode = 200 responseHeaders = { "Content-Type": "application/json" } responseMessage = <any>{ "dataFromBlob": blobData } }
📝 Tip - You find your connection string in the storage explorer in the properties of your storage account:
📝 Tip - Do not place connection strings or passwords in your code. Instead use the app settings in combination with Azure Key Vault or if possible make use of managed identities when accessing resources in Azure.
-
-
Start the Azure Function via
npm run startor by pressing F5 and make a GET call to the GetPlayerFromBlob endpoint.-
Fetch a single player via:
GET http://localhost:7071/api/GetPlayerFromBlob/1 -
Fetch the list of Blobs via:
GET http://localhost:7071/api/GetPlayerFromBlob
-
In this section we will take a look at the Blob Trigger binding for Azure Functions, so this time our Function will react on newly created and updated blobs.
-
Create a storage account (e.g.
azurefuncuniblobts) with a container (e. g.samples-workitems) in Microsoft Azure. You can do that via the Azure Portal or the Azure CLI. You find a short how-to here📝 Tip - You can also do so from the Azure Function extension, but sometimes this does not work.
-
Create a new directory
AzureFunctions.BlobTrigger, switch into the directory and start VSCode. -
Create a new Blob trigger Function App with the following settings:
-
Location: AzureFunctions.BlobTrigger
-
Language: TypeScript
-
Template: Azure Blob Storage Trigger
-
Function name: HelloWorldBlobTrigger
-
Select
Create a new local app setting. -
Select the Azure subscription you will be using.
-
Select the storage account you have created before.
-
Select the path your trigger should react to based on the name of the container you created before.
-
When asked about storage required for debugging choose Use local emulator.
-
-
After the Function App is created, execute
npm installto install the relevant dependencies.
📝 Tip - Check the attribute
"AzureWebJobsStorage"in yourlocal.settings.jsonfile. In case it contains no value, either add"UseDevelopmentStorage=true"(make sure that Azurite is still running) or copy the connection string to your Azure Storage.
Examine the generated code. First take a look at the function.json file:
{
"bindings": [
{
"name": "myBlob",
"type": "blobTrigger",
"direction": "in",
"path": "samples-workitems/{name}",
"connection": "azurefuncuniblobts_STORAGE"
}
],
"scriptFile": "../dist/HelloWorldBlobTrigger/index.js"
}We recognize the well known structure with the type blobTrigger. As for the in- and output binding the path attribute contains the information about the Blob container and comprises a binding expression for the name of the file. This is the path the Function will listen to and react on the create and update events.
The code generator also added the connection attribute that specifies the location of the storage. As best practice the connection string is not directly stored in the function.json file but references the value. We find the value in the local app settings file (local.settings.json) that is used to store environment variables as well as other configurations.
Let us switch to the code and take a look at the index.ts file i.e. the body of the TypeScript function:
const blobTrigger: AzureFunction = async function (context: Context, myBlob: any): Promise<void> {
context.log("Blob trigger function processed blob \n Name:", context.bindingData.name, "\n Blob Size:", myBlob.length, "Bytes");
};The generated code logs some information about the Blob that triggered the function namely:
- The name of the Blob that is accessed via the
bindingDataattribute of the Azure Function context. - The size of the Blob that is available to the function via the binding parameter
myBlobthat is received as input parameter of the function.
Let us run the Function and then add a file to the Blob container that the Function is monitoring. You should see an output in the console similar to this:
🔎 Observation - Great! That's how the Blob trigger works, can you start to see how useful this trigger could be in your work?
Here is the assignment for this lesson.
For more info about the Blob Trigger and bindings have a look at the official Azure Functions Blob Bindings documentation.
We love to hear from you! Was this lesson useful to you? Is anything missing? Let us know in a Feedback discussion post here on GitHub.





