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 easily read & write blob data in your functions. In addition you'll 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.
We're going to be using local storage instead of creating a storage account in Azure, this is great for local development.
- Install Azure Storage Emulator if you are on windows, if you are using Mac OS or Linux, use Azurite.
- Install Azure Storage Explorer.
- Start the Azure Storage Emulator.
- Open Azure Storage Explorer, expand Local & Attached > Storage Accounts > (Emulator - Default Ports) (Keys) > Right click on Blob containers and create a new
players
container. - In the
players
container create a folder calledin
. - Drag player-1.json there. You can create more player json files and add them here if you'd like, we've provided one example.
- You're now all set to work with local storage.
📝 Tip - Read about Azure Storage Emulator and Azure Storage Explorer.
In this exercise, we'll be creating a HTTP Function App with the default HTTPTrigger and extend it with a Blob output binding in order to write a Player
json object to a "players/out" path in Blob Storage.
-
In VSCode Create a new HTTP Trigger Function App with the following settings:
- Location: AzureFunctions.Blob
- Language: C#
- Template: HttpTrigger
- Function name: StorePlayerWithStringBlobOutput
- Namespace: AzureFunctionsUniversity.Demo
- AccessRights: Function
-
Once the Function App is generated, add a reference to the
Microsoft.Azure.WebJobs.Extensions.Storage
NuGet package to the project. This allows us to use bindings for Blobs, Tables and Queues.📝 Tip - One way to easily do this is to use the NuGet Package Manager VSCode extension:
- Run
NuGet Package Manager: Add new Package
in the Command Palette (CTRL+SHIFT+P). - Type:
Microsoft.Azure.WebJobs.Extensions.Storage
- Select the most recent (non-preview) version of the package.
- Run
-
We want to store an object with (game)player data. Create a new file in the project called
Player.cs
and add the contents from this Player.cs file. -
Now open the
StorePlayerWithStringBlobOutput.cs
function file and add the following output binding directly underneath theHttpTrigger
method argument:[Blob( "players/out/string-{rand-guid}.json", FileAccess.Write)] out string playerBlob
🔎 Observation - The first part parameter of the
Blob
attribute is the full path where the blob will be stored. The {rand-guid} section in path is a so-called binding expression. This specific expression creates a random guid. There are more expressions available as is described in the documentation. The second parameter indicates we are writing to Blob Storage. Finally we specify that there is an output argument of typestring
namedplayerBlob
.🔎 Observation - Notice that we're not specifying the Connection property for the
Blob
binding. This means the storage connection of the Function App itself is used for the Blob storage. It now uses the"AzureWebJobsStorage"
setting in thelocal.settings.json
file. 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 the function class. We'll be doing a POST to the function so the
"get"
can be removed from theHttpTrigger
attribute. -
Change the function input type and name from
HttpRequest req
toPlayer player
so we have direct access to thePlayer
object in the request. -
Remove the existing content of the function method, since we'll be writing a new implementation.
-
To return a meaningful response the the client, based on a valid
Player
object, add the following lines of code in the method:playerBlob = default; IActionResult result; if (player == null) { result = new BadRequestObjectResult("No player data in request."); } else { result = new AcceptedResult(); } return result;
-
Since we're using
string
as the output type thePlayer
object needs to be serialized. This can be done as follows inside theelse
statement in the method:playerBlob = JsonConvert.SerializeObject(player, Formatting.Indented);
-
Ensure that the function looks as follows:
public static class StorePlayerWithStringBlobOutput
{
[FunctionName(nameof(StorePlayerWithStringBlobOutput))]
public static IActionResult Run(
[HttpTrigger(
AuthorizationLevel.Function,
nameof(HttpMethods.Post),
Route = null)] Player player,
[Blob(
"players/out/string-{rand-guid}.json",
FileAccess.Write)] out string playerBlob)
{
playerBlob = default;
IActionResult result;
if (player == null)
{
result = new BadRequestObjectResult("No player data in request.");
}
else
{
playerBlob = JsonConvert.SerializeObject(player, Formatting.Indented);
result = new AcceptedResult();
}
return result;
}
}
-
Build & run the
AzureFunctions.Blob
Function App. -
Make a POST call to the
StorePlayerWithStringBlobOutput
endpoint and provide a valid json body with aPlayer
object:POST http://localhost:7071/api/StorePlayerWithStringBlobOutput Content-Type: application/json { "id": "{{$guid}}", "nickName" : "Ada", "email" : "ada@lovelace.org", "region" : "United Kingdom" }
📝 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 blob storage? What is the exact path of the blob?
-
❔ Question - What do you think would happen if you run the function again with the exact same input?
In this exercise, we'll be adding an HttpTrigger function and use the Blob output binding with the CloudBlobContainer
type in order to write a Player
json object to a "players/out" path in Blob Storage.
-
Create a copy of the
StorePlayerWithStringBlobOutput.cs
file and rename the file, the class and the function toStorePlayerWithContainerBlobOutput
. -
Change the
Blob
attribute as follows:[Blob( "players", FileAccess.Write)] CloudBlobContainer cloudBlobContainer
🔎 Observation - The
CloudBlobContainer
refers to a blob container and not directly to a specific blob. Therefore we only have to specify the"players"
container in theBlob
attribute. -
Update the code inside the
else
statement. Remove the line withplayerBlob = JsonConvert.SerializeObject...
and replace it with:var blob = cloudBlobContainer.GetBlockBlobReference($"out/cloudblob-{player.NickName}.json"); var playerBlob = JsonConvert.SerializeObject(player); await blob.UploadTextAsync(playerBlob);
🔎 Observation - Notice that the argument for getting a reference to a blockblob includes the
out/
path. This part is a virtual folder, it is not a real container such as the"player"
container. The filename of the blob is a concatenation of "cloudblob-", the nickname of the player object, and the json extension. -
Build & run the
AzureFunctions.Blob
Function App. -
Make a POST call to the
StorePlayerWithContainerBlobOutput
endpoint and provide a valid json body with aPlayer
object:POST http://localhost:7071/api/StorePlayerWithContainerBlobOutput Content-Type: application/json { "id": "{{$guid}}", "nickName" : "Margaret", "email" : "margaret@hamilton.org", "region" : "United States of America" }
-
❔ Question - Is the blob created in the
players/in
location? -
❔ Question - What happens when you run the function with the exact same input?
-
❔ Question - Use one of the other
Player
properties as the partial filename. Does that work?
In this exercise, we'll be adding an HttpTrigger function and use a dynamic Blob output binding in order to write a Player
json object to a "players/out" path in Blob Storage.
📝 Tip - Dynamic bindings are useful when output or input bindings can only be determined at runtime. In this case we'll use the dynamic binding to create a blob path that contains a property of a
Player
object that is provided in the HTTP request.
-
Create a copy of the
StorePlayerWithStringBlobOutput.cs
file and rename the file, the class and the function toStorePlayerWithStringBlobOutputDynamic
. -
Remove the existing
Blob
attribute from the method and replace it with:IBinder binder
🔎 Observation - The IBinder is the interface of a dynamic binding. It only has one method
BindAsync<T>()
which we'll use in the next step. -
Update the
else
statement to it looks like this:var blobAttribute = new BlobAttribute($"players/out/dynamic-{player.Id}.json"); using (var output = await binder.BindAsync<TextWriter>(blobAttribute)) { await output.WriteAsync(JsonConvert.SerializeObject(player)); } result = new AcceptedResult();
🔎 Observation - First, a new instance of a
BlobAttribute
type is created which contains the path to the blob. A property of thePlayer
object is used as part of the filename. Then, theBindAsync
method is called on theIBinder
interface. Since we'll be writing json to a file, we can use theTextWriter
as the generic type. TheBindAsync
method will return aTask<TextWriter>
. When the method is awaited we can acces methods on theTextWriter
object to write the serializedPlayer
object to the blob. -
Build & run the
AzureFunctions.Blob
Function App. -
Make a POST call to the
StorePlayerWithStringBlobOutputDynamic
endpoint and provide a valid json body with aPlayer
object:POST http://localhost:7071/api/StorePlayerWithStringBlobOutputDynamic Content-Type: application/json { "id": "{{$guid}}", "nickName" : "Grace", "email" : "grace@hopper.org", "region" : "United States of America" }
-
❔ Question - Is the blob created in the
players/in
location? -
❔ Question - Could you think of other scenarios where dynamic bindings are useful?
Let's see how we can use the Stream
type to work with Blobs. We will create an HTTP Trigger function that expects a player ID in the URL, and with that ID it will return the content from the Blob that matches it.
-
Create a new HTTP triggered function, we will name it GetPlayerWithStreamInput.cs
-
We're going to make some changes to the method definition:
-
Change the
HTTPTrigger
Route
value, set it toRoute = "GetPlayerWithStreamInput/{id}"
-
Add a parameter to the method
string id
-
Add the Blob Input Binding
[Blob( "players/in/player-{id}.json", FileAccess.Read)] Stream playerStream
-
Your method definition should should look like this now:
[FunctionName(nameof(GetPlayerWithStreamInput))] public static async Task<IActionResult> Run( [HttpTrigger( AuthorizationLevel.Function, nameof(HttpMethods.Get), Route = "GetPlayerWithStreamInput/{id}")] HttpRequest request, string id, [Blob( "players/in/player-{id}.json", FileAccess.Read)] Stream playerStream)
-
-
Let's make some edits to the body of the method.
-
Remove all the code in the body.
-
Create an object to store our IActionResult:
IActionResult result;
-
Let's make sure the id is not empty or null, if it is, return a BadRequestObjectResult with a custom message:
if (string.IsNullOrEmpty(id)) { result = new BadRequestObjectResult("No player id route."); }
-
If we do have a value for id, use StreamReader to get the contents of playerStream and return it:
else { using var reader = new StreamReader(playerStream); var content = await reader.ReadToEndAsync(); result = new ContentResult { Content = content, ContentType = MediaTypeNames.Application.Json, StatusCode = 200 }; } return result;
🔎 Observation -
StreamReader
reads characters from a byte stream in a particular encoding. In this demo we are creating a newStreamReader
from the playerStream. TheReadToEndAsync()
method reads all characters from the playerStream (which is the content of the blob). We then create a result with the content of the blob, json as theContentType
andStatusCode 200
to indicate success. -
-
Run the Function App, make a request to the endpoint, and provide an ID in the URL. As long as there is a blob with the name matching the ID you provided, you will see the contents of the blob output.
-
URL:
GET http://localhost:7071/api/GetPlayerWithStreamInput/1
-
Output: (this is the contents of player-1.json make sure it's in your local storage blob container, we covered this in the first step of this lesson.)
{ "id":"1", "nickName":"gwyneth", "email":"gwyneth@game.com", "region": "usa" }
-
Let's see how we can use the CloudBlobContainer
type to work with Blobs. We will create an HTTP Trigger function that will return the names of every blob in our players
container.
-
Create a new HTTP Trigger Function App, we will name it
GetBlobNamesWithContainerBlobInput.cs
. -
We're going to make some changes to the method definition:
-
Change the HTTPTrigger to only allow GET calls:
nameof(HttpMethods.Get)
-
Add the Blob Input Binding:
[Blob( "players", FileAccess.Read)] CloudBlobContainer cloudBlobContainer)
-
Your method definition should should look like this now:
public static IActionResult Run( [HttpTrigger( AuthorizationLevel.Function, nameof(HttpMethods.Get), Route = null)] HttpRequest request, [Blob( "players", FileAccess.Read)] CloudBlobContainer cloudBlobContainer)
-
-
Let's make some edits to the body of the method.
-
Remove all the code in the body.
-
Create an object to store our the list of blobs in our container:
var blobList = cloudBlobContainer.ListBlobs(prefix: "in/")OfType<CloudBlockBlob>();
-
Create an object to store the names of each blob from the blobList:
var blobNames = blobList.Select(blob => new { BlobName = blob.Name });
-
Return an OkObjectResult with the blobNames found:
return new OkObjectResult(blobNames);
🔎 Observation - Azure storage service offers three types of blobs. Block blobs are optimized for uploading large amounts of data efficiently (e.g pictures, documents). Page blobs are optimized for random read and writes (e.g VHD). Append blobs are optimized for append operations (e.g logs). Read more here
-
-
Run the Function App and make a request to the endpoint.
-
URL:
GET http://localhost:7071/api/GetBlobNamesWithContainerBlobInput
-
Output: (In my case, I have 2 play json files)
[ {"blobName":"in/player-1.json"}, {"blobName":"in/player-2.json"} ]
-
Often you won't know the path of the blob until runtime, for those cases we can perform the binding imperatively in our code (instead of declaratively via the method definition). Meaning the binding will execute at runtime instead of compile time. For this we can use IBinder
. Let's take a look.
Okay so to summarize, use dynamic when you are getting the path at runtime. String and byte[] load the entire blob into memory, not ideal for large files, but Stream and CloudBlockBlob with the blob binding don’t load it entirely into memory, so ideal for processing large files.
-
Create a new HTTP Trigger Function App, we will name it
GetPlayerWithStringInputDynamic.cs
. -
We're going to make some changes to the method definition:
-
Change the HTTPTrigger to only allow GET calls:
nameof(HttpMethods.Get)
-
Add an IBinder parameter to the method definition:
IBinder binder
-
Your method definition should should look like this now:
public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest request, IBinder binder
-
-
Let's make some edits to the body of the method.
-
Remove all the code in the body.
-
Create a string object to store
id
andresult
:string id = request.Query["id"]; IActionResult result;
-
Let's do some validation to make sure we have a value for
id
. If we do have a value forid
we create aBlobAttribute
with the value in the path. Then we will use `BindAsync to read the contents of the blob and finally assign the content to the result and return the result.if (string.IsNullOrEmpty(id)) { result = new BadRequestObjectResult("No player data in request."); } else { string content; var blobAttribute = new BlobAttribute($"players/in/player-{id}.json"); using (var input = await binder.BindAsync<TextReader>(blobAttribute)) { content = await input.ReadToEndAsync(); } result = new ContentResult { Content = content, ContentType = MediaTypeNames.Application.Json, StatusCode = 200 }; } return result;
🔎 Observation - We are using
TextReader
for this simple example, but other options includeStream
andCloudBlockBlob
which we've used in other examples.🔎 Observation - By wrapping the Binder instance in a
using
we are indicating that the instance must be properly disposed. This just means that once the code inside of theusing
is executed, it will call the Dispose method ofIBinder
and clean up the object. Here is a great explanation.
-
-
Run the Function App, make a request to the endpoint, and provide an ID in the URL. As long as there is a blob with the name matching the ID you provided, you will see the contents of the blob output.
-
URL:
GET http://localhost:7071/api/GetPlayerWithStringInputDynamic/1
-
Output: (this is the contents of player-1.json make sure it's in your local storage blob container, we covered this in the first step of this lesson.)
{ "id":"1", "nickName":"gwyneth", "email":"gwyneth@game.com", "region": "usa" }
-
First, you'll be creating a Function App with the BlobTrigger and review the generated code.
-
Create the Function App by running
AzureFunctions: Create New Project
in the VSCode Command Palette (CTRL+SHIFT+P).📝 Tip - Create a folder with a descriptive name since that will be used as the name for the project.
-
Select the language you'll be using to code the function, in this lesson we'll be using
C#
. -
Select
BlobTrigger
as the template. -
Give the function a name (e.g.
HelloWorldBlobTrigger
). -
Enter a namespace for the function (e.g.
AzureFunctionsUniversity.Demo
). -
Select
Create a new local app setting
.🔎 Observation - The local app settings file (local.settings.json) is used to store environment variables and other useful configurations.
-
Select the Azure subscription you will be using.
-
Since we are using the BlobTrigger, we need to provide a storage account, select one or create a new storage account.
- If you select a new one, provide a name. The name you provide must be unique to all Azure.
-
Select a resource group or create a new one.
- If you create a new one, you must select a region. Use the one closest to you.
-
Enter the path that the trigger will monitor, you can leave the default value
samples-workitems
if you'd like or change it. Make sure to keep this in mind as we will be referencing it later on. -
When asked about storage required for debugging choose Use local emulator.
Now the Function App with a BlobTrigger function will be created.
Great, we've got our Function Project and Blob Trigger created, let's examine what has been generated for us.
public static void Run(
[BlobTrigger(
"samples-workitems/{name}",
Connection = "azfunctionsuniversitygps_STORAGE")]
Stream myBlob,
string name,
ILogger log)
{
log.LogInformation($"C# Blob trigger function
Processed blob\n Name:{name} \n Size: {myBlob.Length} Bytes");
}
This is the function with BlobTrigger created for us. A few things in here were generated and set for us thanks to the wizard. Let's look at the binding.
[BlobTrigger(
"samples-workitems/{name}",
Connection = "azfunctionsuniversitygps_STORAGE")] Stream myBlob
We can see this BlobTrigger has a few parts:
- "samples-workitems/{name}": This is the path we set that the function will monitor.
- Connection = "azfunctionsuniversitygps_STORAGE": This is the value in our local.settings.json file where our connection string to our storage account is stored.
- Stream myBlob: This is the object where the blob that triggered the function will be stored and can be used in code.
As for the body of the function:
log.LogInformation($"C# Blob trigger function Processed blob\n Name:{name} \n Size: {myBlob.Length} Bytes");
the name and size of the blob that triggered the function will print to console.
Okay now it actually is time to fun the function, go ahead and run it, and then add a file to the blob container that the function is monitoring. You should see output similar to this. The name and size of the tile you uploaded will appear in your Visual Studio terminal output.
🔎 Observation - Great! That's how the BlobTrigger 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.