diff --git a/lessons/dotnet6/blob/README.md b/lessons/dotnet6/blob/README.md new file mode 100644 index 00000000..b584aff2 --- /dev/null +++ b/lessons/dotnet6/blob/README.md @@ -0,0 +1,615 @@ +# Blob Bindings + + + +## **Goal** + +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: + +|Nr|Exercise +|-|- +|0|Prerequisites +|1|[Using the Microsoft Azure Storage Explorer and Azurite Emulator](#1-using-the-microsoft-azure-storage-explorer-and-azurite-emulator) +|2|[Using `string` Blob output bindings](#2-using-string-Blob-output-bindings) +|3|[Using `CloudBlobContainer` Blob output bindings](#3-using-cloudblobcontainer-blob-output-bindings) +|4|[Using `dynamic` Blob output bindings](#4-using-dynamic-blob-output-bindings) +|5|[Using `Stream` Blob input bindings](#5-using-stream-Blob-input-bindings) +|6|[Using `CloudBlobContainer` Blob input bindings](#6-using-cloudblobcontainer-blob-input-bindings) +|7|[Using `dynamic` Blob input bindings](#7-using-dynamic-blob-input-bindings) +|8|[Creating a Blob triggered function](#8-creating-a-blob-triggered-function) +|9|[Homework](#9-homework) +|10|[More info](#10-more-info) + +> πŸ“ **Tip** - If you're stuck at any point you can have a look at the [source code](../../../src/dotnet6/AzureFunctions.Blob) in this repository. + +--- +## 0. Prerequisites + +| Prerequisite | Exercise +| - | - +| .NET 6 SDK | 2-8 +| Azure Functions Core Tools | 2-8 +| VS Code with Azure Functions extension| 2-8 +| REST Client for VS Code or Postman | 2-7 +| Azurite | 1-8 +| Azure Storage Explorer | 1-8 + +See [.NET 6 prerequisites](../prerequisites/README.md) for more details. + +## 1. Using the Microsoft Azure Storage Explorer and Azurite Emulator + +We're going to be using local storage instead of creating a storage account in Azure, this is great for local development. + +### Steps + +1. Start the Azurite blob service. If you have installed the Azurite VS Code extension, you can start the blob service via `CTRL+SHIFT+P`, and `Azurite: Start Blob Service`. +2. Open Azure Storage Explorer, expand Local & Attached > Storage Accounts > (Emulator - Default Ports) (Keys) > Right click on Blob containers and create a new `players` container. + ![Storage Explorer sample-items](img/storage-explorer-sample-items.png) +3. In the `players` container create a folder called `in`. + ![In folder](img/in-folder.png) +4. Drag [player-1.json](../../../src/dotnet6/AzFuncUni.Blob/player-1.json) there. You can create more player json files and add them here if you'd like, we've provided one example. + ![player-1 In folder](img/player-1-in-folder.png) +5. You're now all set to work with local storage. + +> πŸ“ **Tip** - Read about [Azurite](https://docs.microsoft.com/azure/storage/common/storage-use-azurite) and [Azure Storage Explorer](https://azure.microsoft.com/features/storage-explorer/). + +## 2. Using `string` Blob output bindings + +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. + +### Steps + +1. In VSCode Create a new HTTP Trigger Function App with the following settings: + 1. Location: *AzFuncUni.Blob* + 2. Language: *C#* + 3. .NET Runtime: *`.NET 6` (isolated)* + + If you don't see .NET 6, choose: + - `Change Azure Functions version` + - Select `Azure Functions v4` + - Select `.NET 6 (isolated)` + + > πŸ“ **Tip** - More information about the isolated process can be found in the [official Azure documentation](https://docs.microsoft.com/azure/azure-functions/dotnet-isolated-process-guide). + + 4. Template: *HttpTrigger* + 5. Function name: *StorePlayerWithStringBlobOutput* + 6. Namespace: *AzFuncUni.Blob* + 7. AccessRights: *Function* +2. 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 (and Queues): + + 1. Ensure you're in the folder where the `.csproj` file is located. + 2. Type: `dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs`. + 3. Your .csproj file should include this line now: + ```xml + + ``` + +3. 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](../../../src/dotnet6/AzFuncUni.Blob/Models/Player.cs) file. +4. Now open the `StorePlayerWithStringBlobOutput.cs` function file and add the following output binding directly underneath the `HttpTrigger` method argument: + + ```csharp + [BlobInput("players/out/string-{rand-guid}.json")] 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](https://docs.microsoft.com/azure/azure-functions/functions-bindings-expressions-patterns). The second parameter indicates we are writing to Blob Storage. Finally we specify that there is an output argument of type `string` named `playerBlob`. + + > πŸ”Ž **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 the `local.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. + +5. Go back to the function class. We'll be doing a POST to the function so the `"get"` can be removed from the `HttpTrigger` attribute. +6. Change the function input type and name from `HttpRequest req` to `Player player` so we have direct access to the `Player` object in the request. +7. Remove the existing content of the function method, since we'll be writing a new implementation. +8. To return a meaningful response the the client, based on a valid `Player` object, add the following lines of code in the method: + + ```csharp + playerBlob = default; + IActionResult result; + + if (player == null) + { + result = new BadRequestObjectResult("No player data in request."); + } + else + { + result = new AcceptedResult(); + } + + return result; + ``` + +12. Since we're using `string` as the output type the `Player` object needs to be serialized. This can be done as follows inside the `else` statement in the method: + + ```csharp + playerBlob = JsonConvert.SerializeObject(player, Formatting.Indented); + ``` + +13. Ensure that the function looks as follows: + + ```csharp + 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; + } + } + ``` + +11. Build & run the `AzureFunctions.Blob` Function App. +12. Make a POST call to the `StorePlayerWithStringBlobOutput` endpoint and provide a valid json body with a `Player` object: + + ```http + 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. + +13. > ❔ **Question** - Is there a blob created blob storage? What is the exact path of the blob? + +14. > ❔ **Question** - What do you think would happen if you run the function again with the exact same input? + +## 3. Using `CloudBlobContainer` Blob output bindings + +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. + +### Steps + +1. Create a copy of the `StorePlayerWithStringBlobOutput.cs` file and rename the file, the class and the function to `StorePlayerWithContainerBlobOutput`. +2. Change the `Blob` attribute as follows: + + ```csharp + [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 the `Blob` attribute. +3. Update the code inside the `else` statement. Remove the line with `playerBlob = JsonConvert.SerializeObject...` and replace it with: + + ```csharp + 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. +4. Build & run the `AzureFunctions.Blob` Function App. +5. Make a POST call to the `StorePlayerWithContainerBlobOutput` endpoint and provide a valid json body with a `Player` object: + + ```http + POST http://localhost:7071/api/StorePlayerWithContainerBlobOutput + Content-Type: application/json + + { + "id": "{{$guid}}", + "nickName" : "Margaret", + "email" : "margaret@hamilton.org", + "region" : "United States of America" + } + ``` + +6. > ❔ **Question** - Is the blob created in the `players/in` location? +7. > ❔ **Question** - What happens when you run the function with the exact same input? +8. > ❔ **Question** - Use one of the other `Player` properties as the partial filename. Does that work? + +## 4. Using `dynamic` Blob output bindings + +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. + +### Steps + +1. Create a copy of the `StorePlayerWithStringBlobOutput.cs` file and rename the file, the class and the function to `StorePlayerWithStringBlobOutputDynamic`. +2. Remove the existing `Blob` attribute from the method and replace it with: + + ```csharp + IBinder binder + ``` + + > πŸ”Ž **Observation** - The IBinder is the interface of a dynamic binding. It only has one method `BindAsync()` which we'll use in the next step. +3. Update the `else` statement to it looks like this: + + ```csharp + var blobAttribute = new BlobAttribute($"players/out/dynamic-{player.Id}.json"); + using (var output = await binder.BindAsync(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 the `Player` object is used as part of the filename. Then, the `BindAsync` method is called on the `IBinder` interface. Since we'll be writing json to a file, we can use the `TextWriter` as the generic type. The `BindAsync` method will return a `Task`. When the method is awaited we can acces methods on the `TextWriter` object to write the serialized `Player` object to the blob. +4. Build & run the `AzureFunctions.Blob` Function App. +5. Make a POST call to the `StorePlayerWithStringBlobOutputDynamic` endpoint and provide a valid json body with a `Player` object: + + ```http + POST http://localhost:7071/api/StorePlayerWithStringBlobOutputDynamic + Content-Type: application/json + + { + "id": "{{$guid}}", + "nickName" : "Grace", + "email" : "grace@hopper.org", + "region" : "United States of America" + } + ``` + +6. > ❔ **Question** - Is the blob created in the `players/in` location? +7. > ❔ **Question** - Could you think of other scenarios where dynamic bindings are useful? + +## 5. Using `Stream` Blob input bindings + +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. + +### Steps + +1. Create a new HTTP triggered function, we will name it GetPlayerWithStreamInput.cs +2. We're going to make some changes to the method definition: + 1. Change the `HTTPTrigger` `Route` value, set it to + + ```csharp + Route = "GetPlayerWithStreamInput/{id}" + ``` + + 2. Add a parameter to the method + + ```csharp + string id + ``` + + 3. Add the Blob Input Binding + + ```csharp + [Blob( + "players/in/player-{id}.json", + FileAccess.Read)] Stream playerStream + ``` + + 4. Your method definition should should look like this now: + + ```csharp + [FunctionName(nameof(GetPlayerWithStreamInput))] + public static async Task Run( + [HttpTrigger( + AuthorizationLevel.Function, + nameof(HttpMethods.Get), + Route = "GetPlayerWithStreamInput/{id}")] HttpRequest request, + string id, + [Blob( + "players/in/player-{id}.json", + FileAccess.Read)] Stream playerStream) + ``` + +3. Let's make some edits to the body of the method. + 1. Remove all the code in the body. + 2. Create an object to store our IActionResult: + + ```csharp + IActionResult result; + ``` + + 3. Let's make sure the id is not empty or null, if it is, return a BadRequestObjectResult with a custom message: + + ```csharp + if (string.IsNullOrEmpty(id)) + { + result = new BadRequestObjectResult("No player id route."); + } + ``` + + 4. If we do have a value for id, use StreamReader to get the contents of playerStream and return it: + + ```csharp + 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 new `StreamReader` from the playerStream. The `ReadToEndAsync()` 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 the `ContentType` and `StatusCode 200` to indicate success. + +4. 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. + 1. URL: + + ```http + GET http://localhost:7071/api/GetPlayerWithStreamInput/1 + ``` + + 2. Output: (this is the contents of [player-1.json](../../../src/dotnetcore31/AzureFunctions.Blob/player-1.json) make sure it's in your local storage blob container, we covered this in the first step of this lesson.) + + ```json + { + "id":"1", + "nickName":"gwyneth", + "email":"gwyneth@game.com", + "region": "usa" + } + ``` + +## 6. Using `CloudBlobContainer` Blob input bindings + +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. + +### Steps + +1. Create a new HTTP Trigger Function App, we will name it `GetBlobNamesWithContainerBlobInput.cs`. +2. We're going to make some changes to the method definition: + 1. Change the HTTPTrigger to only allow GET calls: + + ```csharp + nameof(HttpMethods.Get) + ``` + + 2. Add the Blob Input Binding: + + ```csharp + [Blob( + "players", + FileAccess.Read)] CloudBlobContainer cloudBlobContainer) + ``` + + 3. Your method definition should should look like this now: + + ```csharp + public static IActionResult Run( + [HttpTrigger( + AuthorizationLevel.Function, + nameof(HttpMethods.Get), + Route = null)] HttpRequest request, + [Blob( + "players", + FileAccess.Read)] CloudBlobContainer cloudBlobContainer) + ``` + +3. Let's make some edits to the body of the method. + 1. Remove all the code in the body. + 2. Create an object to store our the list of blobs in our container: + + ```csharp + var blobList = cloudBlobContainer.ListBlobs(prefix: "in/")OfType(); + ``` + + 3. Create an object to store the names of each blob from the blobList: + + ```csharp + var blobNames = blobList.Select(blob => new { BlobName = blob.Name }); + ``` + + 4. Return an OkObjectResult with the blobNames found: + + ```csharp + 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](https://docs.microsoft.com/rest/api/storageservices/understanding-block-blobs--append-blobs--and-page-blobs) + +4. Run the Function App and make a request to the endpoint. + 1. URL: + + ```http + GET http://localhost:7071/api/GetBlobNamesWithContainerBlobInput + ``` + + 2. Output: (In my case, I have 2 play json files) + + ```json + [ + {"blobName":"in/player-1.json"}, + {"blobName":"in/player-2.json"} + ] + ``` + +## 7. Using `dynamic` Blob input bindings + +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. + +### Steps + +1. Create a new HTTP Trigger Function App, we will name it `GetPlayerWithStringInputDynamic.cs`. +2. We're going to make some changes to the method definition: + 1. Change the HTTPTrigger to only allow GET calls: + + ```csharp + nameof(HttpMethods.Get) + ``` + + 2. Add an IBinder parameter to the method definition: + + ```csharp + IBinder binder + ``` + + 3. Your method definition should should look like this now: + + ```csharp + public static async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest request, + IBinder binder + ``` + +3. Let's make some edits to the body of the method. + 1. Remove all the code in the body. + 2. Create a string object to store `id` and `result`: + + ```csharp + string id = request.Query["id"]; + IActionResult result; + ``` + + 3. Let's do some validation to make sure we have a value for `id`. If we do have a value for `id` we create a `BlobAttribute` 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. + + ```csharp + 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(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 include `Stream` and `CloudBlockBlob` 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 the `using` is executed, it will call the Dispose method of `IBinder` and clean up the object. [Here](https://www.codeproject.com/Articles/6564/Understanding-the-using-statement-in-C) is a great explanation. + +4. 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. + 1. URL: + + ```http + GET http://localhost:7071/api/GetPlayerWithStringInputDynamic/1 + ``` + + 2. Output: (this is the contents of [player-1.json](../../../src/dotnetcore31/AzureFunctions.Blob/player-1.json) make sure it's in your local storage blob container, we covered this in the first step of this lesson.) + + ```json + { + "id":"1", + "nickName":"gwyneth", + "email":"gwyneth@game.com", + "region": "usa" + } + ``` + +## 8. Creating a Blob triggered Function App + +First, you'll be creating a Function App with the BlobTrigger and review the generated code. + +### Steps + +1. 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. + +2. Select the language you'll be using to code the function, in this lesson we'll be using `C#`. +3. Select `BlobTrigger` as the template. +4. Give the function a name (e.g. `HelloWorldBlobTrigger`). +5. Enter a namespace for the function (e.g. `AzureFunctionsUniversity.Demo`). +6. 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. + +7. Select the Azure subscription you will be using. +8. Since we are using the BlobTrigger, we need to provide a storage account, select one or create a new storage account. + 1. If you select a new one, provide a name. The name you provide must be unique to all Azure. +9. Select a resource group or create a new one. + 1. If you create a new one, you must select a region. Use the one closest to you. +10. 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. +11. When asked about storage required for debugging choose _Use local emulator_. + + ![AzureFunctionsRuntime storage](img/AzureFunctionsStorage.png) + +Now the Function App with a BlobTrigger function will be created. + +## 8.1 Examining the Function App + +Great, we've got our Function Project and Blob Trigger created, let's examine what has been generated for us. + +```csharp +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. + +```csharp +[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: + +```csharp +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. + +## 8.2 Run the function + +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. + +![Storage Explorer sample-items](img/samples-workitems-output.png) + +> πŸ”Ž **Observation** - Great! That's how the BlobTrigger works, can you start to see how useful this trigger could be in your work? + +## 9. Homework + +[Here](blob-homework-resume-api-dotnet.md) is the assignment for this lesson. + +## 10. More info + +For more info about the Blob Trigger and bindings have a look at the official [Azure Functions Blob Bindings](https://docs.microsoft.com/azure/azure-functions/functions-bindings-storage-blob) documentation. + +--- +[πŸ”Ό Lesson Index](../../README.md) diff --git a/lessons/dotnet6/blob/blob-homework-resume-api-dotnet.md b/lessons/dotnet6/blob/blob-homework-resume-api-dotnet.md new file mode 100644 index 00000000..8a7d6817 --- /dev/null +++ b/lessons/dotnet6/blob/blob-homework-resume-api-dotnet.md @@ -0,0 +1,22 @@ +# Blob lesson Homework, improve your resume API, get the content from blob + +## Goal 🎯 + +The goal for this lesson is to grab the resume API you built in the [first homework assignment](../http/http-homework-dotnet.md) and instead of including the json in your code, upload it to a blob container and read its contents using Blob bindings. + +## Assignment + +Create an Azure Function with HTTP trigger that and Blob binding that returns your resume information in JSON. The JSON should not be in your code, you should upload it to a blob container and grab it's contents from there, when the function is triggered. + +## Resources + +- Solution can be found [here](../../../src/dotnetcore31/homework/resume-api/ResumeFromBlob.cs), try to accomplish it on your own first. +- Make sure to update your local.settings.json to use development storage and to have either storage emulator or Azurite running. +- Make sure your resume json file is in the correct blob container that your function is looking for. + + +## Share + +Please share you solutions on LinkedIn and Twitter using the #AzFuncUni hashtag and mention us. We would love to see it! + +[Gwyneth](https://twitter.com/madebygps) and [Marc](https://twitter.com/marcduiker) \ No newline at end of file diff --git a/lessons/dotnet6/blob/img/AzureFunctionsStorage.png b/lessons/dotnet6/blob/img/AzureFunctionsStorage.png new file mode 100644 index 00000000..d6b09ba2 Binary files /dev/null and b/lessons/dotnet6/blob/img/AzureFunctionsStorage.png differ diff --git a/lessons/dotnet6/blob/img/in-folder.png b/lessons/dotnet6/blob/img/in-folder.png new file mode 100644 index 00000000..662043e7 Binary files /dev/null and b/lessons/dotnet6/blob/img/in-folder.png differ diff --git a/lessons/dotnet6/blob/img/player-1-in-folder.png b/lessons/dotnet6/blob/img/player-1-in-folder.png new file mode 100644 index 00000000..8921c67f Binary files /dev/null and b/lessons/dotnet6/blob/img/player-1-in-folder.png differ diff --git a/lessons/dotnet6/blob/img/samples-workitems-output.png b/lessons/dotnet6/blob/img/samples-workitems-output.png new file mode 100644 index 00000000..72b1387f Binary files /dev/null and b/lessons/dotnet6/blob/img/samples-workitems-output.png differ diff --git a/lessons/dotnet6/blob/img/storage-explorer-sample-items.png b/lessons/dotnet6/blob/img/storage-explorer-sample-items.png new file mode 100644 index 00000000..0acd6c73 Binary files /dev/null and b/lessons/dotnet6/blob/img/storage-explorer-sample-items.png differ diff --git a/lessons/dotnet6/prerequisites/azureite_start_blob_service.png b/lessons/dotnet6/prerequisites/azureite_start_blob_service.png new file mode 100644 index 00000000..14e14bb6 Binary files /dev/null and b/lessons/dotnet6/prerequisites/azureite_start_blob_service.png differ diff --git a/lessons/dotnet6/prerequisites/azurite_blob_service_started.png b/lessons/dotnet6/prerequisites/azurite_blob_service_started.png new file mode 100644 index 00000000..edee9be3 Binary files /dev/null and b/lessons/dotnet6/prerequisites/azurite_blob_service_started.png differ diff --git a/lessons/dotnet6/prerequisites/azurite_files.png b/lessons/dotnet6/prerequisites/azurite_files.png new file mode 100644 index 00000000..a1b9f207 Binary files /dev/null and b/lessons/dotnet6/prerequisites/azurite_files.png differ diff --git a/lessons/dotnet6/prerequisites/azurite_folder_location.png b/lessons/dotnet6/prerequisites/azurite_folder_location.png new file mode 100644 index 00000000..f2c97c4b Binary files /dev/null and b/lessons/dotnet6/prerequisites/azurite_folder_location.png differ diff --git a/src/azurite/README.md b/src/azurite/README.md new file mode 100644 index 00000000..5186f930 --- /dev/null +++ b/src/azurite/README.md @@ -0,0 +1,5 @@ +# Azurite Folder + +This location is intended to be used by the [VSCode Azurite extension](https://docs.microsoft.com/azure/storage/common/storage-use-azurite) to store Azurite files. + +This folder is referenced in lessons related to Blob, Queue and Table triggers & bindings. diff --git a/src/dotnet6/blob/AzFuncUni.Blob/.gitignore b/src/dotnet6/blob/AzFuncUni.Blob/.gitignore new file mode 100644 index 00000000..2e88b724 --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/.gitignore @@ -0,0 +1,268 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +__azurite* +__blobstorage__ +__queuestorage__ + +# Azure Functions localsettings file +#local.settings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/src/dotnet6/blob/AzFuncUni.Blob/.vscode/extensions.json b/src/dotnet6/blob/AzFuncUni.Blob/.vscode/extensions.json new file mode 100644 index 00000000..830ea032 --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-dotnettools.csharp", + "Azurite.azurite" + ] +} diff --git a/src/dotnet6/blob/AzFuncUni.Blob/.vscode/launch.json b/src/dotnet6/blob/AzFuncUni.Blob/.vscode/launch.json new file mode 100644 index 00000000..8d17a193 --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to .NET Functions", + "type": "coreclr", + "request": "attach", + "processId": "${command:azureFunctions.pickProcess}" + } + ] +} diff --git a/src/dotnet6/blob/AzFuncUni.Blob/.vscode/settings.json b/src/dotnet6/blob/AzFuncUni.Blob/.vscode/settings.json new file mode 100644 index 00000000..9aecc121 --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "azureFunctions.deploySubpath": "bin/Release/net6.0/publish", + "azureFunctions.projectLanguage": "C#", + "azureFunctions.projectRuntime": "~4", + "azureFunctions.preDeployTask": "publish (functions)" +} \ No newline at end of file diff --git a/src/dotnet6/blob/AzFuncUni.Blob/.vscode/tasks.json b/src/dotnet6/blob/AzFuncUni.Blob/.vscode/tasks.json new file mode 100644 index 00000000..c8aa348f --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/.vscode/tasks.json @@ -0,0 +1,69 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clean (functions)", + "command": "dotnet", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile" + }, + { + "label": "build (functions)", + "command": "dotnet", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean (functions)", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "clean release (functions)", + "command": "dotnet", + "args": [ + "clean", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "problemMatcher": "$msCompile" + }, + { + "label": "publish (functions)", + "command": "dotnet", + "args": [ + "publish", + "--configuration", + "Release", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "type": "process", + "dependsOn": "clean release (functions)", + "problemMatcher": "$msCompile" + }, + { + "type": "func", + "dependsOn": "build (functions)", + "options": { + "cwd": "${workspaceFolder}/bin/Debug/net6.0" + }, + "command": "host start", + "isBackground": true, + "problemMatcher": "$func-dotnet-watch" + } + ] +} diff --git a/src/dotnet6/blob/AzFuncUni.Blob/AzFuncUni.Blob.csproj b/src/dotnet6/blob/AzFuncUni.Blob/AzFuncUni.Blob.csproj new file mode 100644 index 00000000..aec44bd5 --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/AzFuncUni.Blob.csproj @@ -0,0 +1,27 @@ + + + net6.0 + v4 + Exe + enable + enable + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + \ No newline at end of file diff --git a/src/dotnet6/blob/AzFuncUni.Blob/Models/HttpAndBlobOutput.cs b/src/dotnet6/blob/AzFuncUni.Blob/Models/HttpAndBlobOutput.cs new file mode 100644 index 00000000..53a7c6e2 --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/Models/HttpAndBlobOutput.cs @@ -0,0 +1,21 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; + +namespace AzFuncUni.Blob.Models +{ + public class HttpAndBlobOutput + { + public HttpAndBlobOutput( + string blobData, + HttpResponseData httpData) + { + BlobData = blobData; + HttpData = httpData; + } + + [BlobOutput("players/out/string-{rand-guid}.json", Connection = "AzureWebJobsBlobStorage")] + public string BlobData { get; set; } + + public HttpResponseData HttpData { get; set; } + } +} \ No newline at end of file diff --git a/src/dotnet6/blob/AzFuncUni.Blob/Models/Player.cs b/src/dotnet6/blob/AzFuncUni.Blob/Models/Player.cs new file mode 100644 index 00000000..f749e10e --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/Models/Player.cs @@ -0,0 +1,13 @@ +namespace AzFuncUni.Blob.Models +{ + public class Player + { + public string Id { get; set; } + + public string NickName { get; set; } + + public string Email { get; set; } + + public string Region { get; set; } + } +} \ No newline at end of file diff --git a/src/dotnet6/blob/AzFuncUni.Blob/Program.cs b/src/dotnet6/blob/AzFuncUni.Blob/Program.cs new file mode 100644 index 00000000..cd97ae1f --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/Program.cs @@ -0,0 +1,7 @@ +using Microsoft.Extensions.Hosting; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + +host.Run(); \ No newline at end of file diff --git a/src/dotnet6/blob/AzFuncUni.Blob/StorePlayerWithStringBlobOutput.cs b/src/dotnet6/blob/AzFuncUni.Blob/StorePlayerWithStringBlobOutput.cs new file mode 100644 index 00000000..ecbcad16 --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/StorePlayerWithStringBlobOutput.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Text.Json; +using AzFuncUni.Blob.Models; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace AzFuncUni.Blob +{ + public class StorePlayerWithStringBlobOutput + { + private readonly ILogger _logger; + + public StorePlayerWithStringBlobOutput(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + [Function(nameof(StorePlayerWithStringBlobOutput))] + public async Task Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req) + { + var player = await req.ReadFromJsonAsync(); + HttpResponseData response; + string serializedPlayer = string.Empty; + + if (player == null) + { + response = req.CreateResponse(HttpStatusCode.BadRequest); + } + else + { + response = req.CreateResponse(HttpStatusCode.OK); + serializedPlayer = JsonSerializer.Serialize(player); + } + + return new HttpAndBlobOutput(serializedPlayer, response); + } + } +} diff --git a/src/dotnet6/blob/AzFuncUni.Blob/host.json b/src/dotnet6/blob/AzFuncUni.Blob/host.json new file mode 100644 index 00000000..beb2e402 --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/host.json @@ -0,0 +1,11 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + } +} \ No newline at end of file diff --git a/src/dotnet6/blob/AzFuncUni.Blob/local.http b/src/dotnet6/blob/AzFuncUni.Blob/local.http new file mode 100644 index 00000000..695f14a9 --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/local.http @@ -0,0 +1,12 @@ +POST http://localhost:7071/api/StorePlayerWithStringBlobOutput +Content-Type: application/json + +{ + "id": "1", + "nickName": "Iron Man", + "email": "tony.stark@avengers.com", + "location": { + "region": "california", + "country": "usa" + } +} diff --git a/src/dotnet6/blob/AzFuncUni.Blob/local.settings.json b/src/dotnet6/blob/AzFuncUni.Blob/local.settings.json new file mode 100644 index 00000000..f42b902d --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "AzureWebJobsBlobStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" + } +} diff --git a/src/dotnet6/blob/AzFuncUni.Blob/player-1.json b/src/dotnet6/blob/AzFuncUni.Blob/player-1.json new file mode 100644 index 00000000..2693067f --- /dev/null +++ b/src/dotnet6/blob/AzFuncUni.Blob/player-1.json @@ -0,0 +1,9 @@ +{ + "id": "1", + "nickName": "Iron Man", + "email": "tony.stark@avengers.com", + "location": { + "region": "california", + "country": "usa" + } +} diff --git a/workspaces/dotnet6/blob-dotnet6.code-workspace b/workspaces/dotnet6/blob-dotnet6.code-workspace new file mode 100644 index 00000000..e937c75f --- /dev/null +++ b/workspaces/dotnet6/blob-dotnet6.code-workspace @@ -0,0 +1,34 @@ +{ + "folders": [ + { + "path": "../../lessons/dotnet6/prerequisites", + "name" : ".NET6 Prerequisites" + }, + { + "path": "../../lessons/dotnet6/blob", + "name" : "Blob Lesson" + }, + { + "path": "../../src/dotnet6/blob/AzFuncUni.Blob", + "name" : "AzFuncUni.Blob Function App" + }, + { + "path": "../../src/azurite", + "name" : "Azurite files" + }, + { + "path": "../../src/dotnet6/blob/Homework.Blob", + "name" : "Homework (solution)" + } + ], + "settings": { + "debug.internalConsoleOptions": "neverOpen" + }, + "extensions": { + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-dotnettools.csharp", + "azurite.azurite", + ] + } +} \ No newline at end of file