Azure Functions を Visual Studio 2022で作成してみた

# Azure Functions とは？

こちらの記事にも書いたように、WebAPIなサービスを簡単に作成することが出来る Microsoft のサーバーレス機能である。

https://qiita.com/forest1/items/9b7ccbd8fb2a0db6363a

# 試しにサービスを作ってみる
## 環境
Windows 10
Visual Studio Community 2022 (64bit) Version 17.2.5
Azureアカウント(従量課金)

## プロジェクト作成
1. 事前準備
Visual Studio Installer にて、「Azureの開発」をインストールしておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/39b20739-92ea-3795-f200-58a6c259a597.png)

2. プロジェクトの作成
テンプレートで、Azure Functionsを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/7ec78f45-647e-2479-77fc-5366183be4e8.png)

プロジェクトの追加情報は、この記事ではクライアントからのHTTP要求で起動する Http trigger で作る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/91210f44-a405-7391-6390-0657d22bda05.png)

Authorization levelについては、
Function が、事前発行のAPIキーをクライアントからHTTPヘッダに設定して呼び出さないと弾くタイプなので、ここでは認証無しのAnonymousにしておく。

生成されたソースコードは既に実行可能なものになっている。
(プロジェクト作成直後は自動生成されたソースコードに赤線が引かれたりしているので、まずはビルドしておく)
```
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace FunctionApp1
{
    public static class Function1
    {
        [FunctionName("Function1")]
        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.");

            string name = req.Query["name"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            string responseMessage = string.IsNullOrEmpty(name)
                ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
                : $"Hello, {name}. This HTTP triggered function executed successfully.";

            return new OkObjectResult(responseMessage);
        }
    }
}
```

## デバッグ実行
作成したプロジェクトをデバッグ実行(F5)してみると、コンソールが立ち上がってアクセス先のアドレスが表示される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/c5ec02ee-8a0a-3eb1-cbb9-67d21436d4ed.png)

クライアント側のテストツールとして、ここでは VSCodeの拡張機能、REST Clientを使ってみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/67ce7404-f39b-662e-b0e0-e03b9cc243bd.png)

GETもしくはPOSTの Send Request をクリックすると、以下の応答が帰ってくる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/ab1ca82e-fa84-8296-4e8d-6d0c6fc73bca.png)

## 機能拡張
### 応答でJSONを返す
先ほどのPOSTではクライアントからの送信のみJSONだったのを、応答でもJSONを返すようにしてみる。
```
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace FunctionApp1
{
    public static class Function1
    {
        [FunctionName("Function1")]
        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.");

            string name = req.Query["name"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            string responseMessage = string.IsNullOrEmpty(name)
                ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
                : $"Hello, {name}. This HTTP triggered function executed successfully.";

            ResponseData response = new ResponseData(name, responseMessage);

            return new OkObjectResult(response);
        }
    }


    class ResponseData
    {
        public string Name { get; }
        public string ResponseMessage { get; }

        public ResponseData(string name, string responseMessage)
        {
            Name = name ?? throw new ArgumentNullException(nameof(name));
            ResponseMessage = responseMessage ?? throw new ArgumentNullException(nameof(responseMessage));
        }
    }
}
```

返すデータを表現するResponseDataを作って、そこにクライアントから来た name と、これまで応答で返していた文字列を格納し、return の OkObjectResult()の引数に渡すように変更する。

これで、先ほどのテストクライアントから送ってみると、ResponseDataがJSONに変換されていることが分かる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/b58ace3c-d5ff-99f2-7d2e-0edb048f10cd.png)

