1. Пререквизиты
Для начала работы на вашем компьютере должны быть:-
Установлен Visual studio 2017+. Скачать
-
Установлен Azure development workload в Visual studio.
-
Установлен Azure CLI и обновлен до последней версии. Скачать.
-
Azure subscription. Если у вас её нет, то создайте бесплатный аккаунт. Важно: можно участвовать и без подписки с помощью локальной отладки функций.
-
Альтернативно, вы можете участвовать используя Visual Studio Code или Azure portal, однако демонстрация воркшопа будет в Visual studio.
2. Создание ресурсов Azure
-
Откроем shell. Bash или https://shell.azure.com/
-
Войдём в azure
2.1. Snippet 1
az login
-
Создадим ресурсную группу
2.2. Snippet 2
GROUP = workshop-serverless-rg az group create --location westeurope\ --name$GROUP \ --output table
-
Создадим Storage account
2.3. Snippet 3
STORAGE = "workshop ${ RANDOM } sa" az storage account create --name$STORAGE \ -g$GROUP \ -l westeurope\ --sku Standard_LRS\ --output table
-
Создадим Functions App
2.4. Snippet 4
FUNCTION = workshop-fapp-$RANDOM az functionapp create --name$FUNCTION \ -g$GROUP \ -s$STORAGE \ --consumption-plan-location westeurope\ --functions-version3 \ --os-type Windows\ --runtime dotnet
-
Заметьте, автоматическое создание Application Insights в output
3. Создание проекта Azure Functions в Visual studio
-
Откроем Visual studio.
-
Создадим новый проект по шаблону Azure Functions.
-
Укажем имя, путь к проекту.
-
В меню "Create a new Azure Functions application" укажем:
-
Azure funtions v3 (.NET Core)
-
HTTP trigger
-
В качестве storage account выберем ранее создный через Browse... в выпадающем меню
-
Authorization level: Anonymous
-
-
Нажмем Create
4. Обновим автоматически сгенерированный код
-
Обновим значение атрибута FunctionName c Function1 на GetEricLippertBlogArticle.
-
Удалим из тела функции Run всё, кроме логирования.
-
После изменений код будет выглядеть так:
4.1. Snippet 5
namespace EricLippertBlogRoulette { public static class Function1 { [FunctionName("GetEricLippertBlogArticle")] public static async Task < IActionResult > Run ( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req , ILogger log ) { log . LogInformation ( "C# HTTP trigger function processed a request." ); } } }
5. Добавим обращение к web crawler блога
После записи в лог добавим обращение к функции и десериализацию в массив:5.1. Snippet 6
using var webClient = new WebClient (); string linksJson = await webClient . DownloadStringTaskAsync ( "https://learning-fapp-4928.azurewebsites.net/api/GetEricLippertBlogArticles?code=1gLGWOODXcmJHVs6PLQSBcSyM0dHL/JPt1NTwgUJTvLHTurY61yUbg==" ); var links = JsonConvert . DeserializeObject < string []>( linksJson );
6. Добавим код выбора случайной статьи и Redirect
-
Добавим private static поле для Random
6.1. Snippet 7
private static Random rng = new Random ();
-
Добавим выбор случайной статьи и возврат RedirectResult
6.2. Snippet 8
var randomLink = links [ rng . Next ( links . Length )]; return new RedirectResult ( randomLink );
-
Итоговый код после этого этапа выглядит так:
6.3. Snippet 9
namespace EricLippertBlogRoulette { public static class Function1 { private static Random rng = new Random (); [FunctionName("GetEricLippertBlogArticle")] public static async Task < IActionResult > Run ( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req , ILogger log ) { log . LogInformation ( "C# HTTP trigger function processed a request." ); using var webClient = new WebClient (); string linksJson = await webClient . DownloadStringTaskAsync ( "https://learning-fapp-4928.azurewebsites.net/api/GetEricLippertBlogArticles?code=1gLGWOODXcmJHVs6PLQSBcSyM0dHL/JPt1NTwgUJTvLHTurY61yUbg==" ); var links = JsonConvert . DeserializeObject < string []>( linksJson ); var randomLink = links [ rng . Next ( links . Length )]; return new RedirectResult ( randomLink ); } } }
7. Publish в Azure
-
Создадим Publish профайл, нажав на кнопку Publish в контекстном меню проекта:
-
Target: Azure
-
Specific target: Azure Functions App (Windows)
-
Functions instance: ранее созданную workshop-fapp-{number} (Consumption)
-
-
Выполним Publish
-
Для получения URL вернемся в bash и выполним команду:
7.1. Snippet 10
az functionappfunction show -g$GROUP \ --name$FUNCTION \ --function-name GetEricLippertBlogArticle\ --query"invokeUrlTemplate" -o tsv
-
Скопируем адрес и откроем в браузере.
-
Заметьте, первый вызов функции медленный из-за Cold start
8. Добавим учёт посещенных статей с ипользованием bindings
-
Подключим Nuget пакет
Microsoft.Azure.WebJobs.Extensions.Storage
-
Добавим параметры в метод Run для Blog биндинга:
8.1. Snippet 11
[Blob("functions-data/visited", FileAccess.Read)] TextReader inputBlob , [Blob("functions-data/visited", FileAccess.Write)] TextWriter outputBlob ,
-
Добавим код считывания всех посещенных статей и их учёт при выборе случайной.
8.2. Snippet 12
var visited = new List < string >(); if ( inputBlob != null ) { string visitedLink ; while (( visitedLink = inputBlob . ReadLine ()) != null ) { visited . Add ( visitedLink ); } } var links = JsonConvert . DeserializeObject < string []>( linksJson ) . Except ( visited ) . ToArray ();
-
Добавим вставку выбранной статьи в visited blob
8.3. Snippet 13
visited . Add ( randomLink ); foreach ( var link in visited ) { outputBlob . WriteLine ( link ); }
-
Итоговый код выглядит так:
8.4. Snippet 14
namespace EricLippertBlogRoulette { public static class Function1 { private static Random rng = new Random (); [FunctionName("GetEricLippertBlogArticle")] public static async Task < IActionResult > Run ( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req , [Blob("functions-data/visited", FileAccess.Read)] TextReader inputBlob , [Blob("functions-data/visited", FileAccess.Write)] TextWriter outputBlob , ILogger log ) { log . LogInformation ( "C# HTTP trigger function processed a request." ); using var webClient = new WebClient (); string linksJson = await webClient . DownloadStringTaskAsync ( "https://learning-fapp-4928.azurewebsites.net/api/GetEricLippertBlogArticles?code=1gLGWOODXcmJHVs6PLQSBcSyM0dHL/JPt1NTwgUJTvLHTurY61yUbg==" ); var visited = new List < string >(); if ( inputBlob != null ) { string visitedLink ; while (( visitedLink = inputBlob . ReadLine ()) != null ) { visited . Add ( visitedLink ); } } var links = JsonConvert . DeserializeObject < string []>( linksJson ) . Except ( visited ) . ToArray (); var randomLink = links [ rng . Next ( links . Length )]; visited . Add ( randomLink ); foreach ( var link in visited ) { outputBlob . WriteLine ( link ); } return new RedirectResult ( randomLink ); } } }
9. Аутентификация
-
В методе Run класса Function измените в атрибутe HttpTrigger параметр AuthorizationLevel c Anonymous на Function.
-
Выполните Publish функции
-
Вызовете функцию в браузере (snippet 10)
-
Заметьте: теперь в ответе 401 ошибка (unauthorized)
-
Выведете в консоль default ключ функции командой
9.1. Snippet 15
az functionappfunction keys list -g$GROUP \ -n$FUNCTION \ --function-name GetEricLippertBlogArticle\ --query"default"
-
Скопируйте и дополните в браузере адрес функции как параметр ?code=<скопированный ключ>
-
Наконец, финальная версия функции:
9.2. Snippet 16
namespace EricLippertBlogRoulette { public static class Function1 { private static Random rng = new Random (); [FunctionName("GetEricLippertBlogArticle")] public static async Task < IActionResult > Run ( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req , [Blob("functions-data/visited", FileAccess.Read)] TextReader inputBlob , [Blob("functions-data/visited", FileAccess.Write)] TextWriter outputBlob , ILogger log ) { log . LogInformation ( "C# HTTP trigger function processed a request." ); using var webClient = new WebClient (); string linksJson = await webClient . DownloadStringTaskAsync ( "https://learning-fapp-4928.azurewebsites.net/api/GetEricLippertBlogArticles?code=1gLGWOODXcmJHVs6PLQSBcSyM0dHL/JPt1NTwgUJTvLHTurY61yUbg==" ); var visited = new List < string >(); if ( inputBlob != null ) { string visitedLink ; while (( visitedLink = inputBlob . ReadLine ()) != null ) { visited . Add ( visitedLink ); } } var links = JsonConvert . DeserializeObject < string []>( linksJson ) . Except ( visited ) . ToArray (); var randomLink = links [ rng . Next ( links . Length )]; visited . Add ( randomLink ); foreach ( var link in visited ) { outputBlob . WriteLine ( link ); } return new RedirectResult ( randomLink ); } } }
10. Очистка
-
Удалите ресурсную группу (удалятся все ресурсы внутри)
10.1. Snippet 17
az group delete -g $GROUP --no-wait --yes
-
Удалите Azure CLI по инструкции
-
Удалите Visual Studio по инструкции