### 応答データを Azure Storage の Blobに保存する
今度は、ResponseDataをクライアントに返すだけでなく、StorageのBlobにも保存してみる。
```
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Azure.Storage.Blobs;
using System.Text;

namespace FunctionApp1
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            [Blob("myblob", FileAccess.Write)] BlobContainerClient blobContainer,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string name = req.Query["name"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            string responseMessage = string.IsNullOrEmpty(name)
                ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
                : $"Hello, {name}. This HTTP triggered function executed successfully.";

            ResponseData response = new ResponseData(name, responseMessage);

            await blobContainer.CreateIfNotExistsAsync();
            await blobContainer.DeleteBlobIfExistsAsync("Test1Dir/ResponseData.json");
            var store = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(response)));
            await blobContainer.UploadBlobAsync("Test1Dir/ResponseData.json", store);

            return new OkObjectResult(response);
        }
    }


    class ResponseData
    {
        public string Name { get; }
        public string ResponseMessage { get; }

        public ResponseData(string name, string responseMessage)
        {
            Name = name ?? throw new ArgumentNullException(nameof(name));
            ResponseMessage = responseMessage ?? throw new ArgumentNullException(nameof(responseMessage));
        }
    }
}
```

必要に応じて拡張機能を追加する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/35f8bf8b-d613-f599-691c-e5495c67b919.png)

本関数 Run() の引数にBlobへのアクセスオブジェクトを追加し、
``` 
[Blob("myblob", FileAccess.Write)] BlobContainerClient blobContainer,
```
Blobへの書き込み処理を追加している。
```
await blobContainer.CreateIfNotExistsAsync();
await blobContainer.DeleteBlobIfExistsAsync("Test1Dir/ResponseData.json");
var store = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(response)));
await blobContainer.UploadBlobAsync("Test1Dir/ResponseData.json", store);
```

これで、先ほどはクライアントに返していたJSONが本関数に紐づくStorageのBlobにも出力されることになる。

Microsoft Azure Storage Explorer にて
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/f4ac355e-b903-7f7e-4dc4-b600b4f45cda.png)

今はローカルのエミュレータで動作させているので、そのBlob Containersに、
コンテナ名：myblob
ファイルパス：Test1Dir/ResponseData.json
で保存されている。

# Azureの本番サーバーに上げてみる
## 発行
プロジェクト右クリックから「発行」を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/4e05ad31-a557-e2c3-8684-dd24f69a68b4.png)
Azureを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/6345c413-6aa9-ebcf-cccf-ca7d4e299695.png)
Windowsを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/f3a8b99a-e262-3026-5b99-59d275c03f23.png)
Azure関数を作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/12127a25-d708-cfe8-7221-eeb452b32699.png)
例）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/48efb8c3-31c7-f745-9599-7181d8b3381a.png)
これで関数アプリの入れ物となるリソースグループなどがAzure上に作成される。
次に、「発行」ボタンを押して作成したアプリをアップロードする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/f1bdc1f8-27f0-d661-3041-396efc1ad969.png)

## 動作確認
Azureのポータルにアクセスして、作成したリソースグループ内の「関数アプリ」を開く。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/d499d348-fa1a-159d-977c-f823978a4289.png)

さらに「関数」を選択し、作成したアプリ(ここでは「Function1」)を開く。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/87d3e137-952f-1cd9-b033-cff404d89098.png)
「関数のURLの取得」から本関数のURLを取得する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/f0c9c259-dcdc-eabb-c01a-54083df5012b.png)

上でlocalhostで実行したように、VSCodeのREST Clientで動かしてみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/09d96a1d-873e-df60-8191-df743def29d5.png)
出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/22bec672-0f42-9944-886a-b78e321d2a1c.png)
Azure上のストレージアカウントにもファイルが保存されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120192/7389204f-e1ad-1d7f-135a-2687a71a9b4d.png)

# 追加情報
Visual Studio 2017などの旧開発環境でも開発可能。
ただし、拡張機能で入っている Microsoft.NET.Sdk.Functionsとかをバージョンアップすると動作しなくなるので注意。
また、ストレージエミュレータは最新への更新が必要。(標準インストール版ではエラーが出て動作しなかった)
https://docs.microsoft.com/ja-jp/azure/storage/common/storage-use-emulator
確か、Blob関連の使用クラスも違ったはず。(CloudBlobContainerとか)